Practical .NET2 and C#2: Event programming with C#

The main content for this post is taken from an article published by Patrick Smacchia (author of Practical .NET 2 and C# 2) which was posted here (on CodeProject.com).

Introduction

C# allows an object to offer events. The concept of event encompasses:

  • Subscribers to the event. These are warned each time the event is triggered. They have the possibility of subscribing or unsubscribing themselves dynamically (i.e. during the execution of the program). In C#, a subscriber is represented by a method.
  • The event itself, which can be triggered at any time by the object. Internally, the event is aware of its subscribers. The responsibility of the event is to notify its subscribers when it is triggered. The event has the possibility of supplying information to its subscribers when it is triggered, using the argument object to the event. In C#, events are similar to a delegate field which is a member of a class with the exception that it is declared using the event keyword.

The concept of event/subscriber is not new and is well known in Java. The same behavior can be obtained using the Observer design pattern (Gof).

Events are very useful in an application with a graphical interface (Windows Forms and ASP.NET). In fact, each possible action on an interface (a button click, text typing, combo box selection…) triggers the call of the adequate method. During a mouse click, the argument of the event can, for example, be the position of the mouse on the screen. In the .NET platform, inheritance combined with events is at the base of all graphical controls.

The C# syntax

Let’s suppose that we have created an event in a class called EventName. The names of the entities which are concerned by the event are functions of the EventName name. An event argument is an object of a class derived from System.EventArgs. You thus have the possibility of creating your own event arguments by creating your own classes derived from System.EventArgs.

To improve code readability, it is recommended that the argument class for the EventName be called EventNameEventArgs.

Subscribers to the event are materialized using methods (static or not). The action of notifying subscribers translates itself by a call to each of these methods. These methods generally have the following signature:

  • A return type of void.
  • The first argument is of type object. When the method is called, this argument references the object which contains the event.
  • The second argument is of type System.EventArgs. When a method is called, this argument references the argument to the event.

An event is a member of a class, declared using the event keyword. An event is an instance of the EventNameEventHandler delegate class. An event has the possibility of being static or not. Visibility levels apply themselves to events. Since an event is a member of a class, the declaration can be preceded with one of the following keywords:

 new        public      protected   internal   private   static 
virtual    sealed      override    abstract    extern

An event contains:

  • A delegate object which contains the references to the subscribed methods. The delegate has the same signature as the subscribed methods and must be called EventNameEventHandler.
  • An add accessor which is invoked when a method subscribes to the event.
  • A remove accessor which is invoked when a method unsubscribes to the event.

These three entities are automatically defined by the C# compiler as soon as the event is declared. You can however define your own bodies for the accessors. If you change the body of an accessor, you must also code the body of the other. In the body of an accessor, the value keyword designates the delegate to add or remove. For example, the following two definitions of an event are acceptable:

 // without accessor
public event ClickButtonEventHandler ClickButton;

// with accessors
private event ClickButtonEventHandler m_ClickButton;
public event ClickButtonEventHandler ClickButton{
   add   { m_ClickButton += value; }
   remove   { m_ClickButton -= value; }
}

Note that it is rare to have to modify the add and remove accessors. However, this feature can be useful in order to restrict the visibility level of an event. Understand that the possibility of using polymorphism on an event (i.e. the fact of being able to use the virtual, new, override, and abstract keywords in the declaration of an event) only applies itself to the add and remove accessors.

In C#, the event is triggered by a call to its delegate. This is not the case in other .NET languages. For example, VB.NET triggers an event through the use of the RaiseEvent keyword which does not exist in C#. The compiler imposes that the call to the delegate must be done within the class that declares the event. Also, the class has the possibility of presenting a public method named OnEventName() which allows the event to be triggered from outside the class.

 ...
public void OnClickButton(System.EventArgs Arg){
   if(ClickButton != null ) 
      // Trigger the event.
      ClickButton(this,Arg); 
}
...

A practical example

Here is an example where an object instance of the NewsBoard class publishes press releases throughout the USA and the world. The publication is done through the WorldNews and USANews events. The argument of a press release event (an instance of the InfoEventArgs class) contains the description of the press release. Finally, the instances of the Subscriber class have the possibility of receiving press releases by subscribing to their OnReceivingInfo() methods.

Example1.cs

 using System;

class Subscriber{
     private string m_Name;
     public Subscriber( string name ) { m_Name = name; }
     // Method to call when an event is triggered
     // (i.e when a news report is published).
     public void OnReceivingInfo(object sender, EventArgs e){
        NewsEventArgs info = e as NewsEventArgs;
        if( info != null ){
           Console.WriteLine( m_Name + " receives the news: " +   
                            ((NewsEventArgs)e).GetBulletinInfo() );
     }
   }
}

// An instance of this class represents a news report.
class NewsEventArgs: EventArgs{
     private string m_Report;
     public string GetBulletinInfo() { return m_Report; }
     public NewsEventArgs (string report) { m_Report = report ; }
}

// The delegate class. Its instances reference subscriber methods.
public delegate void NewsEventHandler(object sender, EventArgs e);

// The class which publishes news reports by triggering events.
class NewsBoard  {
   // Events used to publish news.
   public event NewsEventHandler USANews;
   public event NewsEventHandler WorldNews; 
   // Methods used to trigger news publishing from outside NewsBoard.
   public void OnUSANews(NewsEventArgs newsReport) {
      if( USANews != null )
         USANews( this , newsReport );
   }
   public void OnWorldNews(NewsEventArgs newsReport) {
      if( WorldNews != null )     
         WorldNews( this , newsReport );
   }
}

class Program {
   public static void Main(){
      // Create the news board.
      NewsBoard  newsBoard = new NewsBoard  ();
      // Create subscribers.
      Subscriber bob = new Subscriber("Bob");
      Subscriber joe = new Subscriber("Joe");
      Subscriber max = new Subscriber("Max");
      // Link subscribers and the news board.
      newsBoard.USANews += bob.OnReceivingInfo;
      newsBoard.USANews += joe.OnReceivingInfo;
      newsBoard.WorldNews  += joe.OnReceivingInfo;
      newsBoard.WorldNews  += max.OnReceivingInfo;
      // Publish news reports by triggering events.
      newsBoard.OnUSANews(new NewsEventArgs("Oil price increases."));
      newsBoard.OnWorldNews(new NewsEventArgs("New election in France."));
      // Unsubscribe Joe.
      newsBoard.USANews -= new NewsEventHandler(joe.OnReceivingInfo );
      // Publish another news report.
      newsBoard.OnUSANews(new NewsEventArgs("Hurricane alert."));
   }
}

The program displays:

 Bob receives the news: Oil price increases.
Joe receives the news: Oil price increases.
Joe receives the news: New election in France.
Max receives the news: New election in France.
Bob receives the news: Hurricane alert.

In this example, nothing changes if you remove the two occurrences of the event keyword (i.e. nothing changes if we use two delegate fields instead of two events). There are two reasons for this. The first one is that we do not use the event accessors. The second is that we do not trigger this event from outside the NewsBoard class. In fact, one of the advantages of the event keyword is to prevent the event from being triggered from outside its class. The triggering of an event is also forbidden from classes deriving from the class which defines the event, even if the event is virtual. This means that the C# compiler will generate an error if we wrote the following in the body of the Main() method in the previous example:

Example2.cs

 ...

public static void Main(){
   ...
   newsBoard.USANews(newsBoard, 
     new NewsEventArgs("Oil price increases."));
   ...
}

In C#, the notion of event is relatively close to the one of a delegate. The mechanism mostly allows supplying a standard framework for the management of events. Note that the VB.NET language presents several dedicated keywords for the management of events (RaiseEvent, WithEvents…).

Asynchronous event handling

When an event is triggered, you must be aware that the methods which are subscribed to the event are executed by the thread which triggers the event.

A potential problem is that certain subscribed methods may take too long to execute. Another problem is that a subscribed method may raise an exception. This exception will prevent other subscribers which have not yet been notified from being executed and will go back to the method which triggered the event. It is clear that this poses a problem as the method which triggers an event should be decoupled from the methods subscribed to the event.

To deal with these problems, it would be practical that the subscribed method be executed in an asynchronous way. Also, it would be interesting that different threads execute the subscribed methods. Hence, a subscribed method which takes a long time to execute, or that raises an exception, does not affect the method which has triggered the event nor the execution of the other subscribed methods. The thread pool of the CLR is specially adapted to these two constraints.

Those who understand the syntax of asynchronous calls may be tempted to directly use the BeginInvoke() function on the event itself. The problem with this solution is that only a single subscribed method will be executed. The proper solution, illustrated in the following example which is based on our first example, requires the explicit traversal of the subscriber method list:

Example3.cs

 ...

   // Methods used to trigger news publishing from outside NewsBoard. 
   public void OnUSANews(NewsEventArgs newsReport){
      if( USANews != null ){
         Delegate[] subscribers = USANews.GetInvocationList(); 
         foreach(Delegate _subscriber in subscribers ){
            NewsEventHandler subscriber = (NewsEventHandler) _subscriber;
            subscriber.BeginInvoke(this, newsReport, null, null);
         }
         // Wait a bit to allow asynchronous threads to execute
         // their tasks. Indeed, they are thread pool threads and thus,
         // they are background threads. If we don’t wait,
         // threadpool threads wouldn’t have the time to begin
         // their tasks.
         System.Threading.Thread.Sleep(100);
      }
   }
   ...

Finally, take note that there is no need to call the EndInvoke() method since the subscribed methods are not supposed to return any results to the method which triggered the event.

Protecting your code from exceptions raised by subscriber methods in a synchronous scenario

Invoking the methods subscribed to an event in an asynchronous way is an efficient technique to protect the code which triggered the event from exceptions raised by the subscribing methods. It is, however, possible to have the same level of protection when invoking subscribed methods in a synchronous way. For this, you have to invoke them one by one as shown in the following example:

Example4.cs

    ...

   // Methods used to trigger news publishing from outside NewsBoard.
   public void OnUSANews(NewsEventArgs newsReport){
      if( USANews != null ){
         Delegate[] subscribers = USANews.GetInvocationList();
         foreach(Delegate _subscriber in subscribers ){
            NewsEventHandler subscriber = (NewsEventHandler) _subscriber;
            try{
               subscriber(this,newsReport);
            }
            catch(Exception){ /* treat the exception */ }
         }
      }
   }
   ...