Delegates and Events in C#

In this second part of a ten-part series that focuses intensely on C#, we will wrap up our discussion of delegates, then move on to one of the most important subjects in the language: events. This article is excerpted from chapter four of C# 3.0 in a Nutshell, Third Edition, A Desktop Quick Reference, written by Joseph Albahari and Ben Albahari (O’Reilly; ISBN: 0596527578). Copyright © 2007 O’Reilly Media, Inc. All rights reserved. Used with permission from the publisher. Available from booksellers or direct from O’Reilly Media.

Delegate Compatibility

Type compatibility

Delegate types are all incompatible with each other, even if their signatures are the same:

  delegate void D1();
  delegate void D2();
  …

  D1 d1 = Method1;
  D2 d2 = d1;                       // compile-time error

Delegate instances are considered equal if they have the same method targets:

  delegate void D();
  …

  D d1 = Method1;
  D d2 = Method1;
  Console.WriteLine (d1 == d2);    // true

Parameter compatibility

When you call a method, you can supply arguments that have more specific types than the parameters of that method. This is ordinary polymorphic behavior. For exactly the same reason, a delegate can have more specific parameter types than its method target. This is called contravariance.

Consider the following example:

  delegate void SpecificDelegate (SpecificClass s);

  class SpecificClass {}

  class Test
  {
    static void Main()
    {
      SpecificDelegate specificDelegate = GeneralHandler;
      specificDelegate (new SpecificClass());
    }

    static void GeneralHandler(object o)
    {
      Console.WriteLine(o.GetType()); // SpecificClass
    }
  }

A delegate merely calls a method on someone else’s behalf. In this case, the SpecificDelegate is invoked with an argument of type SpecificClass . When the argument is then relayed to the target method, the argument gets implicitly upcast to an object .

The standard event pattern is designed to help you leverage contravariance through its use of the common EventArgs base class. For example, you can have a single method invoked by two different delegates, one passing a MouseEventArgs and the other passing a KeyEventArgs .

Return type compatibility

If you call a method, you may get back a type that is more specific than what you asked for. This is ordinary polymorphic behavior. For exactly the same reason, the return type of a delegate can be less specific than the return type of its target method. This is called covariance. Consider the following example:

  delegate Asset DebtCollector();

  class Asset {}

  class House : Asset {}

  class Test
  {
    static void Main()
    
{
      DebtCollector d = new DebtCollector (GetHomeSweetHome);
      
Asset a = d();
     
Console.WriteLine(a.GetType()); // House
   
}
    
static House GetHomeSweetHome() {return new House(); }
  }

A delegate merely calls a method on someone else’s behalf. In this case, the DebtCollector expects to get back an Asset —but any Asset will do. Delegate return types are said to be covariant.

{mospagebreak title=Events} 

When using delegates, two emergent roles commonly appear: broadcaster and subscriber.

The broadcaster is a type that contains a delegate field. The broadcaster decides when to broadcast, by invoking the delegate.

The subscribers are the method target recipients. A subscriber decides when to start and stop listening, by calling +- and -= on the broadcaster’s delegate. A subscriber does not know about, or interfere with, other subscribers.

Events are a language feature that formalizes this pattern. An event is a wrapper for a delegate that exposes just the subset of delegate features required for the broadcaster/subscriber model. The main purpose of events is to prevent subscribers from interfering with each other.

To declare an event member, you put the event keyword in front of a delegate member. For instance:

  public class Broadcaster
  {
    public event ProgressReporter Progress;
  }

Code within the Broadcaster type has full access to Progress and can treat it as a delegate. Code outside of Broadcaster can only perform += and -= operations on Progress .

Consider the following example. The Stock class invokes its PriceChanged event every time the Price of the Stock changes:

  public delegate void PriceChangedHandler (decimal oldPrice,
                          decimal newPrice);

  public class Stock
  {
   
string symbol;
   
decimal price;

    public Stock (string symbol) {this.symbol = symbol;}

    public event PriceChanged PriceChanged;

    public decimal Price
    {
      get { return price; }
     
set
      {
       
if (price == value) return;       // exit if nothing has changed
       
if (PriceChanged != null)         // if invocation list not empty
        PriceChanged (price, value); // fire event
        price = value;
      }
    }
  }

If we remove the event keyword from our example so that PriceChanged becomes an ordinary delegate field, our example would give the same results. However, Stock would be less robust, in that subscribers could do the following things to interfere with each other:

  1. Replace other subscribers by reassigning PriceChanged (instead of using the += operator).
  2. Clear all subscribers (by setting PriceChanged to null ).
  3. Broadcast to other subscribers by invoking the delegate.

{mospagebreak title=Standard Event Pattern}

The .NET Framework defines a standard pattern for writing events. Its purpose is to provide consistency across both Framework and user code. At the core of the standard event pattern is System.EventArgs: a predefined Framework class with no members (other than the static Empty property). EventArgs is a base class for conveying information for an event. In our Stock example, we would subclass EventArgs to convey the old and new prices when a PriceChanged event is fired:

  public class PriceChangedEventArgs : System.EventArgs
 
{
   
public readonly decimal LastPrice;
   
public readonly decimal NewPrice;

    public PriceChangedEventArgs (decimal lastPrice, decimal newPrice)
   
{
     
LastPrice = lastPrice;
     
NewPrice = newPrice;
   
}
  }

For reusability, the EventArgs subclass is named according to the information it contains (rather than the event for which it will be used). It typically exposes data as properties or as read-only fields.

With an EventArgs subclass in place, the next step is to choose or define a dele gate for the event. There are three rules:

  • It must have a void return type.
  • It must accept two arguments: the first of type object , and the second a subclass of EventArgs . The first argument indicates the event broadcaster, and the second argument contains the extra information to convey.
  • Its name must end in “EventHandler”.

The Framework defines a generic delegate called System.EventHandler<> that satisfies these rules:

  public delegate void EventHandler<TEventArgs >
    (object source, TEventArgs e) where TEventArgs : EventArgs;

Before generics existed in the language (prior to C# 2.0), we would have had to instead write a custom delegate as follows:

  public delegate void PriceChangedHandler (object sender,
    PriceChangedEventArgs e);

For historical reasons, most events within the Framework use dele gates defined in this way.

The next step is to define an event of the chosen delegate type. Here, we use the generic EventHandler delegate:

  public class Stoc k
  {
   

    public event EventHandler<PriceChangedEventArgs> PriceChanged;
 
} 

Finally, the pattern requires that you write a protected virtual method that fires the event. The name must match the name of the event, prefixed with the word “On”, and then accept a single EventArgs argument:

  public class Stock
  {
    …

    public event EventHandler< PriceChangedEventArgs> PriceChanged;

    protected virtual void OnPriceChanged (PriceChangedEventArgs e)
    {
      if (PriceChanged != null) PriceChanged (this, e);
    }
  }

This provides a central point from which subclasses can invoke or override the event.

Here’s the complete example:

  using System;

  public class PriceChangedEventArgs : EventArgs
  {
   
public readonly decimal LastPrice;
   
public readonly decimal NewPrice;

    public PriceChangedEventArgs (decimal lastPrice, decimal newPrice)
    {
     
LastPrice = lastPrice; NewPrice = newPrice;
    }
  }

  public class Stock
  {
    string symbol;
    decimal price;

    public Stock (string symbol) {this.symbol = symbol;}

    public event EventHandler< PriceChangedEventArgs> PriceChanged;

    protected virtual void OnPriceChanged (PriceChangedEventArgs e)
    {
      if (PriceChanged != null) PriceChanged (this, e);
    }

    public decimal Price
   
{
      get { return price; }
      set
      {
        if (price == value) return;
        OnPriceChanged (new PriceChangedEventArgs (price, value));
        price = value;
     
}
    }
  }

  class Test
  {
    static void Main()
    {
     
Stock stock = new Stock ("THPW");
      stock.Price = 27.10M;
      // register with the PriceChanged event
      stock.PriceChanged += stock_PriceChanged;
      stock.Price = 31.59M;
   
}

    static void stock_PriceChanged (object sender, PriceChangedEventArgs e)
    {
      if ((e.NewPrice – e.LastPrice) / e.LastPrice > 0.1M)
        Console.WriteLine ("Alert, 10% stock price increase!");
    }
  }

The predefined nongeneric EventHandler delegate can be used when an event doesn’t carry extra information. In this example, we rewrite Stock such that the PriceChanged event is fired after the price changes, and no information about the event is necessary, other than it happened. We also make use of the EventArgs.Empty property, in order to avoid unnecessarily instantiating an instance of EventArgs .

  public class Stock
  {
    string symbol;
    decimal price;

    public Stock (string symbol) {this.symbol = symbol;}

    public event EventHandler PriceChanged;

    protected virtual void OnPriceChanged (EventArgs e)
    {
      if (PriceChanged != null) PriceChanged (this, e);
    }

    public decimal Price
   
{
      get { return price; }
      set
      {
       
if (price == value) return;
        price = value;
        OnPriceChanged (EventArgs.Empty);
      }
    }
  }

{mospagebreak title=Event Accessors}

An event’s accessors are the implementations of its += and -= functions. By default, accessors are implemented implicitly by the compiler. Consider this event declaration:

  public event EventHandler PriceChanged;

The compiler converts this to the following:

  • A private delegate field
  • A public pair of event accessor functions, whose implementations forward the += and -= operations to the private delegate field

You can take over this process by defining explicit event accessors. Here’s a manual implementation of the PriceChanged event from our previous example:

  private EventHandler _PriceChanged;     // declare a private delegate

  public event EventHandler PriceChanged
  {
    add
    {
     
_PriceChanged += value;
    }
    remove
    {
      _PriceChanged -= value;
    }
 
}

This example is functionally identical to C#’s default accessor implementation. The add and remove keywords after the event declaration instruct C# not to generate a default field and accessor logic.

With explicit event accessors, you can apply more complex strategies to the storage and access of the underlying delegate. There are three scenarios where this is useful:

  1. When the event accessors are merely relays for another class that is broad casting the event.
  2. When the class exposes a large number of events, where most of the time very few subscribers exist, such as a Windows control. In such cases, it is better to store the subscriber’s delegate instances in a dictionary, since a dictionary will contain less storage overhead than dozens of null delegate field references.
  3. When explicitly implementing an interface that declares an event.

Here is an example that illustrates the last point:

  public interface IFo o
  {
   
event EventHandler Ev;
  }

  class Foo : IFoo
  {
   
private EventHandler ev;

    event EventHandler IFoo.Ev
   
{
     
add    { ev += value; }
     
remove { ev -= value; }
   
}
  }

The add and remove parts of an event are compiled to add_XXX  and remove_XXX  methods.

The += and -= operations on an event are compiled to calls to the add_XXX  and remove_XXX  methods.

Event Modifiers

Like methods, events can be virtual, overridden, abstract, and sealed. Events can also be static:

  public class Foo
  {
    public static event EventHandler<EventArgs> StaticEvent;
    public virtual event EventHandler<EventArgs> VirtualEvent; 
  }

Please check back next week for the continuation of this article.

[gp-comments width="770" linklove="off" ]