Delegates, Events and Lambda Expressions in C# – Part 4

Events
Events are a system generated notification indicating that some occurrence has happened within the application, such as mouse click or a key press. They are used to notify the application of the event so the application can respond accordingly. Events are wrappers around delegates and they rely on the delegate to route the event arguments to the handlers attached to the invocation list. The class that contains the event can raise the event and it is called the publisher of the event. Classes that contains methods that are added as handlers to the delegate contained in the event are the subscribers to the event.

Custom Events
The type of an event declared within a class must a be a predefined delegate type. The delegate type can be that of a custom defined delegate defined within the application, or of a system delegate such as Action or Func. To define an event, the ‘event’ keyword is typically used. As a convention, the keyword ‘Handler’ is always prepended to the name of the delegate. The name of the event typically matches the name of the delegate without the ‘Handler’ keyword. Unlike delegates, events must be defined within a class and cannot be defined directly within the namespace.

Example:
In the example below, a TaskProcessor class is defined and it’s main purpose is to execute and process tasks. The TaskProcessor class exposes a ProcessTask() method that takes in the name of the task and the number of times to execute the task. The ProcessTask() method uses a simple for-loop to iterate over the ‘count’ parameter. For simplicity, the details of the tasks execution is omitted:

public class TaskProcessor
{
     public void ProcessTask(string taskName, int count)
     {
          for (int i = 0; i < count; i++)
          {
               // Perform the task here
          }
     }
}

In order to notify all the subscribers of the TaskProcessor class that a task is about to be executed at each iteration, one must raise an event indicating that a task just started processing. In order to define such an event, the delegate TaskProcessingHandler is first defined and it takes in the task name as a parameter. Next, the event is defined using the 'event' keyword within the body of the TaskProcessor class. Finally, the event must be raised within the for loop right before processing the task.

// Declaring the delegate
public event TaskProcessingHandler TaskProcessing;

public class TaskProcessor
{
     // Declaring the event here
     public event TaskProcessingHandler TaskProcessing;

     public void ProcessTask(string taskName, int count)
     {
          for (int i = 0; i < count; i++)
          {
               // Raise event here
               // Perform the task here
          }
     }
}

Raising Events
Events are raised the same way a delegate is invoked. This is expected since events are wrappers around delegates. So in order to raise an event, it is invoked like a method. All the parameters required by the event must be supplied. However, before raising an event, one should check to make sure there are subscribers in the event's invocation list. Otherwise, the value of the event will be null resulting in a null reference exception. While this logic can be easily added inline where it is needed, the convention is to wrap this logic into a separate function dedicate solely for raising the event. Although the this function can have any name, it is conventionally named the same as the event, but it is prefixed with the keyword 'On'. In the example above, the raising of the event looks like the following:

// Declaring the delegate
public event TaskProcessingHandler TaskProcessing;

public class TaskProcessor
{
     // Declaring the event here
     public event TaskProcessingHandler TaskProcessing;

     public void ProcessTask(string taskName, int count)
     {
          for (int i = 0; i < count; i++)
          {
               OnTaskProcessing(taskName);
               // Perform the task here
          }
     }

     protected virtual void OnTaskProcessing(string taskName)
     {
          if (TaskProcessing != null)
          {
               TaskProcessing(taskName);
          }
     }
}

Note that it is a good idea to declare the OnTaskProcessing() method above as a protected virtual method. This allows any child class to override the default behavior of the method if desired. Otherwise, the method can be declared as a private method.

Built-In EventHandler Delegate
The C# .NET FCL library comes with a built-in generic delegate that can be used with any event. Using such delegate saves one the trouble of declaring a custom delegate for event in the class if no custom delegate is needed. This delegate is called the 'EventHandler' delegate. Below is the declaration of the EventHandler delegate in the 'System' namespace:

namespace System
{
    [Serializable]
    [ComVisible(true)]
    public delegate void EventHandler(object sender, EventArgs e);
}

From the declaration of the delegate above, the EventHandler delegate takes in a 'sender' object, which is a reference to the class raising the event. It also takes an EventArgs object, which is an object that contains all the parameters to be passed in to the handler.

Example
To use the EventHandler delegate in the example above, the new event TaskCompleted is declared. The event does not take any parameters. Since the event is of type EventHandler, there is no need to declared a custom delegate explicitly. A new handler method called OnTaskCompleted() is declared to raise the event. The event takes in the current class 'this' as the sender and EventArgs.Empty as the event arguments object:

public delegate void TaskProcessingHandler(string taskName);

public class TaskProcessor
{
     public event TaskProcessingHandler TaskProcessing;
     // Declaring the TaskCompleted event of type EventHandler
     public event EventHandler TaskCompleted;

     public void ProcessTask(string taskName, int count)
     {
          for (int i = 0; i < count; i++)
          {
               OnTaskProcessing(taskName);
          }
          
          // Calling the handler function for the TaskCompleted event
          OnTaskCompleted();
     }

     protected virtual void OnTaskProcessing(string taskName)
     {
          if (TaskProcessing != null)
          {
               TaskProcessing(taskName);
          }
     }

     protected virtual void OnTaskCompleted()
     {
          if (TaskCompleted != null)
          {
               // Raising the event with an empty EventArgs object
               TaskCompleted(this, EventArgs.Empty);
          }
     }
}

Custom EventArgs
As seen in the previous example, the built-in EventHandler delegate is useful and can be used in the place of custom declared delegates. However, in order to exploit the real power of the built-in EventHandler delegate, one will need the ability to pass in any number of parameters. Since the EventHandler delegate expect a parameter of type EventArgs, a new custom class is declared in order to hold the parameters that are to be passed in to the delegate. The new class must extend the built in EventArgs class.

In the example above, in order to rewrite the TaskProcessing event and make it of type EventHandler, one must first declare a custom TaskEventArgs class as the following:

public class TaskEventArgs : EventArgs
{
     public TaskEventArgs(string taskName)
     {
          TaskName = taskName;
     }

     public string TaskName { get; set; }
}

Once the new custom class is declared, the custom delegate TaskProcessingHandler can be dropped and the code will look like the following:

public class TaskProcessor
{
     // The TaskProcessing event is declared with the type EventHandler
     public event EventHandler TaskProcessing;
     public event EventHandler TaskCompleted;

     public void ProcessTask(string taskName, int count)
     {
          for (int i = 0; i < count; i++)
          {
               OnTaskProcessing(taskName);
          }

          OnTaskCompleted();
     }

     protected virtual void OnTaskProcessing(string taskName)
     {
          if (TaskProcessing != null)
          {
               // A new instance of the class TaskEventArgs is created with the taskName string
               TaskProcessing(this, new TaskEventArgs(taskName));
          }
     }

     protected virtual void OnTaskCompleted()
     {
          if (TaskCompleted != null)
          {
               TaskCompleted(this, EventArgs.Empty);
          }
     }
}

EventHandler
One alternative approach is to use the EventHandler built-in generic delegate. Since the delegate takes in any type of parameters, it can continue to take in an instance of the TaksEventArgs class. However, the TaskProcessing event in the example above expects a single parameter. So the code can be simplified by declaring the TaksProcessing event with the type EventHandler. The TaskEventArgs class can be dropped altogether. The code will look as follows:

public class TaskProcessor
{
     // Declare the TaskProcessing event with the type EventHandler<string>
     public event EventHandler<string> TaskProcessing;
     public event EventHandler TaskCompleted;

     public void ProcessTask(string taskName, int count)
     {
          for (int i = 0; i < count; i++)
          {
               OnTaskProcessing(taskName);
          }

          OnTaskCompleted();
     }

     protected virtual void OnTaskProcessing(string taskName)
     {
          if (TaskProcessing != null)
          {
               // Raise the event and pass the task name without the need of a custom EventArgs object
               TaskProcessing(this, taskName);
          }
     }

     protected virtual void OnTaskCompleted()
     {
          if (TaskCompleted != null)
          {
               TaskCompleted(this, EventArgs.Empty);
          }
     }
}  

Overriding Default Delegate Definition
The C# language offers one degree of flexibility by allowing the user to modify how the handlers are added and removed from the invocation list. This is achieved by exposing public Add and Remove accessors. It is also possible to override whether handlers are invoked in a synchronous fashion of not. This is something that is almost never used. However, it is always worth mentioning so that one is aware that it is available in the platform.

Example:

private EventHandler _taskCompleted;

public event EventHandler TaskCompleted
{
     [MethodImpl(MethodImplOptions.Synchronized)]
     add
     {
          _taskCompleted = (EventHandler)Delegate.Combine(_taskCompleted, value);
     }

     [MethodImpl(MethodImplOptions.Synchronized)]
     remove
     {
          _taskCompleted = (EventHandler)Delegate.Remove(_taskCompleted, value);
     }
}

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *