Events


There are two key problems with the delegates as you have used them so far. 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 has covered all that 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 be sufficiently controlled.

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

Listing 13.27. 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 Thermostat.TemperatureChangeHandler(       //       cooler.OnTemperatureChanged) if not C# 2.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.27 is almost identical to Listing 13.21 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, outside of 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.28.

Listing 13.28. 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 Thermostat.TemperatureChangeHandler(       //       cooler.OnTemperatureChanged) if not C# 2.0.       thermostat.OnTemperatureChange +=           heater.OnTemperatureChanged;         thermostat.OnTemperatureChange +=           cooler.OnTemperatureChanged;       thermostat.OnTemperatureChange(42);                                   } } 

In Listing 13.28, 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. event modifies a field declaration, as shown in Listing 13.29.

Listing 13.29. 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 delegate data type                                             public delegate void TemperatureChangeHandler(                               object sender, TemperatureArgs newTemperature);              // Define the event publisher                                                  public event TemperatureChangeHandler OnTemperatureChange;                     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#.

Coding Conventions

All you need to do to gain the desired functionality is to take the original delegate variable declaration, change it 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.29. To follow standard C# coding conventions, you changed OnTemperatureChangeHandler so that the single temperature parameter was replaced with two new parameters, sender and temperatureArgs. This change is not something that the C# compiler will enforce, but passing two parameters of these types is the norm for declaring 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 eventsfor 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().

The second parameter, temperatureArgs, is of type Thermostat.TemperatureArgs. Using a nested class is appropriate because it conforms to the same scope as the OnTemperatureChangeHandler delegate itself. The important part about TemperatureArgs, at least as far as the coding convention goes, is that it derives from System.EventArgs. 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. 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.30 shows an example.

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

Generics and Delegates

The previous section mentioned that the typical pattern for defining delegate data is to specify the first parameter, sender, of type object and the second parameter, eventArgs, to be a type deriving from System.EventArgs. One of the more cumbersome aspects of delegates in C# 1.0 is that you have 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.30, it is necessary to declare the delegate type TemperatureChangeHandler that has TemperatureArgs as a parameter.

With generics, you can use the same delegate data type in many locations with a host of different parameter types, and remain strongly typed. Consider the delegate declaration example shown in Listing 13.31.

Listing 13.31. Declaring a Generic Delegate Type

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

Using EventHandler<T>, each class that requires a particular sender-EventArgs pattern need not declare its own delegate definition. Instead, they can all share the same one, changing the thermostat example as shown in Listing 13.32.

Listing 13.32. Using Generics with Delegates

[View full width]

   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;    }     // TemperatureChangeHandler no longer needed                              // public delegate void TemperatureChangeHandler(                          // object sender, TemperatureArgs newTemperature);                                                                                                                          // Define the event publisher without using                                  //               TemperatureChangeHandler                                   public event EventHandler<TemperatureArgs>                                       OnTemperatureChange;                                                 public float CurrentTemperature     {           ...     }     private float _CurrentTemperature; } 

Listing 13.32 assumes, of course, that EventHandler<T> is defined somewhere. In fact, System.EventHandler<T>, as just declared, is included in the 2.0 framework class library. Therefore, in the majority of circumstances when using events in C# 2.0, it is not necessary to declare a custom delegate data type.

Note that System.EventHandler<T> restricts T to derive from EventArgs using a constraint, exactly what was necessary to correspond with the general convention for the event declaration of C# 1.0.

Advanced Topic: Event Internals

Events restrict 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. To do this the C# compiler takes the public delegate variable with its event keyword modifier and declares the delegate as private. In addition, it adds a couple of methods and two special event blocks. Essentially, the event keyword is a C# shortcut for generating the appropriate encapsulation logic. Consider the example in the event declaration shown in Listing 13.33.

Listing 13.33. Declaring the OnTemperatureChange Event

 public class Thermostat {   // Define the delegate data type   public delegate void TemperatureChangeHandler(      object sender, TemperatureArgs newTemperature);  public event TemperatureChangeHandler OnTemperatureChange   ... } 

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

Listing 13.34. C# Equivalent of the Event CIL Code Generated by the Compiler

[View full width]

 public class Thermostat {   // Define the delegate data type   public delegate void TemperatureChangeHandler(      object sender, TemperatureArgs newTemperature);   // Declaring the delegate field to save the                                         // list of subscribers.                                                             private TemperatureChangeHandler OnTemperatureChange;                public void add_OnTemperatureChange(                         TemperatureChangeHandler handler)                                    {                                                                                      System.Delegate.Combine(OnTemperatureChange, handler);               }                                                                             public void remove_OnTemperatureChange(                       TemperatureChangeHandler handler)                                      {                                                                                  System.Delegate.Remove(OnTemperatureChange, handler);                    }                                                                              public eventTemperatureChangeHandler OnTemperatureChange                  {                                                                                 add                                                                           {                                                                                   add_OnTemperatureChange(value)               }                                                                             remove                                                                        {                                                                                  remove_OnTemperatureChange(value)               }                                                                         }                                                                          } 

In other words, the code shown in Listing 13.33 is the C# shorthand that the compiler uses to trigger the code expansion shown in Listing 13.34.

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

Next, the C# compiler defines two methods, add_OnTemperatureChange() and remove_OnTemperatureChange(), where 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 13.34 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 TemperatureChangeHandler delegate instance, OnTemperatureChange.

Perhaps the most curious part of the code generated from the event keyword is the last part. The syntax is very similar to that of a property's getter and setter methods except that the methods are add and remove. 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 onto remove_OnTemperatureChange.

It is important to notice the similarities between this code and the code generated for a property. Readers will 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 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 are able to provide special functionality because they can recognize the event as a special class member.


Customizing the Event Implementation

The code for += and -= that the compiler generates can be customized. 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.32. 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.35 provides an example.

Listing 13.35. Custom add and remove Handlers

 public class Thermostat {   public class TemperatureArgs: System.EventArgs   {     ...   }   // Define the delegate data type   public delegate void TemperatureChangeHandler(        object sender, TemperatureArgs newTemperature);      // Define the event publisher                     public event TemperatureChangeHandler OnTemperatureChange         {                                                                             add                                  {                                                                           System.Delegate.Combine(value, _OnTemperatureChange);                  }                                                                       remove                               {                                                                           System.Delegate.Remove(_OnTemperatureChange, value);                     }                                                                      }                                                                         protected TemperatureChangeHandler _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.




Essential C# 2.0
Essential C# 2.0
ISBN: 0321150775
EAN: 2147483647
Year: 2007
Pages: 185

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net