Dans la programmation procédurale, on crée un algorithme qu'on peut dessiner d'un bout à l'autre, sans ambiguïté. On sait d'avance tout ce qui va se passer et dans quel ordre.
La programmation événementielle est différente, puisqu'elle dépend des événements qui seront levés - ceci dépend généralement de conditions extérieures imprévisibles, comme par exemple ce que fera l'usager. À ce moment, on programme à l'avance des réactions à différents événements possibles, puis le programme exécutera chaque routine selon les événements qui seront générés ce coup-ci.
La programmation Windows fonctionne de cette façon: on fournit à l'usager une interface souvent complexe, qu'il peut utiliser comme bon lui semble. Notre programme réagira simplement à ses actions.
On peut également se servir des événements pour communiquer de l'information entre deux modules ou entre deux threads - l'un des deux lance un événement et l'autre y réagit.
Les événements en .NET sont gérés par des délégués. Un événement est déclaré (publié) grâce à un délégué et on peut y souscrire en ajoutant une fonction approprié à ce délégué.
Nous avons déjà vu le délégué dans un chapitre précédent et on le considérait alors un peu comme une définition d'un "type de fonction". On pouvait ensuite déclarer une fonction recevant en paramètre une fonction du "bon type", selon le délégué.
En réalité c'est un peu plus complexe que ça. Le délégué est plutôt comme un enrobage à fonctions. Lorsqu'on déclare le délégué, ainsi:
public delegate int MonDéléguéÀMoi(bool param1, string param2);
on définit le délégué et quel type de fonctions il pourra enrober (ici des fonctions recevant en paramètre un booléeen et un string, dans cet ordre, et retournant un entier).
Après coup, on pourra ajouter une fonction à l'intérieur du délégué. Lorsque le délégué lui-même sera appelé, celui-ci appellera sa fonction "interne". Lorsque l'on utilise un délégué en paramètre, comme ici:
public int MaMéthode(MonDéléguéÀMoi georges, int nombre) { //... }
on déclare une référence au délégué. Dans cet exemple, georges est une référence (ou un pointeur implicite, si vous préférez) vers le délégué MonDéléguéÀMoi. Lorsque l'on appelle MaMéthode, on lui passe une fonction en paramètre. Cette fonction sera automatiquement ajoutée au délégué MonDéléguéÀMoi, puis appelée lorsque georges sera appelé dans MaMéthode.
Tout ceci se fait de façon plutôt transparente, d'où le fait qu'on pouvait fort bien ne pas savoir que le délégué était un enrobage à fonctions...
Pour ajouter une fonction à un délégué, on devra en créer une instance et y passer la fonction en paramètre. Par exemple, on pourra faire:
MonDéléguéÀMoi uneInstance = new MonDéléguéÀMoi(uneFonction);
Notez que l'on ne passe aucun paramètre à la fonction, et qu'on omet les () après son nom.
Plus tard on pourra appeler le délégué en faisant simplement:
int i = uneInstance(True, "allo");
La fonction uneFonction sera alors appelée, les paramètres True et "allo" lui seront passés, et le délégué uneInstance retournera l'entier retourné par uneFonction.
Il est également possible d'ajouter un délégué à un autre délégué. De cette façon, un délégué peut en contenir plusieurs - et chacun des délégués internes contiendront une fonction.
Lorsque le délégué multicast est appelé, il appelle lui-même tous ses délégués internes, qui eux à leur tour appellent chacun leur fonction. Les paramètres passés au délégué multicast seront passés aux délégués internes, puis aux fonctions.
Mais qu'en est-il des valeurs de retour? En fait, il n'en est rien: un délégué multicast retourne void, et tous les délégués qu'il contient devront retourner void eux aussi. C'est logique, sinon que retournerait donc le délégué multicast?
Pour ajouter un délégué à un autre, il suffit d'utiliser l'opérateur +, qui est habilement surchargé à cet effet. On peut faire par exemple:
délégué1 = délégué2 + délégué3
ou encore:
délégué1 = délégué1 + délégué2
ce qui revient au même que:
délégué1 += délégué2
(si le + est surchargé, le += l'est toujours aussi automatiquement!)
Lorsque l'on déclare un événement, on doit lui associer un délégué multicast (donc qui retourne void) qui a été déclaré précédemment. L'événement doit être public si on veut qu'il soit accessible à d'autres objets et le délégué doit être au moins aussi permissif que l'événement.
Si quelqu'un veut "souscrire" à un événement, en réalité ce qu'il fera c'est ajouter un délégué (tout neuf) au délégué multicast de l'événement. Ce délégué tout neuf enrobera une fonction de son choix, celle qu'il voudra voir s'exécuter quand l'événement sera levé.
Pour lever l'événement, tout ce qu'on fera c'est appeler l'événement, comme s'il était une fonction, en lui passant les paramètres prévus (plus à ce sujet juste en-dessous). L'événement, quand il est appelé, appellera en fait son délégué multicast associé, qui lui appellera tous les délégués qu'il contient, qui eux appelleront les fonctions qu'ils enrobent - ouf! Les paramètres seront passés à chaque étape.
Lorsque vous faites une application Windows et que vous double-cliquez sur un contrôle dans un formulaire, vous vous retrouvez automatiquement dans le code derrière le formulaire, dans un squelette de fonction fraîchement écrit pour vous par Visual Studio. Par exemple, si vous double-cliquez sur un bouton, vous obtiendrez quelque chose comme:
private void button1_Click(object sender, EventArgs e) { }
La fonction button1_Click, comme son nom l'indique, est celle qui sera appelée lorsque le bouton button1 lancera l'événement Click.
Rien dans cette fonction ne nous permet réellement d'associer la fonction à l'événement (à part son nom, mais ce n'est certainement pas ce sur quoi le compilateur se fie!). Qu'est-ce qui fait que le Click du button1 fera exécuter button1_Click?
La réponse est dans le code "caché" du formulaire, dans le fichier Form1.Designer.cs (évidemment, vous aurez compris que Form1 sera le nom du formulaire). On y trouve (entre autres) une région appelée "Code généré par le Concepteur Windows Form". Vous savez sans doute déjà (sinon vous le saurez très bientôt) qu'on retrouvera dans cette région la déclaration, l'instanciation et les modifications aux propriétés des différents contrôles du formulaire. C'est cette section qui fait que le formulaire existe tel que vous l'avez dessiné.
On y trouvera également, si on cherche un peu, une ligne comme:
this.button1.Click += new System.EventHandler(this.button1_Click);
Voilà la ligne qui associe la méthode à l'événement! Analysons-la un peu:
Si this.button1_Click peut être ajouté à this.button1.Click, c'est qu'elle accepte les mêmes paramètres et ne retourne rien.
Les deux paramètres de this.button1_Click (et par conséquent de System.EventHandler, et donc de la plupart des événements) sont:
Le premier est simple: sender est une référence à l'objet qui a lancé l'événement. Dans notre exemple, c'est le bouton button1. Ceci est un standard qui est généralement respecté partout (du moins qui l'est partout dans .NET): toute fonction réagissant à un événement pourra accéder à l'objet qui a lancé l'événement. Comme System.EventHandler peut être utilisé par n'importe quel événement, sender sera un object, donc vague. Lorsque l'on fait ses propres événements, on peut décider de préciser sender dès le départ, ou de garder object pour respecter le standard - l'utilisateur pourra toujours le caster en son type de départ s'il en a besoin.
e, quant à lui, est un objet conçu pour contenir de l'information supplémentaire pertinente sur l'événement qui vient d'être lancé. Dans notre exemple, cette information est extrêmement simple puisqu'un EventArgs ne contient rien du tout! En effet, lorsqu'un bouton a été cliqué, qu'on en a été informé et qu'on sait quel bouton, que pourrait-on vouloir savoir d'autre?
À quoi sert alors e s'il est une instance d'une classe sans membre? Simplement à respecter le standard qui dit que tout événement retourne un object sender et un e EventArgs (ou dérivé de EventArgs).
Il existe en effet des événements qui ont vraiment de l'information à passer. Prenons par exemple Control.KeyPress, qui est lancé lorsqu'une touche du clavier a été enfoncée puis relâchée. On voudrait bien savoir dans ce cas là quelle touche a été enfoncée. C'est pourquoi cet événement utilise un paramètre de type KeyEventArgs, un dérivé d'EventArgs qui contient une valeur KeyCode contenant le code ASCII de la touche.
Dans ce cas, on ne peut pas utiliser System.EventArgs comme délégué, il faut utiliser un délégué conçu spécialement pour cet événement: KeyPressEventHandler.
Voici un exemple simple montrant les étapes à suivre pour créer ses propres événements et y réagir.
Supposons une classe Horloge, qui peut être démarrée et arrêtée. Lorsqu'elle est démarrée, elle vérifie l'heure du système à toutes les 10 millisecondes. Lorsque les secondes ont changé, elle envoie un événement OnSecondChange.
On peut l'utiliser dans un formulaire Windows pour afficher l'heure dans un textbox et la mettre à jour seulement lorsqu'elle change. Ainsi les fonctionnalités sont bien séparées: un objet vérifie l'heure, le formulaire ne sert qu'à l'interface. Horloge pourrait donc aisément être utilisée dans n'importe quel type d'interface.
Si OnSecondChange nous indique que l'heure a changé, il serait tout de même pratique qu'il nous donne la nouvelle heure en passant. Horloge utilisera donc une classe dérivée de EventArgs pour passer cette information.
Voyons d'abord le code de cette classe dérivée d'EventArgs:
class TimeEventArgs:EventArgs { private DateTime t; // Pour stocker l'heure courante public DateTime T // Pour y accéder en lecture seulement { get { return t; } } // Constructeur qui assigne l'heure courante à t. public TimeEventArgs(DateTime time) { t = time; } }
Un TimeEventArgs dérive de EventArgs et contient un DateTime privé t. Une propriété publique T permet d'y accéder en lecture seule et un constructeur permet d'en construire une instance en populant t.
Très simple. Voyons maintenant l'Horloge elle-même:
class Horloge { // Le délégué OnSecondChangeDelegate enrobera les fonctions appelées lorsque // l'événement OnSecondChange sera levé. public delegate void OnSecondChangeDelegate(Object sender, TimeEventArgs e); // L'événement OnSecondChange, du type du délégué OnSecondChangeDelegate public event OnSecondChangeDelegate OnSecondChange; public bool stop; // Détermine l'état du surveillant private int seconds = 70; // Se rappelle des dernières secondes // la valeur 70 nous assure que la première fois sera un changement. private DateTime dt; // Pour aller chercher l'heure courante. // Démarre le surveillant public void Go() { stop = false; while (!stop) // Jusqu'à ce qu'on l'arrête { System.Windows.Forms.Application.DoEvents(); // Pour ne pas "geler" le système System.Threading.Thread.Sleep(10); // Attend 10 millisecondes (1/100e de seconde) dt = DateTime.Now; // Prend l'heure courante if (dt.Second != seconds) // Si les secondes ont changé depuis la dernière fois { if (OnSecondChange != null) // Si quelqu'un a souscrit à l'événement (facultatif) OnSecondChange(this, new TimeEventArgs(dt)); // On lance l'événement avec dt dans le TimeEventArgs seconds = dt.Second; // On mémorise les secondes actuelles } } } // Arrête le surveillant public void Stop() { stop = true; } }
On remarque les étapes pour créer et lancer un événements:
Notez au passage:
Notre Horloge est donc complète. Pour l'utiliser dans un formulaire Windows, en supposant qu'on a un bouton qui démarre l'horloge, un qui l'arrête et un textbox qui affiche l'heure, on fera:
public partial class Form1 : Form { // Déclare une Horloge et l'instancie - s'en servira pour savoir si l'heure a changé private Horloge h = new Horloge(); public Form1() { InitializeComponent(); // Souscris à l'événement en ajoutant une nouvelle instance du délégué OnSecondChangeDelegate // à l'événement OnSecondChange de h (l'Horloge). Ce délégué enrobe la méthode OnSecondChangeMethod, // méthode décrite plus bas. h.OnSecondChange += new Horloge.OnSecondChangeDelegate(OnSecondChangeMethod); } // La méthode appelée lorsque l'événement OnSecondChange est levé, puisqu'elle est enrobée dans le délégué // OnSecondChangeDelegate qui fut ajouté à l'événement h.OnSecondChange. private void OnSecondChangeMethod(object sender, TimeEventArgs e) { // Change le contenu de la boite de texte pour y mettre le DateTime contenu dans e (fourni par // l'événement), formaté sous forme de temps long (hh:mm:ss) textBox1.Text = String.Format("{0:T}", e.T); } // Bouton démarrant le surveillant de h private void button1_Click(object sender, EventArgs e) { h.Go(); } // Bouton arrêtant le surveillant de h private void button2_Click(object sender, EventArgs e) { h.Stop(); } }
Pour utiliser l'événement, c'est très simple:
Notez en passant l'utilisation de String.Format, qui permet de formater une chaîne de texte en utilisant des jetons positionnels comme {0} et en leur définissant un format, comme dans Console.WriteLine.
Voilà, le tour est joué. L'Horloge h s'occupe de vérifier l'heure constamment et nous avertira quand elle aura changé. Le formulaire quant à lui fait simplement attendre l'avertissement de h pour faire quelque chose.