Skip to content

8. User Events

In the previous chapter, we discussed the concept of events associated with form components. We will now 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 delegate EventHandler: a delegate object has richer functionality than an event object.

The delegate EventHandler is defined as follows:

 

The delegate EventHandler refers to a method template:

  • with a first parameter of type Object
  • with a type EventArgs as its second parameter
  • returns no results

A method corresponding to the model defined by EventHandler could be as follows:


        private void buttonAfficher_Click(object sender, EventArgs e);

To create an object of type EventHandler, proceed as follows:

EventHandler evtHandler=new EventHandler(méthode correspondant au prototype  défini par le type EventHandler);

You can then write:

EventHandler evtHandler=new EventHandler(buttonAfficher_Click);

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 variable evtHandler above, use the following syntax:

evtHandler+=new EvtHandler(M);

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:

            buttonAfficher.Click(source, evt);

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 Opération(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:

int f1(int,int)
int f2(int,int)
...
int fn(int,int)

A fi method is stored in the op variable 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 all the 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 {
        // function prototype definition
        // accepts 2 integers as parameters and returns an integer
        public delegate int Opération(int n1, int n2);
 
        // two instance methods corresponding to the prototype
        public int Ajouter(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
        }//add
 
        public int Soustraire(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
        }//subtract
 
        // a static method corresponding to the prototype
        public static int Augmenter(int n1, int n2) {
            Console.WriteLine("Augmenter(" + n1 + "," + n2 + ")");
            return n1 + 2 * n2;
        }//increase
 
        static void Main(string[] args) {
 
            // define an operation object to store functions
            // we register the static function increase
            Opération op = Augmenter;
            // the delegate is executed
            int n = op(4, 7);
            Console.WriteLine("n=" + n);
 
            // creation of a c1 object of type class1
            Class1 c1 = new Class1();
            // we register c1's add method in the delegate
            op = c1.Ajouter;
            // execution of delegated object
            n = op(2, 3);
            Console.WriteLine("n=" + n);
            // the subtract method of c1 is registered in the delegate
            op = c1.Soustraire;
            n = op(2, 3);
            Console.WriteLine("n=" + n);
            //registration of two functions in the delegate
            op = c1.Ajouter;
            op += c1.Soustraire;
            // execution of delegated object
            op(0, 0);
            // remove a function from the delegate
            op -= c1.Soustraire;
            // the delegate is executed
            op(1, 1);
        }
    }
}
  • Line 3: defines a class named Class1.
  • Line 6: Definition of the Opération delegate: a prototype of methods 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 signature of the Operation delegate type. It is assigned a first method reference, that of the static method Class1.Augmenter.
  • 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.Augmenter will be executed.
  • Line 35: An instance c1 of the Class1 class is created.
  • line 37: the instance method c1.Ajouter is assigned to the op delegate. Increase was a static method; Add is an instance method. We wanted to show that this does not 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 place the Add and Subtract methods in 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 Subtract 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:

1
2
3
4
5
6
7
8
9
Augmenter(4,7)
n=18
Ajouter(2,3)
n=5
Soustraire(2,3)
n=-1
Ajouter(0,0)
Soustraire(0,0)
Ajouter(1,1)

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 consider the following example, which is similar to one we’ve already studied:


using System;
namespace Chap6 {
    class Program1 {
        // function prototype definition
        // accepts 2 integers as parameters and returns an integer
        public delegate int Opération(int n1, int n2);
 
        // two instance methods corresponding to the prototype
        public static int Ajouter(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
        }//add
 
        public static int Soustraire(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
        }//subtract
 
        // Executing a delegate
        public static int Execute(Opération op, int n1, int n2){
            return op(n1, n2);
        }
 
        static void Main(string[] args) {
            // delegate execution Add
            Console.WriteLine(Execute(Ajouter, 2, 3));
            // delegate execution Subtract
            Console.WriteLine(Execute(Soustraire, 2, 3));
            // executing a multicast delegate
            Opération op = Ajouter;
            op += Soustraire;
            Console.WriteLine(Execute(op, 2, 3));
            // remove a function from the delegate
            op -= Soustraire;
            // the delegate is executed
            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);
    }
 
    // class Add
    public class Ajouter : IOperation {
        public int operation(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
        }
    }
 
    // class Subtract
    public class Soustraire : IOperation {
        public int operation(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
        }
    }
 
    // test class
    public static class Program2 {
        // Executing 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() {
            // delegate execution Add
            Console.WriteLine(Execute(new Ajouter(), 2, 3));
            // delegate execution Subtract
            Console.WriteLine(Execute(new Soustraire(), 2, 3));
        }
    }
}
  • lines 6–8: the [IOperation] interface defines an operation method.
  • lines 11–16 and 19–24: The classes [Ajouter] and [Soustraire] 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 you can choose between the following two signatures for the Execute method:


int Execute(IOperation op, int n1, int n2)
int Execute(Opération op, int n1, int n2)

The second one, which uses the delegate, may be more flexible to use. In fact, 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 work. 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:
delegate TResult Evt(T1 param1, T2 param2, ...);
  • Class C1 defines a field of type delegate Evt:
public Evt Evt1;
  • 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 event Evt1.
  • If an object c2 using an object c1 wants to be notified of the occurrence of the Evt1 event on object c1, it will register one of its methods, c2.M, in the delegate object c1.Evt1 of object c1. Thus, its method c2.M will be executed every time the Evt1 event 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
public delegate void MyEventHandler(object source, EventArgs evtInfo);
  • source: the object that raised the event
  • evtInfo: an object of type EventArgs or derived from it 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:
1
2
3
4
public Class C1{
    public event MyEventHandler Evt1;
...
}

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 call Evt1(source,evtInfo), 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 delegate EventHandler uses the generic type TEventArgs, which is the type of its second parameter
  • The type TEventArgs must derive from the type EventsArgs (where TEventArgs : EventArgs)

With this generic delegate, the declaration of an X event in class C will follow the recommended pattern below:

  • define a type XEventArgs derived from EventArgs to encapsulate information about event X
  • define in class C an event of type EventHandler<XEventArgs>.
  • Define a protected method in class C
protected void OnXHandler(XEventArgs e);

intended to "publish" the X event to subscribers.

Consider the following example:

  • A Sender class encapsulates a temperature. This temperature is monitored. When this temperature exceeds a certain threshold, an event must be triggered. We will call this event TemperatureTropHaute. Information about this event will be encapsulated in a type TemperatureTropHauteEventArgs.
  • 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 event TemperatureTropHaute.

To comply with the recommended event handling method, we first define the TemperatureTropHauteEventArgs type to encapsulate the event information:


using System;
 
namespace Chap6 {
    public class TemperatureTropHauteEventArgs:EventArgs {
        // temperature during evt
        public decimal Temperature { get; set; }
        // manufacturers
        public TemperatureTropHauteEventArgs() {
        }
        public TemperatureTropHauteEventArgs(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 Emetteur {
        static decimal SEUIL = 19;
 
        // observed temperature
        private decimal temperature;
        // name of source
        public string Nom { get; set; }
        // event reported
        public event EventHandler<TemperatureTropHauteEventArgs> TemperatureTropHaute;
 
        // read/write temperature
        public decimal Temperature {
            get {
                return temperature;
            }
            set {
                temperature = value;
                if (temperature > SEUIL) {
                    // subscribers are notified of the event
                    OnTemperatureTropHaute(new TemperatureTropHauteEventArgs(temperature));
                }
            }
        }
 
        // reporting an event
        protected virtual void OnTemperatureTropHaute(TemperatureTropHauteEventArgs evt) {
            // issue of event TemperatureTropHaute to subscribers
            TemperatureTropHaute(this, evt);
        }
    }
}
  • line 5: the temperature threshold above which the TemperatureTropHaute event will be published.
  • line 10: the sender has a name for identification
  • line 12: the event TemperatureTropHaute.
  • lines 15–26: the get method that returns the temperature and the set method that records it. It is the set method that triggers the publication of the TemperatureTropHaute event if the temperature to be recorded exceeds the threshold in line 5. It triggers the event via the OnTemperatureTropHauteHandler method in line 29 by passing it a TemperatureTropHauteEventArgs 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 emitter 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 TemperatureTropHaute event is as follows:


using System;
 
namespace Chap6 {
    public class Souscripteur {
        // name
        public string Nom { get; set; }
 
        // event manager TemperatureTropHaute
        public void EvtTemperatureTropHaute(object source, TemperatureTropHauteEventArgs e) {
            // operator console display
            Console.WriteLine("Souscripteur [{0}] : la source [{1}] a signalé une température trop haute : [{2}]", Nom, ((Emetteur)source).Nom, e.Temperature);
        }
    }
}
  • line 6: each subscriber is identified by a name.
  • lines 9–12: the method that will be associated with the TemperatureTropHaute 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 sender that reported the event, and the temperature that triggered it.
  • The subscription to event TemperatureTropHaute for a Publisher object is not handled within the Subscriber class. It will be handled by an external class.

The program [Program.cs] links all these elements together:


using System;
namespace Chap6 {
    class Program {
        static void Main(string[] args) {
            // creation of an evts transmitter
            Emetteur e1 = new Emetteur() { Nom = "e" };
            // creation of a table of 2 subscribers
            Souscripteur[] souscripteurs = new Souscripteur[2];
            for (int i = 0; i < souscripteurs.Length; i++) {
                // creation subscriber
                souscripteurs[i] = new Souscripteur() { Nom = "s" + i };
                // we subscribe him to e1's TemperatureTropHaute event
                e1.TemperatureTropHaute += souscripteurs[i].EvtTemperatureTropHaute;
            }
            // temperatures are read from the keyboard
            decimal temperature;
            Console.Write("Température (rien pour arrêter) : ");
            string saisie = Console.ReadLine().Trim();
            // as long as the line entered is not empty
            while (saisie != "") {
                // is the input a decimal number?
                if (decimal.TryParse(saisie, out temperature)) {
                    // correct temperature - recorded
                    e1.Temperature = temperature;
                } else {
                    // we report the error
                    Console.WriteLine("Température incorrecte");
                }
                // new entry
                Console.Write("Température (rien pour arrêter) : ");
                saisie = Console.ReadLine().Trim();
            }//while
        }
    }
}
  • line 6: creation of the publisher
  • lines 8–14: creation of two subscribers that are subscribed to the TemperatureTropHaute 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 e1 Publisher object, which will trigger the TemperatureTropHaute event if the temperature is above 19 °C.

The results of the execution are as follows:

1
2
3
4
Température (rien pour arrêter) : 17
Température (rien pour arrêter) : 21
Souscripteur [s0] : la source [e] a signalé une température trop haute : [21]
Souscripteur [s1] : la source [e] a signalé une température trop haute : [21]