Maîtriser les événements ASP.NET - Partie 1/5

   Auteur : Frédéric De Lène Mirouze (amethyste16@hotmail.com)

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

1 - Introduction

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 1: Premier contact

Partie 2: Cas pratiques

Partie 3: Architecture client

Partie 4: Architecture serveur

Partie 5: Ordonnancement des événements

Poster Couleur

Poster noir et Blanc

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.

2 - A qui s'adresse l'article?

Tous les développeurs ASP.NET qu'ils soient débutants ou non.

3 - Configuration requise

J'ai préparé cet article avec la configuration suivante:

  • Visual Studio 2005 version US
  • ASP 2.0
  • Windows XP Home
  • IE 7.0

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].

4 - Premier contact

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:

  1. les événement immédiats (postback event)
  2. Les événements différés (non postback event)

Les événements immédiats

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:

  • un paramètre sender qui pointe vers l'instance du composant Button ayant levé l'événement
  • Un paramètre e qui pointe vers une instance d'un paramètre d'événement. La classe qui est implémentée dépend en fait de la nature des événements, mais est toujours dérivée de EventArgs. On peut donc être amené à transformer le paramètre vers le bon type.

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:

  1. les décorer avec un attribut ID, ou laisser ASP s'en charger
  2. Ajouter également l'attribut runat="server", ce que l'on peut faire depuis le menu contextuel "Run as server control" ou de façon manuelle

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

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>
        &nbsp;
        <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.

5 - Quelques nouveautés ASP 2.0

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.

Renvoi inter page

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.

Validation des événements

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>

UseSubmitBehavior

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:

  1. Désactive le bouton lors d'un clic dessus
  2. Remplace son texte par un message d'attente

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.

Annexe A - Bibliographie

  1. Merci à Manoli pour son formateur de code en HTML:
    http://www.manoli.net/csharpformat/
  2. Article de Bertrand leroy sur les attaques par injection de code
    http://weblogs.asp.net/bleroy/archive/2004/08/18/216861.aspx
  3. Articles de Scott Allen sur la validation des événements:
    http://odetocode.com/Blogs/scott/archive/2006/03/20/3145.aspx
    http://odetocode.com/Blogs/scott/archive/2006/03/21/3153.aspx
  4. Developing Microsoft ASP.NET server controls and components
    Microsoft press
    ISBN: 0-7356-1582-9
  5. Building ASP.NET server controls
    Apress
    Rob Cameron, Dale Michalk
  6. Article de Paul Wilson sur le format du viewstate avec le source d'un analyseur et à quoi sert le viewstate:
    http://aspalliance.com/articleViewer.aspx?aId=135&pId=
  7. Analyseur de ViewState:
    http://sharptoolbox.com/tools/page-viewstate-parser
  8. Comment obtenir le Button qui a causé un renvoi:
    http://www.ryanfarley.com/blog/archive/2005/03/11/1886.aspx
  9. Article de Xefteri sur le postback:
    http://www.xefteri.com/articles/show.cfm?id=18
  10. Fonctionnement de doPostBack:
    http://aspalliance.com/895
  11. Un convertisseur de code VB/C# et inversement qui m'a fait gagner pas mal de temps:
    http://www.developerfusion.co.uk/utilities/convertcsharptovb.aspx
  12. Modèle d'événement des contrôles:
    http://msdn2.microsoft.com/en-us/library/y3bwdsh3.aspx
  13. Tout savoir sur le viewstate:
    http://weblogs.asp.net/infinitiesloop/archive/2006/08/03/Truly-Understanding-Viewstate.aspx
  14. Le cycle des événements:
    http://www.15seconds.com/issue/020102.htm
  15. Vous croyez comprendre quelque chose au viewstate? Ben voyons!
    http://weblogs.asp.net/infinitiesloop/archive/2006/08/03/Truly-Understanding-Viewstate.aspx
  16. Est t'il seulement possible de faire du .NET sans l'outil Reflector?
    http://www.aisto.com/roeder/dotnet/
  17. Sur la signature des délégués d'événement:
    http://www.dotnetguru2.org/index.php?p=92&more=1&c=1&tb=1&pb=1
  18. Syntaxe des événements:
    http://www.panopticoncentral.net/archive/2004/08/03/1536.aspx
  19. Les ressources incluses:
    http://www.dotnetguru2.org/index.php?p=506&more=1&c=1&tb=1&pb=1
  20. Tout sur WebForm_DoPostBackWithOptions():
    http://www.carlosag.net/Articles/WebParts/whidbeyfaq.aspx
  21. Fonctionnement du renvoi inter page dans un article dédié à la persistance des informations utilisateur:
    http://www.codeproject.com/books/ASPNET20.asp
  22. Incompatibilités de la validation et certains navigateurs:
    http://aspnet.4guysfromrolla.com/articles/051204-1.aspx
  23. Bouton cliquable une fois:
    http://blog.shkedy.com/
  24. Pourquoi runat=server sur un contrôle HTML?
    http://blogs.msdn.com/dancre/archive/2006/12/17/what-does-runat-server-do-for-html-controls.aspx
  25. Cycle de vie d'une page ASP:
    http://msdn2.microsoft.com/en-us/library/ms178472(VS.80).aspx
  26. Le modèle d'événements des contrôles ASP:
    http://msdn2.microsoft.com/en-us/library/y3bwdsh3(VS.80).aspx
  27. Implémenter un PageStatePersister:
    http://msdn2.microsoft.com/en-us/library/system.web.ui.pagestatepersister(VS.80).aspx#
  28. Aperçu du viewstate
    http://msdn2.microsoft.com/en-us/library/y3bwdsh3(VS.80).aspx
  29. Les coulisses d'ASP
    http://msdn2.microsoft.com/en-us/library/ms379581(VS.80).aspx
  30. Sur le cycle de vie d'ASP:
    http://msdn2.microsoft.com/en-us/library/ms227435(vs.80).aspx
  31. Le modèle objet d'ASP:
    http://msdn2.microsoft.com/en-us/library/aa479007.aspx
  32. Au sujet du message: control XXXX must be placed inside a form tag with runat=server
    http://aspnet.4guysfromrolla.com/demos/printPage.aspx?path=/articles/102203-1.aspx
  33. Création dynamique de contrôles:
    http://www.firoz.name/2006/05/28/working-with-dynamic-controls-basics/
  34. Sur la nouvelle version de __doPostback en ASP 2.0:
    http://weblogs.asp.net/vga/archive/2004/03/01/NoMoreHijackingOfDoPostBackInWhidbey.aspx
  35. Comparaison entre DataGrid et GridView:
    http://msdn2.microsoft.com/fr-fr/library/05yye6k9(VS.80).aspx
  36. Une série d'articles sur les contrôles dynamiques:
    http://scottonwriting.net/sowblog/posts/3962.aspx
    http://msdn2.microsoft.com/en-us/library/aa479330.aspx
    http://aspnet.4guysfromrolla.com/articles/081402-1.aspx
    http://aspnet.4guysfromrolla.com/articles/082102-1.aspx
    http://aspnet.4guysfromrolla.com/articles/092904-1.aspx
    http://www.tutorialized.com/tutorial/Dynamic-Controls-with-Events/2755
  37. La hiérarchie des contrôles:
    http://msdn2.microsoft.com/en-us/library/aa479330.aspx
  38. Un PlaceHolder qui persiste ses contrôles enfants:
    http://www.denisbauer.com/ASPNETControls/DynamicControlsPlaceholder.aspx
  39. Discussion sur la nécessité d'implémenter IPostBackDataHandler dans certains cas:
    http://west-wind.com/weblog/posts/6666.aspx
  40. IPostBackDataHandler et Viewstate
    http://www.codeproject.com/aspnet/ViewState.asp
  41. Comment passer un événement à travers la hiérarchie des composants
    http://aspnet.4guysfromrolla.com/articles/051105-1.aspx
  42. Tutoriel sur les GridView:
    http://aspnet.4guysfromrolla.com/articles/040502-1.aspx
  43. Visualisation du contenu d'un viewstate:
    http://www.develop.com/us/technology/resourcedetail.aspx?id=d8f9ba3c-c75c-4bed-8596-55e7434d8ecd
  44. Implémenter un PageStatePersister:
    http://aspnet.4guysfromrolla.com/articles/011707-1.aspx
  45. Validation d'une chaîne de requête:
    http://msdn.microsoft.com/msdnmag/issues/07/03/CuttingEdge/default.aspx?loc=fr
  46. IPostBackEventHandler et IPostbackDataHandler en ASP 2.0
    http://aspadvice.com/blogs/joteke/archive/2005/09/26/12894.aspx
  47. Les membres de l'objet Page:
    http://msdn2.microsoft.com/fr-fr/library/system.web.ui.page_members(VS.80).aspx
  48. Ordonnancement des événements:
    http://weblogs.asp.net/jeff/archive/2004/07/04/172683.aspx
  49. Les pages maîtres, leurs coulisses:
    http://www.codeproject.com/aspnet/InsideMasterPages.asp?df=100&forumid=208804&exp=0&select=1339977
  50. L'état du contrôle:
    http://fredrik.nsquared2.com/viewpost.aspx?PostID=265
  51. Architecture d'une page ASP 2.0
    http://www.odetocode.com/Articles/406.aspx
  52. Chargement dynamique des contrôles
    http://weblogs.asp.net/vga/archive/2003/08/11/23498.aspx
  53. Professional ASP.NET 2.0
    Wrox, ISBN: 0-7645-7610-0
  54. Concilier thème et page maître:
    http://www.devsource.com/article2/0,1895,1824218,00.asp
  55. Même sujet:
    http://staff.develop.com/ballen/blog/PermaLink.aspx?guid=09befce7-f48e-4555-891c-13818fd75a56
  56. Bibliographie de mes articles:
    http://www.dotnetguru2.org/amethyste/index.php?p=585&more=1&c=1&tb=1&pb=1

Annexe B: La syntaxe des événements

Positionnement du problème

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:

  1. une signature standard pour déclarer un gestionnaire d'événement
  2. des classes standards pour passer des paramètres à travers les événements
  3. une architecture standard pour lever un événement appelé pattern On
  4. une syntaxe standard pour déclarer un événement

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.

Syntaxe standard

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.

Implémentation optimisée

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.