Skip to content

8. User events

In the previous chapter, we discussed the notion of events linked to form components. Now we'll look at how to create events in our own classes.

8.1. Predefined delegate objects

The notion of object delegate was encountered in the previous chapter, but had been skimmed over at the time. When we looked at how the event handlers of a form's components were declared, we came across code similar to the following:


            this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);

where buttonAfficher was a [Button] component. This class has a Click defined as follows :

  • [1]: the [Button] class
  • [2] : its events
  • [3,4]: the event Click
  • [5]: the declaration of event [Control.Click] [4].
    • EventHandler is a prototype (a model) of a method called delegate.
    • event is a keyword that restricts the functionality of the delegate EventHandler : an object delegate has richer functionality than a event.

Visit delegate EventHandler is defined as follows:

 

Visit delegate EventHandler designates a method model :

  • with type as 1st parameter Object
  • whose 2nd parameter is a EventArgs
  • not returning any results

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


        private void buttonAfficher_Click(object sender, EventArgs e);

To create a EventHandler, proceed as follows:

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

We can thus write :

EventHandler evtHandler=new EventHandler(buttonAfficher_Click);

A variable of type delegate is in fact a list of references to methods corresponding to the delegate. To add a new method M to the variable evtHandler above, we use the syntax :

evtHandler+=new EvtHandler(M);

The += notation can be used even if evtHandler is an empty list.

Instruction:


            this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);

adds a EventHandler to the list of event methods buttonAfficher.Click. When the event Click on component buttonAfficher occurs, VB executes the :

            buttonAfficher.Click(source, evt);

where :

  • source is the object behind the event
  • evt type EventArgs and contains no information

All signature methods void M(object,EventArgs) associated with the event Click by :


            this.buttonAfficher.Click += new System.EventHandler(M);

will be called with the parameters (source, evt) transmitted by VB.

8.2. Defining delegate objects

Instruction


    public delegate int Opération(int n1, int n2);

defines a type called Operation which represents a function prototype that accepts two integers and returns one integer. This is the keyword delegate that makes Operation a function prototype definition.

A variable op type Operation will register a list of functions corresponding to the prototype Operation :

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

Registering a method fi in the variable op is done by op=new Operation(fi) or simply op=fi. To add a method fj to the list of registered functions, we write op+= fj. To remove a method fk we write op-=fk. If, in our example, we write n=op(n1,n2), the set of methods stored in the op will be executed with the parameters n1 and n2. The result n recovered will be that of the last method executed. It is not possible to obtain the results produced by all methods. For this reason, if you store a list of methods in a delegated function, they will usually 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 Class1.
  • line 6: definition of delegate Operation : a prototype of methods accepting two parameters of type int and returning a int
  • lines 9-12: the instance method Add to the signature of the delegate Operation.
  • lines 14-17: the S instance methodsubtract to the signature of the delegate Operation.
  • lines 20-23: the method class Ancrease to the signature of the delegate Operation.
  • line 25: the method Main performed
  • line 20: the variable op type is delegate Operation. It will contain a list of methods with the type signature delegate Operation. It is assigned a first method reference, that of the static method Class1.Augmenter.
  • line 31: le delegate op is executed: all methods referenced by op that will be executed. They will be executed with the parameters passed to delegate op. Ici, only the static method Class1.Augmenter will be executed.
  • line 35: an instance c1 class Class1 is created.
  • line 37: the instance method c1.Ajouter is assigned to the delegate op. Increase was a static method, Add is an instance method. We wanted to show that it didn't matter.
  • line 39: the delegate op is executed: the Add will be executed with the parameters passed to delegate op.
  • line 42: do the same with the instance method Subtract.
  • lines 46-47: methods Add and Subtract in the delegate op.
  • line 49: le delegate op is executed: both Add and Subtract will be executed with the parameters passed to delegate op.
  • line 51: the method Subtract is removed from the delegate op.
  • line 53: le delegate op is executed: the remaining Add will be executed.

The results 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 notions of delegates and interfaces may seem rather similar, and we may wonder what exactly the differences are between these two notions. Let's take the following example, close 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 method Execute expects a reference to an object of type delegate Operation defined on line 6. This switches to the Execute, different methods (lines 26, 28, 32 and 36). This polymorphism property can also be achieved with a :


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: interface [IOperation] defines a method operation.
  • lines 11-16 and 19-24: the [Add] and [Subtract] classes implement the [IOperation] interface.
  • lines 29-31: the method Execute whose 1st parameter is of the interface type IOperation. The method Execute will successively receive, as 1st parameter, an instance of the class Add then an instance of the Subtract.

The polymorphic aspect of the delegate of the previous example. At the same time, both examples show the differences between these two notions.

The types delegate and interface are interchangeable

  • if the interface has only one method. In fact, the delegate is a wrapper for a single method, whereas the interface can define several methods.
  • if the multicast aspect of delegate is not used. The notion of multicast does not exist in the interface.

If these two conditions are met, then we can choose between the following two signatures for the method Execute :


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

The second, which uses the delegate can be used more flexibly. In the first signature, the method's first parameter must implement the IOperation. This requires the creation of a class to define the method to be passed as the first parameter to the Execute. In the second signature, any existing method with the right signature will do. No additional construction is required.

8.4. Event management

Objects delegate classes can be used to define events. A class C1 can define an event evt as follows:

  • a type delegate is defined inside or outside the C1 class:
delegate TResult Evt(T1 param1, T2 param2, ...);
  • the class C1 defines a delegate Evt :
public Evt Evt1;
  • when an instance c1 class C1 would like to report an event, it will execute its delegate Evt1 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 warned of the event Evt1.
  • if an object c2 using a c1 wants to be notified when an event occurs Evt1 on object c1, it will register one of its c2.M in the delegated object c1.Evt1 object c1. His c2.M will be executed each time the event Evt1 will occur on object c1. They can also unsubscribe if they no longer wish to be notified of the event.
  • as the delegated object c1.Evt1 can register several methods, different ci can register with the c1.Evt1 to be notified of the event Evt1 on c1.

In this scenario, we have :

  • a class that signals an event
  • classes that are notified of this event. They are said to subscribe to the event.
  • a type delegate which defines the signature of methods that will be notified of the event

The .NET framework defines :

  • a standard signature of the delegate an event
public delegate void MyEventHandler(object source, EventArgs evtInfo);
  • source : the object that reported the event
  • evtInfo : an object of type EventArgs or derivative that provides information about the event
  • the name of the delegate must be terminated by EventHandler
  • a standard way of declaring a MyEventHandler in a class :
1
2
3
4
public Class C1{
    public event MyEventHandler Evt1;
...
}

The Evt1 type is delegate. The keyword event is there to restrict operations that can be performed on him :

  • from outside the classroom C1, only += and -= operations are possible. This prevents methods subscribed to the event from being deleted (by developer error, for example). You can simply subscribe (+=) or unsubscribe (-=) to the event.
  • only an instance of type C1 can perform the call Evt1(source,evtInfo) which triggers the execution of methods subscribed to the Evt1.

The .NET framework provides a generic method satisfying the signature of the delegate of an event :

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 2nd parameter
  • the type TEventArgs must be derived from the EventsArgs (where TEventArgs : EventArgs)

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

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

to "publish" event X to subscribers.

Consider the following example:

  • a class Transmitter encapsulates a temperature. This temperature is observed. When this temperature exceeds a certain threshold, an event is triggered. We'll call this event TemperatureTropHaute. The information about this event will be encapsulated in a type TemperatureTropHauteEventArgs.
  • a class Underwriter subscribes to the previous event. When notified of the event, it displays a message on the console.
  • a console program creates a transmitter and two subscribers. It enters the temperatures from the keyboard and stores them in a Transmitter. If this is too high, the Transmitter publish the event TemperatureTropHaute.

To conform to the recommended method of event management, we first define the type TemperatureTropHauteEventArgs to encapsulate 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: information encapsulated by the TemperatureTropHauteEventArgs is the temperature that caused the event TemperatureTropHaute.

The class Transmitter 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 event TemperatureTropHaute will be published.
  • line 10: the sender has a name for identification purposes
  • line 12: the event TemperatureTropHaute.
  • lines 15-26: the method get which makes temperature and method set which records it. This is the set to publish the event TemperatureTropHaute if the temperature to be recorded exceeds the threshold on line 5. It publishes the event using the OnTemperatureTropHauteHandler from line 29 by passing a TemperatureTropHauteEventArgs in which the temperature that exceeded the threshold was recorded.
  • lines 29-32: the event TemperatureTropHaute is published with the sender itself as 1st parameter and the object TemperatureTropHauteEventArgs received as parameter.

The class Underwriter who will subscribe to the event TemperatureTropHaute 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 name.
  • lines 9-12: the method to be associated with the event TemperatureTropHaute. It has the signature of the type delegate EventHandler<TEventArgs> that an event manager must have. The method displays on the console: the name of the subscriber displaying the message, the name of the sender who reported the event, the temperature that triggered the event.
  • event subscription TemperatureTropHaute of an object Transmitter is not done in the classroom Underwriter. It will be done by an external class.

The [Program.cs] program 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 {
                    // on signale l'erreur
                    Console.WriteLine("Température incorrecte");
                }
                 // new entry
                Console.Write("Température (rien pour arrêter) : ");
                saisie = Console.ReadLine().Trim();
             }//while
        }
    }
}
  • line 6: creation of transmitter
  • lines 8-14: creation of two subscribers to the event TemperatureTropHaute of the transmitter.
  • lines 20-32: keyboard temperature input loop
  • line 24: if the temperature entered is correct, it is transmitted to the object Transmitter e1 which will trigger the TemperatureTropHaute if the temperature is above 19°C.

The results 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]