C# Events and Delegates
In this post I want to take a closer look about what C# events and delegates are and what’s the difference between both. Further we will see them in action in a little sample project.
Delegates can be used without events, but events on the other side depends on a delegate and cannot be used without.
Below both are explained in the following article from Microsoft.
Handle and raise events
https://learn.microsoft.com/en-us/dotnet/standard/events/
You will understand how they work and when to use them much better when you see them in action in a sample project further below.
Delegates
A delegate is a type that holds a reference to a method. A delegate is declared with a signature that shows the return type and parameters for the methods it references, and it can hold references only to methods that match its signature. A delegate is thus equivalent to a type-safe function pointer or a callback. A delegate declaration is sufficient to define a delegate class.
A delegate is a type that holds a reference to a method. A delegate is declared with a signature that shows the return type and parameters for the methods it references, and it can hold references only to methods that match its signature. A delegate is thus equivalent to a type-safe function pointer or a callback. A delegate declaration is sufficient to define a delegate class.
.NET provides the EventHandler and EventHandler<TEventArgs> delegates to support most event scenarios. Use the EventHandler delegate for all events that don’t include event data. Use the EventHandler<TEventArgs> delegate for events that include data about the event. These delegates have no return type value and take two parameters (an object for the source of the event and an object for event data).
Delegates are multicast, which means that they can hold references to more than one event-handling method. For more information, see the Delegate reference page. Delegates provide flexibility and fine-grained control in event handling. A delegate acts as an event dispatcher for the class that raises the event by maintaining a list of registered event handlers for the event.
For scenarios where the EventHandler and EventHandler<TEventArgs> delegates don’t work, you can define a delegate. Scenarios that require you to define a delegate are rare, such as when you must work with code that doesn’t recognize generics. You mark a delegate with the C# delegate and Visual Basic Delegate keyword in the declaration. The following example shows how to declare a delegate named ThresholdReachedEventHandler:
Source: https://learn.microsoft.com/en-us/dotnet/standard/events/#delegates
Delegates Example
As mentioned to the beginning, delegates can be used also without events but events always depends on a delegate and cannot be used alone.
To get a better understanding about how delegates work, I will show below a little example which is originated from the following book about C#8, unfortunately only available in german.
C# 8 mit Visual Studio 2019: Das umfassende C#-Handbuch
https://www.amazon.de/Visual-Studio-2019-Objektorientierung-Programmiertechniken/dp/383626458
using System; namespace Delegate { public delegate double CalculateHandler(double value1, double value2); class Program { static void Main(string[] args) { // variable from typ delegate CalculateHandler calculate; do { // Enter the operands Console.Clear(); Console.Write("Enter the first operand: "); double input1 = Convert.ToDouble(Console.ReadLine()); Console.Write("Enter the second operand: "); double input2 = Convert.ToDouble(Console.ReadLine()); // Select which operation should be executed Console.Write("Operation: addition - (A) or subtraction - (S)? "); string wahl = Console.ReadLine().ToUpper(); if (wahl == "A") calculate = new CalculateHandler(Mathematics.Add); else if (wahl == "S") calculate = new CalculateHandler(Mathematics.Subtract); else { Console.Write("Invalid input"); Console.ReadLine(); return; } // Call the operation 'Add' or 'Subtract' by using the delegate double result = calculate(input1, input2); Console.WriteLine("----------------------------------"); Console.WriteLine($"Result = {result}nn"); Console.WriteLine("Press F12 to close the console."); } while (Console.ReadKey(true).Key != ConsoleKey.F12); } } class Mathematics { public static double Add(double x, double y) { return x + y; } public static double Subtract(double x, double y) { return x - y; } } }
The delegate we declare to the beginning.
public delegate double CalculateHandler(double value1, double value2);
In the class Mathematics two static methods are defined for the operation add and subtract which both will be called from the default console app Main method.
Here the user first will be asked which operation later should be executed, addition or subtraction, depending on the input either the Mathematics.Add method will be added to the delegate or the Mathematics.Subtract method.
Before adding the method to the delegate, we first declared the variable calculate from type CalculateHandler (our delegate). So calculate now is a delegate which can point to a method which is expecting two double parameters and returns a double value.
So far the the calculate delegate doesn’t know which method it should call later.
Therefore we first need to create a new instance by using the following command.
calculate = new CalculateHandler(Mathematics.Add);
or
calculate = new CalculateHandler(Mathematics.Subtract);
The instantiation of a delegate is like creating a new instance of a class by using the new operator and the delegate type.
From now on, the delegate knows which method it should call later. To finally call and execute the method, we need to call the delegate and pass both double values like shown below.
double result = calculate(input1, input2);
You can also simplify the instantiation of the delegate like shown below.
CalculateHandler calculate = new CalculateHandler(Mathematics.Addition); // simplified CalculateHandler calculate = Mathematics.Add;
This example is just to demonstrate how delegates work, the code below without using delegates will also work of course by executing the methods directly within the if statement.
double result; if(wahl == "A") result = Mathematics.Add(input1, input2); else if(wahl == "S") result = Mathematics.Subtract(input1, input2);
So delegates can be used if at the time you programming the code, the method which should be executed later is unknown and depends on the users input during runtime.
Events
An event is a message sent by an object to signal the occurrence of an action. The action can be caused by user interaction, such as a button click, or it can result from some other program logic, such as changing a property’s value. The object that raises the event is called the event sender. The event sender doesn’t know which object or method will receive (handle) the events it raises. The event is typically a member of the event sender; for example, the Click event is a member of the Button class, and the PropertyChanged event is a member of the class that implements the INotifyPropertyChanged interface.
The following example shows how to declare an event named ThresholdReached. The event is associated with the EventHandler delegate and raised in a method named OnThresholdReached.
class Counter { public event EventHandler ThresholdReached; protected virtual void OnThresholdReached(EventArgs e) { ThresholdReached?.Invoke(this, e); } // provide remaining implementation for the class }
Source: https://learn.microsoft.com/en-us/dotnet/standard/events/#events
Sample Project to demonstrate Events and Delegates in Action
Below we will see how events and delegates works by using a simple project.
For this sample project I am using as template a modified version of the video below from Mosh Hamedani, which will explain in a great way events and delegates, I can really recommend to watch it.
Events and Delegates
https://www.youtube.com/watch?v=jQgwEsJISy0
Therefore I will create a new C# console app which I will name EventsAndDelegates.
The app finally should encrypt files and upload them to a cloud service.
So far the app just includes the following three classes.
The File.cs class just includes a property with the name of the file.
The FileEncryption.cs class includes a method which will simulate encryption of the file it gets passed by its parameter.
The Program.cs class has a main method which will first instantiate the File and FileEncryption class and then is calling the Encrypt() method from the FileEncryption class and passing the file object to it.
If we debug the app by pressing F5, we get the following output.
Now we want to raise an event when the encryption of the file is finsihed. That event then can be subscribed by other classes to react on by executing a method and in context of events here called event handler.
To raise an event we first need to create in the class which will raise the event, a delegate and event. By convention you should use for the name of the delegate the name from the event and as suffix EventHandler. The event is based on the delegate and therefore is named same as the delegate.
public delegate void FileEncryptedEventHandler(object source, EventArgs args);
public event FileEncryptedEventHandler FileEncrypted;
The delegate needs by convention two parameters, the first is an object which is the source of the event and the second is EventArgs which can be additional information about the object.
Now to raise the event we need to create a method which should be by convention protected virtual void. The name for this method should have the prefix On and <Name of the Event>.
OnFileEncrypted()
The method above will raise the event and simplify the thread-safe delegate invocation with the null-conditional operator (?.), which will only evaluate the operation when it returns non-null.
thread-safe because it doesn’t matter what other threads will do with this event, the code will not throw a NullReferenceException.
In case the event is not subscribed by any other classes (subscriber or event receiver) and therefore not handeled by them, normally a NullReferenceException is thrown by the method because the event doesn’t have any recipients. For that case we can use the null-conditional operator (?.), to first check if the event is subscribed by some classes or not before it is raised.
The event doesn’t need to be implemented by other classes, it’s just an option.
The null-conditional operator (?.) can be also used for properties to check againt a null value like the example below.
// Using null-conditional operator static void DoSomething(string text) { Console.WriteLine($"Length: {text?.Length}"); } // Using the old way static void DoSomething(string text) { if (text != null) Console.WriteLine($"Length: {text.Length}"); }
String interpolation using $
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/tokens/interpolated
One more thing when using the null-conditional operator (?.) for delegates, by using it you have to explicitly call the method Invoke from the delegate, using the old way with the if statement will call it implicit.
// using the if statement if (FileEncrypted != null) InvalidMeasure(); // using the null-conditional operator FileEncrypted?.Invoke(this, EventArgs.Empty);;
More about access operators, like the null-conditional operator (?.), you will find in the following link.
Member access operators and expressions – the dot, indexer, and invocation operators.
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/member-access-operators
The parameters when invoking the event are the ones we previously defined in the delegate. The first is an object which is the source of the event, here the class itself and therefore the keyword this. The second are EventArgs which can include additional information. So far I don’t want to send additional information and therefore we can use EventArgs.Empty.
protected virtual void OnFileEncrypted() { FileEncrypted?.Invoke(this, EventArgs.Empty); }
The above simplified thread-safe delegate invocation is equivalent to the following code below.
EventHandler handler = this.FileEncrypted; if (handler != null) { handler(this, EventArgs.Empty); }
Next I will create another class named MailService.cs which will send an email when the file is finished encryption. This class will then subsribe the FileEncrypted event from the FileEncryption.cs class. So when the event is raised by the FileEncryption.cs class, the MailService.cs class will send out an email.
Note! The method in the MailService.cs class is our Event handler and the signature must be identical with the signature from the delegate we defined in the FileEncryption.cs class. So the method must include the object source and EventArgs e parameter.
To subscribe the event for the MailService.cs class, I will go back to the main method in my Program.cs class.
Here I will first instantiate the new MailService.cs class and then by using the following command,
fileEncryption.FileEncrypted += mailService.OnFileEncrypted;
we subscribe the FileEncrypted event from the FileEncryption.cs class and if it is raised, the OnFileEncrypted event handler should be executed in the MailService.cs class.
So what happens when we subscribe the event handler with the following code line as shown above.
fileEncryption.FileEncrypted += mailService.OnFileEncrypted;
This line will assign the start address for the mailService.OnFileEncrypted() method in the MailService.cs class, to the delegate FileEncryptedEventHandler from the FileEncryption.cs class. This information is stored in the instance from the FileEncryption.cs class and used to find the method in the subscriber class, here MailService.cs.
So in a nutshell, the delegate from the publisher class, will hold a pointer (start address) to the method in the subscriber class, which should be executed when the corresponding event in the publisher class is raised.
If we debug the app again by pressing F5, this time we get the following output. After encryption by simulating the process with the Thread.Sleep(4000); command, the event handler from the MailService.cs class is executed with its Console.WriteLine command.
So far we just subcribed one event handler for the FileEncrypted event in the FileEncryption.cs class, which will send out an email after the file is encrypted.
We can also subscribe multiple event handler for an event.
Below I will add another class which should upload the encrypted file to a cloud service for example.
The new class UploadService.cs we also first instantiate in our Program.cs class and then we can subscribe the event handler from to upload the file for the same raised event when the file is encrypted in the FileEncryption.cs class
Debugging the app another time by pressing F5, we get the following output this time.
Subscribing multiple event handler for an event is also called Multicast Delegate. In the code below, the event FileEncrypted from the FileEncryption.cs class is nothing else then an encapsulated delegate to which two methods (event handler) are assigned.
fileEncryption.FileEncrypted += mailService.OnFileEncrypted; fileEncryption.FileEncrypted += uploadService.OnFileEncrypted;
To remember here the code from the FileEncryption.cs class below and its delegate named FileEncryptedEventHandler.
This delegate is now holding a reference and start address to the OnFileEncrypted method (event handler) from the MailService.cs class and UploadService.cs class.
namespace EventsAndDelegates { public class FileEncryption { public delegate void FileEncryptedEventHandler(object source, EventArgs args); public event FileEncryptedEventHandler FileEncrypted; ..... .....
Below is a further example for Mulitcast Delegates.
using System; namespace Delegate { public delegate void MyDelegate(); internal class Program { static void Main(string[] args) { MyDelegate del = new MyDelegate(DoSomething); del += new MyDelegate(DoSomethingMore); // Excecute Multicast Delegate del(); Console.ReadLine(); } public static void DoSomething() { Console.WriteLine("Within the method 'DoSomething'"); } public static void DoSomethingMore() { Console.WriteLine("Within the method 'DoSomethingMore'"); } } }
Now back to our sample project, it would be nice if we also get an email message after the encrypted file is successfully uploaded to the cloud service.
To implement this feature we also need to create in the UploadService.cs class a delegate, event and a method which will raise the event like before in the FileEncryption.cs class.
We also need to add a method (event handler) in our MailService.cs class which should send out the email after the event is raised in the UploadService.cs class.
We also need to subscribe the event for the MailService.cs class, therefore I will go back to the main method in my Program.cs class.
The MailService.cs class is already instanced and we subscribe the new event with the following code line.
uploadService.FileUploaded += mailService.OnFileUploaded;
Finally the output will looks like this.
Do you remember when we created our first delegate in the FileEncryption.cs class?
public class FileEncryption { public delegate void FileEncryptedEventHandler(object source, EventArgs args); public event FileEncryptedEventHandler FileEncrypted; ...... ......
The delegate needs by convention two parameters, the first is an object which is the source of the event and the second is EventArgs which can be additional information about the object.
So far we used EventArgs.Empty and therefore didn’t send any additional information about the file itself which is encrypted and uploaded to the subscribers (event receivers).
EventArgs Class
https://learn.microsoft.com/en-us/dotnet/api/system.eventargs?view=net-7.0EventArgs.Empty Field
https://learn.microsoft.com/en-us/dotnet/api/system.eventargs.empty?view=net-7.0
In order to send additional information when the event is raised about the file itself to the subscribers, in our case the MailService.cs and UploadService.cs class, we will first create a new custom event data class named FileEventArgs.cs.
Here we can see again our classes which subscribed the FileEncrypted and FileUploaded event.
Our new custom event data class FileEventArgs.cs derives from the EventArgs class and will include any other additional data which we want to send to the subscribers of the event.
The class here will just include a public property which stores the file object.
To use the new custom event data class, I will first change the EventArgs parameter in the FileEncryption.cs class to the new FileEventArgs parameter as shown below.
Because of changing the parameter in the delegate, I also have to change it in the method which is raising the event plus the parameters to submit it as shown below.
I will do the same for the UploadService.cs class as shown below.
Finally I need to change the parameters in the MailService.cs class which subcribed both events (FileEncrypted and FileUploaded) and is receiving the additional information about the events.
And don’t forget, our UploadService.cs class is not just an event publisher (FileUploaded event), but also an event receiver (subscriber), the class subscribed also the FileEncrypted event from the FileEncryption.cs class in order to upload the file after encryption has finished.
So also the UploadService.cs class is receiving the additional information about the event and not just the MailService.cs class.
Debugging the app another time by pressing F5, we get the following output which now also shows the file name which is encrypted and uploaded.
One main point to understand about events, is that by using them you can build loosely coupled classes which is one of the benefits in the object oriented programming paradigm.
Events are just one technique to realize loosely coupled classes but there are also more.
Loose coupling
https://en.wikipedia.org/wiki/Loose_coupling
You can keep the objects loosely coupled and by changing one class, you don’t need to rewrite other classes which depends on and should react on the change.
In order to better understand the advantage of loosely coupled classes, I will show the difference in a little example below between using events or not.
If we stick to our previous sample project, we can also implement the feature with sending an email after the file is encrypted without events and by just calling the method to send an email from the MailService.cs class directly like shown below.
FileEncryption.cs class
public void Encrypt(File file) { Console.WriteLine("Encrypting File ..."); Thread.Sleep(4000); mailService.Send(new Mail()); }
So here the class itself which is encrypted the file, will call afterwards directly a method from another class to send out an email instead just raising an event that encryption of the file is finished.
This will also work of course like when using an event and it is even faster to code in case you don’t use this class for other projects. But now imagine if you want to add another feature when encryption of the file is finished, like before when uploading the file to a cloud service is finished.
In that case you need to rewrite the class and add another method call like shown below which will send out an email after uploading is finished.
public void Encrypt(File file) { Console.WriteLine("Encrypting File ..."); Thread.Sleep(4000); mailService.Send(new Mail()); uploadService.Send(new Mail()); }
Finally you also need to recompile the class and eventually other classes which depends on this class.
If you publish instead simply an event when encryption is finished, other classes can subscribe this event to react on and execute further code based on that event.
So for our sample project we can implement further features in separate classes which gets executed when an event is raised in the source class.
By using them we doesn’t need to rewrite and recompile our FileEncryption.cs class but can anytime extend the features of our application.
Mosh Hamedani will explain this in a great way in its video below right at the beginning.
Events and Delegates
https://www.youtube.com/watch?v=jQgwEsJISy0
Simplify the Event declaration
Finally I want to show how you can simplify the event declaration where we not have to create a separate delegate and event declaration as shown below in our FileEncryption.cs class previously.
public delegate void FileEncryptedEventHandler(object source, FileEventArgs args); public event FileEncryptedEventHandler FileEncrypted;
We can now just use one delegate type which .NET provides and is called EventHandler. This delegate type comes in two forms, EventHandler and EventHandler<TEventArgs>.
Use the EventHandler delegate for all events that don’t include event data. Use the EventHandler<TEventArgs> delegate for events that include data about the event. These delegates have no return type value and takes two parameters (an object for the source of the event and an object for event data).
Source: https://learn.microsoft.com/en-us/dotnet/standard/events/#delegates
So in our case we are using so far the following delegate which also includes data about the event by using the custom FileEventArgs class and parameter.
public delegate void FileEncryptedEventHandler(object source, FileEventArgs args);
This delegate we can now replace with the mentioned EventHandler and also including the EventArgs EventHandler<TEventArgs>.
So we now just need to create an EventHandler and doesn’t need to first create a delegate and then declare the event based on that delegate.
The same I will do for the UploadServices.cs class. By the way Visual Studio is already suggesting this as shown below and I just need to press Tab to accept.
Finally I will test if it works. Looks good!
C# Access Modifiers
All types and type members have an accessibility level. The accessibility level controls whether they can be used from other code in your assembly or other assemblies. An assembly is a .dll or .exe created by compiling one or more .cs files in a single compilation. Use the following access modifiers to specify the accessibility of a type or member when you declare it:
- public: The type or member can be accessed by any other code in the same assembly or another assembly that references it. The accessibility level of public members of a type is controlled by the accessibility level of the type itself.
- private: The type or member can be accessed only by code in the same class or struct.
- protected: The type or member can be accessed only by code in the same class, or in a class that is derived from that class.
- internal: The type or member can be accessed by any code in the same assembly, but not from another assembly. In other words, internal types or members can be accessed from code that is part of the same compilation.
- protected internal: The type or member can be accessed by any code in the assembly in which it’s declared, or from within a derived class in another assembly.
- private protected: The type or member can be accessed by types derived from the class that are declared within its containing assembly.
Virtual Keyword
The virtual keyword is used to modify a method, property, indexer, or event declaration and allow for it to be overridden in a derived class. For example, this method can be overridden by any class that inherits it
public virtual double Area() { return x * y; }
Source: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/virtual
Links
Handle and raise events
https://learn.microsoft.com/en-us/dotnet/standard/events/Events and Delegates
https://www.youtube.com/watch?v=jQgwEsJISy0