Encapsulating the Subscription
- It is possible to assign one delegate to another using the assignment operator.
- This capability introduces a common source for bugs.
// When code assigns OnHeaterTemperatureChanged to OnCoolerTemperatureChanged,
// OnHeaterTemperatureChanged is cleared out because an entirely new chain is assigned to replace the previous one.
var thermostat = new Thermostat();
thermostat.OnTemperatureChange = OnHeaterTemperatureChanged;
// Bug: assignment operator overrides previous assignment.
thermostat.OnTemperatureChange = OnCoolerTemperatureChanged;
- With use of events you cannot inadvertently cancel other subscribers.
Encapsulating the Publication
- The problem with the delegate is that there is insufficient encapsulation.
- You can invoke a delegate wherever you want.
- Another difference between delegates and events is that events ensure that only the containing class can trigger an event notification.
var thermostat = new Thermostat();
var heater = new Heater(60);
var cooler = new Cooler(80);
thermostat.OnTemperatureChange += heater.OnTemperatureChanged;
thermostat.OnTemperatureChange += cooler.OnTemperatureChanged;
thermostat.OnTemperatureChange(42); // You can invoke it wherever you want.
Declaring an Event
- By adding the
event
keyword, you prevent use of the assignment operator on a public delegate field.
- In addition, only the containing class is able to invoke the delegate that triggers the publication to all subscribers.
- 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.
- Notice that when declaring the event we assign the event to
delegate {}
(an empty delegate representing a collection of zero listeners).
- Assuming null is never assigned, there will be no need to check for null whenever the code invokes the delegate.
- In order to achieve standard C# coding conventions, you changed OnTemperatureChangeHandler so that the single temperature parameter was replaced with two new parameters, sender and temperatureArgs. This change is not something that the C# compiler will enforce.
- The first argument, sender, is of type object and it contains a reference to the object that invoked the delegate.
- The second argument is of type System.EventArgs or something that derives from System.EventArgs but contains aditional data about the event.
- You invoke the delegate exactly as before, except for the additional parameters.
- You usually specify the sender using the container class (this) because that is the only class that can invoke the delegate for events.
public class Thermostat
{
public delegate void TemperatureChangeHandler(object sender, TemperatureArgs newTemperature);
public event TemperatureChangeHandler OnTemperatureChange = delegate { };
public class TemperatureArgs : EventArgs
{
public float NewTemperature { get; set; }
public TemperatureArgs(float newTemperature)
{
NewTemperature = newTemperature;
}
}
}
EventHandler
Delegate
- Many events have no additional information to pass, and thus do not require custom EventArgs.
- Therefore, the signature of the delegates for subscribing to these events typically follows
void EventHandler(object sender, EventArgs e)
pattern.