
How to obtain the invocation list of any event.
Have you ever tried to examine a class using reflection and discover who has subscribed to certain events? It was certainly one of the most puzzling things I've done in a long time. Why? Well, because reflection seems to be a straightforward process that enables us to do any amount of introspection in our code...that all falls apart when we use it on events. If you've use reflection to any extent you'll know that it's easy to obtain a MemberInfo based object for all the members of a class, even the private ones. Properties have PropertyInfo objects, fields have FieldInfo objects and, yes, you guessed it, events have EventInfo objects associated with them. This looks like a nice consistent pattern until you realise that you can obtain the values for the properties and fields or invoke methods but try as you might, there is no way to get the delegate from an EventInfo. If you examine the way that the compiler generates events when we use the event keyword you'll see that a private delegate of the same name is added to the class. Based upon this, all we need to do is search through the private member fields of the class to find it and we should be able to obtain the invocation list. As usual, the best laid plans of mice and men are often upset by the architects at Microsoft because, in the case of System.Windows.Forms.Component based objects, the default method was not deemed good enough and the event accessors maintain the delegates in an EventHandlerList collection. This means of course that in order to get the delegate for, say, the Paint event, we need to dereference the correct entry in the event handler list which is a dictionary using key-value pairs to the delegate with some known reference. The default compiler generated case is simple enough to do with reflection. We can get the full list of private fields of a particular class using GetFields. There is a caveat to do with reflection inasmuch as the TypeDescriptor system will only return private static fields for a specific named type and not for one of it's decendents, even when we use the BindingFlags.FlattenHierarchy option, so if your event is declared in a base class, you have to walk the list of base types to find the corresponding delegate field. The code below shows this technique. public Delegate[] GetEventSubscribers(object target, string eventName) { Type t = target.GetType();
do {
FieldInfo[] fia = t.GetFields( BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic); foreach (FieldInfo fi in fia) { if (fi.Name == eventName) { Delegate d = fi.GetValue(target) as Delegate; if (d != null) return d.GetInvocationList(); } } t = t.BaseType; } while (t != null);
return new Delegate[] { }; } Public Function GetEventSubscribers(ByVal target As Object, ByVal eventName As String) As Delegate() Dim t As Type = target.GetType Do Dim fia As FieldInfo() = t.GetFields((BindingFlags.NonPublic Or (BindingFlags.Static Or BindingFlags.Instance))) Dim fi As FieldInfo For Each fi In fia If (fi.Name Is eventName) Then Dim d As [Delegate] = TryCast(fi.GetValue(target), [Delegate]) If (Not d Is Nothing) Then Return d.GetInvocationList End If End If Next t = t.BaseType Loop While (Not t Is Nothing) Return New [Delegate](0 - 1) {} End Function So, now we come to the case of finding the handlers for an arbitrary event in a Form or control. All Windows Forms based controls, user controls and forms are based on the Component class. This class owns an EventHandlerList collection which is used to store the delegates of the events in a linked list. The list is indexed by a key in a manner similar to a hash table and the key is an object owned by the class that declares the event. The Paint event for example, has a key called EventPaint which is a private static member of the Control class and the Paint event accessors store and retrieve the delegates in the following way.
public event
PaintEventHandler Paint
Public Event
Paint As
PaintEventHandler This means that the extraction of the delegate invocation list requires that we get the key from the right class and dereference the delegate within the Events list. Once again, the limitation of reflection means that we may need to walk the list of bases to discover the reference to the specific key for the event we want to examine. We cannot, for example, rely on the fact that the delegate is of a particular type because perhaps more than one event will use the same type of delegate. To make our event reflection system as generic as possible we can add the code to our reflection method such that if we fail to find the event by the first method, we can continue to search for events provided by Windows Forms objects. The code below shows the expanded method. public Delegate[] GetEventSubscribers(object target, string eventName) { string WinFormsEventName = "Event" + eventName; Type t = target.GetType();
do {
FieldInfo[] fia = t.GetFields(BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic); foreach (FieldInfo fi in fia) { if (fi.Name == eventName) { //we've found the compiler generated event Delegate d = fi.GetValue(target) as Delegate; if (d != null) return d.GetInvocationList(); } if (fi.Name == WinFormsEventName) { //we've found an EventHandlerList key //get the list EventHandlerList ehl = (EventHandlerList)target.GetType().GetProperty("Events", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy).GetValue(target,null); //and dereference the delegate. Delegate d = ehl[fi.GetValue(target)]; if (d != null) return d.GetInvocationList(); } } t = t.BaseType; } while (t != null);
return new Delegate[] { }; } Public Function GetEventSubscribers(ByVal target As Object, ByVal eventName As String) As [Delegate]() Dim WinFormsEventName As String = ("Event" & eventName) Dim t As Type = target.GetType Do Dim fia As FieldInfo() = type1.GetFields((BindingFlags.NonPublic Or (BindingFlags.Static Or BindingFlags.Instance))) Dim fi As FieldInfo For Each fi In fia Dim d As [Delegate] If (fi.Name Is eventName) Then d = TryCast(info1.GetValue(target), [Delegate]) If (Not d Is Nothing) Then Return d.GetInvocationList End If End If If (fi.Name Is WinFormsEventName) Then Dim ehl As EventHandlerList = DirectCast(target.GetType.GetProperty("Events", (BindingFlags.FlattenHierarchy Or (BindingFlags.NonPublic Or BindingFlags.Instance))).GetValue(target, Nothing), EventHandlerList) d = ehl.Item(fi.GetValue(target)) If (Not d Is Nothing) Then Return d.GetInvocationList End If End If Next t = t.BaseType Loop While (Not t Is Nothing) Return New [Delegate](0 - 1) {} End Function So, reflection on an event is possible if not completely straightforward. Sadly, because this is a system that is open to interpretation, we can never guarantee being able to find the invocation lists of every event in any arbitrary object. For example, Infragistics uses the form "EVENT_XXX" to name their event handler keys when they add an event to a Component based class, so, you can see that despite knowing the general formula for this operation, some amount of discovery will be needed if you're doing something like analysing an assembly that uses third-party components. Hopefully though, given the information in this article, you'll have a great head-start next time you need to do it. Return to Windows Forms Tips and Tricks Copyright © Ramuseco Limited 2004-2005 All Rights Reserved.
|
|||