C# Tutorial - Poking at Event Contents [Advanced]


Events in C# always feel like there is a little touch of black magic in the background keeping things running smoothly. We have had tutorials here before on events in C# - we took a look at how to create your own custom events in C# Snippet Tutorial - Custom EventHandlers, and we looked at the syntactic sugar behind the += and -= operators for events in C# Tutorial - EventAccessors. But we have never taken a look at what actually happens when you declare an event, and what happens when you invoke it.

The key behind events in C# in the MulticastDelegate. You've probably seen delegates before (if you haven't, they are just a reference to a method), and knowing that, you can probably wager a guess as to what a MulticastDelegate is. A MulticastDelegate is essentially a list of method references that acts like a regular old delegate in many ways. For instance, take a look at the following:
private void DoIt()
{
  var del = new Action(Method1);
  del();

  var del2 = Delegate.Combine(del, new Action(Method2)) as Action;
  del2();
}

private void Method1()
{ Console.WriteLine("I'm Method 1!"); }

private void Method2()
{ Console.WriteLine("I'm Method 2!"); }
 
If you run the function DoIt, the output is:
I'm Method 1!
I'm Method 1!
I'm Method 2!
 
This is because when del1 is invoked, it just calls Method1. But when del2 is invoked, both Method1 and Method2 get called, because the invocation list for del2 contains both methods. Both methods are in there because of the Combine call.

Once you know about the existence of the invocation list, you can start to do some interesting things. Take a look at the example below:
private void Method1()
{ Console.WriteLine("I'm Method 1!"); }

private void Method2()
{ Console.WriteLine("I'm Method 2!"); }

private void Method3()
{ Console.WriteLine("I'm Method 3!"); }

private void Method4()
{ Console.WriteLine("I'm Method 4!"); }

private void DoItAgain()
{ 
  var del = Delegate.Combine(new Action(Method1), new Action(Method2),
    new Action(Method3), new Action(Method4));

  var list = del.GetInvocationList();
  for (int i = 0; i < list.Length; i+=2)
  { ((Action)list[i])(); }
}
 
The output of the call to DoItAgain is the following:
I'm Method 1!
I'm Method 3!
 
In this case, we are skipping every other entry in the invocation list. Probably not a terribly useful thing to do, but you can see the power that being able to get to the list can give you.

Ok, so enough about the specifics of MulticastDelegates. How exactly are they used in events? Well, every time you write something like this in code:
public event EventHandler MyBestEventEver;
 
The compiler is going in behind you and placing down a MulticastDelegate in the background. Now, this only happens if you aren't declaring your own implementations of the add and remove accessors - when you do that (as we covered in C# Tutorial - EventAccessors), you are on your own for how the invocation list will actually be stored. But when you don't declare your own accessors, the compiler automatically uses a MulticastDelegate.

Now, when you are in the same class as where your event is declared, you can get to all the handy MulticastDelegate methods, like GetInvocationList:
public class MyEventTest
{
  public event EventHandler Changed;

  public void GetChangedHookCount()
  {
    var myList = Changed.GetInvocationList();
    Console.WriteLine(myList.Length);
  }
}
 
But if you are in a different class, the compiler says no:
public class MyEventTest
{
  public event EventHandler Changed;
}

public class MyOtherClass
{
  public void GetChangedHookCount()
  {
    MyEventTest test = new MyEventTest();
    var myList = test.Changed.GetInvocationList();
    Console.WriteLine(myList.Length);
  }
}

//Error: The event 'MyEventTest.Changed' can only appear on the left hand side 
//of += or -= (except when used from within the type 'MyEventTest')
 
This is because from outside MyEventTest the compiler can't make any guarantees about how that event is implemented inside MyEventTest - maybe something crazy was done with the accessors? Maybe there is no MulticastDelegate at all?

This is a real bummer - because it means that it is really hard to get to and/or modify the contents of an event from outside the class it was declared in. Now, granted, from a security and good practices point of view, this is a very good thing - but every once in a while, it would come in really handy.

But don't give up yet! If you know that the event is not using custom accessors (and most events don't), you can still use some reflection to get to these pieces:
public static class EventUtilities
{
  public static Delegate[] GetInvocationList(string eventName, object obj)
  {
    bool success;
    var result = TryGetInvocationList(eventName, obj, out success);
    if (success)
    { return result; }
    else
    { throw new InvalidOperationException(); }
  }

  public static Delegate[] TryGetInvocationList(string eventName, object obj,
      out bool success)
  {
    success = false;

    if (obj == null)
    { throw new ArgumentNullException("obj"); }

    if (eventName == null)
    { throw new ArgumentNullException("eventName"); }

    var field = GetField(eventName, obj.GetType());
    if (field == null)
    { return null; }

    success = true;
    var mDel = field.GetValue(obj) as MulticastDelegate;

    if (mDel == null)
    { return null; }
    else
    { return mDel.GetInvocationList(); }
  }

  public static bool ClearInvocationList(string eventName, object obj)
  {
    if (obj == null)
    { throw new ArgumentNullException("obj"); }

    if (eventName == null)
    { throw new ArgumentNullException("eventName"); }

    var field = GetField(eventName, obj.GetType());

    if (field == null)
    { return false; }

    field.SetValue(obj, null);
    return true;
  }

  private static FieldInfo GetField(string eventName, Type type)
  {
    var field = type.GetField(eventName, BindingFlags.Instance | 
      BindingFlags.NonPublic | BindingFlags.FlattenHierarchy | BindingFlags.Public);

    if (field == null)
    { return null; }

    if (field.FieldType == typeof(MulticastDelegate))
    { return field; }

    if(field.FieldType.IsSubclassOf(typeof(MulticastDelegate)))
    { return field; }

    return null;
  }
}
 
So this crazy code is all about pulling that compiler created MulticastDelegate for an event out into the open where we can mess with it. Given the name of an event and the object that it resides on, we can pull the MulticastDelegate field off of the type using reflection (that is what the GetField method is doing) and then call GetValue to actually pull out the instance of the MulticastDelegate. For more information on how reflection works, you can check out these two tutorials.

An interesting thing to note is that the backing MulticastDelegate field is null when there is nothing hooked to the event - which means that clearing all hooks to an event is as simple as setting that field to null (which is what the ClearInvocationList is doing).

So how do we use these crazy methods? Its pretty simple - lets take the example above that wouldn't compile and fix it up:
public class MyEventTest
{
  public event EventHandler Changed;
}

public class MyOtherClass
{
  public void GetChangedHookCount()
  {
    MyEventTest test = new MyEventTest();
    var myList = EventUtilities.GetInvocationList("Changed", test);
    Console.WriteLine(myList.Length);
  }
}
 
In this case, this would print out 0, since nothing has been attached to the event.

One huge caveat to end this tutorial - these methods will not work for poking at the contents of events on pretty much any WPF element. This is because almost every event on a WPF element implements its own special add/remove accessors - they almost never use the standard MulticastDelegate backing. WPF elements use their own special internal EventHandlersStore, which while in the end still holds MulticastDelegates, is much harder to get to. If you need to get at the contents of a WPF event (and I wouldn't do this unless you really, really need to), I suggest pulling open Reflector to figure out exactly what to poke and prod at using reflection.

That's it for this tutorial on poking at events and MulticastDelegates. I hope it shed some light on what is a mysterious black box to many .NET developers. As always, drop any questions you might have below, and I'll do my best to answer them.

Comments