1 - Introduction
2 - A qui
s'adresse l'article?
3 - Configuration requise
4 - Premier contact
Les événements immédiats
Les
événements différés
5 - Quelques
nouveautés ASP 2.0
Le renvoi
inter page
Validation des événements
UseSubmitBehavior
Annexe A -Bibliographie
Annexe
B: La syntaxe des événements
Positionnement du problème
Syntaxe
standard
Implémentation
optimisée
Il suffit de se balader 5 minutes sur n'importe quel
forum pour constater que la façon dont
fonctionnent les événements en ASP.NET relève largement
de la magie noire pour nombre de développeurs.
Soyons honnête, moi même j'ai souvent buté sur cette question. C'est justement la raison
qui m'a donné envie d'écrire une fois pour toute un
article, qui soit le plus complet possible sur la question.
Environ 100 pages d'explications rien que pour vous !
Le tutoriel s'étend sur 5 parties que voici:
Partie 4: Architecture serveur
Partie 5: Ordonnancement des événements
Concrètement comment lire ce tutoriel ?
Je vous conseille de commencer en douceur avec la première partie qui sera sans doute un rappel pour certains. Mais j'y précise un certain nombre de points, le vocabulaire utilisé dans cet article et je présenterai quelques nouveautés d'ASP 2.0. Lisez cet article au moins en diagonale.
Lisez ensuite l'annexe B sur la syntaxe des
événements. Je suis souvent effaré par les énormités que
l'on relève dans les sites et forums.
Microsoft préconise et supporte une architecture précise
appelée pattern On, il est important de
faire les choses ainsi, même si c'est plus verbeux.
Donc direction obligatoire: annexe B!
Avant d'attaquer la suite, plongez vous dans l'article sur le viewstate[56] inspiré d'un très très bon article de Dave Reed[15]. A l'origine c'était même un des chapitres de ce tutoriel. J'y ferai fréquemment référence et vous aurez du mal à comprendre certaines explications sans l'avoir lu.
Continuez ensuite par la partie sur les cas récurrents. C'est en quelque sorte des recettes de cuisine pour apprendre des méthodes standards de résolution de certains problèmes liés aux événements. Vos cheveux vous remercieront...
Si vous n'envisagez pas d'écrire de contrôles, fussent t'ils ascx, vous pouvez sauter les 2 parties qui suivent et jeter un œil négligeant sur le dernier chapitre consacré à l'ordonnancement des événements. Vous y trouverez pas moins de 2 posters (couleur et noir & blanc) qui ne demandent qu'à se faire imprimer !
Les autres liront avec intérêt les deux chapitres qui abordent les coulisses. Nous allons soulever le capot des événements ASP d'abord côté client, c'est à dire la page HTML, puis côté serveur, le code behind.
Un chapitre suivra ensuite sur une des nouveautés d'ASP 2.0: les événements asynchrones (vous savez, Ajax).
J'espère que vous passerez un bon moment en ma compagnie. Sinon, vous pourrez toujours colorier les petits dessins ...
Note:
Vous trouverez à la fin de ce document une
bibliographie. C'est l'ensemble des documents utilisés
pour rédiger cet article. Je signale dans le texte une
référence bibliographique par la syntaxe suivante: [45,48].
Signifie que les références 45 et 48 permettent d'en savoir un
peu plus.
Tous les développeurs ASP.NET qu'ils soient débutants ou non.
J'ai préparé cet article avec la configuration suivante:
Je ne voudrai pas terminer cet article sans dire un grand merci à Reflector, l'outil de décompilation de Lutz Roeder sans lequel je ne parviens même pas à imaginer que l'on puisse développer en .NET[16].
Faisons un peu le point sur le vocabulaire que nous utiliserons par la suite ainsi que quelques concepts de base, au cas où... Et on en profitera pour lorgner vers certaines nouveautés ASP .NET 2.0. Pour traiter un événement serveur la page ASP doit poster la page vers le serveur comme on s'en doute. Dans le jargon ASP poster une page s'appelle un renvoi (Postback). Mais on peut également dire que l'on poste ou publie la page. Un renvoi est donc aussi bien une requête HTTP POST que GET.
ASP peut lever deux types d'événements:
Les événements immédiats sont déclenchés immédiatement (sic !). C'est le cas par exemple d'un click sur un bouton:
Listing 4-1: Exemple canonique d'événement immédiat
<html xmlns="http://www.w3.org/1999/xhtml" > <body> <form id="form1" runat="server"> <div> <asp:button OnClick="Button1_Click" ID="Button1" runat="server" EnableViewState="False" Text="Evénement bouton" /> </div> </form> </body> </html>
Code C#
public partial class _Default : System.Web.UI.Page { protected void Button1_Click(object sender, EventArgs e) { Response.Write("Click sur un bouton<br/>"); } }
Code VB
Partial Class _Default Inherits System.Web.UI.Page Protected Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click Response.Write("Click sur un bouton<br/>") End Sub End Class
Le fonctionnement est assez simple puisque la page n'héberge qu'un simple bouton. Un click sur celui ci déclenche son événement Click et affiche un message:

Commençons par le code ASP.
La seule chose de particulière est la présence de l'attribut OnClick qui décore le composant ASP Button. Sa valeur correspond au nom d'une méthode présente dans le code behind. La syntaxe de cette méthode est celle d'un gestionnaire d'événement, c'est à dire:
Plusieurs remarques s'imposent.
Tout d'abord
si l'attribut OnClick ressemble également au gestionnaire d'événement du même nom des contrôles
HTML, la comparaison s'arrête là: OnClick déclenche
un événement serveur car Button n'est pas un
contrôle HTML, mais serveur. Si vous tentez d'y placer du code
javascript, la compilation échouera.
Comment faire alors pour déclencher un événement client sur un bouton?
Premièrement on peut utiliser un bouton HTML plutôt qu'un bouton serveur.
Ensuite depuis ASP 2.0 les boutons exposent une nouvelle propriété OnClientClick dans laquelle on peut déclarer un appel à du code javascript. Si vous êtes encore sous ASP 1.1 vous pouvez écrire du code comme celui-ci:
Listing 4-2: associer un événement javascript à un composant ASP en ASP 1.1
Code C#
protected void Page_Load(object sender, EventArgs e) { this.Button1.Attributes.Add("onclick", "javascript:alert('hello');"); }
Code VB
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load Me.Button1.Attributes.Add("onclick", "javascript:alert('hello');") End Sub
Note:
Remarquez que le javascript est terminé par un
point-virgule (;). C'est important pour éviter des
bogues compliqués à comprendre.
Note:
Le code javascript est lancé avant le renvoi de la
page.
Les composants HTML peuvent eux aussi être le siège d'un événement serveur, mais à condition de les transformer en contrôle serveur ce qui se traduit par deux étapes simples:
Note:
L'environnement de développement affiche un composant
serveur ASP ou un composant HTML transformé en composant
serveur avec un petit tag vert en haut à gauche,
regardez bien:

Que l'événement soit client ou serveur, il suffit de cliquer deux fois sur le composant pour que l'éditeur de VS mette automatiquement en place le code correspondant au contexte. On peut se demander ce qui se passe si un contrôle HTML porteur d'un événement client est transformé en contrôle serveur et qu'un événement serveur lui est associé. Un essai rapide montre que cela ne fonctionne pas... si on n'est pas précautionneux !
La sortie HTML générée est la suivante:
Listing 4-3: Contrôle HTML avant d'être transformé en contrôle serveur
<html xmlns="http://www.w3.org/1999/xhtml" > <body> <form name="form1" method="post" action="Default4.aspx" id="form1"> <input onclick="alert('hello')" id="Button1" type="button" value="button" /> </form> </body> </html>
Listing 4-4: Le même contrôle après transformation
<html xmlns="http://www.w3.org/1999/xhtml" > <body> <form name="form1" method="post" action="Default4.aspx" id="form1"> <script type="text/javascript"> <!-- var theForm = document.forms['form1']; if (!theForm) { theForm = document.form1; } function __doPostBack(eventTarget, eventArgument) { if (!theForm.onsubmit || (theForm.onsubmit() != false)) { theForm.__EVENTTARGET.value = eventTarget; theForm.__EVENTARGUMENT.value = eventArgument; theForm.submit(); } } // --> </script> <input language="javascript" onclick="alert('hello') __doPostBack('Button1','')" name="Button1" type="button" id="Button1" value="button" /> </form> </body> </html>
Il manque un point virgule que VS n'ajoute pas. D'une façon générale il est très sain, même si javascript ne l'exige pas, de terminer ses lignes avec un point virgule. Ce n'est pas le seul scénario où cette oubli provoque des problèmes.
Les événements différés sont, eux, émis plus tard, en même temps que le premier événement immédiat rencontré. C'est typiquement le cas des événements CheckedChanged de la case à cocher ou bien SelectedIndexChanged d'une liste déroulante.
Listing 4-5: exemple canonique d'événement différé
<html xmlns="http://www.w3.org/1999/xhtml" > <body> <form id="form1" runat="server"> <div> <asp:checkbox ID="CheckBox1" runat="server" OnCheckedChanged="CheckBox1_CheckedChanged" /> <asp:dropdownlist OnSelectedIndexChanged="DropDownList1_SelectedIndexChanged" ID="DropDownList1" runat="server"> <asp:listitem>Un</asp:listitem> <asp:listitem>Deux</asp:listitem> <asp:listitem>Trois</asp:listitem> <asp:listitem>Quatre</asp:listitem> <asp:listitem>Cinq</asp:listitem> </asp:dropdownlist> <asp:button ID="Button1" runat="server" OnClick="Button1_Click1" Text="Emettre la page" /></div> </form> </body> </html>
Code C#
public partial class _Default : System.Web.UI.Page { protected void Button1_Click1(object sender, EventArgs e) { Response.Write("Click sur un bouton<br/>"); } protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e) { Response.Write("Sélection dans une liste déroulante<br/>"); } protected void CheckBox1_CheckedChanged(object sender, EventArgs e) { Response.Write("Sélection dans une case à cocher<br/>"); } }
Code VB
Partial Class _Default Inherits System.Web.UI.Page Protected Sub Button1_Click1(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click Response.Write("Click sur un bouton<br/>") End Sub Protected Sub DropDownList1_SelectedIndexChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles DropDownList1.SelectedIndexChanged Response.Write("Click sur une liste déroulante<br/>") End Sub Protected Sub CheckBox1_CheckedChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles CheckBox1.CheckedChanged Response.Write("Click sur une case à cocher<br/>") End Sub End Class
Il s'agit d'un formulaire disposant d'une case à cocher, d'une liste déroulante et d'un bouton. Un clic successivement sur ces 3 composants affiche une fois le renvoi effectué:

ASP ne garantit pas l'ordre d'exécution des événements différés. L'événement click se produit toujours en dernier, nous verrons pourquoi lors de l'étude de l'architecture des événements ainsi que l'analyse de leur ordonnancement.
Il est possible de transformer un événement différé en événement immédiat en mettant à true la propriété AutoPostBack du contrôle.
Pour expérimenter, essayez de faire deux changements de sélection dans la liste déroulante. Le message ne s'affiche qu'une seule fois :

De plus si vous faites une sélection, revenez en arrière en rechargeant la valeur d'origine, puis cliquez sur le bouton, aucun événement n'est détecté. Nous reviendrons sur cette expérience lors d'une prochaine partie.
Note:
Il est possible de savoir si la page a été chargée
suite à un renvoi en interrogeant sa propriété
IsPostBack.
Pour finir ce tour d'horizon, signalons aussi qu'ASP 2.0 apporte un support natif aux événement asynchrones qui sont la clef de voûte des application Ajax/Atlas. Un chapitre sera consacré à ce type d'événement dans la partie architecture serveur.
ASP 2.0 améliore pas mal de choses dans la prise en charge des événements. Nous en avons déjà abordé certaines et d'autres vont suivre. En particulier les pages et les contrôles se sont enrichis d'événements nouveaux. Nous verrons cela dans la partie consacrée à l'ordonnancement des événements.
Je souhaite ici parler de nouvelles fonctionnalités en liaison avec les événements importants à connaître, mais qui n'ont pas spécialement leur place dans le reste du document.
En ASP 1.1, Le renvoi a lieu exclusivement sur la page d'origine ce qui constitue une régression par rapport à ASP standard. On s'en sort en général en émettant une redirection côté serveur. ASP 2.0 rétablit l'équilibre grâce à la nouvelle propriété PostBackUrl. On dispose aussi de la propriété IsCrossPostBack qui indique si on a réalisé un renvoi inter page tandis que IsPostBack détecte un renvoi sur la même page.
La propriété PreviousPage pointe alors vers une instance de l'objet Page désignant la page d'origine.
Si on connaît à l'avance la page, ou le type de page, qui appelle la page courante, Il est également possible de poser une directive <%@ PreviousPageType %> dans la page pour typer PreviousPage. On peut donc accéder à tous les membres publics de la page précédente depuis le code behind.
L'attaque par injection de code consiste à tenter de fournir dans une zone de saisie une valeur inattendue pour essayer de planter le site ou de provoquer des dégâts dans la base de données.
Se protéger des attaques par injection de code est
largement du ressort des développeurs[2]. Mais ASP 2.0
fournit un mécanisme pour simplifier le problème appelé
validation des événements. Il
est important de les connaître car la plupart du temps,
notre premier contact avec ces derniers consiste à le désactiver faute de comprendre
son mode de fonctionnament.
Notons également une porte d'entrée à laquelle on ne
pense pas toujours: la chaîne de requête (querystring)
que l'on doit elle aussi protéger [45].
Prenons l'exemple d'une liste déroulante.
Son rendu HTML contient un
certain nombre d'articles qui constituent la liste des
choix possibles dans la combo, par exemple la liste des
nombres de 1 à 5.
Ainsi si un renvoi de la page retourne 6, alors on est certain que cette valeur n'est
pas correcte et que le site est probablement la cible
d'une attaque par injection de code.
Pour détecter cette attaque il suffit donc de vérifier que la saisie retournée est bien une valeur comprise entre 1 et 5. C'est cette vérification qu'ASP 2.0 prend en charge automatiquement. De quelle manière ?
Reprenons l'exemple 4-5 précédent. Si l'on examine la sortie HTML on trouve ceci:
Listing 5-1: Sortie HTML du listing 4-5
<html xmlns="http://www.w3.org/1999/xhtml" > <body> <form name="form1" method="post" action="Default.aspx" id="form1"> <div> <input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="/wEPDwUKLTE3...HXBJHbs0eEPvjPV88DjZnD9w==" /> </div> <div> <input id="CheckBox1" type="checkbox" name="CheckBox1" /> <select name="DropDownList1" id="DropDownList1"> <option selected="selected" value="Un">Un</option> <option value="Deux">Deux</option> <option value="Trois">Trois</option> <option value="Quatre">Quatre</option> <option value="Cinq">Cinq</option> </select> <input type="submit" name="Button1" value="Emettre la page" id="Button1" /></div> <div> <input type="hidden" value="/wEWCALI4M7ADAKC5Ne7CQL...jY/Us270aPGS" name="__EVENTVALIDATION" id="__EVENTVALIDATION" /> </div></form> </body> </html>
Le point intéressant est la présence d'un champ caché __EVENTVALIDATION et de son attribut value au contenu que l'on croirait extrait d'un roman de John Le Carré. Dans son blog, Scott Allen nous explique la suite[3].
Lorsque ASP construit le rendu HTML de la page il balaye tous les contrôles et trouve par exemple notre liste déroulante. Pour chaque valeur trouvée dans la liste, il génère une trace en faisant l'opération suivante:
HASH(DropDownList1.UniqueID) XOR HASH(Valeur de l'article dans la combo)
La collection de valeurs ainsi obtenue est sérialisée dans le champ caché. Lors de la validation des événements, il suffit de faire le même calcul pour chaque saisie de l'utilisateur et de voir si on trouve la valeur obtenue dans la liste des valeurs possibles.
On trouvera en [3] la liste des contrôles qui réalisent ce test.
Tout cela est bien, mais présente des effets indésirables. Supposons par exemple que côté client un code javascript ajoute 6 à ma liste. 6 devient alors une valeur possible pour l'utilisateur, mais pas du point de vue d'ASP. On obtient le message d'erreur suivant:
Invalid postback or callback argument. Event validation is enabled using <pages enableeventvalidation="true" /> in configuration or <%@ page enableeventvalidation="true" %> in a page. For security purposes, this feature verifies that arguments to postback or callback events originate from the server control that originally rendered them. If the data is valid and expected, use the ClientScriptManager.RegisterForEventValidation method in order to register the postback or callback data for validation.
On peut évidemment désactiver la validation. Mais c'est dommage en terme de sécurité, d'autant plus que l'on ne peut désactiver à l'échelle d'un contrôle. Les granularités possibles sont le site ou bien la page.
Si on peut prévoir la liste des valeurs qui seront ajoutées par le code client, il est possible de compléter au niveau du code behind la liste des choix autorisés. Cette opération doit absolument se faire avant d'effectuer le rendu. Le mieux est de surcharger la méthode Render.
Listing 5-2: Activation de la validation des événements côté serveur
Code C#
protected override void Render(HtmlTextWriter writer) { ClientScript.RegisterForEventValidation(DropDownList1.UniqueID, "6"); base.Render(writer); }
Code VB
Protected Overrides Sub Render(ByVal writer As System.Web.UI.HtmlTextWriter) ClientScript.RegisterForEventValidation(DropDownList1.UniqueID, "6") MyBase.Render(writer) End Sub
Si vous souhaitez éviter de placer au niveau d'une page un code qui soit spécifique au fonctionnement d'un composant, le mieux est d'écrire un contrôle personnalisé. Dans ce cas il ne faut pas oublier de le marquer de l'attribut SupportsEventValidation. Sans cet attribut, le contrôle ne prend pas en charge la validation des événements ce qui fournit un moyen (non réversible) de désactiver la protection à l'échelle d'un composant. Evidemment, il n'est pas compliqué de voir que cette méthode n'est pas toujours praticable. Mais en attendant mieux...
Pour être complet sachez que l'on peut agir sur l'activation de la validation des événements soit à l'aide d'une directive de page EnableEventValidation ou à l'aide de la propriété de la page de même nom. On peut aussi intervenir dans le fichier de configuration pour une désactivation au niveau du site:
Listing 5-3: Désactiver la validation des événements au niveau d'un site
<system.web> <pages enableEventValidation="false"/> </system.web>
HTML reconnaît deux types de boutons: les boutons type="submit" et les bouton type="button". Je les appellerai par la suite boutons submit et boutons HTML respectivement.
La différence importante est que le premier va intrinsèquement lever un événement submit contrairement au second. On peut lui adjoindre un code javascript qui effectue des actions comme décocher les CheckBox d'une page, ce code javascript pouvant ou non déclencher un submit.
La propriété UseSubmitBehavior permet de transformer un bouton submit en bouton HTML. Elle vaut true par défaut ce qui signifie que le bouton agit en mode submit. Cela est illustré par l'exemple qui suit:
Listing 5-4: Rendu du listing 5-3
<html xmlns="http://www.w3.org/1999/xhtml" > <body> <form id="form1" runat="server"> <asp:button ID="Button1" runat="server" Text="Button" /> <asp:button UseSubmitBehavior="False" ID="Button2" runat="server" Text="Button" /> </form> </body> </html>
Examinons la page HTML générée:
Listing 5-5: Rendu HTML
<input type="submit" name="Button1" value="Button" id="Button1" /> <input onclick="javascript:__doPostBack('Button2','')" type="button" name="Button2" value="Button" id="Button2" />
On constate donc bien le résultat annoncé. Malheureusement la belle aventure s'arrête sur ce constat car pour d'incompréhensibles raisons Microsoft a décidé d'ajouter un appel à __doPostBack qui pour l'essentiel lève un submit, ce qui fait perdre l'essentiel de l'intérêt de UseSubmitBehavior. Il ne semble pas possible de modifier ce comportement. On est alors contraint de se rabattre sur le bouton HTML et perdre certaines fonctionnalités comme les skins.
Voici tout de même une utilisation possible, un bouton cliquable une seule fois[23]. L'idée est simplement de désactiver le bouton une fois que quelqu'un clique dessus. Seulement, si on désactive le bouton, on désactive son gestionnaire d'événement. Si le renvoi s'effectue, il ne sera pas traité côté serveur dans l'événement OnClick. L'idée est donc de se servir d'un bouton pressoir et déclencher le renvoi depuis le code javascript. C'est la qu'intervient notre propriété.
Listing 5-6: Bouton cliquable une fois
<html xmlns="http://www.w3.org/1999/xhtml" > <body> <form id="form1" runat="server"> <div> <asp:button UseSubmitBehavior="False" ID="Button2" runat="server" Text="Button" /></div> </form> </body> </html>
Code C#
protected void Page_Load(object sender, EventArgs e) { if (!ClientScript.IsOnSubmitStatementRegistered(this.GetType(), "OnSubmitScript")) { String cstext = "if (typeof(ValidatorOnSubmit) == 'function' && ValidatorOnSubmit() == false)return false; else {var myCtl = document.getElementById('" + this.Button2.ClientID + "'); myCtl.value = 'Un instant...'; myCtl.disabled = true;}"; ClientScript.RegisterOnSubmitStatement(this.GetType(), "OnSubmitScript", cstext); } } protected void Button1_Click1(object sender, EventArgs e) { Response.Write("Hello<br/>"); Thread.Sleep(5000); // attend 5 secondes }
Code VB
Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) If Not ClientScript.IsOnSubmitStatementRegistered(Me.GetType, "OnSubmitScript") Then Dim cstext As String = "if (typeof(ValidatorOnSubmit) == 'function' && ValidatorOnSubmit() == false)return false; else {var myCtl = document.getElementById('" + Me.Button2.ClientID + "'); myCtl.value = 'Un instant...'; myCtl.disabled = true;}" ClientScript.RegisterOnSubmitStatement(Me.GetType, "OnSubmitScript", cstext) End If End Sub Protected Sub Button2_Click(ByVal sender As Object, ByVal e As EventArgs) Response.Write("Hello<br/>") Thread.Sleep(5000) ' attend 5 secondes End Sub
Comment cela fonctionne ?
Remarquez en premier lieu le gestionnaire d'événements du bouton. Il attend 5 secondes pour donner le temps de voir quelque chose. La séquence visuelle est la suivante:
Arrivée sur la page:

On clique sur le bouton une première fois, cela le désactive le temps que se termine le traitement serveur:

Au bout de 5 secondes:

Dans l'événement Load de la page on enregistre un script déclenché en cas de renvoi et qui fait deux choses:
Avant cela vous voyez un script curieux qui concerne la validation. Nous verrons les détails plus loin, mais ce script vérifie si une fonction de validation a été définie car un contrôle Validator se trouve sur la page. Si c'est le cas, il lance la fonction de validation et bloque le renvoi si celle-ci échoue.
Il est important de ne pas oublier ce traitement car autrement, au cas où la validation échoue, le script désactiverai le bouton comme prévu, mais puisqu'il n'y a pas eu de renvoi, il resterai dans cet état.
Un formulaire héberge un ou plusieurs contrôles. Ces contrôles peuvent à tout moment lever un événement en fonction de diverses circonstances. Par exemple l'utilisateur clique sur un bouton, un mail vient d'arriver, une certaine date est atteinte...
Si la page souhaite recevoir une notification lorsqu'un événement survient elle va s'abonner à cet événement. Elle va pour cela fournir au composant une méthode lui appartenant. Le composant aura pour consigne d'appeler cette méthode lorsque l'événement sera levé. La méthode en question est appelée gestionnaire d'événement.
.NET fournit un support complet de ce scénario, cela signifie:
On pourrait évidemment développer sa propre syntaxe et sa propre architecture. Mais en .NET comme ailleurs il est toujours malsain de générer des codes non standards. Cela complique son intégration avec .NET, sa maintenance par les équipes qui vont vous succéder et éventuellement mettre en échec des outils d'analyse ou des comportements de .NET.
Il est donc crucial de faire à la manière de .NET.
Examinons la classe suivante:
Code C#
using System.Collections.Generic;
namespace ClassesGourmandes { class ClasseActive { private List<string> Gateaux = new List<string>(); public void AjouterUnGateau(string nomGateau) { Gateaux.Add(nomGateau); } } }
Code VB
Class ClasseActive Private Gateaux As List(Of String) = New List(Of String)() Public Sub AjouterUnGateau(ByVal nomGateau As String) Gateaux.Add(nomGateau) End Sub End Class
L'activité de la classe est surveillée par la classe PrefetDeDiscipline qui a pour mission de sévir chaque fois qu'un croissant est reçu parce que ceux-ci sont interdits:
Code C#
using ClassesGourmandes; namespace IciCaPlaisantePas { class PrefetDeDiscipline { public PrefetDeDiscipline() { VilainGarnement = new ClasseActive(); } public ClasseActive VilainGarnement; } }
Code VB
Class PrefetDeDiscipline Public Sub New() VilainGarnement = New ClasseActive() End Sub Public VilainGarnement As ClasseActive End Class
Comment PrefetDeDiscipline pourrait faire pour remplir sa mission? Concrètement, c'est dans ce type de situations qu'interviennent les événements. On va commencer par modifier ClasseActive pour lui faire déclarer un événement appelé Huuum. Il sera levé chaque fois qu'un gâteau est reçu.
On a d'abord besoin d'une classe HuumEventArgs qui hérite de EventArgs. Cette classe contient les informations relatives à l'événement.
Code C#
class HuumEventArgs : EventArgs { public HuumEventArgs(string nom) { this.NomGateau = nom; } private string _NomGateau; public string NomGateau { get { return _NomGateau; } set { _NomGateau = value; } } }
Code VB
Class HuumEventArgs Inherits EventArgs Public Sub New(ByVal nom As String) Me.NomGateau = nom End Sub Private _NomGateau As String Public Property NomGateau() As String Get Return _NomGateau End Get Set _NomGateau = value End Set End Property End Class
Peu de choses à dire si ce n'est qu'elle doit hériter de EventArgs. Cette classe n'apporte pas de fonctionnalités particulières, elle ne sert que de classe de base à tous les arguments d'événement dans l'univers .NET. Si votre événement n'a pas besoin de passer d'informations, on peut l'utiliser directement car elle est concrète.
ClasseActive devient:
Code C#
class ClasseActive { private List<string> Gateaux=new List<string>(); public void AjouterUnGateau(string nomGateau) { Gateaux.Add(nomGateau); OnHuum(new HuumEventArgs(nomGateau)); } public event HuumEventHandler Huum; public delegate void HuumEventHandler(object sender, HuumEventArgs e); protected virtual void OnHuum(HuumEventArgs e) { if (Huum != null) { Huum(this, e); } } }
Code VB
Class ClasseActive Private Gateaux As List(Of String) = New List(Of String)() Public Sub AjouterUnGateau(ByVal nomGateau As String) Gateaux.Add(nomGateau) OnHuum(New HuumEventArgs(nomGateau)) End Sub Public Event Huum As HuumEventHandler Public Delegate Sub HuumEventHandler(ByVal sender As Object, ByVal e As HuumEventArgs) Protected Overridable Sub OnHuum(ByVal e As HuumEventArgs) RaiseEvent Huum(Me, e) End Sub End Class
Le mot clef event sert à déclarer un événement. Il s'agit aussi d'une indication donné au compilateur pour interdire certaines opérations, nous verrons lesquelles plus loin. Donc ne l'oubliez pas, même si vous constatez que cela fonctionne très bien sans.
Que déclare t-on ?
Puisqu'un événement est une fonction
de PrefetDeDiscipline que ClasseActive
va appeler, il est naturel qu'elle soit fondamentalement
un delegate. Le delegate en question
est HuumEventHandler qui déclare la signature
que devra offrir le gestionnaire d'événement de
PrefetDeDiscipline pour que ClasseActive
puisse l'invoquer.
La méthode AjouterGateau reçoit les gâteaux, c'est donc elle qui lève l'événement. En réalité elle ne lève pas l'événement directement, mais appelle une méthode qui va, elle, faire ce travail, mais aussi d'autres choses.
Traditionnellement le nom de cette méthode commence par On suivit du nom de l'événement dont elle est responsable. On aurait pu choisir une autre convention, mais la suivre permet en quelque sorte d'auto documenter votre code. Elle est la base du pattern On.
La méthode On lève l'événement en appelant le délégué. Mais avant elle vérifie qu'au moins une méthode est abonné à l'événement. On pourrait mettre d'autres logiques de codage comme un log de tous les appels à cet événement ou plus simplement un point d'arrêt dans un scénario de débogage.
La méthode On doit également être déclarée virtual et protected. Protected parce qu'elle ne doit pas pouvoir être appelée depuis une classe cliente, cela n'aurai aucun sens. Virtual pour que les classes dérivées puissent la surcharger.
Notez pour terminer la signature du gestionnaire d'événement. Sans surprise il contient une référence à HuumEventArgs. Il contient également une référence à l'instance d'objet qui est responsable de l'événement.
On pourrait imaginer de changer l'ordre des paramètres, d'en ajouter ou supprimer voire d'en mettre aucun. N'en faites surtout rien. Cette signature est une façon d'auto documenter votre code, elle est aussi une façon d'éviter certains trous de sécurité [17].
Maintenant que se passe t'il chez PrefetDeDiscipline?
Code C#
class PrefetDeDiscipline { public PrefetDeDiscipline() { VilainGarnement = new ClasseActive(); VilainGarnement.Huum += new ClasseActive.HuumEventHandler(VilainGarnement_Huum); } void VilainGarnement_Huum(object sender, HuumEventArgs e) { if (e.NomGateau == "croissant") { Console.WriteLine("Un croissant? Vous me ferez 2 heures de colle"); } else { Console.WriteLine(string.Format("{0}, bon appétit", e.NomGateau)); } } public ClasseActive VilainGarnement; }
Code VB
Class PrefetDeDiscipline Public Sub New() VilainGarnement = New ClasseActive AddHandler VilainGarnement.Huum, AddressOf VilainGarnement_Huum End Sub Sub VilainGarnement_Huum(ByVal sender As Object, ByVal e As HuumEventArgs) If e.NomGateau = "croissant" Then Console.WriteLine("Un croissant? Vous me ferez 2 heures de colle") Else Console.WriteLine(String.Format("{0}, bon appétit", e.NomGateau)) End If End Sub Public VilainGarnement As ClasseActive End Class
On remarque tout de suite le gestionnaire d'événement. Il est appelé chaque fois qu'un gâteau est reçu et effectue son traitement de surveillance. Rien de particulier à dire si ce n'est souligner la signature de la méthode qui est conforme à celle du délégué HuumEventHandler.
Les choses intéressantes se passent dans le constructeur de la classe. La classe PrefetDeDiscipline instancie un objet ClasseActive et s'abonne à un de ses événements grâce à la syntaxe += (syntaxe C#). Cette syntaxe est importante car elle souligne que plusieurs observateurs pourraient s'abonner au même événement. Elle est aussi exigée par le compilateur parce que l'événement a été déclaré avec le mot clef event. C'est ce mot clef qui interdit à l'utilisateur de faire une assignation simple en écrivant directement:
VilainGarnement.Huum = new ClasseActive.HuumEventHandler(VilainGarnement_Huum);
Ce faisant on écraserait les abonnements qui auraient pu déjà exister.
Il existe divers raffinements comme les accesseurs de délégués add et remove, mais ce n'est pas l'objet de cet article et je vous renvoie à la bibliographie [18].
Si vous souhaitez tester le code:
Code C#
class Program { static void Main(string[] args) { PrefetDeDiscipline Prefet = new PrefetDeDiscipline(); Prefet.VilainGarnement.AjouterUnGateau("Tartelette aux fraises"); Prefet.VilainGarnement.AjouterUnGateau("croissant"); Console.Read(); } }
Code VB
Class Program Shared Sub Main(ByVal args As String()) Dim Prefet As PrefetDeDiscipline = New PrefetDeDiscipline Prefet.VilainGarnement.AjouterUnGateau("Tartelette aux fraises") Prefet.VilainGarnement.AjouterUnGateau("croissant") Console.Read End Sub End Class

Le diagramme de classe vous donnera une vue d'ensemble:

Concernant le dernier listing, VB offre une syntaxe alternative avec le mot clef WithEvents:
Class PrefetDeDiscipline Public Sub New() VilainGarnement = New ClasseActive() End Sub Sub VilainGarnement_Huum(ByVal sender As Object, ByVal e As HuumEventArgs) Handles VilainGarnement.Huum If e.NomGateau = "croissant" Then Throw New System.Exception("Vous me ferez 2 heures de colle") End If End Sub Public WithEvents VilainGarnement As ClasseActive End Class
Il s'agit de la syntaxe en vigueur depuis VB 6.
Notons pour finir un bug assez subtil qui peut conduire à une fuite de mémoire ; Ce n'est pas le cas dans notre exemple, mais si ClasseActive était instanciée en dehors de PrefetDeDiscipline on pourrait imaginer qu'à un certain moment il n'existe plus de référence actives vers PrefetDeDiscipline. Normalement l'instance devient un candidat pour être purgée par le ramasse miettes. Seulement, le fait d'abonner un gestionnaire d'événement de PrefetDeDiscipline à un événement maintient une référence active vers cette classe. Dans ce cas, le ramasse miettes ne pourra faire son travail. Je ne sais pas dire s'il existe des solutions simples à ce problème, mais il est bien de l'avoir en tête.
Nous avons vu une syntaxe standard avec le mot clef event pour déclarer un événement. Cette syntaxe ne pose pas de problèmes en soi, mais dans le cas où un contrôle déclare de nombreux événements elle peut altérer les performances de votre contrôle. Il y a deux raisons:
Examinons le code suivant:
Code C#
public class DemoControl: WebControl { public event EventHandler MonEvenement; }
Code VB
Public Class DemoControl Inherits WebControl Public Event MonEvenement As EventHandler End Class
Si on décompile ce code avec Reflector on trouve en fait:
Code C#
public class DemoControl : WebControl { private EventHandler MonEvenement; public event EventHandler MonEvenement; public DemoControl(); }
Code VB
Public Class DemoControl Inherits WebControl Public Event MonEvenement As EventHandler Public Sub New() Private MonEvenement As EventHandler End Class
On note l'apparition d'un champ private. Ce champ est instancié pour chaque événement, même si aucun gestionnaire n'est attaché à l'événement. Cela entraîne une consommation de mémoire pas forcément utile. Ce n'est pas tout. Nous avons précisé qu'en réalité un événement est généré selon une syntaxe semblable à une propriété avec des accesseurs add et remove.
Ces accesseurs sont synchronisés et acquièrent un verrou à chaque appel. Outre le poids de la plomberie supplémentaire ajouté au contrôle dû à la synchronisation, l'acquisition d'un verrou à chaque appel est lourde et le plus souvent on n'a pas besoin d'événement thread-safe.
Nous allons voir une façon alternative de prendre en charge un événement sans ces différents inconvénients.
La classe EventHandlerList est au cœur de l'affaire. Il s'agit d'une liste liée optimisée pour les délégués. La plomberie provient en partie de la classe Control dont héritent tous les contrôles Web. Reflector met en évidence la propriété Events:
Code C#
protected EventHandlerList Events {get;}
Code VB
Protected ReadOnly Property Events As EventHandlerList Get End Property
Une clef est définie pour chaque événement afin de charger son délégué dans Events. Pour l'événement Init on trouve:
Code C#
internal static readonly object EventInit;
Code VB
Friend Shared ReadOnly EventInit As Object
On note que la propriété est static et donc commune à toutes les instances de la classe. Une autre façon d'écrire notre classe ClasseActive est alors:
Code C#
class ClasseActive { private List<string> Gateaux = new List<string>(); public void AjouterUnGateau(string nomGateau) { Gateaux.Add(nomGateau); OnHuum(new HuumEventArgs(nomGateau)); } static ClasseActive() { EventHuum = new object(); } private EventHandlerList _Events; protected EventHandlerList Events { get { if (_Events == null) { _Events = new EventHandlerList(); } return _Events; } } internal static readonly object EventHuum; public event HuumEventHandler Huum { add { this.Events.AddHandler(EventHuum, value); } remove { this.Events.RemoveHandler(EventHuum, value); } } public delegate void HuumEventHandler(object sender, HuumEventArgs e); protected virtual void OnHuum(HuumEventArgs e) { HuumEventHandler handler = this.Events[EventHuum] as HuumEventHandler; if (handler != null) { handler(this, e); } } }
Code VB
Class ClasseActive Private Gateaux As List(Of String) = New List(Of String)() Public Sub AjouterUnGateau(ByVal nomGateau As String) Gateaux.Add(nomGateau) OnHuum(New HuumEventArgs(nomGateau)) End Sub Shared Sub New() EventHuum = New Object End Sub Private _Events As EventHandlerList Protected ReadOnly Property Events() As EventHandlerList Get If _Events Is Nothing Then _Events = New EventHandlerList End If Return _Events End Get End Property Friend Shared ReadOnly EventHuum As Object Public Event Huum As HuumEventHandler Public Delegate Sub HuumEventHandler(ByVal sender As Object, ByVal e As HuumEventArgs) Protected Overridable Sub OnHuum(ByVal e As HuumEventArgs) Dim handler As HuumEventHandler = CType(ConversionHelpers.AsWorkaround(Me.Events(EventHuum), GetType(HuumEventHandler)), HuumEventHandler) If Not (handler Is Nothing) Then handler(Me, e) End If End Sub End Class
Si vous créez un composant il est préférable d'utiliser cette syntaxe. Toutefois elle est légèrement plus verbeuse, c'est pourquoi dans cet article nous allons en rester à la première syntaxe examinée, mais il est intéressant de la connaître puisque c'est celle que l'on rencontre si on décompile les classe ASP avec Reflector.