Introduction
Saturday, July 26, 2003
Suppose you're writing a generic test application for controls. The tester can invoke any property or method by reflection, but also needs to be notified of events. You can discover the events published by a class using reflection, and you can also dynamically attach event handlers - even late-bound, using System.Reflection.EventInfo.AddEventHandler.
The problem is though, you don't know the exact signature of the event - the first parameter is of type System.Object, the second of a class derived from System.EventArgs (let's assume the controls to test follow that pattern). In order to attach an event, you need to create a compatible delegate object - again, no problem, but the delegate needs a compatible method that it can forward the call to. And the signature of that method isn't known when your test application is being compiled.
One approach is to use System.Reflection.Emit, creating the code of the event handler at run time. It involves a lot of work, and frankly, I haven't tried this yet.
Another approach is using generic methods, and binding them at runtime. The process is discussed here.
How to do it
So my approach is to use an event handler method that is not compatible with the delegate signature (and can't be used for instantiating the delegate - neither early nor late bound), but that can be called nevertheless by the event-raising code, because its parameters are indeed compatible with the event signature.
The type checking on delegates is very strict (like it is on function pointers in general). There is no "parameter contravariance" - if the delegate specifies a certain parameter type, it doesn't help if the actual function defines the parameter in a way that is assignable from what is mandated by the delegate:
Delegate Sub PhoneEventHandler(ByVal sender As Object, ByVal e As PhoneEventArgs) Sub Phony(ByVal sender As Object, ByVal e As EventArgs) End Sub
There is no problem with calling this sub using an instance of PhoneEventArgs (since it derives from EventArgs), but you can't instantiate a PhoneEventHandler with this sub (and you can't assign a generic EventHandler to an event of the PhoneEventHandler type either).
Therefore, the problem needs to solved at the function pointer level. Pointers in .NET (represented by System.IntPtr) are typeless, which is fine, since all we need to do with them is pass it arround, not operate on it. So we instantiate the delegate, using an object reference (Nothing if the handling method is static), and a function pointer. Getting the function pointer involves querying the method info object for the method handle.
The code
This is example does not use controls (so it's readily runnable), but the principles are the same:
Option Strict On Imports System Imports System.Reflection Public Class CSender ' to handle this event, we need a method that matches ' this delegate's signature exactly, right? Public Event Hell As UnhandledExceptionEventHandler Public Sub RaiseHell() RaiseEvent Hell(Me, New UnhandledExceptionEventArgs(New Exception(), False)) End Sub End Class Public Module MDeepReflect Public Sub Main(ByVal args() As String) ' get info about an event whose delegate type we don't know; ' we assume it follows common event signature conventions; of ' course, discovery of the event is not part of this exercise Dim tpSender As Type = GetType(CSender) Dim ei As EventInfo = tpSender.GetEvent("Hell") Dim tpEventHandler As Type = ei.EventHandlerType Dim ciTypes() As Type = New Type(){GetType(Object), GetType(IntPtr)} Dim ci As ConstructorInfo = tpEventHandler.GetConstructor(ciTypes) ' get the pointer to a function whose parameter types are assignable ' from the parameter types specified by the event's delegate type, ' but whose signature does not match the event Dim ptr As IntPtr = GetFunctionPointer(GetType(MDeepReflect), "HandleIt") ' create a delegate instance of the event's type, using our ' untyped pointer to bypass type checking; note that we cast ' as little as necessary Dim o As Object = ci.Invoke(New Object(){Nothing, ptr}) Dim del As System.Delegate = DirectCast(o, System.Delegate) ' create an object that can fire the event, and add our handler; ' note that a System.EventHandler could be not assigned here Dim sender As New CSender() ei.AddEventHandler(sender, del) ' the first formal parameter's name ' ("target") confuses me here ' raise the event sender.RaiseHell() End Sub Private Function GetFunctionPointer(ByVal tp As Type, ByVal sMethod As String) As IntPtr ' get the MethodInfo object (allow for static and private methods) Dim flags As BindingFlags = BindingFlags.Instance Or BindingFlags.Static _ Or BindingFlags.Public Or BindingFlags.NonPublic Dim mi As MethodInfo = tp.GetMethod(sMethod, flags) ' get the method handle from the MethodInfo object Dim mh As RuntimeMethodHandle = mi.MethodHandle ' if we got the RuntimeMethodHandle structure, ask for ' the actual function pointer Dim ptr As IntPtr = mh.GetFunctionPointer() Return ptr End Function Private Sub HandleIt(ByVal sender As Object, ByVal e As EventArgs) ' normally, this handler is not suitable for events of type ' System.UnhandledExceptionEventHandler Console.WriteLine("MDeepReflect.HandleIt") Console.WriteLine("Sender: " & sender.ToString()) Console.WriteLine("e: " & e.ToString()) End Sub End Module
Follow-up: testing control events
So here's what a control tester would look like. Since delegates know about the objects handler methods operate on, we'll wrap a class arround the event handler, thus associating context information with the event (read: our generic handler will tell us which event was actually fired).
A demo version of Gregor.Core.CHandler
The Gregor.NET framework offers the CHandler class, which wraps up hooking late-bound event handlers. Here is a demo version with somewhat denormalized code:
using System; using System.Reflection; using System.Windows.Forms; class Test { public static void Main(string[] args){ TextBox txt = new TextBox(); CHandler.Create(txt, "MyTextBox", "Click"); CHandler.Create(txt, "MyTextBox", "TextChanged"); CHandler.Create(txt, "MyTextBox", "KeyDown"); CHandler.Create(txt, "MyTextBox", "MouseUp"); Form frm = new Form(); frm.Controls.Add(txt); Application.Run(frm); } } // class Test class CHandler { public static CHandler Create(object sender, string sSenderName, string sEventName){ return new CHandler(sender, sSenderName, sEventName); } private object m_Sender; private string m_SenderName; private string m_EventName; private CHandler(object sender, string sSenderName, string sEventName){ m_Sender = sender; m_SenderName = sSenderName; m_EventName = sEventName; // get event by name Type tpSender = sender.GetType(); EventInfo ei = tpSender.GetEvent(m_EventName); // get the delegate type for the event Type tpEventHandler = ei.EventHandlerType; this.CheckEventHandlerType(tpEventHandler); // get constructor info of delegate type Type[] ciTypes = new Type[]{typeof(object), typeof(IntPtr)}; ConstructorInfo ci = tpEventHandler.GetConstructor(ciTypes); // get function pointer of actual handler IntPtr ptr = this.GetFunctionPointer(this.GetType(), "HandleIt"); // instantiate proper delegate System.Delegate del = (System.Delegate) ci.Invoke(new object[]{this, ptr}); // add proper delegate to event ei.AddEventHandler(m_Sender, del); } public object Sender{ get{ return m_Sender; } } public string EventName{ get{ return m_EventName; } } public override string ToString(){ return m_SenderName + "." + m_EventName; } private void CheckEventHandlerType(Type tpEventHandler){ MethodInfo mi = tpEventHandler.GetMethod("Invoke"); // there must be two parameters ParameterInfo [] parms = mi.GetParameters(); if(parms.Length != 2){ this.ThrowDelegateException(tpEventHandler); } // first parameter must be a System.Object-compatible reference type Type tp1 = parms[0].ParameterType; if(tp1.IsValueType){ this.ThrowDelegateException(tpEventHandler); } // second parameter must be assignable to System.EventArgs Type tp2 = parms[1].ParameterType; if(false == typeof(System.EventArgs).IsAssignableFrom(tp2)){ this.ThrowDelegateException(tpEventHandler); } } private void ThrowDelegateException(Type tpEventHandler){ throw new ArgumentException("Delegate type '" + tpEventHandler.FullName + "' of event '" + m_EventName + "' is not supported."); } private IntPtr GetFunctionPointer(Type tp, string sMethod){ // get the method info from the type BindingFlags flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; MethodInfo mi = tp.GetMethod(sMethod, flags); // get method handle RuntimeMethodHandle mh = mi.MethodHandle; // ask method handle for function pointer return mh.GetFunctionPointer(); } private void HandleIt(object sender, EventArgs e){ Console.WriteLine(this.ToString() + " {" + e + "}"); } } // class CHandler
You'll notice that dynamic discovery of events published by a class is still not part of this exercise.
However, the CHandler class checks whether the central event handler is compatible with the delegate type of the event. An interesting aspect of this check is that the type of the first parameter of our handler, System.Object, cannot be considered assignable from a value type. That is because we lie about the event handler's signature. Suppose an event passes a (n unboxed) value type for the "sender" parameter (let's say an event delegate type is so declared) - the event raising code thinks a value is required, because the event's delegate so specifies; System.Object is universal only through boxing, but nothing's boxed until it's boxed. This is part of the more general concept of strict type checking on function (and related) types: prototypes, function pointer types, and the actual function, or delegates and the handler method - everything must match. There may be conversions (or boxing) available (even implicitly), but if the calling code isn't aware of these conversions, they don't happen.
Using Gregor.Core.CHandler
Here's a more realistic solution for a control tester app, using the current version of Gregor.Core.CHandler (different from the code above), which can be invoked like this:
ControlTester System.Windows.Forms.dll System.Windows.Forms.CheckBox *Changed* Click
Alas, the code:
// <shell:csc.exe /t:exe /debug+ /r:Gregor.Core.dll $Vars.DocName%> using System; using System.Drawing; using System.Reflection; using System.Windows.Forms; using Gregor.Core; namespace Gregor { public class ControlTester { public static void Main(string[] args) { if(args.Length < 3){ W("Syntax:"); W(" ControlTester <assembly> <control-class> <event-name-pattern-1> [<event-name-pattern-n>+]"); Environment.Exit(1); } try{ Run(args); }catch(Exception ex){ W(ex); } } private static void Run(string[] args){ CAssemblyLoader loader = new CAssemblyLoader(); Assembly a = loader.LoadAssembly(args[0]); Check.Reference(a, "Cannot load assembly '" + args[0] + "'."); Type tp = a.GetType(args[1]); Check.Reference(tp, "Cannot find type '" + args[1] + "'."); Control ctl = (Control) Activator.CreateInstance(tp); Form frm = CreateTestForm(ctl); string[] asEventNamePatterns = new string[args.Length - 2]; Array.Copy(args, 2, asEventNamePatterns, 0, asEventNamePatterns.Length); HookEventHandlers(ctl, asEventNamePatterns); Application.Run(frm); } private static Form CreateTestForm(Control ctl){ Form frm = new Form(); frm.Text = "Control Tester"; frm.Size = new Size(500, 300); ctl.Location = new Point(5, 5); ctl.Anchor = AnchorStyles.Left | AnchorStyles.Top; frm.Controls.Add(ctl); return frm; } private static void HookEventHandlers(Control ctl, string[] asEventNamePatterns){ try{ foreach(string sEventNamePattern in asEventNamePatterns){ CHandler[] handlers = CHandler.CreateForMultipleEvents(ctl, sEventNamePattern); foreach(CHandler handler in handlers){ handler.EventFired += new EventHandler(HandleControlEvent); } } }catch(Exception ex){ W(ex); } } private static void HandleControlEvent(object sender, EventArgs e){ try{ CHandler handler = (CHandler) sender; W(handler.EventName); }catch(Exception ex){ W(ex); } } private static void W(object obj){ Console.WriteLine(obj); } } // class ControlTester } // namespace Gregor
Follow-up: C# 2005
With C#'s new method group conversion feature, it is possible to assign a more general handler method to a more specific delegate. The .NET class library offers support for this as well in the CreateDelegate methods of System.Delegate. For further details, please refer to the Languages 2005 article.