Fun with events and delegates

So how can you verify that a given class raises an event? Well, you can simply write some code that registers an event handler and then cause the target to raise that event. Now, how do you verify that a given class does actually handle an event raised by another class? Basically it’s the same thing – just the other way round. You create test code which raises the event in question, makes the target register an event handler, raises the event and verifies that the target handles the event correctly. But how do you verify that a given class does not handle an event raised by another class? And why do you even need to worry about this?

The last two questions only make sense in a scenario in which a class under test registers an event handler in the first place and is then expected to unregister that handler later on. The obvious reason why you need to care is that the behavior of your software is probably incorrect if it keeps on handling an event although it shouldn't. The less obvious reason is that event handlers affect the lifetime of the object that handles the event because the delegate passed to the object that raises the event contains a reference back to the event handling object. This means that failure to unregister event handlers before getting rid of all other references to an object can lead to issues that are hard to debug. The best case scenario is that because of the remaining event handlers your software is behaving strangely so you at least notice that there is something going wrong (which might still be difficult to track down). The worst case scenario is that the remaining event handlers won't really do anything because of their internal state so unless you inspect your software and specifically look for method invocation counts, class instance counts or memory consumption you may not even notice the defect.

With all that said, how can we actually verify that a given object unregisters its event handlers? As mentioned before, simply raising the event may not give us any clue on whether or not there are handlers left. However, we can examine an event at runtime. But before I show some simple code that will help us to achieve this let's have a look at what an event really is. I most cases when you declare an event it will probably look something like this:

public event EventHandler SomeEvent;

The fun begins when we look at the IL generated by the C# compiler because of that one innocent line of code:

.field private class [mscorlib]System.EventHandler SomeEvent

.event [mscorlib]System.EventHandler SomeEvent

{

    .addon instance void TestProject1.TestClass::add_SomeEvent(class [mscorlib]System.EventHandler)

    .removeon instance void TestProject1.TestClass::remove_SomeEvent(class [mscorlib]System.EventHandler)

}

 

.method public hidebysig specialname newslot virtual final instance void add_SomeEvent(class [mscorlib]System.EventHandler 'value') cil managed synchronized
{
.maxstack 8
L_0000: ldarg.0
L_0001: ldarg.0
L_0002: ldfld class [mscorlib]System.EventHandler TestProject1.TestClass::SomeEvent
L_0007: ldarg.1
L_0008: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate)
L_000d: castclass [mscorlib]System.EventHandler
L_0012: stfld class [mscorlib]System.EventHandler TestProject1.TestClass::SomeEvent
L_0017: ret
}

 

.method public hidebysig specialname newslot virtual final instance void remove_SomeEvent(class [mscorlib]System.EventHandler 'value') cil managed synchronized
{
.maxstack 8
L_0000: ldarg.0
L_0001: ldarg.0
L_0002: ldfld class [mscorlib]System.EventHandler TestProject1.TestClass::SomeEvent
L_0007: ldarg.1
L_0008: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Remove(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate)
L_000d: castclass [mscorlib]System.EventHandler
L_0012: stfld class [mscorlib]System.EventHandler TestProject1.TestClass::SomeEvent
L_0017: ret
}

 

First, we have a private field of the type we used to declare our event. This type is a delegate type derived from MulticastDelegate which means that it holds an array of delegates. The field being declared as private is also the reason why you cannot raise an event from a derived class. Second, there is the actual event which points to the methods add_SomeEvent() and remove_SomeEvent(). This is similar to how properties are compiled into getter and setter methods. The add and remove methods of an event are automatically called when you use the += and -= operators. Now that we have a clear understanding of what an event is we can go through some sample code. Say I have an interface that contains an event and a class that hooks up event handlers for that event in my production code:

public interface IRaisesEvent

{

    event EventHandler SomeEvent;

}

public static class MyClass

{

    private static Collection<IRaisesEvent> registeredObjects = new Collection<IRaisesEvent>();

    public static void RegisterObject(IRaisesEvent obj)

    {

        if (obj == null)

            throw new ArgumentNullException();

        registeredObjects.Add(obj);

        obj.SomeEvent += new EventHandler(EventHandler);

    }

    public static void ReleaseObjects()

    {

        foreach (IRaisesEvent item in registeredObjects)

            item.SomeEvent -= new EventHandler(EventHandler);

        registeredObjects.Clear();

    }

    private static void EventHandler(object sender, EventArgs e)

    {

        Debug.Print("Object of type {0} raised IRaisesEvent.SomeEvent", sender.GetType());

    }
}

 

The next thing we need is a test class that implements the interface so we have something to pass to the static class. In addition to implementing the interface I also defined a read-only property that allows a caller to access the private MulticastDelegate instance. Please note that I do not advice you to do so in production code since this exposes detailed information about the event (which, however, is exactly what we want for testing purposes).

public class TestClass : IRaisesEvent

{

    public event EventHandler SomeEvent;

    public MulticastDelegate SomeEventMulticastDelegate

    {

        get

        {

            return SomeEvent;

        }

    }

}

 

This actually does the trick. We can now examine the state of the event (i.e. its currently registered event handlers) by calling MulticastDelegate.GetInvocationList() and the properties Delegate.Method and Delegate.Target allow us to get detailed information on each of those delegates. Of course creating some extension methods once for the most interesting scenarios will simplify our actual test code.

public static class MulticastDelegateExtensions

{

    public static int GetDelegateCount(this MulticastDelegate d)

    {

        if (d == null)

            return 0;

        else

            return d.GetInvocationList().Length;

    }

    public static Type[] GetTargetTypes(this MulticastDelegate d)

    {

        Delegate[] invocationList;

        Type[] types;

        if (d == null)

            return new Type[0];

        else

        {

            invocationList = d.GetInvocationList();

            types = new Type[invocationList.Length];

            for (int i = 0; i < invocationList.Length; i++)

                types[i] = invocationList[i].Method.ReflectedType;

            return types;

        }

    }

}

 

The methods above for example allow us to get the number of event handlers and the types of the objects that will handle the event. This is a reasonable setup for the test given that you can reuse the extension methods throughout your test projects. The test method itself is short and easy to understand:

[TestMethod]

public void DelegateTest()

{

    TestClass c = new TestClass();

    MyClass.RegisterObject(c);

    Assert.AreEqual<int>(1, c.SomeEventMulticastDelegate.GetDelegateCount());

    Assert.AreEqual<Type>(typeof(MyClass), c.SomeEventMulticastDelegate.GetTargetTypes()[0]);

    MyClass.ReleaseObjects();

    Assert.AreEqual<int>(0, c.SomeEventMulticastDelegate.GetDelegateCount());

}

 


This posting is provided "AS IS" with no warranties, and confers no rights.