[Late Events]

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.