14. Events

In Chapter 13, you saw how to reference a method with an instance of a delegate type and invoke that method via the delegate. Delegates are the building blocks of a larger pattern called publish–subscribe or observer. The use of delegates for the publish–subscribe pattern is the focus of this chapter. Almost everything described in this chapter can be done using delegates alone. However, the event constructs that this chapter highlights provide additional encapsulation, making the publish–subscribe pattern easier to implement and less error-prone.

Begin 2.0

In Chapter 13, all delegates referenced a single method. More broadly, a single delegate value can reference a whole collection of methods to be called in sequence; such a delegate is called a multicast delegate. Its application enables scenarios where notifications of single events, such as a change in object state, are published to multiple subscribers.

An illustration describes the six phases of events. They are the introduction of a publish-subscribe pattern; why events; event declaration; coding conventions; generics and delegates; and customizing the event implementation.

Although events existed in C# 1.0, the introduction of generics in C# 2.0 significantly changed the coding conventions because using a generic delegate data type meant that it was no longer necessary to declare a delegate for every possible event signature. For this reason, the current chapter assumes a minimum of C# 2.0 throughout. Readers still living in the world of C# 1.0 can also use events, but they will have to declare their own delegate data types (as discussed in Chapter 13).

Coding the Publish–Subscribe Pattern with Multicast Delegates

Consider a temperature control, where a heater and a cooler are hooked up to the same thermostat. For each unit to turn on and off appropriately, you must notify the units of changes in temperature. One thermostat publishes temperature changes to multiple subscribers—the heating and cooling units. The next section investigates the code.1

1.In this example, we use the term thermostat because people more commonly think of it in the context of heating and cooling systems. Technically, thermometer would be more appropriate.

Defining Subscriber Methods

Begin by defining the Heater and Cooler objects (see Listing 14.1).

Listing 14.1: Heater and Cooler Event Subscriber Implementations

class Cooler
{
  public Cooler(float temperature)
  {
      Temperature = temperature;
  }
  // Cooler is activated when ambient temperature is higher than this
  public float Temperature { get; set; }

  // Notifies that the temperature changed on this instance
  public void OnTemperatureChanged(float newTemperature)
  {
      if (newTemperature > Temperature)
      {
          System.Console.WriteLine("Cooler: On");
      }
      else
      {
          System.Console.WriteLine("Cooler: Off");
      }
  }
}

class Heater
{
  public Heater(float temperature)
  {
      Temperature = temperature;
  }

  public float Temperature { get; set; }

  public void OnTemperatureChanged(float newTemperature)
  {
      if (newTemperature < Temperature)
      {
          System.Console.WriteLine("Heater: On");
      }
      else
      {
          System.Console.WriteLine("Heater: Off");
      }
  }
}

The two classes are essentially identical except for the temperature comparison. (In fact, you could eliminate one of the classes if you used a delegate to a comparison method within the OnTemperatureChanged method.) Each class stores the temperature at which the unit should be turned on. In addition, both classes provide an OnTemperatureChanged() method. Calling the OnTemperatureChanged() method is the means to indicate to the Heater and Cooler classes that the temperature has changed. The method implementation uses newTemperature to compare against the stored trigger temperature to determine whether to turn on the device.

The OnTemperatureChanged() methods are the subscriber (also called listener) methods. They must have the parameters and a return type that matches the delegate from the Thermostat class, which we discuss next.

2.0

Defining the Publisher

The Thermostat class is responsible for reporting temperature changes to the heater and cooler object instances. The Thermostat class code appears in Listing 14.2.

Listing 14.2: Defining the Event Publisher, Thermostat

public class Thermostat
{
  // Define the event publisher (initially without the sender)
  public Action<float>? OnTemperatureChange { get; set; }

  public float CurrentTemperature { get; set; }
}

The Thermostat includes a property called OnTemperatureChange that is of the Action<float> delegate type. OnTemperatureChange stores a list of subscribers. Notice that only one delegate property is required to store all the subscribers. In other words, both the Cooler and the Heater instances will receive notifications of a change in the temperature from this single publisher.

The last member of Thermostat is the CurrentTemperature property. This property sets and retrieves the value of the current temperature reported by the Thermostat class.

Hooking Up the Publisher and Subscribers

Finally, we put all these pieces together in a Main() method. Listing 14.3 shows a sample of what Main() could look like.

Listing 14.3: Connecting the Publisher and Subscribers

class Program
{
  public static void Main()
  {
      Thermostat thermostat = new Thermostat();
      Heater heater = new Heater(60);
      Cooler cooler = new Cooler(80);
      string temperature;

      thermostat.OnTemperatureChange +=                             
          heater.OnTemperatureChanged;                              
      thermostat.OnTemperatureChange +=                             
          cooler.OnTemperatureChanged;                              

      Console.Write("Enter temperature: ");
      temperature = Console.ReadLine();
      thermostat.CurrentTemperature = int.Parse(temperature);
  }
}
2.0

The code in this listing has registered two subscribers (heater.OnTemperatureChanged and cooler.OnTemperatureChanged) to the OnTemperatureChange delegate by directly assigning them using the += operator.

By taking the temperature value the user has entered as input, you can set the CurrentTemperature of thermostat. However, you have not yet written any code to publish the change temperature event to subscribers.

Invoking a Delegate

Every time the CurrentTemperature property on the Thermostat class changes, you want to invoke the delegate to notify the subscribers (heater and cooler) of the change in temperature. To achieve this goal, you must modify the CurrentTemperature property to save the new value and publish a notification to each subscriber. The code modification appears in Listing 14.4.

Listing 14.4: Invoking a Delegate without Checking for null

public class Thermostat
{
  ...
  public float CurrentTemperature
  {
      get { return _CurrentTemperature; }
      set
      {
          if (value != CurrentTemperature)                      
          {
              _CurrentTemperature = value;                      

              // INCOMPLETE: Check for null needed
              // Call subscribers                               
              OnTemperatureChange(value);                       
          }
      }
  }
  private float _CurrentTemperature;
}

Now the assignment of CurrentTemperature includes some special logic to notify subscribers of changes in CurrentTemperature. The call to notify all subscribers is simply the single C# statement, OnTemperatureChange(value). This single statement publishes the temperature change to both the cooler and heater objects. Here, you see in practice that the ability to notify multiple subscribers using a single call is why delegates are more specifically known as multicast delegates, as discussed further later in the chapter.

2.0
Begin 8.0

In C# 8.0, however, invoking the CurrentTemperature delegate directly will trigger a nullable dereference warning, indicating that a null check is required.

End 8.0
Begin 6.0

Check for null

One important part of event publishing code is missing from Listing 14.4. If no subscriber has registered to receive the notification, OnTemperatureChange would be null, and executing the OnTemperatureChange(value) statement would throw a NullReferenceException. To avoid this scenario, it is necessary to check for null before firing the event. Listing 14.5 demonstrates how to do this using C# 6.0’s null-conditional operator before calling Invoke().

Listing 14.5: Invoking a Delegate

public class Thermostat
{
  // Define the event publisher
  public Action<float>? OnTemperatureChange { get; set; }

  public float CurrentTemperature
  {
      get { return _CurrentTemperature; }
      set
      {
          if (value != CurrentTemperature)
          {
              _CurrentTemperature = value;
              // If there are any subscribers,
              // notify them of changes in temperature
              // by invoking said subscribers
              OnTemperatureChange?.Invoke(value);  // C# 6.0                         
          }
      }
  }
  private float _CurrentTemperature;
}

Notice the call to the Invoke() method that follows the null-conditional operator. Although this method may be called using only a dot operator, there is little point, since that is the equivalent of calling the delegate directly (see OnTemperatureChange(value) in Listing 14.4). An important advantage underlying the use of the null-conditional operator is special logic to ensure that after checking for null, there is no possibility that a subscriber might invoke a stale handler (one that has changed after checking for null), leaving the delegate null again.

2.0
End 6.0

Delegate Operators

To combine the two subscribers in the Thermostat example, you used the += operator. This operator takes the first delegate and adds the second delegate to the chain. Now, after the first delegate’s method returns, the second delegate is called. To remove delegates from a delegate chain, use the -= operator, as shown in Listing 14.7.

Listing 14.7: Using the += and -= Delegate Operators

// ...
Thermostat thermostat = new Thermostat();
Heater heater = new Heater(60);
Cooler cooler = new Cooler(80);

Action<float> delegate1;
Action<float> delegate2;
Action<float>? delegate3;

delegate1 = heater.OnTemperatureChanged;
delegate2 = cooler.OnTemperatureChanged;

Console.WriteLine("Invoke both delegates:");
delegate3 = delegate1;
delegate3 += delegate2;                                                 
delegate3(90);

Console.WriteLine("Invoke only delegate2");
delegate3 -= delegate1;                                                 
delegate3!(30);
// ...

The results of Listing 14.7 appear in Output 14.1.

Output 14.1

Invoke both delegates:
Heater: Off
Cooler: On
Invoke only delegate2
Cooler: Off

You can also use the + and operators to combine delegates, as shown in Listing 14.8.

Begin 8.0
2.0

Listing 14.8: Using the + and - Delegate Operators

// ...
Thermostat thermostat = new Thermostat();
Heater heater = new Heater(60);
Cooler cooler = new Cooler(80);
Action<float> delegate1;
Action<float> delegate2;
Action<float> delegate3;

// Note: Use new Action (cooler.OnTemperatureChanged)
// for C# 1.0 syntax
delegate1 = heater.OnTemperatureChanged;
delegate2 = cooler.OnTemperatureChanged;

Console.WriteLine("Combine delegates using + operator:");
delegate3 = delegate1 + delegate2;                                  
delegate3(60);

Console.WriteLine("Uncombine delegates using - operator:");
delegate3 = (delegate3 - delegate2)!;                               
delegate3(60);
// ...

Using the assignment operator clears out all previous subscribers and allows you to replace them with new subscribers. This is an unfortunate characteristic of a delegate. It is simply too easy to mistakenly code an assignment when, in fact, the += operator is intended. The solution, called events, is described in the “Understanding Events” section later in this chapter.

Both the + and - operators and their assignment equivalents, += and -=, are implemented internally using the static methods System.Delegate.Combine() and System.Delegate.Remove(), respectively. These methods take two parameters of type delegate. The first method, Combine(), joins the two parameters so that the first parameter refers to the second within the list of delegates. The second, Remove(), searches through the chain of delegates specified in the first parameter and then removes the delegate specified by the second parameter. And, since the Remove() method could potentially return null, we use the C# 8.0 null-forgiving operator to tell the compiler to assume a valid delegate instance remains.

End 8.0

One interesting thing to note about the Combine() method is that either or both of its parameters can be null. If one of them is null, Combine() returns the non-null parameter. If both are null, Combine() returns null. This explains why you can call thermostat.OnTemperatureChange += heater.OnTemperatureChanged; and not throw an exception, even if the value of thermostat.OnTemperatureChange is still null.

2.0

Sequential Invocation

Figure 14.1 highlights the sequential notification of both heater and cooler.

The sequence diagram of the delegate action is shown.

Figure 14.1: Delegate invocation sequence diagram

Although you coded only a single call to OnTemperatureChange(), the call is broadcast to both subscribers. Thus, with just one call, both cooler and heater are notified of the change in temperature. If you added more subscribers, they, too, would be notified by OnTemperatureChange().

Although a single call, OnTemperatureChange(), caused the notification of each subscriber, the subscribers are still called sequentially, not simultaneously, because they are all called on the same thread of execution.

Begin 3.0

Error Handling

Error handling makes awareness of the sequential notification critical. If one subscriber throws an exception, later subscribers will not receive the notification. Consider, for example, what would happen if you changed Heater’s OnTemperatureChanged() method so that it threw an exception, as shown in Listing 14.9.

Listing 14.9: OnTemperatureChanged() Throwing an Exception

class Program
{
  public static void Main()
  {
      Thermostat thermostat = new Thermostat();
      Heater heater = new Heater(60);
      Cooler cooler = new Cooler(80);
      string temperature;

      thermostat.OnTemperatureChange +=
          heater.OnTemperatureChanged;
      // Using C# 3.0. Change to anonymous method
      // if using C# 2.0
      thermostat.OnTemperatureChange +=                                  
          (newTemperature) =>                                            
              {                                                          
                  throw new InvalidOperationException();                 
              };                                                         
      thermostat.OnTemperatureChange +=
          cooler.OnTemperatureChanged;

      Console.Write("Enter temperature: ");
      temperature = Console.ReadLine();
      thermostat.CurrentTemperature = int.Parse(temperature);
  }
}
2.0

Figure 14.3 shows an updated sequence diagram. Even though cooler and heater subscribed to receive messages, the lambda expression exception terminates the chain and prevents the cooler object from receiving notification.

The sequence diagram of the delegate action is shown.

Figure 14.3: Delegate invocation with exception sequence diagram

End 3.0

To avoid this problem so that all subscribers receive notification, regardless of the behavior of earlier subscribers, you must manually enumerate through the list of subscribers and call them individually. Listing 14.10 shows the updates required in the CurrentTemperature property. The results appear in Output 14.2.

2.0

Listing 14.10: Handling Exceptions from Subscribers

public class Thermostat
{
  // Define the event publisher
  public Action<float>? OnTemperatureChange;

  public float CurrentTemperature
  {
      get { return _CurrentTemperature; }
      set
      {
          if (value != CurrentTemperature)
          {
              _CurrentTemperature = value;
              Action<float>? onTemperatureChange = OnTemperatureChange;
              if(onTemperatureChange != null)
              {
                  List<Exception> exceptionCollection =                           
                      new List<Exception>();                                      
                  foreach (                                                       
                      Delegate handler in                                         
                      onTemperatureChange.GetInvocationList())                    
                  {                                                               
                      try                                                         
                      {                                                           
                          ((Action<float>) handler)(value);                       
                      }                                                           
                      catch (Exception exception)                                 
                      {                                                           
                          exceptionCollection.Add(exception);                     
                      }                                                           
                  }                                                               
                  if (exceptionCollection.Count > 0)                              
                  {                                                               
                      throw new AggregateException(                               
                          "There were exceptions thrown by                        
OnTemperatureChange Event subscribers.",                                       
                          exceptionCollection);                                   
                  }                                                               
                }
          }
      }
  }
  private float _CurrentTemperature;
}
2.0

Output 14.2

Enter temperature: 45
Heater: On
Error in the application
Cooler: Off

Listing 14.10 demonstrates that you can retrieve a list of subscribers from a delegate’s GetInvocationList() method. Enumerating over each item in this list returns the individual subscribers. If you then place each invocation of a subscriber within a try/catch block, you can handle any error conditions before continuing with the enumeration loop. In this example, even though the delegate subscriber throws an exception, cooler still receives notification of the temperature change. After all notifications have been sent, Listing 14.10 reports any exceptions by throwing an AggregateException, which wraps a collection of exceptions that are accessible by the InnerExceptions property. In this way, all exceptions are still reported and, at the same time, all subscribers are notified.

Method Returns and Pass-by-Reference

There is another scenario in which it is useful to iterate over the delegate invocation list instead of simply invoking a delegate directly. This scenario relates to delegates that either do not return void or have ref or out parameters. In the thermostat example, the OnTemperatureChange delegate is of type Action<float>, which returns void and has no out or ref parameters. As a result, no data is returned to the publisher. This consideration is important, because an invocation of a delegate potentially triggers notification to multiple subscribers. If each of the subscribers returns a value, it is ambiguous as to which subscriber’s return value would be used.

If you changed OnTemperatureChange to return an enumeration value indicating whether the device was on because of the temperature change, the new delegate would be of type Func<float, Status>, where Status was an enum with elements On and Off. All subscriber methods would have to use the same method signature as the delegate, and thus each would be required to return a status value. Also, since OnTemperatureChange might potentially correspond to a chain of delegates, it is necessary to follow the same pattern that you used for error handling. In other words, you must iterate through each delegate invocation list, using the GetInvocationList() method, to retrieve each individual return value. Similarly, delegate types that use ref and out parameters need special consideration. However, although it is possible to use this approach in exceptional circumstances, the best advice is to avoid this scenario entirely by only returning void.

2.0

Understanding Events

There are two key problems with the delegates as you have used them so far in this chapter. To overcome these issues, C# uses the keyword event. In this section, you will see why you would use events and how they work.

Why Events?

This chapter and the preceding one covered all you need to know about how delegates work. Unfortunately, weaknesses in the delegate structure may inadvertently allow the programmer to introduce a bug. These issues relate to encapsulation that neither the subscription nor the publication of events can sufficiently control. Specifically, using events restricts external classes from doing anything other than adding subscribing methods to the publisher via the += operator and then unsubscribing using the -= operator. In addition, they restrict classes, other than the containing class, from invoking the event.

Encapsulating the Subscription

As demonstrated earlier, it is possible to assign one delegate to another using the assignment operator. Unfortunately, this capability introduces a common source of bugs. Consider Listing 14.11.

Listing 14.11: Using the Assignment Operator = Rather Than +=

class Program
{
  public static void Main()
  {
      Thermostat thermostat = new Thermostat();
      Heater heater = new Heater(60);
      Cooler cooler = new Cooler(80);
      string temperature;

      // Note: Use new Action (cooler.OnTemperatureChanged)
      // if C# 1.0
      thermostat.OnTemperatureChange =
          heater.OnTemperatureChanged;

      // Bug: Assignment operator overrides
      // previous assignment
      thermostat.OnTemperatureChange =                                
          cooler.OnTemperatureChanged;                                

      Console.Write("Enter temperature: ");
      temperature = Console.ReadLine();
      thermostat.CurrentTemperature = int.Parse(temperature);
  }
}
2.0

Listing 14.11 is almost identical to Listing 14.7, except that instead of using the += operator, you use a simple assignment operator. As a result, when the code assigns cooler.OnTemperatureChanged to OnTemperatureChange, heater.OnTemperatureChanged is cleared out because an entirely new chain is assigned to replace the previous one. The potential for mistakenly using an assignment operator, when the += assignment was intended, is so high that it would be preferable if the assignment operator were not even supported for objects except within the containing class. The event keyword provides this additional encapsulation so that you cannot inadvertently cancel other subscribers.

Encapsulating the Publication

The second important difference between delegates and events is that events ensure that only the containing class can trigger an event notification. Consider Listing 14.12.

Listing 14.12: Firing the Event from Outside the Events Container

class Program
{
  public static void Main()
  {
      Thermostat thermostat = new Thermostat();
      Heater heater = new Heater(60);
      Cooler cooler = new Cooler(80);
      string temperature;

      // Note: Use new Action (cooler.OnTemperatureChanged)
      // if C# 1.0
      thermostat.OnTemperatureChange +=
          heater.OnTemperatureChanged;

      thermostat.OnTemperatureChange +=
          cooler.OnTemperatureChanged;

        thermostat.OnTemperatureChange(42);                                 
    }
}
2.0

In Listing 14.12, Program can invoke the OnTemperatureChange delegate even though the CurrentTemperature on thermostat did not change. Program, therefore, triggers a notification to all thermostat subscribers that the temperature changed, even though the thermostat temperature did not change. As before, the problem with the delegate is insufficient encapsulation. Thermostat should prevent any other class from being able to invoke the OnTemperatureChange delegate.

Declaring an Event

C# provides the event keyword to deal with both problems. Although seemingly like a field modifier, event defines a new type of member (see Listing 14.13).

Listing 14.13: Using the event Keyword with the Event-Coding Pattern

public class Thermostat
{
    public class TemperatureArgs: System.EventArgs                          
  {                                                                         
      public TemperatureArgs( float newTemperature )                        
      {                                                                     
          NewTemperature = newTemperature;                                  
      }                                                                     
                                                                            
      public float NewTemperature { get; set; }                             
  }                                                                         
                                                                            
  // Define the event publisher                                             
  public event EventHandler<TemperatureArgs> OnTemperatureChange =          
      delegate { };                                                         

  public float CurrentTemperature
  {
      ...
  }
  private float _CurrentTemperature;
}
2.0

The new Thermostat class has four changes relative to the original class. First, the OnTemperatureChange property has been removed, and OnTemperatureChange has instead been declared as a public field. This seems contrary to solving the earlier encapsulation problem. It would make more sense to increase the encapsulation, not decrease it by making a field public. However, the second change was to add the event keyword immediately before the field declaration. This simple change provides all the encapsulation needed. By adding the event keyword, you prevent the use of the assignment operator on a public delegate field (e.g., thermostat.OnTemperatureChange = cooler.OnTemperatureChanged). In addition, only the containing class is able to invoke the delegate that triggers the publication to all subscribers (e.g., disallowing thermostat.OnTemperatureChange(42) from outside the class). In other words, the event keyword provides the needed encapsulation that prevents any external class from publishing an event or unsubscribing previous subscribers it did not add. This resolves the two previously mentioned issues with plain delegates and is one of the key reasons for the inclusion of the event keyword in C#.

Another potential pitfall with plain delegates is that it is all too easy to forget to check for null (ideally using the null-conditional operator in C# 6.0 code) before invoking the delegate. This omission may result in an unexpected NullReferenceException. Fortunately, the encapsulation that the event keyword provides an alternative possibility during declaration (or within the constructor), as shown in Listing 14.13. Notice that when declaring the event, we assign delegate { }—a non-null delegate, which does nothing. By assigning the empty delegate, we can raise the event without checking whether there are any subscribers. (This behavior is similar to assigning an array of zero items to a variable. Doing so allows the invocation of an array member without first checking whether the variable is null.) Of course, if there is any chance that the delegate could be reassigned to have a null value, a check is still required. However, because the event keyword restricts assignment to occur only within the class, any reassignment of the delegate could occur only from within the class. Assuming null is never assigned, there will be no need to check for null whenever the code invokes the delegate.2

2. While rare, note that this pattern doesn’t work when the event is contained within a struct.

Coding Conventions

All you need to do to gain the desired functionality is to change the original delegate variable declaration to a field and add the event keyword. With these two changes, you provide the necessary encapsulation; all other functionality remains the same. However, an additional change occurs in the delegate declaration in the code in Listing 14.13. To follow standard C# coding conventions, you should replace Action<float> with a new delegate type: EventHandler<TemperatureArgs>, a Common Language Runtime (CLR) type whose declaration is shown in Listing 14.14.

2.0

Listing 14.14: The Generic Event Handler Type

public delegate void EventHandler<TEventArgs>(
   object sender, TEventArgs e);

The result is that the single temperature parameter in the Action<TEventArgs> delegate type is replaced with two new parameters—one for the publisher or “sender” and a second for the event data. This change is not something that the C# compiler will enforce, but passing two parameters of these types is the norm for a delegate intended for an event.

The first parameter, sender, contains an instance of the class that invoked the delegate. This is especially helpful if the same subscriber method registers with multiple events—for example, if the heater.OnTemperatureChanged event subscribes to two different Thermostat instances. In such a scenario, either Thermostat instance can trigger a call to heater.OnTemperatureChanged. To determine which instance of Thermostat triggered the event, you use the sender parameter from inside Heater.OnTemperatureChanged(). If the event is static, this option will not be available, so you would pass null for the sender argument value.

The second parameter, TEventArgs e, is specified as type Thermostat.TemperatureArgs. The important thing about TemperatureArgs, at least as far as the coding convention goes, is that it derives from System.EventArgs. (In fact, derivation from System.EventArgs is something that the framework forced with a generic constraint until Microsoft .NET Framework 4.5.) The only significant property on System.EventArgs is Empty, which is used to indicate that there is no event data. When you derive TemperatureArgs from System.EventArgs, however, you add an additional property, NewTemperature, to pass the temperature from the thermostat to the subscribers.

Let’s summarize the coding convention for events: The first argument, sender, is of type object and contains a reference to the object that invoked the delegate or null if the event is static. The second argument is of type System.EventArgs or something that derives from System.EventArgs but contains additional data about the event. You invoke the delegate exactly as before, except for the additional parameters. Listing 14.15 shows an example.

2.0

Listing 14.15: Firing the Event Notification

public class Thermostat
{
  ...
  public float CurrentTemperature
  {
      get{return _CurrentTemperature;}
      set
      {
          if (value != CurrentTemperature)
          {
              _CurrentTemperature = value;
              // If there are any subscribers,
              // notify them of changes in temperature
              // by invoking said subscribers
              OnTemperatureChange?.Invoke(  // Using C# 6.0                 
                      this, new TemperatureArgs(value) );                   
                }
          }
      }
  }
  private float _CurrentTemperature;
}

You usually specify the sender using the container class (this) because it is the only class that can invoke the delegate for events.

In this example, the subscriber could cast the sender parameter to Thermostat and access the current temperature that way, as well as via the TemperatureArgs instance. However, the current temperature on the Thermostat instance may change via a different thread. When events may occur due to state changes, passing the previous value along with the new value is a pattern frequently used to control which state transitions are allowable.

2.0

Generics and Delegates

As noted in the preceding section, the guideline for defining a type for an event is to use a delegate type of EventHandler<TEventArgs>. In theory, any delegate type could be used, but by convention, the first parameter, sender, is of type object and the second parameter, e, should be of a type deriving from System.EventArgs. One of the more cumbersome aspects of delegates in C# 1.0 was that you had to declare a new delegate type whenever the parameters on the handler changed. Every creation of a new derivation from System.EventArgs (a relatively common occurrence) required the declaration of a new delegate data type that used the new EventArgs-derived type. For example, to use TemperatureArgs within the event notification code in Listing 14.15, it would be necessary to declare the delegate type TemperatureChangeHandler that has TemperatureArgs as a parameter (see Listing 14.16).

Listing 14.16: Using a Custom Delegate Type

public class Thermostat
{
  public class TemperatureArgs: System.EventArgs
  {
      public TemperatureArgs( float newTemperature )
      {
          NewTemperature = newTemperature;
      }

      public float NewTemperature { get; set; }

  public delegate void TemperatureChangeHandler(                                       
      object sender, TemperatureArgs newTemperature);                                  
                                                                                       
  public event TemperatureChangeHandler?                                               
      OnTemperatureChange;                                                             

  public float CurrentTemperature
  {
      ...
  }
  private float _CurrentTemperature;
}
2.0

Although generally EventHandler<TEventArgs> is preferred over creating a custom delegate type such as TemperatureChangeHandler, there is one advantage associated with the latter type. Specifically, if a custom type is used, the parameter names can be specific to the event. In Listing 14.16, for example, when invoking the delegate to raise the event, the second parameter name will appear as newTemperature rather than as simply e.

Another reason why a custom delegate type might be used concerns parts of the CLR API that were defined prior to C# 2.0. It is not uncommon to encounter specific delegate types rather than the generic form on events coming from the CLR API. Regardless, in the majority of circumstances when using events in C# 2.0 and later, it is unnecessary to declare a custom delegate data type.

When the C# compiler encounters the event keyword, it generates CIL code equivalent to the C# code shown in Listing 14.18.

Listing 14.18: C# Conceptual Equivalent of the Event CIL Code Generated by the Compiler

public class Thermostat
{
  // ...
  // Declaring the delegate field to save the                                          
  // list of subscribers                                                               
  private EventHandler<TemperatureArgs> _OnTemperatureChange;                          
                                                                                       
  public void add_OnTemperatureChange(                                                 
      EventHandler<TemperatureArgs> handler)                                           
  {                                                                                    
      System.Delegate.Combine(_OnTemperatureChange, handler);                          
  }                                                                                    
                                                                                       
  public void remove_OnTemperatureChange(                                              
      EventHandler<TemperatureArgs> handler)                                           
  {                                                                                    
      System.Delegate.Remove(_OnTemperatureChange, handler);                           
  }                                                                                    
                                                                                       
  public event EventHandler<TemperatureArgs> OnTemperatureChange                       
  {                                                                                    
      add                                                                              
      {                                                                                
          add_OnTemperatureChange(value)                                               
      }                                                                                
      remove                                                                           
      {                                                                                
          remove_OnTemperatureChange(value)                                            
      }                                                                                
  }                                                                                    
}

In other words, the code shown in Listing 14.17 is (conceptually) the C# shorthand that the compiler uses to trigger the code expansion shown in Listing 14.18. (The “conceptually” qualifier is needed because some details regarding thread synchronization have been eliminated for elucidation.)

The C# compiler first takes the original event definition and defines a private delegate variable in its place. As a result, the delegate becomes unavailable to any external class—even to classes derived from it.

2.0

Next, the C# compiler defines two methods, add_OnTemperatureChange() and remove_OnTemperatureChange(), in which the OnTemperatureChange suffix is taken from the original name of the event. These methods are responsible for implementing the += and -= assignment operators, respectively. As Listing 14.18 shows, these methods are implemented using the static System.Delegate.Combine() and System.Delegate.Remove() methods, discussed earlier in the chapter. The first parameter passed to each of these methods is the private EventHandler<TemperatureArgs> delegate instance, OnTemperatureChange.

Perhaps the most curious part of the code generated from the event keyword is the last segment. The syntax is very similar to that of a property’s getter and setter methods, except that the methods are called add and remove, respectively. The add block takes care of handling the += operator on the event by passing the call to add_OnTemperatureChange(). In a similar manner, the remove block operator handles the -= operator by passing the call on to remove_OnTemperatureChange.

Take careful note of the similarities between this code and the code generated for a property. Recall that the C# implementation of a property is to create get_<propertyname> and set_<propertyname> and then to pass calls to the get and set blocks on to these methods. Clearly, the event syntax in such cases is very similar.

Another important characteristic to note about the generated CIL code is that the CIL equivalent of the event keyword remains in the CIL. In other words, an event is something that the CIL code recognizes explicitly; it is not just a C# construct. By keeping an equivalent event keyword in the CIL code, all languages and editors can provide special functionality because they can recognize the event as a special class member.

Customizing the Event Implementation

You can customize the code that the compiler generates for += and -=. Consider, for example, changing the scope of the OnTemperatureChange delegate so that it is protected rather than private. This, of course, would allow classes derived from Thermostat to access the delegate directly instead of being limited to the same restrictions as external classes. To enable this behavior, C# allows the same property as the syntax shown in Listing 14.16. In other words, C# allows you to define custom add and remove blocks to provide a unique implementation for each aspect of the event encapsulation. Listing 14.19 provides an example.

2.0

Listing 14.19: Custom add and remove Handlers

public class Thermostat
{
  public class TemperatureArgs: System.EventArgs
  {
      ...
  }

    // Define the event publisher                                                      
  public event EventHandler<TemperatureArgs> OnTemperatureChange                       
  {                                                                                    
      add                                                                              
      {                                                                                
          _OnTemperatureChange =                                                       
              (TemperatureChangeHandler)                                               
                  System.Delegate.Combine(value, _OnTemperatureChange);                
      }                                                                                
      remove                                                                           
      {                                                                                
          _OnTemperatureChange =                                                       
              (TemperatureChangeHandler?)                                              
                  System.Delegate.Remove(_OnTemperatureChange, value);                 
      }                                                                                
  }                                                                                    
  protected EventHandler<TemperatureArgs>? _OnTemperatureChange;                       

  public float CurrentTemperature
  {
      ...
  }
  private float _CurrentTemperature;
}

Here, the delegate that stores each subscriber, _OnTemperatureChange, was changed to protected. In addition, implementation of the add block switches around the delegate storage so that the last delegate added to the chain is the first delegate to receive a notification. However, code should not rely on this implementation.

Summary

2.0

Now that we have described events, we note that, in general, method references are the only cases where it is advisable to work with a delegate variable outside the context of an event. In other words, given the additional encapsulation features of an event and the ability to customize the implementation when necessary, the best practice is always to use events for the publish–subscribe pattern.

It may take a little practice before you can code events from scratch without referring to sample code. However, events are a critical foundation for the asynchronous, multithreaded coding described in later chapters of this book.

End 2.0
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.173.227