Event Browsing - .NET Event Concepts and Customizations






4.86/5 (6 votes)
.NET Events concepts and customizations

Introduction
This article will detail how the Event/Delegate mechanism can be implemented in .NET, the most common default implementation, the implementation model followed by System.Windows.Forms.Control
, and options that are available for custom implementations. Additionally I will cover how we can use knowledge of the most common implementations of the Event/Delegate mechanism to browse for and interact with events and their subscribing delegates as well as the limitations and pitfalls of doing so. A sample Event browser is included in the attached source code. I'll start with some background, the basic stock event/delegate implementation, cover customizations, and then detail the custom methodology employed by Microsoft in their user interface controls (type control and its derivatives).
The Basics
Let's start with a simple example - what I will term the basic default implementation of the event/delegate mechanism. We start by declaring a delegate that effectively functions as a message sent from publisher to subscriber and an event that will act as the publisher using the delegate we declared.
delegate void DelegateFunction(int iValue);
public event DelegateFunction EventSource;
When the event keyword is used as above, it instructs the compiler to add a new non-public member field to its containing class of type MulticastDelegate
with the same name as the event. It also adds an event implementation, automatically generating the event's associated "add" subscriber and "remove" subscriber operations. The MulticastDelegate
field is null
unless there are subscribers listening to the event. Building on the previous event/delegate declaration, a simple sample class can be used to help us visualize what is implemented for us behind the scenes.
class EventObject
{
public event DelegateFunction EventSource;
public EventObject()
{
FieldInfo FI = this.GetType().GetField("EventSource",
BindingFlags.NonPublic | BindingFlags.Instance);
EventInfo EI = this.GetType().GetEvent("EventSource");
System.Diagnostics.Debug.WriteLine("Event Name: " + EI.Name);
System.Diagnostics.Debug.WriteLine("Field Name: " + FI.Name);
EventSource += new DelegateFunction(EventSubscriberFunction);
object Value = FI.GetValue(this);
if (Value != null)
System.Diagnostics.Debug.WriteLine("Field Type: " + Value.GetType().ToString()
+ " (Base: " + Value.GetType().BaseType.ToString() + ")");
else
System.Diagnostics.Debug.WriteLine("Currently zero subscribers to the event");
MemberInfo[] Members = this.GetType().GetMembers(
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
foreach (MemberInfo MI in Members)
{
if (MI.Name.StartsWith("add_") || MI.Name.StartsWith("remove_"))
System.Diagnostics.Debug.WriteLine("Event Property Method: " + MI.Name);
}
}
private void EventSubscriberFunction(int iValue)
{ System.Diagnostics.Debug.WriteLine("Subscriber received value of " + iValue); }
}
If we instantiate our "EventObject
" class, we should see output similar to the following:
Event Name: EventSource
Field Name: EventSource
Field Type: DelegateFunction (Base: System.MulticastDelegate)
Event Property Method: add_EventSource
Event Property Method: remove_EventSource
What the output is showing us is that the compiler has implemented our event, EventSource
, for us. In doing so, it has generated storage for the subscribers to our event in the form of a hidden MulticastDelegate
field named EventSource
. It has also added a matching pair of add and remove functions to allow subscribers to be added and removed from that storage. When a subscriber is added via a "+=" or removed via a "-=" to our event behind the scenes, .NET is invoking the generated "add" and "remove" functions for our event. Further, a call to invoke our event becomes basically shorthand notation to loop over all the subscribers held within the MulticastDelegate
field and invoke each function in turn. Syntactic sugar. Let's prove this out by adding the functions below and calling them from our main.
public void RaiseEvent()
{
System.Diagnostics.Debug.WriteLine("Auto Invovation:");
if (EventSource != null)
EventSource(5);
}
public void RaiseEventManually()
{
System.Diagnostics.Debug.WriteLine("Manual Invovation:");
FieldInfo FI = this.GetType().GetField("EventSource",
BindingFlags.NonPublic | BindingFlags.Instance);
MulticastDelegate MD = FI.GetValue(this) as MulticastDelegate;
if (MD != null)
{
foreach (Delegate D in MD.GetInvocationList())
D.Method.Invoke(D.Target, new object[] { 5 });
}
}
Our output now includes the lines:
Auto Invocation:
Subscriber received value of 5
Manual Invocation:
Subscriber received value of 5
We see the output is the same regardless if we invoke through our event object or access the hidden delegate field directly to perform the invocation.
Building on this just a bit more, let's replace the line:
EventSource += new DelegateFunction(EventSubscriberFunction);
with the line:
this.GetType().GetMethod("add_EventSource").Invoke(this, new object[]
{ new DelegateFunction(EventSubscriberFunction) });
If we rerun the example, we can see the net result is the same. A subscriber is added to our event. Interestingly enough, the method is in the public
interface of the class but not only is the function hidden by intellisense if we attempt to call it directly as follows:
this.add_EventSource(new DelegateFunction(EventSubscriberFunction));
We receive the error "'EventObject.EventSource.add
': cannot explicitly call operator or accessor". Furthermore, if we attempt to manually add the generated add or remove functions to our EventObject
class as defined below:
void add_EventSource(DelegateFunction EventSubscriber) { }
void remove_EventSource(DelegateFunction EventSubscriber) { }
We receive the error "Type 'EventObject
' already reserves a member called add_EventSource
with the same parameter types". Clearly the compiler is not going to tolerate us mucking about with its conventions.
Custom Event Implementations
With the background out of the way, let's delve into customizations. So I've put forward how .NET implements an event/delegate pair for us. Basically what happens is storage is allocated for the subscribers to the event and functions are generated to allow us to add, remove, and invoke those subscribers. We now take a look at what type of customization we can do regarding the implementation of this mechanism. As it turns out, .NET offers us considerable latitude in customizing events, allowing us to exploit any advantages we may see in the decoupling of events from the storage and invocation of their subscribers. We'll create a second class that does a simple customization of an event/delegate implementation. Replicating the functionality of our initial example, we produce the code below:
class CustomEventObject
{
private MulticastDelegate m_MulticastDelegate = null;
public event DelegateFunction EventSource
{
add
{
System.Diagnostics.Debug.WriteLine("Adding Subscription");
m_MulticastDelegate = Delegate.Combine(m_MulticastDelegate,
value) as MulticastDelegate;
}
remove
{
System.Diagnostics.Debug.WriteLine("Removing Subscription");
m_MulticastDelegate = Delegate.Remove(m_MulticastDelegate,
value) as MulticastDelegate;
}
}
public CustomEventObject()
{
EventInfo EI = this.GetType().GetEvent("EventSource");
System.Diagnostics.Debug.WriteLine("Event Name: " + EI.Name);
EventSource += new DelegateFunction(EventSubscriberFunction);
if (m_MulticastDelegate != null)
{
System.Diagnostics.Debug.WriteLine("There are currently "
+ m_MulticastDelegate.GetInvocationList().Length + " subscribers");
}
else
{
System.Diagnostics.Debug.WriteLine("Currently no subscribers to the event");
}
MemberInfo[] Members = this.GetType().GetMembers(
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
foreach (MemberInfo MI in Members)
{
if (MI.Name.StartsWith("add_") || MI.Name.StartsWith("remove_"))
System.Diagnostics.Debug.WriteLine(MI.MemberType + ": " + MI.Name);
}
}
private void EventSubscriberFunction(int iValue)
{ System.Diagnostics.Debug.WriteLine("Subscriber received value of " + iValue); }
public void RaiseEventManually()
{
System.Diagnostics.Debug.WriteLine("Manual Invocation:");
if (m_MulticastDelegate != null)
{
foreach (Delegate D in m_MulticastDelegate.GetInvocationList())
D.Method.Invoke(D.Target, new object[] { 5 });
}
}
}
Running the program, we see the following output:
Event Name: EventSource
Adding Subscription
There are currently 1 subscribers
Event Property Method: add_EventSource
Event Property Method: remove_EventSource
Manual Invocation:
Subscriber received value of 5
Very similar to the output of the implementation .NET created for us. While we've customized our implementation, we'd still like it to behave externally like any other event. Let's go over the implementation and results of the program. The first thing that strikes us is the declaration of the MulticastDelegate
field m_MulticastDelegate
and the "manual" implementation of the event property "EventSource
". Event properties require the implementation of add and remove accessors. Somewhat similar to property get and set accessors. The compiler still generates "add_
" and "remove_
" functions, but now these invoke our custom add and remove properties. Both the add and remove accessors have an implicit "value
" parameter that contains the delegate instance that is to be added or removed. We are now however responsible for managing the storage of the delegates that subscribe to our event. Notice how our "RaiseEvent
" function has been removed. It will now produce a compile error if we attempt to invoke our subscribers using this method. There's a good reason for this. Our event has now been decoupled from the storage of its subscribers (aka Delegates). Previously .NET implemented the event and its associated subscriber storage and therefore knew how to access and invoke the subscriber delegates. With our new custom implementation however, .NET has no way of knowing where our subscribed delegates are located. We are not limited to storing subscribers in a MulticastDelegate
either. Consider the following alternate implementation:
private List<KeyValuePair<object, MethodInfo>> m_SubscribersList
= new List<KeyValuePair<object, MethodInfo>>();
public event DelegateFunction EventSource
{
add
{
System.Diagnostics.Debug.WriteLine("Adding Subscription");
m_SubscribersList.Add(
new KeyValuePair<object, MethodInfo>(((Delegate)value).Target,
((Delegate)value).Method));
}
remove
{
System.Diagnostics.Debug.WriteLine("Removing Subscription");
KeyValuePair<object, MethodInfo> Subscriber
= new KeyValuePair<object, MethodInfo>(((Delegate)value).Target,
((Delegate)value).Method);
if (m_SubscribersList.Contains(Subscriber))
m_SubscribersList.Remove(Subscriber);
}
}
Our subscribing delegates have been deconstructed and appears as a list of key/value pairs. The target being the key and the method being the value. Below, we have re-worked the invocation of our subscribed delegates in accordance with our new storage implementation:
public void RaiseEventManually()
{
System.Diagnostics.Debug.WriteLine("Manual Invocation:");
foreach (KeyValuePair<object, MethodInfo> Subscriber in m_SubscribersList)
{
Subscriber.Value.Invoke(Subscriber.Key, new object[] { 5 });
}
}
There is in effect no limitation on the type of storage than can be used to hold our subscribers. Given this, .NET is unable to provide us with an automated way in which to invoke the subscribed delegates of our custom implementations.
EventHandlerList - How .NET Controls Implement Events
A more elaborate implementation of the event/delegate mechanism can be seen in the events exposed by System.Windows.Forms.Control
and the objects that derive from it. Specifically, the type "control" has a non-public field (protected
access) named "Events
" that returns an EventHandlerList
. EventHandlerList
acts as a hash table of sorts in that it's used to store MulticastDelegates
keyed by object. There are a series non-public, static
fields on the type System.Windows.Forms.Controls
and many of its derived classes. These act as keys into this hash. Each type defines its own keys and as an instance receives subscribers to their corresponding events, they add the Delegate
objects to the EventHandlerList
storage residing in the base, "Control
". If we chose to leverage this storage for our own control derivations then, just like other types of custom event implementations, we are responsible for the management of the associated storage and invocation of the subscribers. Once we implement the add and remove properties on our event, we lose the convenience of the compiler generated event implementation that allows us to invoke subscribed delegates using the event object as a function. Both compiler generated and custom implementations can and do exist in the derivations of the type "control". We'll use the MonthCalendar
derivation of Control
as the basis of our example.
class CustomCalendar : MonthCalendar
{
public delegate void DayDivisibleByTwoDelegate(DateTime DT);
private static object DayDivisibleByTwoEventKey = new object();
public event DayDivisibleByTwoDelegate DayDivisibleByTwoEvent
{
add
{
this.Events.AddHandler(DayDivisibleByTwoEventKey, value);
}
remove
{
this.Events.RemoveHandler(DayDivisibleByTwoEventKey, value);
}
}
public CustomCalendar()
{
this.DateChanged += new DateRangeEventHandler(OnCustomCalendarDateChanged);
}
void OnCustomCalendarDateChanged(object sender, DateRangeEventArgs e)
{
DayDivisibleByTwoDelegate DivByTwoDelegate
= this.Events[DayDivisibleByTwoEventKey] as DayDivisibleByTwoDelegate;
if (DivByTwoDelegate != null && e.End.Day%2 == 0)
DivByTwoDelegate(DateTime.Now);
}
}
Below I've included excerpts of a smallish class, BasicControlEventExplorer
, that allows us to inspect the underpinnings of Control and its exposed events. I don't go into the specifics of wiring up the events. That's been covered in the included sample application for this article which contains the complete code. Briefly, let's go over the code. We define a function "ExtractEvents
" that is passed a Control and returns a list of key/value pairs. The key is an event name and the value will be the MulitcastDelegate
instance associated with that event.
public List<KeyValuePair<string, MulticastDelegate>> ExtractEvents(Control C)
The first thing we do in our function is obtain a reference to the non-public "Events
" field and the EventHandlerList
object it returns. This field serves as storage for the MulitcastDelegates
of Control
and its derived.
PropertyInfo EventsPI = typeof(Control).GetProperty("Events",
BindingFlags.Instance | BindingFlags.NonPublic);
EventHandlerList Handlers = EventsPI.GetValue(C, null) as EventHandlerList;
We then obtain all the non-public, static
fields for the passed control's type as well as all the types it inherits from.
List<FieldInfo> ControlFields
= new List<FieldInfo>(C.GetType().GetFields(BindingFlags.NonPublic
| BindingFlags.Static));
Type BaseType = C.GetType().BaseType; //Gather up keys that index into the
while (BaseType != null) //Control's EventHandlerList. Iterate
{ //over the passed object's bases
ControlFields.AddRange(BaseType.GetFields(
BindingFlags.NonPublic | BindingFlags.Static));
BaseType = BaseType.BaseType;
}
That list of fields is then iterated over and each field is tested as a key to the MulticastDelegate
's for a given event in the EventHandlerList
structure. For each object/key that returns a MulticastDelegate
in the EventHandlerList
we iterate over all the passed Control's public
events probing each to determine if it's associated with the active key's MulticastDelegate
. The function "locateAssociatedEvent
" does the actual probing. In the implementation of "locateAssociatedEvent
", the MulticastDelegate
object is leveraged to generate a new subscriber for an event that supports the same function signature. Each event is then checked to determine which event has had its subscribers incremented by one. The one that has is the associated event. The subscriber used to probe the event is then removed.
//Iterate over the keys we've assembled above.
//Add each we can determine a corresponding event for
foreach (FieldInfo FI in ControlFields)
{
object KeyValue = FI.GetValue(C);
EventInfo AssociatedEvent = locateAssociatedEvent(C, KeyValue, Handlers);
if (AssociatedEvent != null)
{
List<string> EventSubscribers = new List<string>();
Delegate[] Delegates
= (Handlers[KeyValue] as MulticastDelegate).GetInvocationList();
foreach (Delegate D in Delegates)
{
EventSubscribers.Add(D.Method.ReflectedType.ToString() + "."
+ D.Method.Name);
}
SubscriptionDetailsList.Add(
new KeyValuePair<string, MulticastDelegate>(
AssociatedEvent.Name, Handlers[KeyValue] as MulticastDelegate));
}
}
Below, I show the usage of the BasicControlEventExplorer
. First I create an instance of the CustomCalendar
class. I subscribe to a few of its events including the custom event "DayDivisibleByTwoEvent
". I then pass the CustomCalendar
instance to the "ExtractEvents
" function. The extracted events highlight an interesting nuance about the base, MonthCalendar
. The event "DateSelected
" doesn't appear among the extracted events. The writers of the MonthCalendar
class chose to depart from the Microsoft convention of adding derived control type events to the EventHandlerList
object in the base type "Control
". If we were to employ the same method, I used for enumerating hidden generated MulticastDelegate
fields we would find our subscribers there. In the event explorer contained in the sample application, I show all events of the MonthCalendar
by doing just that.
BasicControlEventExplorer BCEE = new BasicControlEventExplorer();
CustomCalendar CC = new CustomCalendar();
CC.GotFocus += new EventHandler(delegate(object sender, EventArgs e) { });
CC.DateSelected += new DateRangeEventHandler(
delegate(object sender, DateRangeEventArgs e) { });
CC.DayDivisibleByTwoEvent += new CustomCalendar.DayDivisibleByTwoDelegate(
delegate(DateTime DT) { });
List<KeyValuePair<string, MulticastDelegate>> SubscribersList
= BCEE.ExtractEvents(CC);
Summary
In the attached sample application, I've used what I've learned and put forward here about the default event/delegate implementation for objects and the EventHandlerList
mechanism used by Control to create a simple event explorer application. The event explorer allows us to visually browse an objects events and their subscribers. I won't go into the specifics of the code here, but I'll provide an overview of the functionality. There are two forms pictured in the screenshot for this article. The larger is the event browser. The smaller is the target form containing sample controls the event explorer will display event subscription information on. The combobox
on the event explorer allows you to select the active control. Right clicking on the root of the event tree or any of the event names will present an unsubscribe context menu. The child nodes under each event name display the subscribers to that event. The application builds on the examples covered here and is pretty straightforward. I'd like to mention one particularly important caveat about all this. The methods I use to iterate over and interact with the events and their associated delegates pierce encapsulation. They rely on the private details of the classes involved. They will only work for the specific implementations outlined. Simply put, when you depend on the implementation details of classes you don't have ownership of, you are at the mercy of the implementers of those classes. They are under no obligation to preserve the implementation you are relying on and they will not be sympathetic to your broken code if they change their methods. These examples are meant as learning exercises, to help understand the underpinnings of the event/delegate mechanisms present in .NET. I do not recommend the use of reflection to pierce encapsulation in production applications.
History
- 19th January, 2012: Initial version