Events in C# are a mechanism that classes use to send notifications or messages to other classes. They are a specialized delegate type that we use to notify other classes when something they listen to happens. Events are a vital part of many applications, and a perfect way to decouple and create flexible and extendable applications.

In this article, we are going to learn what events in C# are and how to use events in C# applications.

To download the source code for this article, you can visit our GitHub repository.

We are going to go through these topics:

Let’s start.

What are Delegates?

As we’ve mentioned in the introduction, events in C# are a specialized delegate type.

But what does this exactly mean?

To be able to explain that properly, let’s explain what delegates are briefly. A delegate is a reference to a method. If we call a delegate, the referenced method is called instead. Delegates have all the necessary information to call a method or group of methods with a specific signature and return type. We can pass a delegate as an argument to other methods or we can store it in a structure or a class. We use delegates when we don’t know which method is going to be called at design time but only during runtime.

This might sound complicated but it’s not that hard. Let’s see how a delegate works in a simple example.

To declare a delegate we use the delegate keyword:

delegate void SendMessage(string text);

And then we define a method that this delegate references:

void WriteText(string text)
{
    Console.WriteLine($"Text: {text}");
}

Now we can connect the delegate to the method that has the same signature and return type as the delegate:

SendMesage delegate1 = new SendMessage(WriteText);

There’s also a shorter version:

SendMessage delegate2 = WriteText;

We can do it with an anonymous method:

SendMessage delegate3 = delegate(string text)
    { Console.WriteLine($"Text: {text}"); };

Or even more concisely with a lambda method:

SendMessage delegate4 = text =>  
    { Console.WriteLine($"Text: {text}"); };

The last example is probably something you’re used to seeing most of the time since it’s the most modern approach (C# 3 onwards).

Now we can use these delegates in other methods or store them in structures or classes.

But why is this important? We want to learn about events, right?

What are Events?

Events, being a subset of delegates are defined by using… delegates. To raise an event in C# you need a publisher, and to receive and handle an event you need a subscriber or multiple subscribers.

These are usually implemented as publisher and subscriber classes.

So why do we exactly use events in C#? 

We use events for:

  • decoupling our applications, or rather creating loosely coupled applications
  • implementing mechanisms for communication between objects
  • providing a simple yet effective way to extend the application without changing the existing code

Loosely coupled applications are easily extendable and we want to be able to do that without breaking the existing code.

That said, let’s see how events in C# work in practice by creating an application without events and try to find a problem.

Demo Application without Events

Our application is a simple food ordering service.

So we have the Order class:

public class Order
{
    public string Item { get; set; }
    public string Ingredients { get; set; }
}

A simple class containing the food item name, and ingredients.

Then, we have an actual service that processes our orders:

public class FoodOrderingService
{
    ...
    public void PrepareOrder(Order order)
    {
        Console.WriteLine($"Preparing your order '{order.Item}', please wait...");
        Thread.Sleep(4000);

        _appService.SendAppNotification();
    }
}

As we can see we get the order we need to prepare, simulate a 4-second wait in order to see something’s happening and then we send a notification to the user application that the order is prepared. Of course, this is only the demo so the example is rather simple and we use console output to simulate the notification. This would include many more steps in the real application.

Finally, we have to call the service to order a pizza with extra cheese on top:

static void Main(string[] args)
{
    var order = new Order { Item = "Pizza with extra cheese" };

    var orderingService = new FoodOrderingService();
    orderingService.PrepareOrder(order);

    Console.ReadKey();
}

If we run the application we’ll see that our order is being processed and it takes 4 seconds to do that.

As far as the application goes, this is a rather simple implementation and at the first glance, there is nothing wrong with it.

That is until we decide to extend the application by notifying our users by sending them an email that their order is prepared.

To do that, we’ll have to extend our service class:

public class FoodOrderingService
{
    ...
    public void PrepareOrder(Order order)
    {
        Console.WriteLine($"Preparing your order '{order.Item}', please wait...");
        Thread.Sleep(4000);

        _appService.SendAppNotification();
        _mailService.SendEmailNotification();
    }
}

As you can see, as our requirements changed, we’ve changed the food processing service as well by adding another service call. This can easily introduce bugs into our application, and even if we wrote unit tests we would probably need to revisit and modify them. Also, we need to inject both the app service and the mail service in the FoodOrderingService class to be able to send these notifications and thus creating a tightly coupled application.

So we’ve seen that this approach is not exactly what we’re looking for.

Let’s try to improve this example by using events. This is a perfect scenario to introduce a publisher-subscriber interaction.

We’ll create a simple event using a delegate explicitly first, in order to understand what’s happening behind the curtains. Later on, we’ll learn of an easier way to do the same thing.

Create a Simple Event in C#

Okay, now let’s see how we can create an event and raise it.

We need to:

  • define a delegate
  • define an event that relies on that delegate
  • raise an event

So let’s begin by declaring a delegate:

public class FoodOrderingService
{
    // define a delegate
    public delegate void FoodPreparedEventHandler(object source, EventArgs args);

    public void PrepareOrder(Order order)
    {
        Console.WriteLine($"Preparing your order '{order.Item}', please wait...");
        Thread.Sleep(4000);
    }
}

Our delegate FoodPreparedEventHandler returns void, and delegates typically have two parameters. The first one is for the source, or rather the class that will be publishing the event, and the second one of the type EventArgs, which is any additional data related to the event.

Typically we give the delegate a descriptive name like “FoodPrepared” and then we append “EventHandler” at the end. This way anyone can easily understand what the delegate is for.

Okay, great, now let’s create an event that relies on that delegate:

public class FoodOrderingService
{
    // define a delegage
    public delegate void FoodPreparedEventHandler(object source, EventArgs args);
    // declare the event
    public event FoodPreparedEventHandler FoodPrepared;

    public void PrepareOrder(Order order)
    {
        Console.WriteLine($"Preparing your order '{order.Item}', please wait...");
        Thread.Sleep(4000);
    }
}

As you can see, the event is of the FoodPreparedEventHandler type, and since we’re defining the event that triggers once the operation is finished, we give it a name in the past tense – FoodPrepared. It’s entirely possible to have events that indicate that something is happening in the present moment. For example, we could define another event that indicates that food is in the process of preparation. In that case, we would call it FoodPreparation

Now that we’ve defined our delegate and event, in order to raise an event  we should create an actual handler method:

public class FoodOrderingService
{
    // define a delegage
    public delegate void FoodPreparedEventHandler(object source, EventArgs args);
    // declare the event
    public event FoodPreparedEventHandler FoodPrepared;

    public void PrepareOrder(Order order)
    {
        Console.WriteLine($"Preparing your order '{order.Item}', please wait...");
        Thread.Sleep(4000);

        OnFoodPrepared();
    }

    protected virtual void OnFoodPrepared()
    {
        if (FoodPrepared != null)
            FoodPrepared(this, null);
    }
}

This method should be protected virtual void by convention, and have an “On” prefix in its name. In the method itself, we check if there are any subscribers and if there are, we call the event and pass on this as an argument, which is the current class, and null as the event arguments (for now).

Once our order is completed, we call this method in the PrepareOrder method and we’re ready to create some subscribers.

How to Subscribe to an Event?

Let’s create the AppService first:

public class AppService
{
    public void OnFoodPrepared(object source, EventArgs eventArgs)
    {
        Console.WriteLine("AppService: your food is prepared.");
    }
}

We’ve just created a simple class called AppService, that has one method with a signature same as our event delegate.

Now we can instantiate the service, and subscribe to the event.

static void Main(string[] args)
{
    var order = new Order { Item = "Pizza with extra cheese" };

    var orderingService = new FoodOrderingService();
    var appService = new AppService();

    orderingService.FoodPrepared += appService.OnFoodPrepared;

    orderingService.PrepareOrder(order);

    Console.ReadKey();
}

We can subscribe to the event by using the += operator, and in our case, we’re subscribing to the FoodPrepared event, and using the OnFoodPrepared method of the AppService class to handle the event.

If we run the application right now, we should see that the AppService class handles the event and writes to the console.

This is exactly what we want. We didn’t have to inject the AppService in the FoodOrderingService class or change its implementation in any way.

We can easily extend the application further with another subscriber. Let’s create another class called MailService:

public class MailService
{
    public void OnFoodPrepared(object source, EventArgs eventArgs)
    {
        Console.WriteLine("MailService: your food is prepared.");
    }
}

This class is almost the same as the AppService, we’ve changed the output slightly so we can see what happens in the console.

Now we can add the MailService as a subscriber to the event as well:

class Program
{
    static void Main(string[] args)
    {
        var order = new Order { Item = "Pizza with extra cheese" };

        var orderingService = new FoodOrderingService();
        var appService = new AppService();
        var mailService = new MailService();

        orderingService.FoodPrepared += appService.OnFoodPrepared;
        orderingService.FoodPrepared += mailService.OnFoodPrepared;

        orderingService.PrepareOrder(order);

        Console.ReadKey();
    }
}

If we run the application right now, we’ll be able to see that both subscribers have handled the event:

output

That’s exactly what we want, and on top of that, once again, we didn’t have to change anything in our FoodOrderingService class.

We can extend our application like this indefinitely, and we can even move our FoodOrderingService class to another class library or however else we want.

Extending EventArgs

As we’ve mentioned before, we use EventArgs to send the event data. We can create our own specific event arguments called FoodPreparedEventArgs derived form EventArgs in order to send data to subscribers:

public class FoodPreparedEventArgs : EventArgs
{
    public Order Order { get; set; }
}

And then we can modify our FoodOrderingService:

public class FoodOrderingService
{
    // define a delegage
    public delegate void FoodPreparedEventHandler(object source, FoodPreparedEventArgs args);
    // declare the event
    public event FoodPreparedEventHandler FoodPrepared;

    public void PrepareOrder(Order order)
    {
        Console.WriteLine($"Preparing your order '{order.Item}', please wait...");
        Thread.Sleep(4000);

        OnFoodPrepared(order);
    }

    protected virtual void OnFoodPrepared(Order order)
    {
        if (FoodPrepared != null)
            FoodPrepared(this, new FoodPreparedEventArgs { Order = order });
    }
}

Now we’re sending the order data to subscribers as well. We’ve just replaced EventArgs with FoodPreparedEventArgs and passed on our order information to the subscribers.

So let’s change subscribers to process that data. First, let’s change the AppService:

public class AppService
{
    public void OnFoodPrepared(object source, FoodPreparedEventArgs eventArgs)
    {
        Console.WriteLine($"AppService: your food '{eventArgs.Order.Item}' is prepared.");
    }
}

We’re now using FoodPreparedEventArgs instead of the generic EventArgs, and you can see that we can access our order item name through the eventArgs parameter.

We can change the MailService as well:

public class MailService
{
    public void OnFoodPrepared(object source, FoodPreparedEventArgs eventArgs)
    {
        Console.WriteLine($"MailService: your food '{eventArgs.Order.Item}' is prepared.");
    }
}

Now our output should look like this:

output2

Our services have read the order information and used it to print it on the console.

Use the EventHandler Class to Simplify the Solution

Now let’s improve our solution a bit. The way we’ve been doing things is not something you’ll see every day. We did it so we can have a clear understanding of the underlying mechanisms of events in C#.

Now we can modify the solution to use some modern ways available to us through .NET.

Nowadays, events are created primarily using the EventHandler and EventHandler<TEventArgs> classes. These are specialized wrappers that can simplify event creation.

So let’s modify our FoodOrdering service:

public class FoodOrderingService
{
    public event EventHandler<FoodPreparedEventArgs> FoodPrepared;

    public void PrepareOrder(Order order)
    {
        Console.WriteLine($"Preparing your order '{order.Item}', please wait...");
        Thread.Sleep(4000);

        OnFoodPrepared(order);
    }

    protected virtual void OnFoodPrepared(Order order)
    {
        FoodPrepared?.Invoke(this, new FoodPreparedEventArgs { Order = order });
    }
}

Now instead of the two lines we used for the delegate and event declaration, we’re using EventHandler with FoodPreparedEventArgs. This makes our code much cleaner and more readable. This is probably something that you’re used to seeing when working on other projects with events.

The other improvement is that we can use the conditional Invoke() method to raise an event, avoiding explicit null checks and cleaning up our solution some more.

If we run the application again, nothing should have changed.

Great. 

Unsubscribe to the Event

There’s one more thing left to discuss. As we’ve seen so far, events work on the publish-subscribe principle, and that means once we subscribe to the event, we’re subscribed until the service is alive.

But sometimes our business logic dictates who should be subscribed and who shouldn’t. For example, users might be able to choose to receive only the app notifications or just the mail notifications.

In that case, we should be able to unsubscribe to the event we want. We can do that by using the  -= operator:

orderingService.FoodPrepared -= appService.OnFoodPrepared;
orderingService.FoodPrepared -= mailService.OnFoodPrepared;

Sometimes, when the component is instantiated more than once, we can subscribe to the same event multiple times. That’s why we need to take care of it and dispose of it properly in the Dispose method.

That’s it. Let’s summarize the article.

Conclusion

In this article, we’ve learned the basic concepts in regards to events in C#.

We’ve learned how events work behind the curtains, and how delegates play a big role in event usage.

We’ve created a simple demo application in which we’ve demonstrated how to implement a simple event, raise it, subscribe to it, and unsubscribe to it. 

The last thing we did was to improve the solution using some of the syntactic sugar that .NET provides for us.