Begin 2.0
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. Almost everything described within 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.
In the preceding chapter, 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.
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 also use events, but they will have to declare their own delegate data types (as discussed in Chapter 12).
Consider a temperature control, where a heater and a cooler are hooked up to the same thermostat. For the unit to turn on and off appropriately, you must 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, thermometer would be more appropriate.
Begin by defining the Heater
and Cooler
objects (see Listing 13.1).
class Cooler
{
public Cooler(float temperature)
{
Temperature = temperature;
}
public float Temperature { get; set; }
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, 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 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 methods. They must 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.
public class Thermostat
{
// Define the event publisher
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 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 property sets and retrieves the value of the current temperature reported by the Thermostat
class.
Finally, we put all these pieces together in a Main()
method. Listing 13.3 shows a sample of what Main()
could look like.
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 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.
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 13.4.
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.
One important part of event publishing code is missing from Listing 13.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 13.5 demonstrates how to do this using C# 6.0’s null conditional operator before calling Invoke()
.
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(value); // C# 6.0
}
}
}
private float _CurrentTemperature;
}
Notice the call to the Invoke()
method that follows the null conditional. 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 13.4). The important advantage underlying null conditional operator is special logic to ensure that after checking for null
, there is no possibility that a subscriber might unsubscribe, leaving the delegate null
again.
End 6.0
Unfortunately, no such special uninterruptable null-checking logic exists prior to C# 6.0. As such, the implementation is significantly more verbose in earlier C# versions, as shown in Listing 13.6.
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
Action<float> localOnChange =
OnTemperatureChange;
if(localOnChange != null)
{
// Call subscribers
localOnChange(value);
}
}
}
}
private float _CurrentTemperature;
}
Instead of checking for null
directly, this code first assigns 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
.
For the remainder of the book all samples will rely on the C# 6.0 null conditional operator for delegate invocation.
Guidelines
DO check that the value of a delegate is not null
before invoking it.
DO use the null conditional operator prior to calling Invoke()
starting in C# 6.0.
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 13.7.
// ...
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.7 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.8 shows.
// ...
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.
Both the +
and -
operators and their assignment equivalents, +=
and -=
, are implemented internally using the static methods System.Delegate.Combine()
and System.Delegate.Remove()
. These 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 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
.
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. 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 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.9.
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. Even though cooler
and heater
subscribed to receive messages, the lambda expression exception terminates the chain and prevents the cooler
object from receiving notification.
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 13.10 shows the updates required in the CurrentTemperature
property. The results appear in Output 13.2.
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 (
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;
}
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 example, even though the delegate listener throws an exception, cooler
still receives notification of the temperature change. After all notifications have been sent, Listing 13.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.
Parenthetically, no null conditional was used in this example because of the if condition that verified onTemperatureChange
was not null.
There is another scenario in which 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, 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, therefore, 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 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. 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.
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.11.
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.11 is almost identical to Listing 13.7, 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 actually 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.
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.12.
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.12, 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, even though there was actually 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.13).
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;
}
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 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 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 a null conditional 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 enables an alternative possibility during declaration (or within the constructor), as shown in Listing 13.13. 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. (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 with null
, 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.
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.13. To follow standard C# coding conventions, you should replace Action<float>
with a new delegate type: EventHandler<TemperatureArgs>
, a CLR type whose declaration is shown in Listing 13.14 (new in .NET Framework 2.0).
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e)
where TEventArgs : EventArgs;
The result is that the single temperature parameter in the Action<TEventArgs>
delegate type is 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
, 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 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
, 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
, 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 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.15 shows an example.
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 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 pattern frequently used to control which state transitions are allowable.
Guidelines
DO check that the value of a delegate is not null
before invoking it (possibly by using the null conditional operator in C# 6.0).
DO NOT pass null
as the value of the sender for nonstatic events, but DO pass null
as the same value for 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.
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 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 13.15, it would be necessary to declare the delegate type TemperatureChangeHandler
that has TemperatureArgs
as a parameter (see Listing 13.16).
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 associated with the latter type. Specifically, if a custom type is used, the parameter names can be specific to the event. In Listing 13.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. Given that these parts represent a fairly significant percentage of the more common types within the framework, 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 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.
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 behavior, C# allows the same property as the syntax shown in Listing 13.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 13.19 provides an example.
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.
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 before you can code events from scratch without referring to sample code. However, events are a critical foundation for the asynchronous, multithreaded coding of later chapters.