DEV Community

Gus Pear 🍐
Gus Pear 🍐

Posted on • Updated on

Understanding C# Events

The title should be more like "Going from delegates to events" because events are literally built on top of delegates(read functions).

If you are not familiar with c# delegates like I wasn't, head over to the post below where I do my best to break down and explain what it is and how it works.

=> https://dev.to/gustavupp/understanding-c-delegates-5cmk

What are events in c#?

Events are a way of enabling two classes(objects) to engage in a two way conversation.

One class can send a "notification" to another, normally to inform about it's current status, that it has just finished completing a task, that is about to begin a task, etc.

Another class can subscribe to that "event" if interested in receiving notifications from that class (usually to execute an action when the notification comes in).

If you are familiar with JavaScript DOM event listeners, I'd say it is just about the same concept.

In short: Do something when something happens.

A notification system like that can easily be build using delegates as callback functions(functions passed as an argument to another function/method), and that is exactly how it used to be done.

Than Microsoft introduced the "event" keyword as a way to reduce the amount of code one would have to write to achieve the same result.

So c# "event" keyword is nothing but a type saver as you will see and understand with this post.

Alright.

Let's start with a code example of a notification system using delegates, and iteratively convert it to use the event keyword and apply code conventions.

Class sending the notifications
Image description

Class calling the sender to receive notifications
Image description

Think of the UserWithDelegates class as a Controller that has methods to add and remove users from the database.

Let's examine the members of this class.

On the very top we simply have two properties and a constructor that takes two arguments and assigns them to their respective properties.

Image description

OBS: all code snippets that will be shown from here onwards will not include the above mentioned props and constructor, so we don't repeat ourselves and focus on the essential members of the class.

The next four members of this class are the core of the code.

Image description

The topmost public delegate void UserStatusHandler(string msg); is a declaration of a custom delegate type called 'UserStatusHandler'.

It specifies the signature of the method that it can point to, in this case methods receiving a single string argument and returning void.

Again if you need a refresher on delegates, check this post => https://dev.to/gustavupp/understanding-c-delegates-5cmk

Then on the next line, we declare a private field named listOfMethodsToCall whose type is the custom delegate type we have just created 'UserStatusHandler'. This is where the list of pointers will be stored.

private UserStatusHandler listOfMethodsToCall;

Note that it can hold multiple pointers, not just one (the reason why I named it listOfMethodsToCall), as long as they all have the same signature.

Moving next we have two registration methods, that are supposed to add and remove methods to and from the listOfMethodsToCall.

Image description

In reality, we didn't need the two registration methods, we could've made listOfMethodsToCall public and the caller could directly add or remove methods to the list. But that would've violated encapsulation and exposed our list to the caller to do anything he wants, even setting it to null or removing all methods in it.

The last two methods are the ones actually invoking the list of delegates.

Think of the first method AddUserToDb as an action that is responsible for adding a new user to the database, and once the user is added it calls the list of delegates that subscribed to be notified, passing in a message to let them know that the user was added. Same concept is for the removeUserFromDd.

Image description

On the caller side we:

  • create the User object and add the two methods we created to the list of methods to be called.

Image description

***The methods are just below
Image description

Note: see the method's signatures matches that of the delegate type we created on the sender.

Now let us convert the core members of this notification example to use the event keywork instead.

Image description

Wait a minute, where are the registration methods and the private delegate object?

They are gone... Microsoft created the event keyword so you can type less code and achieve the same result.

let's take a peek.

public delegate void UserStatusHandler(string msg);

we still declare the delegate type on top (spoiler: it won't even be needed on our next iteration)

And on the next line we have replaced the actual delegate object and both registration methods with a single event declaration:

public event UserStatusHandler listOfMethodsToCall;

Note: You wouldn't call the event listOfMethodsToCall, you would give it a name that represents the event being invoked, in this case, something like: userStatusChanged or similar.

See that the type of the event is still our custom delegate type.

But how are we going to register and unregister methods for this event? And the event is not even private, others would be able to mess with it..

Calm down pal..

What is actually going on under the hood is that the compiler is creating for us our private delegate object and our registration and unregistration methods!

The caller would be this:

Image description

You register methods directly on the event
newUser1.ListOfMethodsToCall += new UserWithEvents.UserStatusHandler(SendWelcomeEmail);

You can also simplify it by just using += the method name and the compiler will infer the type.

newUser1.ListOfMethodsToCall += SendWelcomeEmail;

And now we have the same initial notification system converted from using delegates directly to using the event keyword.

But there is more to it... there are code conventions that we usually follow when passing arguments to events..

There is nothing wrong with passing a string directly like we are doing in this case, it works just fine.. but you know, we have to abide by the standards and conventions when working on a large scale project.

What is the convention when passing arguments to events, you ask?

Grab a coffee and let's talk about it as we improve our current example.

Microsoft recommends and uses in the base class libraries as the first argument passed to an event:

  • System.Object the object that sent the event(User obj in our case)

Second argument:

  • if you are not sending any arguments:
    • System.EventArgs.Empty
  • if you are sending arguments:
    • an object that inherits from System.EventArgs

Let us now change our code to reflect the above pattern.

First, since we are sending a message string as an argument we should create a class that inherits from EventArgs and has a single string field and a constructor.

Image description

In our code class code, we need to change the arguments passed in to all references of listOfMethodsToCall and the signature of the delegate type we created.

From accepting a single string to an object and an object of type UserStatusEventArgs which class we have just created.

let's see what it would look like:

Image description

The caller code won't change aside from the methods you are adding to the listOfMethodsToCall, their signatures will have to change to match the updated delegate signature (object, UserStatusEventArgs)

Image description

And we are done with this iteration.

But there is more...

Given that in the vast majority of cases custom delegates types used for events would take these same two arguments due to the convention (object and EventArgs descendant), microsoft created the Generic EventHandler<T> where T is our EventArgs type.

Let's us once again change our core members to reflect this change.

Image description

Wait, where is the delegate type declaration? not even that is needed?

Not even that... The only thing you need is to pass the EventArgs descendent and the compiler would create the delegate for you since it will know the signature from the type you pass in..

have a look at System.EventHandler<T>

Image description

Easy as... but confusing if you don't understand how we got here in the first place. We are far away from the first example using raw delegates.

Does anything change on the caller code with this last update?
Not really.. have a look

Image description

The only change is that if you want to new up the event handler when adding it to the list you have to new up the EventHandler type.

newUser3.ListOfMethodsToCall += new EventHandler<UserStatusEventArgs>(SendWelcomeEmailWithArgs);

but you can always simplify by just pointing to a method name directly and let the compiler do the rest.

newUser3.ListOfEventsToCall += SendWelcomeEmailWithArgs

To wrap it up, have a look of how the core members of the sender class looked like at first:

Image description

We boiled it down to a single one:

Image description

I hope you now understand a bit better how delegates were used as a notification system and how events are nothing more than a convenient way of writing less code, saving typing and time, as long as you understand what is going on under the hood.

Hope it wasn't too boring, although I sense it might have been.
Enjoy the rest of your day master coder.

Gus out.

Top comments (0)