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
Post a Comment