Skip to content

8. Evénements utilisateur

Nous avons dans le chapitre précédent abordé la notion d'événements liés à des composants de formulaire. Nous voyons maintenant comment créer des événements dans nos propres classes.

8.1. Objets delegate prédéfinis

La notion d'objet delegate a été rencontrée dans le chapitre précédent mais elle avait été alors survolée. Lorsque nous avions regardé comment les gestionnaires des événements des composants d'un formulaire étaient déclarés, nous avions rencontré du code similaire au suivant :


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

buttonAfficher était un composant de type [Button]. Cette classe a un champ Click défini comme suit :

  • [1] : la classe [Button]
  • [2] : ses événements
  • [3,4] : l'événement Click
  • [5] : la déclaration de l'événement [Control.Click] [4].
    • EventHandler est un prototype (un modèle ) de méthode appelé delegate.
    • event est un mot clé qui restreint les fonctionnalités du delegate EventHandler : un objet delegate a des fonctionnalités plus riches qu'un objet event.

Le delegate EventHandler est défini comme suit :

 

Le delegate EventHandler désigne un modèle de méthode :

  • ayant pour 1er paramètre un type Object
  • ayant pour 2ième paramètre un type EventArgs
  • ne rendant aucun résultat

Une méthode correspondant au modèle défini par EventHandler pourrait être la suivante :


        private void buttonAfficher_Click(object sender, EventArgs e);

Pour créer un objet de type EventHandler, on procède comme suit :

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

On pourra ainsi écrire :

EventHandler evtHandler=new EventHandler(buttonAfficher_Click);

Une variable de type delegate est en fait une liste de références sur des méthodes correspondant au modèle du delegate. Pour ajouter une nouvelle méthode M à la variable evtHandler ci-dessus, on utilise la syntaxe :

evtHandler+=new EvtHandler(M);

La notation += peut être utilisée même si evtHandler est une liste vide.

L'instruction :


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

ajoute une méthode de type EventHandler à la liste des méthodes de l'événement buttonAfficher.Click. Lorsque l'événement Click sur le composant buttonAfficher se produit, VB exécute l'instruction :

            buttonAfficher.Click(source, evt);

où :

  • source est le composant de type object à l'origine de l'événement
  • evt de type EventArgs et ne contient pas d'information

Toutes les méthodes de signature void M(object,EventArgs) qui ont été associées à l'événement Click par :


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

seront appelées avec les paramètres (source, evt) transmis par VB.

8.2. Définir des objets delegate

L'instruction


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

définit un type appelé Opération qui représente un prototype de fonction acceptant deux entiers et rendant un entier. C'est le mot clé delegate qui fait de Opération une définition de prototype de fonction.

Une variable op de type Opération aura pour rôle d'enregistrer une liste de fonctions correspondant au prototype Opération :

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

L'enregistrement d'une méthode fi dans la variable op se fait par op=new Opération(fi) ou plus simplement par op=fi. Pour ajouter une méthode fj à la liste des fonctions déjà enregistrées, on écrit op+= fj. Pour enlever une méthode fk déjà enregistrée on écrit op-=fk. Si dans notre exemple on écrit n=op(n1,n2), l'ensemble des méthodes enregistrées dans la variable op seront exécutées avec les paramètres n1 et n2. Le résultat n récupéré sera celui de la dernière méthode exécutée. Il n'est pas possible d'obtenir les résultats produits par l'ensemble des méthodes. Pour cette raison, si on enregistre une liste de méthodes dans une fonction déléguée, celles-ci rendent le plus souvent un résultat de type void.

Considérons l'exemple suivant :


using System;
namespace Chap6 {
    class Class1 {
        // définition d'un prototype de fonction
        // accepte 2 entiers en paramètre et rend un entier
        public delegate int Opération(int n1, int n2);

        // deux méthodes d'instance correspondant au prototype
        public int Ajouter(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
        }//ajouter

        public int Soustraire(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
        }//soustraire

        // une méthode statique correspondant au prototype
        public static int Augmenter(int n1, int n2) {
            Console.WriteLine("Augmenter(" + n1 + "," + n2 + ")");
            return n1 + 2 * n2;
        }//augmenter

        static void Main(string[] args) {

            // on définit un objet de type opération pour y enregistrer des fonctions
            // on enregistre la fonction statique augmenter
            Opération op = Augmenter;
            // on exécute le délégué
            int n = op(4, 7);
            Console.WriteLine("n=" + n);

            // création d'un objet c1 de type class1
            Class1 c1 = new Class1();
            // on enregistre dans le délégué la méthode ajouter de c1
            op = c1.Ajouter;
            // exécution de l'objet délégué
            n = op(2, 3);
            Console.WriteLine("n=" + n);
            // on enregistre dans le délégué la méthode soustraire de c1
            op = c1.Soustraire;
            n = op(2, 3);
            Console.WriteLine("n=" + n);
            //enregistrement de deux fonctions dans le délégué
            op = c1.Ajouter;
            op += c1.Soustraire;
            // exécution de l'objet délégué
            op(0, 0);
            // on retire une fonction du délégué
            op -= c1.Soustraire;
            // on exécute le délégué
            op(1, 1);
        }
    }
}
  • ligne 3 : définit une classe Class1.
  • ligne 6 : définition du delegate Opération : un prototype de méthodes acceptant deux paramètres de type int et rendant un résultat de type int
  • lignes 9-12 : la méthode d'instance Ajouter a la signature du delegate Opération.
  • lignes 14-17 : la méthode d'instance Soustraire a la signature du delegate Opération.
  • lignes 20-23 : la méthode de classe Augmenter a la signature du delegate Opération.
  • ligne 25 : la méthode Main exécutée
  • ligne 20 : la variable op est de type delegate Opération. Elle contiendra une liste de méthodes ayant la signature du type delegate Opération. On lui affecte une première référence de méthode, celle sur la méthode statique Class1.Augmenter.
  • ligne 31 : le delegate op est exécuté : ce sont toutes les méthodes référencées par op qui vont être exécutées. Elles le seront avec les paramètres passés au delegate op. Ici, seule la méthode statique Class1.Augmenter va être exécutée.
  • ligne 35 : une instance c1 de la classe Class1 est créée.
  • ligne 37 : la méthode d'instance c1.Ajouter est affectée au delegate op. Augmenter était une méthode statique, Ajouter est une méthode d'instance. On a voulu montrer que cela n'avait pas d'importance.
  • ligne 39 : le delegate op est exécuté : la méthode Ajouter va être exécutée avec les paramètres passés au delegate op.
  • ligne 42 : on refait de même avec la méthode d'instance Soustraire.
  • lignes 46-47 : on met les méthodes Ajouter et Soustraire dans le delegate op.
  • ligne 49 : le delegate op est exécuté : les deux méthodes Ajouter et Soustraire vont être exécutées avec les paramètres passés au delegate op.
  • ligne 51 : la méthode Soustraire est enlevée du delegate op.
  • ligne 53 : le delegate op est exécuté : la méthode restante Ajouter va être exécutée.

Les résultats de l'exécution sont les suivants :

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 ou interfaces ?

Les notions de delegates et d'interfaces peuvent sembler assez proches et on peut se demander quelles sont exactement les différences entre ces deux notions. Prenons l'exemple suivant proche d'un exemple déjà étudié :


using System;
namespace Chap6 {
    class Program1 {
        // définition d'un prototype de fonction
        // accepte 2 entiers en paramètre et rend un entier
        public delegate int Opération(int n1, int n2);

        // deux méthodes d'instance correspondant au prototype
        public static int Ajouter(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
        }//ajouter

        public static int Soustraire(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
        }//soustraire

        // Exécution d'un délégué
        public static int Execute(Opération op, int n1, int n2){
            return op(n1, n2);
        }

        static void Main(string[] args) {
            // exécution du délégué Ajouter
            Console.WriteLine(Execute(Ajouter, 2, 3));
            // exécution du délégué Soustraire
            Console.WriteLine(Execute(Soustraire, 2, 3));
            // exécution d'un délégué multicast
            Opération op = Ajouter;
            op += Soustraire;
            Console.WriteLine(Execute(op, 2, 3));
            // on retire une fonction du délégué
            op -= Soustraire;
            // on exécute le délégué
            Console.WriteLine(Execute(op, 2, 3));
        }
    }
}

Ligne 20, la méthode Execute attend une référence sur un objet du type delegate Opération défini ligne 6. Cela permet de passer à la méthode Execute, différentes méthodes (lignes 26, 28, 32 et 36). Cette propriété de polymorphisme peut être obtenue également avec une interface :


using System;

namespace Chap6 {

    // interface IOperation
    public interface IOperation {
        int operation(int n1, int n2);
    }

    // classe Ajouter
    public class Ajouter : IOperation {
        public int operation(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
        }
    }

    // classe Soustraire
    public class Soustraire : IOperation {
        public int operation(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
        }
    }

    // classe de test
    public static class Program2 {
        // Exécution de la méthode unique de l'interface IOperation
        public static int Execute(IOperation op, int n1, int n2) {
            return op.operation(n1, n2);
        }

        public static void Main() {
            // exécution du délégué Ajouter
            Console.WriteLine(Execute(new Ajouter(), 2, 3));
            // exécution du délégué Soustraire
            Console.WriteLine(Execute(new Soustraire(), 2, 3));
        }
    }
}
  • lignes 6-8 : l'interface [IOperation] définit une méthode operation.
  • lignes 11-16 et 19-24 : les classes [Ajouter] et [Soustraire] implémentent l'interface [IOperation].
  • lignes 29-31 : la méthode Execute dont le 1er paramètre est du type de l'interface IOperation. La méthode Execute va recevoir successivement comme 1er paramètre, une instance de la classe Ajouter puis une instance de la classe Soustraire.

On retrouve bien l'aspect polymorphique qu'avait le paramètre de type delegate de l'exemple précédent. Les deux exemples montrent en même temps des différences entre ces deux notions.

Les types delegate et interface sont interchangeables

  • si l'interface n'a qu'une méthode. En effet, le type delegate est une enveloppe pour une unique méthode alors que l'interface peut, elle, définir plusieurs méthodes.
  • si l'aspect multicast du delegate n'est pas utilisé. Cette notion de multicast n'existe en effet pas dans l'interface.

Si ces deux conditions sont vérifiées, alors on a le choix entre les deux signatures suivantes pour la méthode Execute :


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

La seconde qui utilise le delegate peut se montrer plus souple d'utilisation. En effet dans la première signature, le premier paramètre de la méthode doit implémenter l'interface IOperation. Cela oblige à créer une classe pour y définir la méthode appelée à être passée en premier paramètre à la méthode Execute. Dans la seconde signature, toute méthode existante ayant la bonne signature fait l'affaire. Il n'y a pas de construction supplémentaire à faire.

8.4. Gestion d'événements

Les objets delegate peuvent servir à définir des événements. Une classe C1 peut définir un événement evt de la façon suivante :

  • un type delegate est défini dans ou en-dehors de la classe C1 :
delegate TResult Evt(T1 param1, T2 param2, ...);
  • la classe C1 définit un champ de type delegate Evt :
public Evt Evt1;
  • lorsque une instance c1 de la classe C1 voudra signaler un événement, elle exécutera son delegate Evt1 en lui passant les paramètres définis par le delegate Evt. Toutes les méthodes enregistrées dans le delegate Evt1 seront alors exécutées avec ces paramètres. On peut dire qu'elles ont été averties de l'événement Evt1.
  • si un objet c2 utilisant un objet c1 veut être averti de l'occurrence de l'événement Evt1 sur l'objet c1, il enregistrera l'une de ses méthodes c2.M dans l'objet délégué c1.Evt1 de l'objet c1. Ainsi sa méthode c2.M sera exécutée à chaque fois que l'événement Evt1 se produira sur l'objet c1. Il pourra également de désinscrire lorsqu'il ne voudra plus être averti de l'événement.
  • comme l'objet délégué c1.Evt1 peut enregistrer plusieurs méthodes, différents objets ci pourront s'enregistrer auprès du délégué c1.Evt1 pour être prévenus de l'événement Evt1 sur c1.

Dans ce scénario, on a :

  • une classe qui signale un événement
  • des classes qui sont averties de cet événement. On dit qu'elles souscrivent à l'événement ou s'abonnent à l'événement.
  • un type delegate qui définit la signature des méthodes qui seront averties de l'événement

Le framework .NET définit :

  • une signature standard du delegate d'un événement
public delegate void MyEventHandler(object source, EventArgs evtInfo);
  • source : l'objet qui a signalé l'événement
  • evtInfo : un objet de type EventArgs ou dérivé qui apporte des informations sur l'événement
  • le nom du delegate doit être terminé par EventHandler

  • une façon standard pour déclarer un événement de type MyEventHandler dans une classe :

1
2
3
4
public Class C1{
    public event MyEventHandler Evt1;
...
}

Le champ Evt1 est de type delegate. Le mot clé event est là pour restreindre les opérations qu'on peut faire sur lui :

  • de l'extérieur de la classe C1, seules les opérations += et -= sont possibles. Cela empêche la suppression (par erreur du développeur par exemple) des méthodes abonnées à l'événement. On peut simplement s'abonner (+=) ou se désabonner (-=) de l'événement.
  • seule une instance de type C1 peut exécuter l'appel Evt1(source,evtInfo) qui déclenche l'exécution des méthodes abonnées à l'événement Evt1.

Le framework .NET fournit une méthode générique satisfaisant à la signature du delegate d'un événement :

public delegate void EventHandler<TEventArgs>(object source, TEventArgs evtInfo) where TEventArgs : EventArgs
  • le delegate EventHandler utilise le type générique TEventArgs qui est le type de son 2ième paramètre
  • le type TEventArgs doit dériver du type EventsArgs (where TEventArgs : EventArgs)

Avec ce delegate générique, la déclaration d'un événement X dans la classe C suivra le schéma conseillé suivant :

  • définir un type XEventArgs dérivé de EventArgs pour encapsuler les informations sur l'événement X
  • définir dans la classe C un événement de type EventHandler<XEventArgs>.
  • définir dans la classe C une méthode protégée
protected void OnXHandler(XEventArgs e);

destinée à "publier" l'événement X aux abonnés.

Considérons l'exemple suivant :

  • une classe Emetteur encapsule une température. Cette température est observée. Lorsque cette température dépasse un certain seuil, un événement doit être lancé. Nous appellerons cet événement TemperatureTropHaute. Les informations sur cet événement seront encapsulées dans un type TemperatureTropHauteEventArgs.
  • une classe Souscripteur s'abonne à l'événement précédent. Lorsqu'elle est avertie de l'événement, elle affiche un message sur la console.
  • un programme console crée un émetteur et deux abonnés. Il saisit les températures au clavier et les enregistre dans une instance Emetteur. Si celle-ci est trop haute, l'instance Emetteur publie l'événement TemperatureTropHaute.

Pour se conformer à la méthode conseillée de gestion des événements, nous définissons tout d'abord le type TemperatureTropHauteEventArgs pour encapsuler les informations sur l'événement :


using System;

namespace Chap6 {
    public class TemperatureTropHauteEventArgs:EventArgs {
        // température lors de l'évt
        public decimal Temperature { get; set; }
        // constructeurs
        public TemperatureTropHauteEventArgs() {
        }
        public TemperatureTropHauteEventArgs(decimal temperature) {
            Temperature = temperature;
        }
    }
}
  • ligne 6 : l'information encapsulée par la classe TemperatureTropHauteEventArgs est la température qui a provoqué l'événement TemperatureTropHaute.

La classe Emetteur est la suivante :


using System;

namespace Chap6 {
    public class Emetteur {
        static decimal SEUIL = 19;

        // température observée
        private decimal temperature;
        // nom de la source
        public string Nom { get; set; }
        // évt signalé
        public event EventHandler<TemperatureTropHauteEventArgs> TemperatureTropHaute;

        // lecture / écriture température
        public decimal Temperature {
            get {
                return temperature;
            }
            set {
                temperature = value;
                if (temperature > SEUIL) {
                    // on signale l'événement aux abonnés
                    OnTemperatureTropHaute(new TemperatureTropHauteEventArgs(temperature));
                }
            }
        }

        // signalement d'un évt
        protected virtual void OnTemperatureTropHaute(TemperatureTropHauteEventArgs evt) {
            // émission de l'évt TemperatureTropHaute aux abonnés
            TemperatureTropHaute(this, evt);
        }
    }
}
  • ligne 5 : le seuil de température au-delà duquel l'événement TemperatureTropHaute sera publié.
  • ligne 10 : l'émetteur a un nom pour être identifié
  • ligne 12 : l'événement TemperatureTropHaute.
  • lignes 15-26 : la méthode get qui rend la température et la méthode set qui l'enregistre. C'est la méthode set qui fait publier l'événement TemperatureTropHaute si la température à enregistrer dépasse le seuil de la ligne 5. Elle fait publier l'événement par la méthode OnTemperatureTropHauteHandler de la ligne 29 en lui passant pour paramètre un objet TemperatureTropHauteEventArgs dans lequel on a enregistré la température qui a dépassé le seuil.
  • lignes 29-32 : l'événement TemperatureTropHaute est publié avec pour 1er paramètre l'émetteur lui-même et pour second paramètre l'objet TemperatureTropHauteEventArgs reçu en paramètre.

La classe Souscripteur qui va s'abonner à l'événement TemperatureTropHaute est la suivante :


using System;

namespace Chap6 {
    public class Souscripteur {
        // nom
        public string Nom { get; set; }

        // gestionnaire de l'évt TemperatureTropHaute
        public void EvtTemperatureTropHaute(object source, TemperatureTropHauteEventArgs e) {
            // affichage console opérateur
            Console.WriteLine("Souscripteur [{0}] : la source [{1}] a signalé une température trop haute : [{2}]", Nom, ((Emetteur)source).Nom, e.Temperature);
        }
    }
}
  • ligne 6 : chaque souscripteur est identifié par un nom.
  • lignes 9-12 : la méthode qui sera associée à l'événement TemperatureTropHaute. Elle a la signature du type delegate EventHandler<TEventArgs> qu'un gestionnaire d'événement doit avoir. La méthode affiche sur la console : le nom du souscripteur qui affiche le message, le nom de l'émetteur qui a signalé l'événement, la température qui a déclenché ce dernier.
  • l'abonnement à l'événement TemperatureTropHaute d'un objet Emetteur n'est pas fait dans la classe Souscripteur. Il sera fait par une classe externe.

Le programme [Program.cs] lie tous ces éléments entre-eux :


using System;
namespace Chap6 {
    class Program {
        static void Main(string[] args) {
            // création d'un émetteur d'evts
            Emetteur e1 = new Emetteur() { Nom = "e" };
            // création d'un tableau de 2 souscripteurs
            Souscripteur[] souscripteurs = new Souscripteur[2];
            for (int i = 0; i < souscripteurs.Length; i++) {
                // création souscripteur
                souscripteurs[i] = new Souscripteur() { Nom = "s" + i };
                // on l'abonne à l'évt TemperatureTropHaute de e1
                e1.TemperatureTropHaute += souscripteurs[i].EvtTemperatureTropHaute;
            }
            // on lit les températures au clavier
            decimal temperature;
            Console.Write("Température (rien pour arrêter) : ");
            string saisie = Console.ReadLine().Trim();
            // tant que la ligne saisie est non vide
            while (saisie != "") {
                // la saisie est-elle un nombre décimal ?
                if (decimal.TryParse(saisie, out temperature)) {
                    // température correcte - on l'enregistre
                    e1.Temperature = temperature;
                } else {
                    // on signale l'erreur
                    Console.WriteLine("Température incorrecte");
                }
                // nouvelle saisie
                Console.Write("Température (rien pour arrêter) : ");
                saisie = Console.ReadLine().Trim();
            }//while
        }
    }
}
  • ligne 6 : création de l'émetteur
  • lignes 8-14 : création de deux souscripteurs qu'on abonne à l'événement TemperatureTropHaute de l'émetteur.
  • lignes 20-32 : boucle de saisie des températures au clavier
  • ligne 24 : si la température saisie est correcte, elle est transmise à l'objet Emetteur e1 qui déclenchera l'événement TemperatureTropHaute si la température est supérieure à 19 ° C.

Les résultats de l'exécution sont les suivants :

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]