C Sharp

Defining Events with Delegates

Almost all applications for Windows have some sort of asynchronous event processing needs. Some of these events are generic, such as Windows sending messages to the application message queue when the user has interacted with the application in some fashion. Some are more problem domain"specific, such as printing the invoice for an order being updated.

Events in C# follow the publish-subscribe design pattern in which a class publishes an event that it can "raise" and any number of classes can then subscribe to that event. Once the event is raised, the runtime takes care of notifying each subscriber that the event has occurred. The method called as a result of an event being raised is defined by a delegate. However, keep in mind some strict rules concerning a delegate that's used in this fashion. First, the delegate must be defined as taking two arguments. Second, these arguments always represent two objects: the object that raised the event (the publisher) and an event information object. Additionally, this second object must be derived from the .NET Framework's EventArgs class.

Let's say we wanted to monitor changes to inventory levels. We could create a class called InventoryManager that would always be used to update inventory. This InventoryManager class would publish an event that would be raised any time inventory is changed via such actions as the receipt of inventory, sales, and physical inventory updates. Then, any class needing to be kept updated would subscribe to the event. Here's how this would be coded in C# by using delegates and events: -

using System;
class InventoryChangeEventArgs : EventArgs
{
    public InventoryChangeEventArgs(string sku, int change)
    {
        this.sku = sku;
        this.change = change;
    }
    string sku;
    public string Sku
    {
        get
        {
            return sku;
        }
    }
    int change;
    public int Change
    {
        get
        {
            return change;
        }
    }
}
class InventoryManager // Publisher.
{
    public delegate void InventoryChangeEventHandler
        (object source, InventoryChangeEventArgs e);
    public event InventoryChangeEventHandler OnInventoryChangeHandler;
    public void UpdateInventory(string sku, int change)
    {
        if (0 == change)
            return; // No update on null change.
        // Code to update database would go here.
        InventoryChangeEventArgs e = new
            InventoryChangeEventArgs(sku, change);
        if (OnInventoryChangeHandler != null)
            OnInventoryChangeHandler(this, e);
    }
}
class InventoryWatcher // Subscriber.
{
    public InventoryWatcher(InventoryManager inventoryManager)
    {
        this.inventoryManager = inventoryManager;
        inventoryManager.OnInventoryChangeHandler += new
InventoryManager.InventoryChangeEventHandler(OnInventoryChange);
    }
    void OnInventoryChange(object source, InventoryChangeEventArgs e)
    {
        int change = e.Change;
        Console.WriteLine("Part '{0}' was {1} by {2} units",
            e.Sku,
            change > 0 ? "increased" : "decreased",
            Math.Abs(e.Change));
    }
    InventoryManager inventoryManager;
}
class Events1App
{
    public static void Main()
    {
        InventoryManager inventoryManager =
            new InventoryManager();
        InventoryWatcher inventoryWatch =
            new InventoryWatcher(inventoryManager);
        inventoryManager.UpdateInventory("111 006 116", -2);
        inventoryManager.UpdateInventory("111 005 383", 5);
    }
}

Let's look at the first two members of the InventoryManager class: -

    public delegate void InventoryChangeEventHandler
                               (object source, InventoryChangeEventArgs e);
    public event InventoryChangeEventHandler OnInventoryChangeHandler;

The first line of code is a delegate, which by now you know is a definition for a method signature. As mentioned earlier, all delegates that are used in events must be defined as taking two arguments: a publisher object(in this case, source) and an event information object(an EventArgs-derived object). The second line uses the event keyword, a member type with which you specify the delegate and the method (or methods) that will be called when the event is raised.

The last method in the InventoryManager class is the UpdateInventory method, which is called anytime inventory is changed. As you can see, this method creates an object of type InventoryChangeEventArgs. This object is passed to all subscribers and is used to describe the event that took place.

Now look at the next two lines of code: -

        if (OnInventoryChangeHandler != null)
            OnInventoryChangeHandler(this, e);

The conditional if statement checks to see whether the event has any subscribers associated with the OnInventoryChangeHandler method. If it does-in other words, OnInventoryChangeHandler is not null-the event is raised. That's really all there is on the publisher side of things. Now let's look at the subscriber code.

The subscriber in this case is the class called InventoryWatcher. All it needs to do is perform two simple tasks. First, it adds itself as a subscriber by instantiating a new delegate of type InventoryManager.InventoryChangeEventHandler and adding that delegate to the InventoryManager.OnInventoryChangeHandler event. Pay special attention to the syntax used-it's using the += compound assignment operator to add itself to the list of subscribers so as not to erase any previous subscribers.

inventoryManager.OnInventoryChangeHandler
+= new InventoryManager.InventoryChangeEventHandler(OnInventoryChange);

The only argument that needs to be supplied here is the name of the method that will be called if and when the event is raised.

The only other task the subscriber needs to do is implement its event handler. In this case, the event handler is InventoryWatcher.OnInventoryChange, which prints a message stating the part number and the change in inventory.

Finally, the code that runs this application instantiates InventoryManager and InventoryWatcher classes and, every time the InventoryManager.UpdateInventory method is called, an event is automatically raised that causes the InventoryWatcher.OnInventoryChanged method to be called.

Summary

Delegates in C# are type-safe, secure managed objects that serve the same purpose as function pointers in C++. Delegates are different from classes and interfaces in that rather than being defined at compile time, they refer to single methods and are defined at run time. Delegates are commonly used to perform asynchronous processing and to inject custom code into a class's code path. Delegates can be used for a number of general purposes, including using them as callback methods, defining static methods, and using them to define events.