13. Events

In the preceding chapter, 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. The use of delegates for the publish-subscribe pattern is the focus of this chapter. Virtually everything described within this chapter is possible to do using delegates alone. However, the event constructs that this chapter focuses on provide additional encapsulation, making the publish-subscribe pattern easier to implement and less error-prone.

In the preceding chapter, all delegates referenced a single method. However, a single delegate value can reference a whole collection of methods to be called in sequence; such a delegate is called a multicast delegate. This enables scenarios where notifications of single events, such as a change in object state, are published to multiple subscribers.

Image

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 chapter assumes a minimum of C# 2.0 throughout. Readers still living in the world of C# 1.0 can still use events; however, they will have to declare their own delegate data types (as discussed in Chapter 12).

Coding the Observer Pattern with Multicast Delegates

Consider a temperature control example, where a heater and a cooler are hooked up to the same thermostat. In order for a unit to turn on and off appropriately, you notify the unit 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, however, thermometer would be more appropriate.

Defining Subscriber Methods

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

Listing 13.1. Heater and Cooler Event Subscriber Implementations


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

  public float Temperature
  {
      get{return _Temperature;}
      set{_Temperature = value;}
  }
  private float _Temperature;

  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{return _Temperature;}
      set{_Temperature = value;}
  }
  private float _Temperature;

  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, with the exception of 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 for when to turn on the unit. 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 methods. It is important that they have the parameters and a return type that matches the delegate from the Thermostat class, which we discuss next.

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 13.2.

Listing 13.2. Defining the Event Publisher, Thermostat


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;
          }
      }
  }
  private float _CurrentTemperature;
}

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 field is required to store all the subscribers. In other words, both the Cooler and the Heater classes will receive notifications of a change in the temperature from this single publisher.

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

Hooking Up the Publisher and Subscribers

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

Listing 13.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);
  }
}

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, 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 do this, modify the CurrentTemperature property to save the new value and publish a notification to each subscriber. The code modification appears in Listing 13.4.

Listing 13.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 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.

Check for null

One important part of event publishing code is missing from Listing 13.4. If no subscriber registered to receive the notification, OnTemperatureChange would be null and executing the OnTemperatureChange(value) statement would throw a NullReferenceException. To avoid this, it is necessary to check for null before firing the event. Listing 13.5 demonstrates how to do this.

Listing 13.5. Invoking a Delegate


public class Thermostat
{
  ...
  public float CurrentTemperature
  {
      get{return _CurrentTemperature;}
      set
      {
          if (value != CurrentTemperature)
          {
              _CurrentTemperature = value;
              // If there are any subscribers
              // then notify them of changes in
              // temperature
              Action<float> localOnChange =
                  OnTemperatureChange;
              if(localOnChange != null)
              {
                  // Call subscribers
                  localOnChange(value);
              }

          }
      }
  }
  private float _CurrentTemperature;
}

Instead of checking for null directly, first assign OnTemperatureChange to a second delegate variable, localOnChange. This simple modification ensures that if all OnTemperatureChange subscribers are removed (by a different thread) between checking for null and sending the notification, you will not raise a NullReferenceException.


Guidelines

DO check that the value of a delegate is not null before invoking it.


Delegate Operators

To combine the two subscribers in the Thermostat example, you used the += operator. This 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 13.6.

Listing 13.6. 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 13.6 appear in Output 13.1.

Output 13.1.

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

Furthermore, you can also use the + and operators to combine delegates, as Listing 13.7 shows.

Listing 13.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;

// 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);
// ...

Use of 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, appears in the Events section, later in this chapter.

It should be noted that both the + and - operators and their assignment equivalents, += and -=, are implemented internally using the static methods System.Delegate.Combine() and System.Delegate.Remove(). Both methods take two parameters of type delegate. The first method, Combine(), joins the two parameters so that the first parameter points 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.

One interesting thing to note about the Combine() method is that either or both of the 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.

Sequential Invocation

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

Image

Figure 13.1. Delegate Invocation Sequence Diagram

Although you coded only a single call to OnTemperatureChange(), the call is broadcast to both subscribers so that from that 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.

Error Handling

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

Listing 13.8. 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);
  }
}

Figure 13.3 shows an updated sequence diagram.

Image

Figure 13.3. Delegate Invocation with Exception 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.

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 13.9 shows the updates required in the CurrentTemperature property. The results appear in Output 13.2.

Listing 13.9. 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;
              if(OnTemperatureChange != null)
              {
                  List<Exception> exceptionCollection =
                      new List<Exception>();
                  foreach (
                      Action<float> handler in
                      OnTemperatureChange.GetInvocationList())
                  {
                      try
                      {
                          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;
}

Output 13.2.

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

This listing 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 sample, even though the delegate listener throws an exception, cooler still receives notification of the temperature change. After all notifications have been sent, Listing 13.9 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 while at the same time, all subscribers are notified.

Method Returns and Pass-by-Reference

There is another scenario where it is useful to iterate over the delegate invocation list instead of simply activating a notification directly. This scenario relates to delegates that either do not return void or have ref or out parameters. In the thermostat example so far, the OnTemperatureChange delegate is of type Action<float>, which returns void and has no out or ref parameters. The result is that there is no data returned back to the publisher. This is important because an invocation of a delegate potentially triggers notification to multiple subscribers. If the subscribers return a value, it is ambiguous 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 therefore, each would be required to return a status value. And since OnTemperatureChange potentially corresponds 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 possible in exceptional circumstances, the guideline is to avoid this scenario entirely by returning void.

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. However, weaknesses in the delegate structure may inadvertently allow the programmer to introduce a bug. The issues relate to encapsulation that neither the subscription nor the publication of events can sufficiently control.

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 for bugs. Consider Listing 13.10.

Listing 13.10. 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);
  }
}

Listing 13.10 is almost identical to Listing 13.6, except that instead of using the += operator, you use a simple assignment operator. As a result, when 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 in fact 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. It is the purpose of the event keyword to provide additional encapsulation such 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 13.11.

Listing 13.11. 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);

    }
}

In Listing 13.11, Program is able to 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, but in reality, there was no change in the thermostat temperature. As before, the problem with the delegate is that there 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 of these problems. Although seemingly like a field modifier, event defines a new type of member (see Listing 13.12).

Listing 13.12. 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{return _newTemperature;}
          set{_newTemperature = value;}
      }
      private float _newTemperature;
  }

  // Define the event publisher
  public event EventHandler<TemperatureArgs> OnTemperatureChange =
      delegate { };


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

The new Thermostat class has four changes from the original class. First, the OnTemperatureChange property has been removed, and instead, OnTemperatureChange has 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 use of the assignment operator on a public delegate field (for example, thermostat.OnTemperatureChange = cooler.OnTemperatureChanged). In addition, only the containing class is able to invoke the delegate that triggers the publication to all subscribers (for example, 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 they did not add. This resolves the two issues with plain delegates and is one of the key reasons for the event keyword in C#.

Another potential pitfall with plain delegates was the fact that it was easy to forget to check for null before invoking the delegate. This resulted in an unexpected NullReferenceException. Fortunately, the encapsulation that the event keyword provides enables an alternative possibility during declaration (or within the constructor), as shown in Listing 13.12. Notice that when declaring the event we assign delegate { }—an empty delegate representing a collection of zero listeners. By assigning the empty delegate we can raise the event without checking whether there are any listeners. (The behavior is similar to assigning a variable with an array of zero items. 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 with null, a check will still be 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.

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 and all other functionality remains the same. However, an additional change occurs in the delegate declaration in the code in Listing 13.12. To follow standard C# coding conventions, you replaced Action<float> with a new delegate type: EventHandler<TemperatureArgs>, a CLR type whose declaration is shown in Listing 13.13 (new in .NET Framework 2.0).

Listing 13.13. Declaring a Generic Delegate Type


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

The result was that the single temperature parameter in the Action<TEventArgs> delegate type was replaced with two new parameters, one for the 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, should contain 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. In order to determine which instance of Thermostat triggered the event, you use the sender parameter from inside Heater.OnTemperatureChanged(). If the event is static this will not be available, so pass null for the sender argument value.

The second parameter, TEventArgs e, is specified as type Thermostat.TemperatureArgs. The important part 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 .NET Framework 4.5.) The only significant property on System.EventArgs is Empty and it is used to indicate that there is no event data. When you derive TemperatureArgs from System.EventArgs, however, you add an additional property, NewTemperature, as a means to pass the temperature from the thermostat to the subscribers.

To summarize the coding convention for events: The first argument, sender, is of type object and it 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 13.14 shows an example.

Listing 13.14. Firing the Event Notification


public class Thermostat
{
  ...
  public float CurrentTemperature
  {
      get{return _CurrentTemperature;}
      set
      {
          if (value != CurrentTemperature)
          {
              _CurrentTemperature = value;
              // If there are any subscribers
              // then notify them of changes in
              // temperature
              if(OnTemperatureChange != null)
              {
                  // Call subscribers
                    OnTemperatureChange(
                      this, new TemperatureArgs(value) );

                }
          }
      }
  }
  private float _CurrentTemperature;
}

You usually specify the sender using the container class (this) because that 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. In the case of events that occur due to state changes, passing the previous value along with the new value is a frequent pattern used to control what state transitions are allowable.


Guidelines

DO check that the value of a delegate is not null before invoking it.

DO NOT pass null as the value of the sender on nonstatic events, but DO pass null for the same value on static events.

DO NOT pass null as the value of eventArgs argument.

DO use a delegate type of EventHandler<TEventArgs> for the events.

DO use System.EventArgs or a type that derives from System.EventArgs for a TEventArgs.

CONSIDER using a subclass of System.EventArgs as the event argument type (TEventArgs), unless you are absolutely sure the event will never need to carry any data.


Generics and Delegates

The preceding section discussed that 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 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 change. Every creation of a new derivation from System.EventArgs (a relatively common occurrence) required the declaration of a new delegate data type that uses the new EventArgs derived type. For example, in order to use TemperatureArgs within the event notification code in Listing 13.14, it is necessary to declare the delegate type TemperatureChangeHandler that has TemperatureArgs as a parameter (see Listing 13.15).

Listing 13.15. Using a Custom Delegate Type


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

      public float NewTemperature
      {
          get{return _newTemperature;}
          set{_newTemperature = value;}
      }
      private float _newTemperature;
  }

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

  public event TemperatureChangeHandler
      OnTemperatureChange;


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

Although generally EventHandler<TEventArgs> is preferred over creating a custom delegate type such as TemperatureChangeHandler, there is one advantage of such a type. Specifically, if a custom type is used, the parameter names can be specific to the event. In Listing 13.15, for example, when invoking the delegate to raise the event, the second parameter name will appear as newTemperature rather than 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. Since this is a pretty significant percentage of the more common types within the framework, it is therefore 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 not necessary to declare a custom delegate data type.


Guidelines

DO use System.EventHandler<T> instead of manually creating new delegate types for event handlers, unless the parameter names of a custom type offer significant clarification.


Customizing the Event Implementation

You can customize the code for += and -= that the compiler generates. 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, C# allows the same property as the syntax shown in Listing 13.15. In other words, C# allows you to define custom add and remove blocks to provide implementation for each aspect of the event encapsulation. Listing 13.18 provides an example.

Listing 13.18. Custom add and remove Handlers


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

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


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

In this case, 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.

Summary

Now that we have described events, it is worth mentioning 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 observer pattern.

It may take a little practice to be able to code events from scratch without sample code. However, they are a critical foundation to the asynchronous, multithreaded coding of later chapters.

..................Content has been hidden....................

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