You noticed that throughout pretty much all the previous lessons where I discussed events, I have been using Action as the delegate type for the event. Obviously, you can use whatever delegate type you’d like, but in the vast majority of the cases, by convention (and only by convention!) programmers use a delegate type named EventHandler. Let’s take an example of how we used events up until now:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
using System; namespace HelloWorld { public class TrainSignal { public event Action TrainsArecoming; public void TriggerTrainSignal() { // call all subscribers if (TrainsArecoming != null) // make sure there are subscribers! TrainsArecoming(); // trigger the event } } public class Program { static void Main() { Console.Read(); } } } |
We have a class called TrainSignal, which hosts an event named TrainsAreComing, and a public method named TriggerTrainSignal(), which we use to invoke the event and call any subscriber method. Now, I will change the delegate type of the event from Action to EventHandler:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
using System; namespace HelloWorld { public class TrainSignal { public event EventHandler TrainsArecoming; public void TriggerTrainSignal() { // call all subscribers if (TrainsArecoming != null) // make sure there are subscribers! TrainsArecoming(); // trigger the event } } public class Program { static void Main() { Console.Read(); } } } |
Nothing really changed, except for the fact that the compiler will now underline our event invocation with a squiggly red line, denoting an error: Error CS7036 There is no argument given that corresponds to the required formal parameter ‘sender’ of ‘EventHandler’. In order to understand what this error means and why is it showing up in the first place, we only have to navigate to the declaration of EventHandler, and look at its signature:
It is easy to notice that it is just a delegate, but it takes two arguments, unlike Action, which took none: an object named sender and an EventArgs named e; this is the only reason why we have an error, the compiler expects us to provide these two parameters when we invoke our event.
The first parameter is self-explanatory, we already learned about object variable type. But EventArgs is new to us. Let’s navigate to its definition too:
You can really see that this class is mostly empty: it only keeps a static reference to itself, but other than that, it’s empty. So, really, don’t let all this scare you, it’s nothing to it, and I’ll show it in a moment.
Returning to our original code, we know that the event invocation expects two parameters, because that’s what EventHandler expects. The first parameter, sender, it is always the object that is invoking the event, while the second parameter, e, is just some extra details about the event (which can be anything), if you wish to pass them.
To correct our error, let’s pass these two parameters. Since I just explained that the first parameter is always the object that is invoking the event, we will use the this C# keyword as the sender parameter, because this refers to the TrainSignal class instance we are currently using, and TrainSignal IS the object that is invoking the TrainsAreComing event. As for the second parameter, since I said that we can use it to pass additional information, we could just pass null, because at this point we don’t really have any additional information we could pass, but passing null as parameters is generally a very bad idea, for various reasons (passing null IS NOT same thing as passing nothing at all). We could also pass a new instance of EventArgs, like this: TrainsArecoming(this, new EventArgs());, but if you remember, we just had a look at the EventArgs class, and we noticed it has a static instance of itself, called Empty, which we can use here, to indicate we do not have any additional information to pass: TrainsArecoming(this, EventArgs.Empty);. EventArgs.Empty is basically just a newed up empty EventArgs for us to use.
However, let’s also use this second parameter, let’s actually pass some additional information, just so you understand how this works, and why is it useful. For start, let’s create an enum named TrainSignalType, with two elements, AudioSignal and VisualSignal, and then add a property of this enumeration type to our TrainSignal class, named SignalType:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
using System; namespace HelloWorld { public enum TrainSignalType { AudioSignal, VisualSignal } public class TrainSignal { public event EventHandler TrainsArecoming; public TrainSignalType SignalType { get; set; } public void TriggerTrainSignal() { // call all subscribers if (TrainsArecoming != null) // make sure there are subscribers! TrainsArecoming(this, EventArgs.Empty); // trigger the event } } public class Program { static void Main() { Console.Read(); } } } |
Next, let’s declare a few instances of TrainSignal class, and give them each some TrainSignalType values:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
using System; namespace HelloWorld { public enum TrainSignalType { AudioSignal, VisualSignal } public class TrainSignal { public event EventHandler TrainsArecoming; public TrainSignalType SignalType { get; set; } public void TriggerTrainSignal() { // call all subscribers if (TrainsArecoming != null) // make sure there are subscribers! TrainsArecoming(this, EventArgs.Empty); // trigger the event } } public class Program { static void Main() { TrainSignal trainSignalAudio = new TrainSignal() { SignalType = TrainSignalType.AudioSignal }; TrainSignal trainSignalVisual = new TrainSignal() { SignalType = TrainSignalType.VisualSignal }; Console.Read(); } } } |
Next, we need a method that can handle the event, or, as we already learned, a subscriber method that gets called when the event is invoked:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
using System; namespace HelloWorld { public enum TrainSignalType { AudioSignal, VisualSignal } public class TrainSignal { public event EventHandler TrainsArecoming; public TrainSignalType SignalType { get; set; } public void TriggerTrainSignal() { // call all subscribers if (TrainsArecoming != null) // make sure there are subscribers! TrainsArecoming(this, EventArgs.Empty); // trigger the event } } public class Program { static void Main() { TrainSignal trainSignalAudio = new TrainSignal() { SignalType = TrainSignalType.AudioSignal }; TrainSignal trainSignalVisual = new TrainSignal() { SignalType = TrainSignalType.VisualSignal }; Console.Read(); } private static void StopTheCar(object sender, EventArgs e) { } } } |
You can notice that unlike the example in the previous lessons, StopTheCar() now also takes two parameters of the same type as our event delegate, because, as we learned, we can only assign methods with same signature as the delegate to which we are assigning them. Some of you might also notice the fact that when we are invoking TrainsAreComing event, we are passing this as the sender parameter, and this in that context refers to a TrainSignal type. However, in our event handler method StopTheCar(), we are using object as the type of sender. And this is only because we can’t really know what type the class that will invoke the event will be, so, we are using object because object is the parent of all other types in .NET, they all inherit from it, therefor, even if the compile type of sender is object, the runtime type will be converted to TrainSignal. For this reason, we are allowed to cast our sender parameter back to the type it actually is (if we know beforehand what this type is), in our case, TrainSignal:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
using System; namespace HelloWorld { public enum TrainSignalType { AudioSignal, VisualSignal } public class TrainSignal { public event EventHandler TrainsArecoming; public TrainSignalType SignalType { get; set; } public void TriggerTrainSignal() { // call all subscribers if (TrainsArecoming != null) // make sure there are subscribers! TrainsArecoming(this, EventArgs.Empty); // trigger the event } } public class Program { static void Main() { TrainSignal trainSignalAudio = new TrainSignal() { SignalType = TrainSignalType.AudioSignal }; TrainSignal trainSignalVisual = new TrainSignal() { SignalType = TrainSignalType.VisualSignal }; Console.Read(); } private static void StopTheCar(object sender, EventArgs e) { TrainSignal trainSignal = sender as TrainSignal; // similar to TrainSignal trainSignal = (TrainSignal)sender; } } } |
Obviously, we can also access the SignalType properties of these TrainSignal instances, after we subscribe our StopTheCar() method to the TrainsAreComing event of any of the two TrainSignal instances, and we call the TriggerTrainSignal() method, that is actually triggering the event:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
using System; namespace HelloWorld { public enum TrainSignalType { AudioSignal, VisualSignal } public class TrainSignal { public event EventHandler TrainsArecoming; public TrainSignalType SignalType { get; set; } public void TriggerTrainSignal() { // call all subscribers if (TrainsArecoming != null) // make sure there are subscribers! TrainsArecoming(this, EventArgs.Empty); // trigger the event } } public class Program { static void Main() { TrainSignal trainSignalAudio = new TrainSignal() { SignalType = TrainSignalType.AudioSignal }; trainSignalAudio.TrainsArecoming += StopTheCar; TrainSignal trainSignalVisual = new TrainSignal() { SignalType = TrainSignalType.VisualSignal }; trainSignalVisual.TrainsArecoming += StopTheCar; trainSignalAudio.TriggerTrainSignal(); trainSignalVisual.TriggerTrainSignal(); Console.Read(); } private static void StopTheCar(object sender, EventArgs e) { TrainSignal trainSignal = sender as TrainSignal; // similar to TrainSignal trainSignal = (TrainSignal)sender; Console.WriteLine("The instance signal type is " + trainSignal.SignalType); } } } |
The output would look like this:
So, this could be a way of using some arbitrary data (the signal type) of our TrainSignal instances. Another way we can do this is by using the second parameter of EventHandler, which I said it can be empty, if we don’t need to pass any additional information (using EventArgs.Empty). Let’s just assume for the sake of this lesson that we do want to pass this extra information, let’s say we want to pass exactly this SignalType property of our instances. If you would take a look at EventHandler signature, you will notice that it comes in two forms, one of which is a generic form:
That means that I can modify my declaration of EventHandler to pass my own version of EventArgs type, as opposed to using the default EventHandler declaration, when EventArgs is used. I will declare another class named TrainSignalEventArgs (by convention, a custom EventArgs type’s name is suffixed with the EventArgs word), and I will use this class as the EventArgs parameter of EventHandler generic version:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
using System; namespace HelloWorld { public class TrainSignalEventArgs { } public enum TrainSignalType { AudioSignal, VisualSignal } public class TrainSignal { public event EventHandler<TrainSignalEventArgs> TrainsArecoming; public TrainSignalType SignalType { get; set; } public void TriggerTrainSignal() { // call all subscribers if (TrainsArecoming != null) // make sure there are subscribers! TrainsArecoming(this, new TrainSignalEventArgs()); // trigger the event } } public class Program { static void Main() { TrainSignal trainSignalAudio = new TrainSignal() { SignalType = TrainSignalType.AudioSignal }; trainSignalAudio.TrainsArecoming += StopTheCar; TrainSignal trainSignalVisual = new TrainSignal() { SignalType = TrainSignalType.VisualSignal }; trainSignalVisual.TrainsArecoming += StopTheCar; trainSignalAudio.TriggerTrainSignal(); trainSignalVisual.TriggerTrainSignal(); Console.Read(); } private static void StopTheCar(object sender, TrainSignalEventArgs e) { TrainSignal trainSignal = sender as TrainSignal; // similar to TrainSignal trainSignal = (TrainSignal)sender; Console.WriteLine("The instance signal type is " + trainSignal.SignalType); } } } |
1 2 |
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e) where TEventArgs : EventArgs; |
1 2 3 4 |
public class TrainSignalEventArgs : EventArgs { } |
1 |
public event EventHandler<TrainSignalEventArgs> TrainsArecoming; |
1 2 3 4 5 6 |
public void TriggerTrainSignal() { // call all subscribers if (TrainsArecoming != null) // make sure there are subscribers! TrainsArecoming(this, new TrainSignalEventArgs()); // trigger the event } |
1 2 3 4 5 6 |
private static void StopTheCar(object sender, TrainSignalEventArgs e) { TrainSignal trainSignal = sender as TrainSignal; // similar to TrainSignal trainSignal = (TrainSignal)sender; Console.WriteLine("The instance signal type is " + trainSignal.SignalType); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
using System; namespace HelloWorld { public class TrainSignalEventArgs { public TrainSignalType SignalType { get; set; } } public enum TrainSignalType { AudioSignal, VisualSignal } public class TrainSignal { public event EventHandler<TrainSignalEventArgs> TrainsArecoming; public void TriggerTrainSignal() { // call all subscribers if (TrainsArecoming != null) // make sure there are subscribers! TrainsArecoming(this, new TrainSignalEventArgs() { SignalType = TrainSignalType.AudioSignal }); // trigger the event } } public class Program { static void Main() { TrainSignal trainSignalAudio = new TrainSignal(); trainSignalAudio.TrainsArecoming += StopTheCar; TrainSignal trainSignalVisual = new TrainSignal(); trainSignalVisual.TrainsArecoming += StopTheCar; trainSignalAudio.TriggerTrainSignal(); trainSignalVisual.TriggerTrainSignal(); Console.Read(); } private static void StopTheCar(object sender, TrainSignalEventArgs e) { TrainSignal trainSignal = sender as TrainSignal; // similar to TrainSignal trainSignal = (TrainSignal)sender; Console.WriteLine("The instance signal type is " + e.SignalType); } } } |
1 2 3 4 |
public class TrainSignalEventArgs { public TrainSignalType SignalType { get; set; } } |
1 2 3 4 5 6 |
public void TriggerTrainSignal() { // call all subscribers if (TrainsArecoming != null) // make sure there are subscribers! TrainsArecoming(this, new TrainSignalEventArgs() { SignalType = TrainSignalType.AudioSignal }); // trigger the event } |
1 2 3 4 5 6 |
private static void StopTheCar(object sender, TrainSignalEventArgs e) { TrainSignal trainSignal = sender as TrainSignal; // similar to TrainSignal trainSignal = (TrainSignal)sender; Console.WriteLine("The instance signal type is " + e.SignalType); } |
So, there you have it, EventArgs, in its generic form, allows us to pass useful information, in case we decide we need it, and this is one of the reasons EventHandler is the most common type of delegate used for events.
Tags: Action, delegates, EventArgs, EventHandler, events, sender