Events - Essential C# 6.0 (2016)

Essential C# 6.0 (2016)

13. Events

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.

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 also use events, but 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, 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.

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; 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 usesnewTemperature 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.

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; 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 Coolerand 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.

Hooking up the Publisher and Subscribers

Finally, we 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 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.

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

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

Begin 6.0

Check for null

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 aNullReferenceException. 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().

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,
// 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.

LISTING 13.6: Invoking a Delegate with Null Check Prior to C# 6.0


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



Advanced Topic: -= Operator for a Delegate Returns a New Instance

Given that a delegate is a reference type, it is perhaps somewhat surprising that assigning a local variable and then using that local variable is sufficient for making the null check thread-safe. Since localOnChange points at the same location as OnTemperatureChange does, you might imagine that any changes in OnTemperatureChange would be reflected in localOnChange as well.

This is not the case because, effectively, any calls to OnTemperatureChange -= <listener> will not simply remove a delegate from OnTemperatureChange so that it contains one less delegate than before. Rather, such a call will assign an entirely new multicast delegate without having any effect on the original multicast delegate to which localOnChange also points.



Advanced Topic: Thread-Safe Delegate Invocation

If subscribers can be added and removed from the delegate on different threads, it is wise (as noted earlier) to copy the delegate reference into a local variable before checking it for null. Although this approach prevents the invocation of a null delegate, it does not avoid all possible race conditions. For example, one thread could make the copy, and then another thread could reset the delegate to null, and then the original thread could invoke the previous value of the delegate, thereby notifying a subscriber that is no longer on the list of subscribers. Subscribers in multithreaded programs should ensure that their code remains robust in this scenario; it is always possible that a “stale” subscriber will be invoked.


Delegate Operators

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.

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;

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.

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.

LISTING 13.8: 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.

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 callthermostat.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. 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 byOnTemperatureChange().

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.


Advanced Topic: Multicast Delegate Internals

To understand how events work, you need to revisit the first examination of the System.Delegate type internals. Recall that the delegate keyword is an alias for a type derived from System.MulticastDelegate. In turn, System.MulticastDelegate is derived from System.Delegate, which, for its part, is composed of an object reference (needed for nonstatic methods) and a method reference. When you create a delegate, the compiler automatically employs the System.MulticastDelegate type rather than theSystem.Delegate type. The MulticastDelegate class includes an object reference and a method reference, just like its Delegate base class, but it also contains a reference to another System.MulticastDelegate object.

When you add a method to a multicast delegate, the MulticastDelegate class creates a new instance of the delegate type, stores the object reference and the method reference for the added method into the new instance, and adds the new delegate instance as the next item in a list of delegate instances. In effect, the MulticastDelegate class maintains a linked list of Delegate objects. Conceptually, you can represent the thermostat example as shown in Figure 13.2 on the facing page (denoted as a continuation of this advanced topic).

Image

FIGURE 13.2: Multicast Delegates Chained Together

When invoking a multicast delegate, each delegate instance in the linked list is called sequentially. Generally, delegates are called in the order they were added, but this behavior is not specified within the CLI specification. Furthermore, it can be overridden. Therefore, programmers should not depend on an invocation order.


Error Handling

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.

LISTING 13.9: 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. Even though cooler and heater subscribed to receive messages, the lambda expression exception terminates the chain and prevents the cooler object from receiving notification.

Image

FIGURE 13.3: Delegate Invocation with Exception Sequence Diagram

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.

LISTING 13.10: 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;
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;
}


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 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 anAggregateException, 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.

Method Returns and Pass-by-Reference

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, theOnTemperatureChange 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.

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

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

LISTING 13.11: 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.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.

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

LISTING 13.12: 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.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.

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

LISTING 13.13: 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; 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.

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

LISTING 13.14: Declaring a Generic Delegate Type


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 Thermostatinstances. 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 fromSystem.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 fromSystem.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 fromSystem.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.

LISTING 13.15: Firing the Event Notification


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.


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 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 typeTemperatureChangeHandler that has TemperatureArgs as a parameter (see Listing 13.16).

LISTING 13.16: 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 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.



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 so, 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.17.

LISTING 13.17: Declaring the OnTemperatureChange Event


public class Thermostat
{
public event EventHandler<TemperatureArgs> OnTemperatureChange;

...
}


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

LISTING 13.18: C# Conceptual Equivalent of the Event CIL Code Generated by the Compiler


public class Thermostat
{
// ...
// Declaring the delegate field to save the
// list of subscribers.
private EventHandler<TemperatureArgs> _OnTemperatureChange;

public void add_OnTemperatureChange(
EventHandler<TemperatureArgs> handler)
{
System.Delegate.Combine(_OnTemperatureChange, handler);
}

public void remove_OnTemperatureChange(
EventHandler<TemperatureArgs> handler)
{
System.Delegate.Remove(_OnTemperatureChange, handler);
}

public event EventHandler<TemperatureArgs> OnTemperatureChange
{
add
{
add_OnTemperatureChange(value)
}
remove
{
remove_OnTemperatureChange(value)
}
}
}


In other words, the code shown in Listing 13.17 is (conceptually) the C# shorthand that the compiler uses to trigger the code expansion shown in Listing 13.18. (The “conceptually” qualifier is needed because some details regarding thread synchronization have been eliminated for the purpose of elucidation.)

The C# compiler first takes the original event definition and defines a private delegate variable in its place. As a result, 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(), in which 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.18 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 EventHandler<TemperatureArgs> delegate instance, OnTemperatureChange.

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

Take careful note of 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 in such cases 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 eventkeyword 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

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.

LISTING 13.19: 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 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.

End 2.0