99. Events#

Events, together with delegates, essentially constitute a native implementation of the Observer pattern in C#. They provide a simple and built-in way to allow objects, known as publishers, to notify other objects, known as subscribers, of some event which allows the subscribers to execute code in response to the occurrence of the event.

Key points

Here’s how to work with events in a nutshell:

  1. Declare a delegate (that will be used as the event handler delegate).

  2. Declare an event (whose subscribers must be values of the delegate).

  3. Define methods whose signatures follows the delegate.

  4. Subscribe the methods to the event.

  5. Raise the event and watch the methods get executed one by one.

Important

Events are like observables and delegates are like observers.

../_images/cover-events.jpg

Fig. 99.1 Like a fire watch tower sending out smoke alerts, weather updates, or lightning strikes, events can send different kinds of data to subscribers when the event occurs.#

Relation to Observer pattern#

Just like with the Observer pattern, an event in C# is a way for a class to provide notifications to clients of that class when some interesting activity happens or some state changes. The class that raises the event is known as the ‘publisher’, and the classes that handle the event are known as ‘subscribers’. In the Observer pattern we call the publishers ‘observables’ or ‘subjects’ and the subscribers ‘observers’.

Note

In events lingo we say that an event is ‘raised’, ‘fired’, or ‘invoked’. This corresponds to the state of the observable being updated in the Observer pattern. The consequence in both cases is that we have to notify the subscribers or the observers.

In the Observer pattern, the observable maintains a list of observers and notifies them of state changes, typically by calling one of their methods. The observable and observers are loosely coupled. The observable knows nothing about the observers, other than that they implement a particular interface.

When using events, the publisher has an event that we register subscribers to. They are loosely coupled since the only thing they know about each other is the signature of, what’s known as the ‘event handler delegate’.

Note

The method in a subscriber that is called when an event is fired is often called a ‘listener’. Subscribing to an event is therefore sometimes referred to as ‘listening to an event’.

Important

Events in C# take the Observer pattern a step further by adding an extra layer of encapsulation. Events can only be raised from within the class that they are declared.

Relation to delegates#

As you know from the chapters on Delegates, delegates allow us to treat methods as values that can be stored and passed around. We’ve already learned that delegates essentially solve the same problem as the Strategy pattern. Since observers in the Observer pattern are essentially equivalent to strategies in the Strategy pattern, delegates in C# can effectively serve the same purpose. In other words, when we have delegates, we don’t need a special class or interface for observers.

Think about it, what do observers do? When the observable is updated, a certain method in the observer is executed. Observers, like strategies, are merely verbs turned into nouns. Delegates is the most compact way of solving that problem in C#.

Syntax#

Let’s look at the basic syntax of events. We start by declaring a delegate whose instances will be the subscribers or observers of some event.

// Define a delegate that handlers of the event will use.
delegate void MyEventHandler(string message);

Let’s then define a publisher, or in other words, an observable who can raise events.

class Publisher
{
    // Declare the event using the delegate.
    public event MyEventHandler MyEvent;

    // This is how the event is raised.
    public void RaiseEvent()
        => MyEvent?.Invoke("Event raised!");
}

The event is essentially a variable of a delegate type while the handler is essentially a method whose type signature matches that delegate.

Note

Event handlers are sometimes also called ‘event listeners’.

Note

The question mark at the end of MyEvent? is the null-conditional operator and has nothing specific to do with events or delegates. Its purpose here is to avoid null reference exceptions with as little boilerplate as possible.

Basic example#

Let’s take an example involving podcasts and subscribers that get notified when new episodes are published. Here’s a definition of the Podcast class which will serve as the publisher.

class Podcast
{
    public string Title { get; private set; }

    public Podcast(string title) => Title = title;

    // The delegate used for event handlers.
    public delegate void EpisodeReleasedHandler(Podcast sender, string episodeTitle);

    // The dispatchable event that handlers can be registered to.
    public event EpisodeReleasedHandler EpisodeReleased;

    public void ReleaseNewEpisode(string episodeName)
    {
        // Some actual code for releasing the episode...
        EpisodeReleased?.Invoke(this, episodeName);
    }

    // More podcast and episode related logic...
}

Let’s now define a method that we will register as an event handler. In other words, a method that will be run when the event is raised. Meaning, a subscriber.

class Subscriber
{
    public string Name { get; private set; }

    public Subscriber(string name)
        => Name = name;

    // This instance method will be our event handler.
    public void HandleEpisodeReleased(Podcast podcast, string episode)
        => Console.WriteLine($"New episode for \"{Name}\" in \"{podcast.Title}\": \"{episode}\"");
}

At this point the subscriber is not yet subscribed to the podcast. For the subscriber to get notified when new episodes are added we must add the handler of an instance of Subscriber to the event in our instance of Podcast.

// A publisher
Podcast podcast = new Podcast("My pod");

// A subscriber who can subscribe to podcasts.
Subscriber subscriber = new Subscriber("Chris");

// Register the event handler to event (i.e. subscribe).
podcast.EpisodeReleased += subscriber.HandleEpisodeReleased;

Hint

Notice how we here make use of multicast delegates through the += syntax.

Now that we’ve registered the subscriber’s handler to the event, that method will be run when we fire the event. Let’s try it out by publishing a new episode which will fire the event.

// Raising the event invokes all subscribed handlers.
podcast.ReleaseNewEpisode("Episode A");
New episode for "Chris" in "My pod": "Episode A"

As discussed in the chapter on multicast delegates, we can stack any number of callbacks in a delegate instance. In the case of the Observer pattern all our subscribers have to implement some interface (usually called IObserver), but in the case of events the only requirement is that the handler has the signature required by the event.

To exemplify that we don’t even need a subscriber class, let’s write a simple local function that we can use as an event handler.

// Simple local function that counts the number of episodes released.
int numEpisodesReleased = 0;
void IncrementNumEpisodesReleased(Podcast podcast, string episodeTitle)
    => numEpisodesReleased++;

Let’s add the local function as a listener to the event.

// Register the new event handler to the event (i.e. subscribe).
podcast.EpisodeReleased += IncrementNumEpisodesReleased;

Since we’ve learned that lambdas can be implicitly converted to delegates we can of course also use a lambda. Let’s define and add a lambda statement that prints the number of episodes released by ‘capturing’ the variable that we declared above.

// Adding a simple lambda that prints the number of episodes released.
podcast.EpisodeReleased += (podcast, episodeTitle) =>
    Console.WriteLine($"{numEpisodesReleased}");

Tip

By adding the lambda at the same time as we define it we can make use of type inference which means that we don’t even have to specify any of the types.

Now, how many listeners have we registered to the event? What do you think happens when we fire the event?

// Raise the event again a few times.
podcast.ReleaseNewEpisode("Episode B");
podcast.ReleaseNewEpisode("Episode C");
podcast.ReleaseNewEpisode("Episode D");
New episode for "Chris" in "My pod": "Episode B"
1
New episode for "Chris" in "My pod": "Episode C"
2
New episode for "Chris" in "My pod": "Episode D"
3

Question

Why does the output say that we’ve released 3 and not 4 episodes, even though we raised the event 4 times in total in this chapter?

Tip

Events in C# can have modifiers such as virtual, abstract, override, sealed, static, and more. These can help you control, for example, how events are used in inherited classes.

Conclusion#

Events in C# elegantly solve the same problem as the Observer pattern. They enable an object (the publisher) to notify other objects (the subscribers) when something of interest occurs. By using events, which are built on top of delegates, you enable a clean separation of concerns, which results in more maintainable code.

By understanding events, delegates and the Observer pattern in C#, you will have grasped the mechanics behind event-based programming which is a powerful architecture that some even call its own paradigm. You’re also on your way to understand the asynchronous programming model which we’ll talk about much later.

In the next chapter we’ll talk about important conventions in C# related to events.