Events in C# explained

Dinesh Jethoe
7 min readMay 6, 2020
Image source: pro6cranendonck.nl

Everyone that understands English knows what an event means, namely that something that is going to happen or something that happens. The same is meant with event in the C# programming language. In programming terms, it means that an object raises an event and another object or other objects handle the event. The object (publisher) that raises the event actually notifies the other objects (subscribers) that something has occurred so that the objects (subscribers) can do (handle) something on and with that event. This behavior between objects you will see mostly in the observer (aka publish-subscribe) software design pattern.

Delegate

Since events and delegates are inextricably linked together, it is important to understand how a delegate works. A delegate is a type in C#. That being said, an object of a delegate type can be used as a variable. But a delegate variable is not pointing to a value (data that an object holds) but in stead to a concrete method. Hence a delegate object can only refer to methods whose signature match the delegate’s (do not think about variance yet).

Here’s the syntax to declare a delegate:

access_specifier delegate retun_type delegateName(parameter_list)

For example:

public delegate int CalculateDelegate(int a, int b);

The following methods match the CalculateDelegate delegate:

public int Sum(int a, int b) { return a + b; }
public int Subtract(int a, int b) { return a - b; }
public int Average(int a, int b) { return (a + b) / 2; }
public int Multiply(int a, int b) { return a * b; }
public int Divide(int a, int b) { return a / b; }
Place the delegate keyword between the access modifier and return type of the method to create a delegate from the method. ;)

Using multicasting we can combine these methods together and keep these methods in one object of the CalculateDelegate, then execute all the (referenced) methods using this one object.

var a = 25;
var b = 5;

CalculateDelegate calc = Sum; //or var calc = new CalculateDelegate(Sum);
calc += Subtract;
calc += Average;
calc += Multiply;
calc += Divide;

//By using -=, delegate can remove a method’s reference from the delegate’s instance.
//For example: calc -= Multiply; //the method Multiply is removed from the invocation list of calc.

To invoke (execute or call) all the methods using the calc object, you can utilize the InvocationList of the delegate.

foreach (CalculateDelegate cd in calc.GetInvocationList())
{
Console.WriteLine(cd(a, b));
}

Variance in delegate

When you assign a method to a delegate, the method signature does not have to match the delegate exactly. This is called covariance and contravariance.

Covariance is applied on a method’s return type whereas contravariance is applied on a method’s parameter type.

Covariance

class Parent { }

class Child : Parent { }

delegate Parent CovarianceDelegate(); // the delegate's return type is the base class

static Child CovarianceMethod() // the method's return type is the derived class
{
return new Child();
}

static void Main(string[] args)
{
CovarianceDelegate delegateObject = CovarianceMethod;
Parent result = delegateObject();

/*
cast is needed if you want to hold the return value in an instance of the derived class e.g.:
Child result = (Child)del(); or Child result = del() as Child;
*/
}

Contravariance

class Parent { }

class Child : Parent { }

delegate void ContravarianceDelegate(Child c); // the delegate's parameter type is of the derived class

static void Method(Parent p) // the method's parameter type is of the base class
{
var childObject = p as Child;
}

static void Main(string[] args)
{
ContravarianceDelegate delegateObject = Method;
Child child = new Child();
delegateObject(child);
}

Built-in delegates

C# provides some built-in delegates that are useful for common purposes. These provide a shorthand notation that virtually eliminates the need to declare delegate types.

  • Action: used with methods that don’t return a value and have no parameter list.
  • Action<>: used with methods that at least have one argument and don’t return a value.
  • Func<>: used with methods that return a value and may have a parameter list.
  • Predicate<>: represents a method that takes one input parameter and returns a bool value on the basis of some criteria.

So, enough about delegates! You can find a lot of resources about delegate on the internet. The focus of this article is to explain the use of delegate in an event.

Handling and raising an event

Once a delegate is declared, anyone can overwrite its method reference with the = (equal sign). Suppose you have a delegate object that hold multiple method references. If someone uses the = to refer a new method then all the other method references will be lost. Event encapsulates a delegate; it avoids overwriting of a method reference by restricting the use of the assignment = operator.

Another issue is that a delegate can be invoked outside of a class (thus it can be called anywhere). Event overcomes this issue because an event cannot be invoked outside the class (no outside users can raise the event), which makes sure the event will only be invoked by the code that’s part of the class (publisher) that defined the event when a certain condition satisfies. And an event is always a data member of a class or struct. It cannot be declared inside a method.

Let’s say, you want the following to happen when you tap a button on your phone or when the temperature increases to a certain level:

  1. turn on the air-conditioner,
  2. close the doors/windows and
  3. order mint chocolate chip ice-cream, :).

These three things that must happen are the actions (methods) that will be delegated by a delegate object when the event (either manually by pressing a button or automatically by temperature increase) occurs.

So, there are two different events that will trigger one delegate to execute some methods.

Now that we have two event triggers: the mobile and the thermometer, we can make use of an interface because both of them will have the same code. So, let’s start with the interface first:

public interface IEventTrigger
{
event EventHandler<TemperatureEventArgs> OnTemperatureChanged;

int Temperature { set; }
}

Notice, I am using the .NET generic delegate ‘EventHandler<>’. And passing my own custom EventArgs.

Here’s the custom EventArgs class:

public class TemperatureEventArgs : EventArgs
{
public int Value { get; set; }
}

TemperatureEventArgs inherited the .NET EventArgs class and it will hold only one piece of information; the temperature value.

Let’s create the publisher class that will implement the event of IEventTrigger:

public class Thermometer : IEventTrigger
{
private int maxTemperature;

public Thermometer(int maxTemperature)
{
this.maxTemperature = maxTemperature;
}

/*
Initialize the event to an empty delegate
so that you don't need the null check around raising the event
because the event will never be null.
Only members in this class can set the event to null, outsiders can't.
*/
public event EventHandler<TemperatureEventArgs> OnTemperatureChanged = delegate{};

public int Temperature
{
set
{
var temperature = value;

if(temperature > maxTemperature)
{
var temperatureValue = new TemperatureEventArgs
{
Value = temperature,
};

OnTemperatureChanged(this, temperatureValue);
}
}
}
}

The mobile class is another trigger and it will have the same code of the thermometer class.

The thermometer class and mobile class have a private field that will hold the highest temperature value that is set through the constructor.

Whenever the temperature increases above the highest temperature value (certain level), the thermometer will invoke the delegate: OnTemperatureChanged(this, temperatureValue);

The delegate OnTemperatureChanged will take care of performing the actions of the subscriber.

How will the subscriber know when its methods will be executed? The publisher will not only inform the subscriber(s) when but also triggers the execution of the subscriber’s methods via the OnTemperatureChanged delegate.

The keyword this refers to the control/trigger that has executed the delegate, in this case it’s the thermometer. The temperatureValue parameter is the current temperature value (that is measured by the thermometer) and is passed on via the event argument to the subscriber (the application or device that will perform the actions or execute the methods that are being hold by the delegate).

Here is the subscriber that listens to the triggers to perform some actions:

static void Main(string[] args)
{
//IEventTrigger trigger = new Mobile(30);
IEventTrigger trigger = new Thermometer(30);

trigger.OnTemperatureChanged += (o, e) => Console.WriteLine($"Temperature is changed to {e.Value} °C. {((o is Mobile) ? "Triggered manually by mobile." : "Triggered automatically by the thermometer.")}");
trigger.OnTemperatureChanged += (o, e) => Console.WriteLine("1. Ordering mint chocolate chip ice-cream!");
trigger.OnTemperatureChanged += (o, e) => Console.WriteLine("2. Turning on the A/C.");
trigger.OnTemperatureChanged += (o, e) => Console.WriteLine("3. Closing all doors & windows.");

trigger.Temperature = 32; //when the temperature is below 30 then none of the actions will be performed.
}

The trigger (mobile/thermometer) is created and the highest temperature value is set: IEventTrigger trigger = new Thermometer(30);

Some actions are added to the delegate named OnTemperatureChanged. Once the Temperature is changed (oustide of the publisher and subscriber), the trigger’s set accessor will be fired. The set accessor method checks whether the current temperature is higher than the highest temperature level. If it’s higher then the delegate OnTemperatureChanged is executed in the publisher's object (trigger: thermometer or mobile). That delegate represents the delegate on the trigger’s object: trigger.OnTemperatureChanged. So when that delegate in the set accessor method is invoked then all its referred/attached methods (which are defined in the subscriber) will be invoked.

Only one subscriber is used in this example but you may have multiple subscribers that subscribe to one or more subscriptions. Subscriptions are published by a publisher.

The subscriber is using the += to subscribe to the event(s) of the publisher. It requests/tells the publisher: “please perform these actions when some condition is met.” The condition is defined by the publisher. The actions are defined by the subscribers. Each subscriber can have different actions to perform when the same event takes place.

I hope this article has helped to shed some light on the relation between event and delegate in C#.

--

--