Events

Events

The Crash Course

  • Events are a simple and easy way for one section of code to notify other sections that a certain condition has been met.
  • Events rely on delegates to do their work, so a basic understanding of how delegates work is very helpful.
  • To create an event inside of a class, you would use code similar to this: public event EventHandler PointChanged; In this case, EventHandler is the name of the delegate event handlers must use, and PointChanged is the name of the event.
  • You raise events by first checking to make sure that there are event handlers attached, then raising the event: if(PointChanged != null) { PointChanged(this, EventArgs.Empty); }
  • To attach a method (it must meet the requirements of the delegate being used) to an event, you use the += operator: PointChanged += PointChangedHandler;
  • To detach a method from an event, you use the -= operator: PointChanged -= PointChangedHandler;

Introduction

One of the cool things about C# is a feature called events. Events allow classes to notify others when something specific occurs. This is extremely common in GUI-based applications, where there are things like buttons and checkboxes. These things have the ability to indicate when something of interest happens, like the button being pressed or the user checking the checkbox, and other classes will be notified of the change and can handle the event in whatever way they need to.

In the previous tutorial, we talked about delegates. Delegates are going to be a key part of events, so if you don't have at least a basic understanding of delegates, you should jump back there and learn a bit about them. We'll be using delegates to receive notifications from events.

Defining an Event

Our first step will be to create an event. Let's say we have a class representing a point in two dimensions. So it has an x-coordinate and a y-coordinate. This class might look like this:

public class Point
{
    private double _x;
    private double _y;
 
    public double X
    {
        get { return _x; }
        set { _x = value; }
    }
 
    public double Y
    {
        get { return _y; }
        set { _y = value; }
    }
}

Now let's suppose we want to be able to notify others whenever the point is changed.

We'll need to add an event to our Point class. The first step of adding an event is to pick a delegate type for the event. This is the form we expect handlers/listeners of the event to take.

The simplest delegate type we could imagine is one that is void and has no parameters. We could easily define a delegate type for this (it would look something like public delegate void BasicHandler();), but we're in luck because the Base Class Library, to which every C# program has access, has already defined a delegate type like this that we can already use. It is Action.

Now that we know what delegate type we want to use, we'll add our event definition to our class like this:

public class Point
{
    private double _x;
    private double _y;
 
    public double X
    {
        get { return _x; }
        set { _x = value; }
    }
 
    public double Y
    {
        get { return _y; }
        set { _y = value; }
    }
 
    public event Action Changed;
}

The last line within the class is the event definition. Events can have any access modifier applied, so we could make this private or protected if we wanted, but events are typically meant to be used by others, so most events are public. Following this is the event keyword, which indicates that we're making an event. Without that, it would be interpreted as a plain old field with a delegate type. Then comes the delegate type we're using (Action), and finally, the event's name (Changed).

Next, we want to raise the event when it is time. You can only raise an event from within the class that defines it.

The conditions that lead to an event happening will depend on the event. In this case, it makes sense to raise (or fire) the event whenever a new value is set for the point's x- or y-coordinate. That happens within the setters for X and Y, so that's where our new code will go.

Raising an event is not hard, and the simplest solution, which has a limitation, is done with the following:

    public double X
    {
        get { return _x; }
        set
        {
            _x = value;
            Changed();
        }
    }

That Changed(); part looks like a method call—and it is because an event is literally just the same as running a delegate. This will cause all event handlers (there can be more than one!) to be notified about the change.

However, before we move on to how you go about subscribing to an event, we need to talk through a few other things.

A second flavor for raising an event is the following:

Changed.Invoke();

This is functionally identical, but it treats the delegate as an object and calls its Invoke method rather than treating it like a method directly. Even though this version is longer, this flavor turns out to be more common for events, for the reason we'll talk about next.

There's one big thing we haven't accounted for. We've mentioned null in the past, and it is about to make another appearance now. If an event has nobody listening for it, the event itself will be null. An attempt to invoke an event or to call the Invoke method on the event will cause a NullReferenceException. To get around this, we need to check to see if the event is null before raising it.

In theory, that could be done with something like this:

Action changed = Changed;
if (changed != null)
{
    changed.Invoke();
}

This code copies the reference in Changed to a temporary local variable, which is done to ensure that Changed doesn't become null between our null check and the Invoke call.

But this pattern is so simple that C# provides a shorthand way to check for null and only continue down the chain if it is not null:

Changed?.Invoke();

This code is functionally the same as before but does it in a single line. Note that the ?. can be used anywhere you call a member on an object reference that might be null. It isn't just limited to events. But events are a common place to use it.

Subscribing to Events

We've now got an event defined in our class, and we're raising it when needed.

The next step is for other code that cares about the event to subscribe to the event, so it can be informed when it happens.

A very simple example of this might look like this:

Point a = new Point() { X = 3, Y = 2 };
a.Changed += HandlePointChanged;
 
a.X = 5;
a.Y = 22; // Make sure you raise the event when `Y` changes too!
 
static void HandlePointChanged()
{
    Console.WriteLine("The point changed.");
}
 
public class Point { ... }

The part that says a.Changed += HandlePointChanged; is the part that subscribes to the event. The += is the key part. We're telling it to add the method HandlePointChanged to the set of methods to call when the event is raised.

This code should see the text "The point changed." appear twice on the screen, once when X is set to 5 and once when {{Y} is set to 22.

Unsubscribing from an Event

In our short program above, we can survive without ever unsubscribing from the point's Changed event. But in most situations, you'll want to unsubscribe from the event when you're done. That is as simple as the following:

a.Changed -= HandlePointChanged;

We use -= to unsubscribe in a way similar to subscribing.

Other Event Types

We've been using the simplest delegate type for our events so far, but we can use any delegate type. For example, it could make sense for our Changed event to send over the point that changed. That way, handlers could subscribe to many point changes and still be able to know which of several changed. We can manage that by using a delegate type that includes a Point as a parameter.

Once again, we could define a delegate type to do this (public delegate void PointHandler(Point source); or something), but there's also something in the Base Class Library that we can use for this. It is the generic delegate type Action<T>. Or, in this case, being more specific, we'll use Action<Point>, which is for methods that have a void return type and a single Point parameter.

So we upgrade our event to:

public event Action<Point> Changed;

And then raise it like this:

Changed?.Invoke(this);

Since this is a delegate type that needs a parameter, that will go in the parameter list for Invoke. We're using this because it is a reference to the current object and is exactly what our event listeners are hoping to know when they handle the event.

The code that subscribes (and unsubscribes) does not need to change, but we do need the actual handler method to include a Point parameter for things to work:

static void HandlePointChanged(Point source)
{
    Console.WriteLine("The point is now at (" + source.X + ", " + source.Y + ")");
}

We can supply any data we want as parameters in an event handler as needed. We're not limited to just supplying the source, nor are we limited to a single parameter. We'd just need a delegate type for whatever we're doing.

What's Next?

Events can be extremely powerful because they are a convenient way for one section of code to notify other sections that something specific has happened. This is a very common need, so events are a great solution in many cases.

The next thing we're going to tackle is a quick introduction to threading in C#. This means will allow us to run multiple sections of code at the same time, speeding things up.