From the August 2001 issue of MSDN Magazine

MSDN Magazine

Implementation of Events with Delegates
Jeffrey Richter
Download the code for this article: Net0108.exe (36KB)
I

n my previous two columns, I looked at delegates and how they are designed and used in Microsoft® .NET Framework applications. In this column, I'll introduce you to what is probably the most common use of delegates: events.
      Events allow an object to notify other objects that something special has happened. For example, when a button is clicked, several objects in the application may want to receive a notification and perform some action. Events are type members that allow for this interaction. Specifically, defining an event member means that a type is offering the following things:

  • The ability for objects to register their interest in the event.
  • The ability for objects to unregister their interest in the event.
  • The ability for the object owning the event to maintain the set of registered objects and to notify these objects when something special happens.

      To fully understand events, let's start by defining a scenario where events are useful. Suppose you want to design an e-mail application. When an e-mail message arrives, the user might like the message to be forwarded to a fax machine or a pager. In architecting this application, I'd first design a type, called MailManager, which receives the incoming e-mail messages. The MailManager will expose an event called MailMsg. Other types (such as Fax and Pager) may register interest in this event. Then, when the MailManager receives a new e-mail message, it will fire the event, distributing the message to each of the registered objects. Each object may process the message any way it desires.
      When the application initializes, I'll instantiate just one instance of the MailManager, but I can instantiate any number of Fax and Pager types that the user wants. Figure 1 shows how the application initializes and what happens when a new e-mail message arrives.

Figure 1 The MailManager
Figure 1 The MailManager

      Let me explain how the system works. My application initializes by constructing an instance of the MailManager. The MailManager offers a MailMsg event. When the Fax and Pager objects are constructed, they register themselves with MailManager's MailMsg event so that the MailManager knows to notify the Fax and Pager objects when new e-mail messages arrive. Now, when the MailManager receives a new e-mail message, it will fire the MailMsg event, giving all the registered objects an opportunity to process the new message any way they want.

Designing a Type that Exposes an Event

      So now that you know what I want to build, let's look at the type definition for the MailManager. The code in Figure 2 demonstrates the recommended design pattern that should be used to expose events. All of the work related to implementing this architecture is the burden of the developer defining the MailManager type. The developer must do the following five things.
      First, define a type which will hold any additional information that should be sent to receivers of the event notification. By convention, types that hold event information are derived from System.EventArgs and the name of the type should end with "EventArgs". In this example, the MailMsgEventArgs type has fields identifying who sent the message (from), who is receiving the message (to), the subject of the message (subject), and the message text itself (body).
      The EventArgs type is inherited from Object and looks like this:

  [Serializable]
  
  public class EventArgs {
  
     public static readonly EventArgs Empty = new EventArgs();
  
     public EventArgs() {  }
  
  }
  

 

As you can see, this type is nothing to write home about. It simply serves as a base type from which other types may derive. Many events don't have any additional information to pass on. For example, when a button notifies its registered receivers that the button has been clicked, just invoking the callback method is enough information. When you are defining an event that doesn't have any additional data to pass on, just use EventArgs.Empty.
      Second, define a delegate type specifying the prototype of the method that will be called when the event fires. By convention, the name of the delegate should end with "EventHandler". It is also by convention that the prototype have a void return value and take two parameters (although some event handlers, like ResolveEventHandler, violate this convention). The first parameter is an Object that refers to the object sending the notification, and the second parameter is an EventArgs-derived type containing any additional information that receivers of the notification require.
      If you're defining an event that has no additional information that you want to pass to receivers of the event, then you do not have to define a new delegate. You can use the System.EventHandler delegate and pass EventArgs.Empty for the second parameter. The prototype of EventHandler is as follows:

  public delegate void EventHandler(Object sender, EventArgs e); 
  

 

      Third, define an event. In this example, MailMsg is the name of the event. This event is of the MailMsgEventHandler type, meaning that all receivers of the event notification must supply a callback method whose prototype matches that of the MailMsgEventHandler delegate.
      Fourth, define a protected, virtual method responsible for notifying registered objects of the event. The OnMailMsg method is called when a new e-mail message arrives. This method receives an initialized MailMsgEventArgs object containing additional information about the event. This method should first check to see if any objects have registered interest in the event and if so, the event should be fired.
      A type that uses MailManager as a base type is free to override the OnMailMsg method. This gives the derived type control over the firing of the event. The derived type can handle the new mail message in any way it sees fit. Usually, a derived type will call the base type's OnMailMsg method so that the registered object will receive the notification. However, the derived type may decide not to have the event forwarded on.
      Finally, define a method that translates the input into the desired event. Your type must have some method that takes some input and translates this input into the firing of an event. In this example, the SimulateArrivingMsg method is called to indicate that a new e-mail message has arrived into the MailManager. SimulateArrivingMsg accepts information about the message and constructs a new MailMsgEventArgs object, passing the message information to its constructor. Then, MailManager's own virtual OnMailMsg method is called to formally notify the MailManager object of the new mail message. Normally, this will cause the event to be fired, notifying all the registered objects. However, a type using MailManager as a base type can override this behavior.
      Let's now take a closer look at what it really means to define the MailMsg event. When the compiler examines the source code, it comes across the line that defines the event:

  public event MailMsgEventHandler MailMsg; 
  

 

      The C# compiler translates this single line of source code into three constructs, as shown in Figure 3. The first construct is simply a field that is defined in the type. This field is a reference to the head of a linked list of delegates that want to be notified of this event. This field is initialized to null, meaning that no listeners have registered interest in the event. When an object registers interest in the event, this field will refer to an instance of the MailMsgEventHandler delegate. Each MailMsgEventHandler delegate instance has a pointer to yet another MailMsgEventHandler delegate or null to mark the end of the linked list. When a listener registers interest in an event, the listener is simply adding an instance of the delegate type to the linked list. Obviously, unregistering means removing the delegate from the linked list.
      You'll notice that event fields (MailMsg in this example) are always private even though the original line of source code defines the event as public. The reason for making the field private is to prevent code outside the defining type from manipulating the field improperly. For example, only the MailManager knows when a new e-mail message arrives and so only it should be able to fire the event. If the field were public, then any code would be able to fire the event at any time even if a mail message hasn't yet arrived.
      The second construct generated by the C# compiler is a method that allows other objects to register their interest in the event. The C# compiler automatically names this function by prepending "add_" to the field's name (MailMsg). The C# compiler automatically generates the code that is inside this method. The code always calls System.Delegate's static Combine method, which adds the instance of a delegate to the linked list of delegates and returns the new head of the linked list.
      The third and final construct generated by the C# compiler is a method that allows an object to unregister its interest in the event. Again, the C# compiler automatically names this function by prepending "remove_" to the field's name (MailMsg). The code inside this method always calls Delegate's static Remove method, which removes the instance of a delegate from the linked list of delegates and returns the new head of the linked list.
      Notice that both the add and remove methods have a MethodImplAttribute attribute applied to them. More specifically, these methods are marked as synchronized, making them thread-safe: multiple listeners may register or unregister themselves with the event at the same time, but the linked list will not become corrupted.
      In my example, the add and remove methods are public. The reason why they are public is that the original line of source code declared the event to be public. If the event had been declared protected, then the add and remove methods generated by the compiler would also have been declared protected. So, when you define an event in a type, the accessibility of the event determines what code can register and unregister interest in the event; however, only the type itself can ever fire the event.
      In addition to emitting the three constructs shown earlier, compilers also emit an event definition entry into the managed module's metadata. This entry contains some flags, the underlying delegate type, and refers to the add and remove accessor methods. This information exists simply to draw an association between the abstract concept of an "event" and its accessor methods. Compilers and other tools may use this metadata, and certainly this information may be obtained by using the System.Reflection.EventInfo class. However, the common language runtime (CLR) itself does not use this metadata information and requires only the accessor methods at runtime.

Designing a Type that Listens for an Event

      The hard work is definitely behind you now. In this section, I'll take a look at how to define a type that uses an event provided by another type. I'll start off by examining the code for the Fax type, as shown in Figure 4.
      When the e-mail application initializes, it would first construct a MailManager object and save the reference to this object in a variable. Then, the application would construct a Fax object, passing the reference to the MailManager object as a parameter. In the Fax constructor, a new MailManager.MailMsgEventHandler delegate object is constructed and the reference to it is saved in the callback variable. This new delegate object is a wrapper around the Fax type's FaxMsg method. You'll notice that the FaxMsg method returns void and takes the same two parameters as defined by the MailManager's MailMsgEventHandler delegate. This is required or the code would not compile.
      After constructing the delegate, the Fax object registers its interest in MailManager's MailMsg event using this line:

  mm.MailMsg += callback
  

 

Because the C# compiler has built-in support for events, the compiler translates the use of the += operator into the following line of code to add the object's interest in the event:

  mm.add_MailMsg(callback};
  

 

      If you are using a programming language that doesn't directly support events, then you may still register a delegate with the event by calling the add accessor method explicitly. The effect is exactly the same; it's just that the source code doesn't look as pretty. It is the add method that registers the delegate with the event by adding it to the event's linked list of delegates.
      When the MailManager fires the event, the Fax object's FaxMsg method gets called. The method is passed a reference to the MailManager object. Most of the time, this parameter is ignored, but it may be used if the Fax object wants to access fields or methods of the MailManager object in response to the event notification. The second parameter is a reference to a MailMsgEventArgs object. This object contains any additional information that the MailManager thought would be useful to event receivers.
      From the MailMsgEventArgs object, the FaxMsg method has easy access to the following information pertaining to the message: the sender, the message recipient, the subject, and the text. In a real Fax object, this information would be faxed to somewhere. In this example code, the information is simply displayed in the console window.
      While it is uncommon, an object can unregister its interest with another object's event. Code that demonstrates how to unregister for an event is shown in Fax's Unregister method (see Figure 4). This method is practically identical to the code shown in the Fax type's constructor. The only difference is that this code uses -= instead of +=. When the C# compiler sees code using the -= operator to unregister a delegate with an event, the compiler actually emits a call to the event's remove method:

  mm.remove_MailMsg(callback};
  

 

      If you are using a programming language that doesn't directly support events, then you may still unregister a delegate with the event by calling the remove accessor method explicitly. The remove method unregisters the delegate from the event by scanning the linked list for a delegate that wraps the same method as the callback passed in. If a match is found, the existing delegate is removed from the event's linked list of delegates. If a match isn't found, then no error occurs and the linked list is unaltered.
      By the way, C# requires that your code use the += and -= operators to add and remove delegates from the linked list. If you try to call the add or remove methods explicitly, then the C# compiler produces a "cannot explicitly call operator or accessor" error.
      The MailManager sample application shows all of the source code for the MailManager type, the Fax type, and the Pager type. You'll notice that the Pager type is implemented similarly to the Fax type.

Send questions and comments for Jeff to dot-net@microsoft.com.

Jeffrey Richter is the author of Programming Applications for Microsoft Windows (Microsoft Press, 1999), and is a cofounder of Wintellect (http://www.Wintellect.com), a software education, debugging and consulting firm. He specializes in programming/design for .NET and Win32. Jeff is currently writing a Microsoft .NET Framework programming book and offers .NET seminars.