Click here to Skip to main content
15,867,568 members
Articles / Programming Languages / C#

Event Browsing - .NET Event Concepts and Customizations

Rate me:
Please Sign up or sign in to vote.
4.86/5 (6 votes)
21 Jan 2012CPOL10 min read 18.2K   236   15  
.NET Events concepts and customizations
Image 1

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.

C#
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.

C#
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.

C#
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:

C#
EventSource += new DelegateFunction(EventSubscriberFunction); 

with the line:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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.

C#
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.

C#
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.

C#
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.

C#
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.

C#
//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.

C#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
President Phoenix Roberts LLC
United States United States
We learn a subject by doing but we gain a real understanding of it when we teach others about it.

Comments and Discussions

 
-- There are no messages in this forum --