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.
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 +=
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
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
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#.
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
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
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
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.
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
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.