Implementing C# Events

January 7, 2021 —John Koster

A common requirement when designing and implementing software is the ability to respond to certain code conditions, or at key points in an application's execution lifecycle. These conditions and lifecycle points can be thought of as events, where some action occurs and some other piece of code can react to it, or some other action then happens. An area where these types of systems commonly appear are graphical user interface libraries: application developers can subscribe to a "click" event to execute some code when a user clicks a button, for instance.

So how might we go about implementing this in our own code? A trap I see new developers, who are still unfamiliar with C#, is the heavy use of custom delegate management for creating event systems.

C# has a data type called delegate, which are a type of value that understands how to execute a method, and can hold a reference to that method. They are similar to interfaces in that they can define a method signature, but differ in that they can be defined as a class member, can hold a reference to a method, and can also be directly invoked by other code. To demonstrate how they may be used, we will create a simple class that will allow other developers to "hook" into it and do something when a class method is invoked.

In this example, we've defined a delegate Barked which describes a method that has a return type of void, and accepts no arguments. Additionally, we've created a class member OnBarked that can be assigned any method that matches the delegate (a void return type and accepts no parameters). When the Bark instance method is invoked, it will check if anything has been assigned to OnBarked and if so, it will call it. When we run the following code example, we see the text The dog barked in the output:

1using System;
2 
3namespace EventExample
4{
5 
6 public class Dog
7 {
8 public delegate void Barked();
9 
10 public Barked OnBarked;
11 
12 public void Bark()
13 {
14 this.OnBarked?.Invoke();
15 }
16 
17 }
18 
19 class Program
20 {
21 
22 static void Main(string[] args)
23 {
24 var dog = new Dog();
25 
26 dog.OnBarked = DogBarked;
27 
28 dog.Bark();
29 }
30 
31 static void DogBarked()
32 {
33 Console.WriteLine("The dog barked");
34 }
35 
36 }
37 
38}

Great! We've done it! Well, not quite. We have created a simple system that will allow a single method to be invoked by our code. If we attempt to have two methods invoked when our Dog class barks we can start to see where things are falling apart with this strategy:

1class Program
2{
3 
4 static void Main(string[] args)
5 {
6 var dog = new Dog();
7 
8 dog.OnBarked = DogBarkOne;
9 dog.OnBarked = DogBarkTwo;
10 
11 dog.Bark();
12 }
13 
14 static void DogBarkOne()
15 {
16 Console.WriteLine("The dog barked #1");
17 }
18 
19 static void DogBarkTwo()
20 {
21 Console.WriteLine("The dog barked #2");
22 }
23 
24}

When running the above code, we will only see the following output:

1The dog barked #2

Because the OnBarked can only be assigned a single value, our second assignment overwrites the first and thus only the DogBarkTwo() method is called when the dog barks. If the problem is that the assignment overwrites our previous handler, why not keep a list of them? We could easily create a Barked[] list, iterate them all and invoke them right?

While it is true that this is something we could do, we can easily achieve the same thing using C#'s event features. We will keep our delegate Barked, but will change out our Barked implementation to use C#'s event keyword:

1using System;
2 
3namespace EventArticle
4{
5 
6 public class Dog
7 {
8 public delegate void Barked();
9 
10 // Adding the event keyword provides
11 // us with many useful features.
12 public event Barked OnBarked;
13 
14 
15 public void Bark()
16 {
17 this.OnBarked?.Invoke();
18 }
19 
20 }
21 
22 class Program
23 {
24 
25 static void Main(string[] args)
26 {
27 var dog = new Dog();
28 
29 dog.OnBarked += DogBarkOne;
30 dog.OnBarked += DogBarkTwo;
31 
32 dog.Bark();
33 }
34 
35 static void DogBarkOne()
36 {
37 Console.WriteLine("The dog barked #1");
38 }
39 
40 static void DogBarkTwo()
41 {
42 Console.WriteLine("The dog barked #2");
43 }
44 
45 }
46 
47}

The output of our program is now what we would expect, and both methods are invoked when the dog barks:

1The dog barked #1
2The dog barked #2

At first glance, it does not appear that much of anything in our code has changed. Instead of assigning a value to our OnBarked, we are now using a weird += syntax. C# events are something that can be subscribed to and un-subscribed to (or adding some method to the delegate's list of methods to call, or removing a method from that list of methods).

The following snippet adds our method to the internal list of subscribers that will be notified when the dog barks. These are also referred to as event handlers:

1dog.OnBarked += DogBarkOne;

We can also remove event handlers from an event using the -= syntax:

1dog.OnBarked -= DogBarkOne;
2dog.OnBarked -= DogBarkTwo;

#Conclusion

Events and delegates can seem confusing at first, and appear to have many overlapping feature sets at first. C#'s event feature is simply a wrapper around the delegate type, and provides us with some convenient features so that we do not have to implement our own delegate management systems in order to effectively implement a robust event system. For more information, consider reading the following Microsoft documentation: Handle and Raise Events and How to: Raise and Consume Events.

Some absolutely amazing
people

The following amazing people help support this site and my open source projects ♥️
If you're interesting in supporting my work and want to show up on this list, check out my GitHub Sponsors Profile.