For the purposes of this post, I'm assuming the reader is already familiar with delegates in C# and the Observer pattern.
I'll be using the word "event" in two different contexts: when a particular state is reached within a subject or a behavior is invoked that observers may wish to be notified of, and later the event
keyword in C#.
For the code samples I use a partially written Button
class for a hypothetical GUI framework. The would-be author of Button
wishes to define a Clicked
event on the class so that observers can respond to (you guessed it) the button being clicked by the user.
Defining an event: The dangerous way.
public class Button
{
public delegate void ClickedEventHandler();
// Public field of a delegate type. Never do this!
public ClickedEventHandler Clicked;
// ...
protected void OnClicked()
{
// Trigger the Clicked event.
if (Clicked != null)
{
Clicked();
}
}
}
In the above example, a nested delegate type named ClickedEventHandler
is defined and a public field of this delegate type named Clicked
is declared. A protected method named OnClicked
is provided by Button
as a convenience for it or any sub-classes to trigger the event.
Given a variable button
of type Button
, observers are expected to subscribe to the event like so:
void button_Clicked()
{
// Respond to the button being clicked...
}
// ...
button.Clicked += button_Clicked; // Subscribe to the Clicked event.
button.Clicked -= button_Clicked; // Unsubscribe from the Clicked event.
But what happens if a client coder accidentally uses =
instead of +=
?
button.Clicked = button_Clicked;
The answer: Any prior observers subscribed to the event are lost and replaced by the single new observer! But it gets worse. Client code can trigger the event directly!
button.Clicked(); // R.I.P. encapsulation.
Only the Button class or its sub-classes should be able to do that.
Defining an event: The safe way.
Enter C#'s event
keyword. Similarly to how properties are syntactic wrappers for get
and set
methods, C# events are syntactic wrappers for add
and remove
methods. Let's define one now:
public class Button
{
public delegate void ClickedEventHandler();
private ClickedEventHandler clicked;
public event ClickedEventHandler Clicked
{
add
{
clicked += value;
}
remove
{
clicked -= value;
}
}
// ...
protected void OnClicked()
{
// Trigger the Clicked event.
if (clicked != null)
{
clicked();
}
}
}
Our field of type ClickedEventHandler
is now private and renamed clicked
in lowercase. The Clicked
event wraps the field and modifies it via its add
and remove
methods. Just as set
methods have an implicit variable named value
of the property type, so to do add
and remove
methods have such a variable of the event type.
Client code still subscribes observers to the event just like before, but that's now all it can do.
void button_Clicked()
{
// Respond to the button being clicked...
}
// ...
button.Clicked += button_Clicked; // Invokes the event's add method.
button.Clicked -= button_Clicked; // Invokes the event's remove method.
// ...
button.Clicked = button_Clicked; // Won't compile!
button.Clicked(); // Won't compile!
Defining an event: The simple (but still safe) way.
But, we don't actually have to define add
and remove
methods explicitly. If we leave them out, the compiler will define them and a corresponding field behind the scenes for us. The add
and remove
methods that it defines are much like the example (but thread-safe).
We can declare and trigger the event like so:
public class Button
{
public delegate void ClickedEventHandler();
public event ClickedEventHandler Clicked;
// ...
protected void OnClicked()
{
// Trigger the Clicked event.
if (Clicked != null)
{
Clicked();
}
}
}
From the Button
class's perspective, Clicked
is a field. But from the perspective of client code, Clicked
is an event that observers can be subscribed to and unsubscribed from only, just as before when we defined our own add
and remove
methods.
If you've paid close attention, you may have noticed that the only difference between the first code sample and the last is the event
keyword! But now you know what it does, how it does it, and how to customize its behaviour.
When to define explicit add
and remove
methods.
So why would anyone ever want to write there own add
and remove
methods for an event? The only circumstance where I've ever needed to, is to add observers as individual items to a list, and I've only ever wanted to do that for two reasons: to avoid memory allocations when two non-null delegates are combined, or because the underlying delegate returns objects that I want to process for every observer (by default, only the return values of the last method in a delegate's invocation list are returned to the caller, while all others are ignored).