Events

Problem Statement

The Observer design pattern is one of the most common one patterns in software development. For those who’ve forgotten about it, its purpose is to make an object “observable”, i.e., that one can be notified of and react to changes.

Take for example the file system. A simplified FileSystem class could look as follows:

public class FileSystem
{
    public List<string> GetDirectoryContents(string directory);
    public void CreateFile(string path);
    public void DeleteFile(string path);
    public void WriteToFile(string path, string contents);
    public string ReadFile(string path);
}

Say we are writing an application that is interested in being notified of all changes to the file system. For example:

Writing such an application relying solely on the functionality FileSystem currently provides is bound to be quite inefficient: the only way to know whether a new file has been created is to routinely call GetDirectoryContents:

void WaitForNewFile(FileSystem fs, string directory)
{
    var oldList = fs.GetDirectoryContents(directory);

    while ( true )
    {
        var newList = fs.GetDirectoryContents(directory);

        if ( newList.Count > oldList.Count )
        {
            return;
        }

        oldList = newList;
    }
}

To put succinctly, the problem we are trying to solve is: “We want to take certain actions whenever a specific event occurs. How do we achieve this efficiently?”

Observer Design Pattern

Let’s extend FileSystem such that detecting changes can be done more efficiently. For the sake of simplicity, let’s focus solely on detecting the creation of new files.

Instead of us having to repeatedly call GetDirectoryContents, we’d like to turn things around and have FileSystem to call us whenever a new file has been created. We want to give FileSystem a bit of code that it should execute on file creation. Conceptually, it would look something like this:

// Pseudocode!
public void OnFileCreated(string path)
{
    Console.WriteLine("A new file has been created at " + path);
}

FileSystem fs;
fs.CallOnFileCreation(OnFileCreated);

This code should cause A new file has been created at ... to be printed whenever a new file is created. Let’s first implement it the “manual” way, as one would do in Java.

public interface IFileCreationObserver
{
    void OnFileCreated(string path);
}

public class FileSystem
{
    // Field to store the file creation observer
    private IFileCreationObserver fileCreationObserver;

    public void CreateFile(string path)
    {
        // ...

        // Notify the observer, if it exists
        fileCreationObserver?.OnFileCreated(path);
    }

    public void CallOnFileCreation(IFileCreationObserver observer)
    {
        // Store observer in field
        this.fileCreationObserver = observer;
    }

    // ...
}

Relying on this new functionality can be done as follows:

public class FileCreationPrinter : IFileCreationObserver
{
    public void OnFileCreated(string path)
    {
        Console.WriteLine("A new file has been created at " + path);
    }
}

var fs = new FileSystem();
fs.CallOnFileCreation( new FileCreationPrinter() );

Make sure you understand the above code:

C# Events

The Observer Pattern is one solution to the problem stated above. One could say it is a bit clumsy:

You can see there’s quite a gap between the idea of “passing a bunch of instructions” on the one hand, and the implementation consisting of an object, class and interface on the other. Now imagine that we also need to be notified of file deletion, writes, and so on: the prospect of having to create all these additional interfaces, classes and objects is not an enchanting one.

Given that the need for this Observer functionality is quite common, the C# designers decided to have the language support it directly.

Let’s update FileSystem so that it makes use of these events:

public class FileSystem
{
    public void CreateFile(string path)
    {
        // ...

        // Signals the event OnFileCreated occurred
        FileCreated?.Invoke(path);
    }

    public event Action<string> FileCreated;
}

// Usage:
public void PrintCreatedFile(string path)
{
    Console.WriteLine("File created!");
}

var fs = new FileSystem();
fs.FileCreated += PrintCreatedFile;

Summary

// Defining event
public event Action<T1, T2, ...> EventName;

// Raising the event (calling all functions in the list)
EventName?.Invoke(arg1, arg2, ...);

// Adding function to event list
EventName += MethodName;

Exercises

Exercises can be found on the master branch.

Further Reading