8. User Events
In the previous chapter, we discussed the concept of events associated with form components. Now we will see how to create events in our own classes.
8.1. Predefined delegate objects
We encountered the concept of delegate objects in the previous chapter, but only briefly. When we looked at how event handlers for form components were declared, we saw code similar to the following:
this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);
where buttonAfficher was a component of type [Button]. This class has a Click field defined as follows:
![]() |
- [1]: the [Button] class
- [2]: its events
- [3,4]: the Click event
- [5]: the declaration of the [Control.Click] event [4].
- EventHandler is a method prototype (a template) called a delegate.
- event is a keyword that restricts the functionality of the EventHandler delegate: a delegate object has richer functionality than an event object.
The EventHandler delegate is defined as follows:
![]() |
The EventHandler delegate designates a method template:
- with a first parameter of type Object
- with a second parameter of type EventArgs
- returning no result
A method corresponding to the pattern defined by EventHandler could be as follows:
private void buttonDisplay_Click(object sender, EventArgs e);
To create an object of type EventHandler, proceed as follows:
EventHandler evtHandler = new EventHandler(method corresponding to the prototype defined by the EventHandler type);
You can then write:
A variable of type delegate is actually a list of references to methods corresponding to the delegate's prototype. To add a new method M to the evtHandler variable above, use the following syntax:
The += notation can be used even if evtHandler is an empty list.
The statement:
this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);
adds a method of type EventHandler to the list of methods for the buttonAfficher.Click event. When the Click event on the buttonAfficher component occurs, VB executes the statement:
where:
- source is the object-type component that triggered the event
- evt is of type EventArgs and contains no information
All methods with the signature void M(object, EventArgs) that have been associated with the Click event by:
this.buttonAfficher.Click += new System.EventHandler(M);
will be called with the parameters (source, evt) passed by VB.
8.2. Defining delegate objects
The statement
public delegate int Operation(int n1, int n2);
defines a type called Operation that represents a function prototype accepting two integers and returning an integer. It is the delegate keyword that makes Operation a function prototype definition.
A variable op of type Operation will be used to store a list of functions corresponding to the Operation prototype:
A method fi is stored in the variable op using op=new Operation(fi) or, more simply, op=fi. To add a method fj to the list of already registered functions, we write op+= fj. To remove an already registered method fk, we write op-=fk. If in our example we write n=op(n1,n2), all methods registered in the variable op will be executed with the parameters n1 and n2. The result n retrieved will be that of the last method executed. It is not possible to obtain the results produced by the entire set of methods. For this reason, if a list of methods is stored in a delegate function, they most often return a result of type void.
Consider the following example:
using System;
namespace Chap6 {
class Class1 {
// definition of a function prototype
// accepts 2 integers as parameters and returns an integer
public delegate int Operation(int n1, int n2);
// two instance methods corresponding to the prototype
public int Add(int n1, int n2) {
Console.WriteLine("Add(" + n1 + "," + n2 + ")");
return n1 + n2;
}//add
public int Subtract(int n1, int n2) {
Console.WriteLine("Subtract(" + n1 + "," + n2 + ")");
return n1 - n2;
}//subtract
// a static method corresponding to the prototype
public static int Increment(int n1, int n2) {
Console.WriteLine("Increment(" + n1 + "," + n2 + ")");
return n1 + 2 * n2;
}//increase
static void Main(string[] args) {
// We define an object of type Operation to store functions
// store the static function increase
Operation op = Increase;
// execute the delegate
int n = op(4, 7);
Console.WriteLine("n=" + n);
// Create an object c1 of type Class1
Class1 c1 = new Class1();
// Register the Add method of c1 in the delegate
op = c1.Add;
// Execute the delegate object
n = op(2, 3);
Console.WriteLine("n=" + n);
// We register the subtract method of c1 in the delegate
op = c1.Subtract;
n = op(2, 3);
Console.WriteLine("n=" + n);
// Register two functions in the delegate
op = c1.Add;
op += c1.Subtract;
// executing the delegate object
op(0, 0);
// removing a function from the delegate
op -= c1.Subtract;
// execute the delegate
op(1, 1);
}
}
}
- Line 3: defines a Class1 class.
- line 6: definition of the Opération delegate: a method prototype accepting two parameters of type int and returning a result of type int
- lines 9–12: the instance method Add has the signature of the Operation delegate.
- lines 14–17: the instance method Subtract has the signature of the Operation delegate.
- lines 20–23: the class method Increase has the signature of the Operation delegate.
- Line 25: The Main method is executed
- line 20: the variable op is of type Operation delegate. It will contain a list of methods with the Operation delegate signature. It is assigned a first method reference, that of the static method Class1.Increment.
- Line 31: The op delegate is executed: all methods referenced by op will be executed. They will be executed with the parameters passed to the op delegate. Here, only the static method Class1.Increment will be executed.
- Line 35: An instance c1 of the Class1 class is created.
- Line 37: The instance method `c1.Add` is assigned to the `op` delegate. `Increment` was a static method; `Add` is an instance method. The point was to show that this doesn't matter.
- Line 39: The op delegate is executed: the Add method will be executed with the parameters passed to the op delegate.
- Line 42: We do the same with the instance method Subtract.
- Lines 46–47: We put the Add and Subtract methods into the op delegate.
- Line 49: The op delegate is executed: both the Add and Subtract methods will be executed with the parameters passed to the op delegate.
- line 51: the Soustraire method is removed from the op delegate.
- line 53: the op delegate is executed: the remaining Add method will be executed.
The results of the execution are as follows:
8.3. Delegates or Interfaces?
The concepts of delegates and interfaces may seem quite similar, and one might wonder what exactly the differences are between these two concepts. Let’s take the following example, which is similar to one we’ve already studied:
using System;
namespace Chap6 {
class Program1 {
// definition of a function prototype
// accepts 2 integers as parameters and returns an integer
public delegate int Operation(int n1, int n2);
// two instance methods corresponding to the prototype
public static int Add(int n1, int n2) {
Console.WriteLine("Add(" + n1 + "," + n2 + ")");
return n1 + n2;
}//add
public static int Subtract(int n1, int n2) {
Console.WriteLine("Subtract(" + n1 + "," + n2 + ")");
return n1 - n2;
}//subtract
// Execute a delegate
public static int Execute(Operation op, int n1, int n2){
return op(n1, n2);
}
static void Main(string[] args) {
// Executing the Add delegate
Console.WriteLine(Execute(Add, 2, 3));
// Execute the Subtract delegate
Console.WriteLine(Execute(Subtract, 2, 3));
// Execute a multicast delegate
Operation op = Add;
op += Subtract;
Console.WriteLine(Execute(op, 2, 3));
// removing a function from the delegate
op -= Subtract;
// execute the delegate
Console.WriteLine(Execute(op, 2, 3));
}
}
}
Line 20: The Execute method expects a reference to an object of type Operation delegate, defined on line 6. This allows different methods (lines 26, 28, 32, and 36) to be passed to the Execute method. This polymorphism can also be achieved using an interface:
using System;
namespace Chap6 {
// interface IOperation
public interface IOperation {
int operation(int n1, int n2);
}
// Add class
public class Add : IOperation {
public int operation(int n1, int n2) {
Console.WriteLine("Add(" + n1 + "," + n2 + ")");
return n1 + n2;
}
}
// Subtract class
public class Subtract : IOperation {
public int operation(int n1, int n2) {
Console.WriteLine("Subtract(" + n1 + "," + n2 + ")");
return n1 - n2;
}
}
// test class
public static class Program2 {
// Execution of the single method of the IOperation interface
public static int Execute(IOperation op, int n1, int n2) {
return op.operation(n1, n2);
}
public static void Main() {
// Execute the Add delegate
Console.WriteLine(Execute(new Add(), 2, 3));
// Execute the Subtract delegate
Console.WriteLine(Execute(new Subtract(), 2, 3));
}
}
}
- lines 6–8: the [IOperation] interface defines an operation method.
- lines 11–16 and 19–24: The [Add] and [Subtract] classes implement the [IOperation] interface.
- lines 29–31: the Execute method, whose first parameter is of the type of the IOperation interface. The Execute method will successively receive, as its first parameter, an instance of the Add class and then an instance of the Subtract class.
We can clearly see the polymorphic aspect that the delegate-type parameter had in the previous example. Both examples also highlight the differences between these two concepts.
Delegate and interface types are interchangeable
- if the interface has only one method. Indeed, the delegate type is a wrapper for a single method, whereas the interface can define multiple methods.
- if the delegate’s multicast aspect is not used. This concept of multicasting does not exist in the interface.
If both of these conditions are met, then we have a choice between the following two signatures for the Execute method:
int Execute(IOperation op, int n1, int n2)
int Execute(Operation op, int n1, int n2)
The second one, which uses the delegate, may prove more flexible to use. Indeed, in the first signature, the first parameter of the method must implement the IOperation interface. This requires creating a class to define the method that is to be passed as the first parameter to the Execute method. In the second signature, any existing method with the correct signature will do. No additional construction is required.
8.4. Event Handling
Delegate objects can be used to define events. A class C1 can define an event evt as follows:
- A delegate type is defined inside or outside class C1:
- Class C1 defines a field of type delegate Evt:
- When an instance c1 of class C1 wants to signal an event, it will execute its delegate Evt1 by passing it the parameters defined by the delegate Evt. All methods registered in the delegate Evt1 will then be executed with these parameters. We can say that they have been notified of the Evt1 event.
- If an object c2 that uses an object c1 wants to be notified when the event Evt1 occurs on object c1, it will register one of its methods c2.M with the delegate object c1.Evt1 of object c1. Thus, its method c2.M will be executed every time the event Evt1 occurs on object c1. It can also unsubscribe when it no longer wishes to be notified of the event.
- Since the delegate object c1.Evt1 can register multiple methods, different objects can register with the delegate c1.Evt1 to be notified of the Evt1 event on c1.
In this scenario, we have:
- a class that signals an event
- classes that are notified of this event. We say that they subscribe to the event.
- a delegate type that defines the signature of the methods that will be notified of the event
The .NET framework defines:
- a standard signature for an event delegate
- source: the object that raised the event
- evtInfo: an object of type EventArgs or a derived type that provides information about the event
- the delegate name must end with EventHandler
- A standard way to declare an event of type MyEventHandler in a class:
The Evt1 field is of type delegate. The keyword *event* is there to restrict the operations that can be performed on it:
- from outside class C1, only the += and -= operations are allowed. This prevents the accidental removal (e.g., by a developer error) of methods subscribed to the event. You can simply subscribe (+=) or unsubscribe (-=) from the event.
- Only an instance of type C1 can execute the Evt1(source, evtInfo) call, which triggers the execution of the methods subscribed to the Evt1 event.
The .NET framework provides a generic method that matches the signature of an event delegate:
public delegate void EventHandler<TEventArgs>(object source, TEventArgs evtInfo) where TEventArgs : EventArgs
- The EventHandler delegate uses the generic type TEventArgs, which is the type of its second parameter
- The TEventArgs type must derive from the EventArgs type (where TEventArgs : EventArgs)
With this generic delegate, the declaration of an event X in class C will follow the recommended pattern below:
- Define a type XEventArgs derived from EventArgs to encapsulate information about event X
- Define an event of type EventHandler<XEventArgs> in class C.
- Define a protected method in class C
intended to "publish" event X to subscribers.
Consider the following example:
- An Emitter class encapsulates a temperature. This temperature is monitored. When the temperature exceeds a certain threshold, an event must be triggered. We will call this event TemperatureTooHigh. Information about this event will be encapsulated in a TemperatureTooHighEventArgs type.
- A Subscriber class subscribes to the previous event. When it is notified of the event, it displays a message on the console.
- A console program creates a Publisher and two Subscribers. It enters temperatures via the keyboard and stores them in a Publisher instance. If the temperature is too high, the Publisher instance publishes the TemperatureTooHigh event.
To follow the recommended event handling method, we first define the TemperatureTropHauteEventArgs type to encapsulate the event information:
using System;
namespace Chap6 {
public class TemperatureTooHighEventArgs:EventArgs {
// temperature at the time of the event
public decimal Temperature { get; set; }
// constructors
public TemperatureTooHighEventArgs() {
}
public TemperatureTooHighEventArgs(decimal temperature) {
Temperature = temperature;
}
}
}
- Line 6: The information encapsulated by the TemperatureTropHauteEventArgs class is the temperature that triggered the TemperatureTropHaute event.
The Emitter class is as follows:
using System;
namespace Chap6 {
public class Emitter {
static decimal THRESHOLD = 19;
// observed temperature
private decimal temperature;
// source name
public string Name { get; set; }
// reported event
public event EventHandler<TemperatureTropHauteEventArgs> TemperatureTropHaute;
// read/write temperature
public decimal Temperature {
get {
return temperature;
}
set {
temperature = value;
if (temperature > THRESHOLD) {
// notify subscribers of the event
OnTemperatureTooHigh(new TemperatureTooHighEventArgs(temperature));
}
}
}
// Report an event
protected virtual void OnTemperatureTooHigh(TemperatureTooHighEventArgs evt) {
// Dispatch the TemperatureTooHigh event to subscribers
TemperatureTooHigh(this, evt);
}
}
}
- Line 5: the temperature threshold above which the TemperatureTropHaute event will be published.
- line 10: the publisher has a name for identification
- line 12: the TemperatureTooHigh event.
- lines 15–26: the get method that returns the temperature and the set method that saves it. It is the set method that publishes the TemperatureTooHigh event if the temperature to be recorded exceeds the threshold in line 5. It publishes the event via the OnTemperatureTooHighHandler method in line 29, passing it a TemperatureTooHighEventArgs object as a parameter, in which the temperature that exceeded the threshold has been recorded.
- Lines 29–32: The TemperatureTropHaute event is published with the publisher itself as the first parameter and the TemperatureTropHauteEventArgs object received as a parameter as the second parameter.
The Subscriber class that will subscribe to the TemperatureTooHigh event is as follows:
using System;
namespace Chap6 {
public class Subscriber {
// name
public string Name { get; set; }
// TemperatureTooHigh event handler
public void TemperatureTooHighEvent(object source, TemperatureTooHighEventArgs e) {
// operator console output
Console.WriteLine("Subscriber [{0}]: Source [{1}] reported an excessively high temperature: [{2}]", Name, ((Emitter)source).Name, e.Temperature);
}
}
}
- line 6: each subscriber is identified by a name.
- lines 9–12: the method that will be associated with the TemperatureTooHigh event. It has the signature of the EventHandler<TEventArgs> delegate type that an event handler must have. The method displays on the console: the name of the subscriber displaying the message, the name of the emitter that reported the event, and the temperature that triggered it.
- Subscription to the TemperatureTooHigh event of an Emitter object is not done in the Subscriber class. It will be done by an external class.
The program [Program.cs] links all these elements together:
using System;
namespace Chap6 {
class Program {
static void Main(string[] args) {
// Create an event emitter
EventPublisher e1 = new EventPublisher() { Name = "e" };
// Create an array of 2 subscribers
Subscriber[] subscribers = new Subscriber[2];
for (int i = 0; i < subscribers.Length; i++) {
// Create a subscriber
subscribers[i] = new Subscriber() { Name = "s" + i };
// subscribe it to the TemperatureTooHigh event of e1
e1.TemperatureTooHigh += subscribers[i].TemperatureTooHighEvent;
}
// Read temperatures from the keyboard
decimal temperature;
Console.Write("Temperature (press any key to exit): ");
string input = Console.ReadLine().Trim();
// as long as the entered line is not empty
while (input != "") {
// Is the input a decimal number?
if (decimal.TryParse(input, out temperature)) {
// Correct temperature - save it
e1.Temperature = temperature;
} else {
// Report the error
Console.WriteLine("Incorrect temperature");
}
// new input
Console.Write("Temperature (press any key to exit): ");
input = Console.ReadLine().Trim();
}//while
}
}
}
- line 6: creation of the emitter
- lines 8–14: creation of two subscribers that are subscribed to the TemperatureTooHigh event of the publisher.
- lines 20–32: loop for entering temperatures via the keyboard
- line 24: if the entered temperature is valid, it is sent to the Emitter e1 object, which will trigger the TemperatureTooHigh event if the temperature is above 19 °C.
The results of the execution are as follows:

