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.
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).
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.
Begin by defining the Heater
and Cooler
objects (see Listing 13.1).
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.
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.
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.
Finally, put all these pieces together in a Main()
method. Listing 13.3 shows a sample of what Main()
could look like.
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.
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.
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.
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.
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
.
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.
The results of Listing 13.6 appear in 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.
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
.
Figure 13.1 highlights the sequential notification of both heater
and cooler
.
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 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.
Figure 13.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.
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.
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.
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
.
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.
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.
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 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.
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.
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.
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).
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.
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).
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.
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.
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).
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.
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.
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.
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.
3.147.83.28