7 - Architecture des événements clients
Nous allons maintenant entrer plus en avant dans l'intimité d'ASP et décortiquer ce qui se passe entre le moment où un événement se produit sur la page Web (le côté client) et un code s'exécute côté serveur. Comprendre l'architecture des événements ASP est indispensable si on souhaite en tirer le meilleurs partie et créer ses propres composants.
Les contrôles Button et ImageButton
sont rendus en HTML par une balise <input>
dont l'attribut type prend la valeur submit ou
image respectivement. Ils sont nativement
capables de déclencher le renvoi d'une page Web. Ce sont les
seuls contrôles intrinsèquement capables de poster une page.
On peut donc prévoir qu'aucun mécanisme particulier sera
nécessaire.
C'est différent pour les autres contrôles qui doivent
agir autrement et lancer explicitement la méthode submit() du
formulaire.
Voyons les détails en commençant par Button. Notre exemple est une page avec deux contrôles Button.
Listing 7-1: Evénements immédiats levés par un bouton
<body> <form id="form1" runat="server"> <asp:button ID="Button1" runat="server" OnClick="Button1_Click1" Text="Bouton 1" /> <asp:button ID="Button2" runat="server" OnClick="Button2_Click" Text="Bouton 2" /> </form> <body>
Code C#
protected void Button1_Click1(object sender, EventArgs e) { // clic sur Button1 } protected void Button2_Click1(object sender, EventArgs e) { // clic sur Boutton2 }
Code VB
Protected Sub Button1_Click1(ByVal sender As Object, ByVal e As EventArgs) ' clic sur Button1 End Sub Protected Sub Button2_Click1(ByVal sender As Object, ByVal e As EventArgs) ' clic sur Button2 End Sub
Si on examine le rendu HTML on trouve:
Listing 7-2:
Sortie HTML du listing 7-1<form name="form1" method="post" action="Default2.aspx" id="form1"> <input type="submit" name="Button1" value="Bouton 1" id="Button1" /> <input type="submit" name="Button2" value="Bouton 2" id="Button2" /> <form>
Rien de particulier apparaît sur cette page comme prévu puisque OnClick est une déclaration serveur. Pourtant à l'exécution ASP exécute correctement le gestionnaire d'événement du bouton cliqué et pas de l'autre. Comment s'y prend ASP pour savoir que quel gestionnaire exécuter? Ryan Farley[8] nous donne l'explication dans un blog. Pour comprendre complétons le code behind de la façon suivante:
Listing 7-3:
Obtenir le contrôle responsable d'un événement immédiatCode C#
protected voidPage_Load(object sender, EventArgs e) { if (this.IsPostBack) { // parcourt la liste des contrôles foreach (string ctl in this.Request.Form) { Control c = this.FindControl(ctl); if (c is System.Web.UI.WebControls.Button) { // on ne s'intéresse qu'au Button Response.Write(c.ID + "<BR/>"); } } } }
Code VB
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load If (Me.IsPostBack) Then ' parcourt la liste des contrôles For Each ctl As String In Me.Request.Form Dim c As Control = Me.FindControl(ctl) If TypeOf c Is System.Web.UI.WebControls.Button Then ' on ne s'intéresse qu'au Button Response.Write(c.ID & "<br/>") End If Next End If End Sub
Le code parcours simplement la collection des variables de formulaire (Request.Form) retournée par la requête POST et affiche les ID des boutons rencontrés. Si on clique par exemple sur le bouton 2 on obtient:

Le fait est que seul le bouton responsable du renvoi est renvoyé par HTTP. C'est ce qui permet à ASP de câbler correctement le gestionnaire d'événement.
Construisons une autre page avec simplement un radio bouton dont la propriété AutoPostback vaut true. Cette propriété transforme les événements différés en événements immédiats.
Listing 7-4: Cas d'un événement transformé en événement immédiat
<html xmlns="http://www.w3.org/1999/xhtml" > <body> <form id="form1" runat="server"> <div> <asp:checkbox AutoPostBack="True" ID="CheckBox1" runat="server"/></div> </form>/span> </body> </html>
Cette fois un code similaire au précédent ne détecte pas la présence d'un bouton radio. La plomberie est différente. Si on examine la sortie HTML de la page on trouve:
Listing 7-5: Sortie HTML du listing 7-4
<body> <form name="form1" method="post" action="Default1.aspx" id="form1"> <div> <input type="hidden" name="__EVENTTARGET" id="__EVENTTARGET" value="" /> <input type="hidden" name="__EVENTARGUMENT" id="__EVENTARGUMENT" value="" /> </div> <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 onclick="javascript:setTimeout('__doPostBack(\'CheckBox1\',\'\')', 0)" id="CheckBox1" type="checkbox" name="CheckBox1" checked="checked" /> </form> </body>
Pas mal de choses ont changées!
Tout d'abord on remarque que le radio bouton appelle une méthode javascript __doPostback() dans son événement client onclick. Cette méthode effectue un renvoi du formulaire (submit) après avoir renseigné deux champs cachés. Les paramètres cachés permettent à ASP de faire la liaison avec le code behind.
Nous avons expliqué dans la partie précédente du tutoriel comment fonctionne __doPostback.
Note:
le fait qu'ASP soit susceptible de compléter vos
déclarations d'événement avec son propre code explique
pourquoi je vous recommande de ne pas oublier de terminer vos propres
appels javascript par un point virgule parce qu'ASP ne
prend aucune précaution en ce sens.
Il devient alors facile de compléter le code précédent pour qu'il affiche le nom du composant ayant levé l'événement de renvoi, quel que soit sa nature:
Listing 7-6: Détection du composant qui a levé un événement
Code C#
protected void Page_Load(object sender, EventArgs e) { if (this.IsPostBack) { Control control = null; string ctrlname = this.Request.Params.Get("__EVENTTARGET"); if (!string.IsNullOrEmpty( ctrlname)) { control = this.FindControl(ctrlname); Response.Write(control.ID + "<BR/>"); } else { foreach (string ctl in this.Request.Form) { Control c = this.FindControl(ctl); if (c is System.Web.UI.WebControls.Button) { Response.Write(c.ID + "<BR/>"); } } } } }
Code VB
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load If Me.IsPostBack Then Dim control As Control = Nothing Dim ctrlname As String = Me.Request.Params.Get("__EVENTTARGET") If Not String.IsNullOrEmpty(ctrlname) Then control = Me.FindControl(ctrlname) Response.Write(control.ID + "<BR/>") Else For Each ctl As String In Me.Request.Form Dim c As Control = Me.FindControl(ctl) If TypeOf c Is System.Web.UI.WebControls.Button Then Response.Write(c.ID + "<BR/>") End If Next End If End If End Sub
Je ne sais pas bien pourquoi, mais l'écriture d'un tel code est une des questions les plus souvent posées dans les forums.
La situation est maintenant claire pour les événements immédiats. Voyons le cas des événements différés.
Comme toujours un exemple:
Listing 7-7: Evénement différé
<html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Untitled Page</title> </head> <body> <form id="form1" runat="server"> <div> <asp:checkbox OnCheckedChanged="CheckBox1_CheckedChanged" ID="CheckBox1" runat="server" /> <asp:checkbox OnCheckedChanged="CheckBox1_CheckedChanged" ID="CheckBox2" runat="server" /> <asp:button ID="Button1" runat="server" Text="Button" /></div> </form> </body> </html>
Code C#
protected void CheckBox1_CheckedChanged(object sender, EventArgs e) { Response.Write("CheckBox1 cliqué<br/>"); } protected void CheckBox2_CheckedChanged(object sender, EventArgs e) { Response.Write("CheckBox2 cliqué<br/>"); }
Code VB
Protected Sub CheckBox1_CheckedChanged(ByVal sender As Object, ByVal e As EventArgs) Response.Write("CheckBox1 cliqué<br/>") End Sub Protected Sub CheckBox2_CheckedChanged(ByVal sender As Object, ByVal e As EventArgs) Response.Write("CheckBox2 cliqué<br/>") End Sub
La page affiche 2 cases à cocher et un bouton pour soumettre le formulaire On peut constater que cela fonctionne correctement:

Examinons la sortie HTML:
Listing 7-8: Sortie HTML du listing 7-7
<html xmlns="http://www.w3.org/1999/xhtml" > <body> <form name="form1" method="post" action="Default3.aspx" id="form1"> <div> <input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="/wEPDwUKMTE4OD...nnr1Q==" /> </div> <div> <input id="CheckBox1" type="checkbox" name="CheckBox1" /> <input id="CheckBox2" type="checkbox" name="CheckBox2" /> <input type="submit" name="Button1" value="Button" id="Button1" /></div> </form> </body> <html>
Cette fois pas de __doPostBack. Rien ne semble distinguer cette page d'une page normale. Comment s'y prend donc ASP pour savoir qu'il doit lever des événements?
Souvenez vous de l'expérience faite au chapitre 5. Nous avons vu que ce qui compte n'est pas ce qui a été fait côté client, mais l'état de la page au moment du renvoi. On en conclu facilement que c'est en comparant la valeur viewstate des composants avec la valeur postée qu'ASP détecte s'il doit ou non lever un événement.
Note:
Nous verrons dans la partie suivante de ce tutoriel
que côté serveur le contrôle effectue ce travail parce
qu'il implémente IPostBackDataHandler.
Notre exemple est constitué d'une zone de texte, d'un contrôle RequiredFieldValidator et d'un bouton:
Listing 7-9: Contrôle de validation
<html xmlns="http://www.w3.org/1999/xhtml" > <body> <form id="form1" runat="server"> <div> <asp:textbox ID="TextBox1" runat="server"></asp:textbox> <asp:requiredfieldvalidator ID="RequiredFieldValidator1" runat="server" ControlToValidate="TextBox1" Display="Dynamic" ErrorMessage="Champ obligatoire"> </asp:requiredfieldvalidator> <br /> <asp:button ID="Button1" runat="server" Text="Button" /></div> </form> </body> </html>
Examinons directement la sortie HTML obtenue, nous avons mis en gras les lignes intéressantes:
Listing 7-10: Sortie HTML du listing 7-9
1: <html xmlns="http://www.w3.org/1999/xhtml" >
2: <body>
3: <form onsubmit="javascript:return WebForm_OnSubmit();" name="form1" method="post" action="Default5.aspx" id="form1">
4: <div>
5: <input type="hidden" name="__EVENTTARGET" id="__EVENTTARGET" value="" />
6: <input type="hidden" name="__EVENTARGUMENT" id="__EVENTARGUMENT" value="" />
7: </div>
8:
9: <script type="text/javascript">
10: <!--
11: var theForm = document.forms['form1'];
12: if (!theForm) {
13: theForm = document.form1;
14: }
15: function __doPostBack(eventTarget, eventArgument) {
16: if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
17: theForm.__EVENTTARGET.value = eventTarget;
18: theForm.__EVENTARGUMENT.value = eventArgument;
19: theForm.submit();
20: }
21: }
22: // -->
23: </script>
24:
25: <script src="/Chap1/WebResource.axd?d=YSX16s4XJNTkQa6NL2y-BQ2&t=632965033199531250" type="text/javascript"></script>
26:
27: <script src="/Chap1/WebResource.axd?d=9R1zbTAEwx52hU5ylMm04Kba0z9H9nzafh1ovFYTGLc1&t=632965033199531250" type="text/javascript"></script>
28: <script type="text/javascript">
29: <!--
30: function WebForm_OnSubmit() {
31: if (typeof(ValidatorOnSubmit) == "function" && ValidatorOnSubmit() == false) return false;
32: return true;
33: }
34: // -->
35: </script>
36:
37: <div>
38: <input name="TextBox1" type="text" id="TextBox1" />
39: <span id="RequiredFieldValidator1" style="color:Red;display:none;">Champ obligatoire</span>
40: <br />
41: <input type="submit" onclick="javascript:WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions('Button1', '', true, '', '', false, false))" name="Button1" value="Button" id="Button1" /></div>
42:
43: <script type="text/javascript">
44: <!--
45: var Page_Validators = new Array(document.getElementById("RequiredFieldValidator1"));
46: // -->
47: </script>
48:
49: <script type="text/javascript">
50: <!--
51: var RequiredFieldValidator1 = document.all ? document.all["RequiredFieldValidator1"] : document.getElementById("RequiredFieldValidator1");
52: RequiredFieldValidator1.controltovalidate = "TextBox1";
53: RequiredFieldValidator1.errormessage = "Champ obligatoire";
54: RequiredFieldValidator1.display = "Dynamic";
55: RequiredFieldValidator1.evaluationfunction = "RequiredFieldValidatorEvaluateIsValid";
56: RequiredFieldValidator1.initialvalue = "";
57: // -->
58: </script>
59:
60: <script type="text/javascript">
61: <!--
62: var Page_ValidationActive = false;
63: if (typeof(ValidatorOnLoad) == "function") {
64: ValidatorOnLoad();
65: }
66:
67: function ValidatorOnSubmit() {
68: if (Page_ValidationActive) {
69: return ValidatorCommonOnSubmit();
70: }
71: else {
72: return true;
73: }
74: }
75: // -->
76: </script>
77: </form>
78: </body>
79: </html>
Ca fait pas mal de monde. Pour info sans le contrôle de validation on aurai obtenu:
Listing 7-11: Même page, mais sans validation
<html xmlns="http://www.w3.org/1999/xhtml" > <body> <form name="form1" method="post" action="Default5.aspx" id="form1"> <div> <input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="/wEPDwUKMTkwNjc4NTIwMWRkhg0mXjHaqmG9gmmE+NrzlD7TbQE=" /> </div> <div> <input name="TextBox1" type="text" id="TextBox1" /> <br /> <input type="submit" name="Button1" value="Button" id="Button1" /></div> <div> <input type="hidden" name="__EVENTVALIDATION" id="__EVENTVALIDATION" value="/wEWAwLd8YzzAwLs0bLrBgKM54rGBs9dIrS53B36KGfOtKJqfYWS9F+G" /> </div></form> </body> </html>
Impressionnant non?
L'examen des javascript générés montre un certain progrès par rapport à ASP 1.1 puisque les document.all, spécifiques à IE, ont disparus. Mais malheureusement il en reste encore d'autres. Il y a à parier que ce script ne fonctionne pas avec certains navigateurs[22] mais je n'ai pas trop fait d'essais.
Commençons par le début, c'est à dire le bouton de renvoi du formulaire.
Ligne 41 on voit un appel à la fonction javascript
WebForm_DoPostBackWithOptions. Ce script
provient du fichier WebUIValidation.js qui est une
ressource incluse dans la librairie System.Web.dll. Le
lien vers cette ressource se trouve lignes 25, la ligne
27 charge un script avec diverses fonctions annexes
utilisées lors de la validation.
L'utilisation des ressources incluses évite certains
problèmes liés à l'utilisation habituelle du répertoire aspnet_clientfiles. Si vous souhaitez en savoir plus sur
les ressources incluses vous pouvez consulter un de mes
blogs à ce sujet [19].
Côté contrôle, la déclaration est faite dans la méthode
protected
BaseValidator.RegisterValidatorCommonScript().
Cette méthode est apparue avec ASP 2.0. Comme le suggère son nom, elle agit comme l'ancienne méthode __doPostBack(), qu'elle encapsule d'ailleurs, mais offre beaucoup plus de possibilités[20] comme la prise en charge du renvoi inter page. Voyez également l'aide en ligne de la nouvelle méthode .NET GetPostBackEventReference().
WebForm_DoPostBackWithOptions attend un objet WebForm_PostBackOptions. Les deux premiers paramètres ressemblent à ceux de __doPostBack et ce n'est pas un hasard car elle agit essentiellement comme un __doPostBack que par ailleurs elle encapsule.
Le paramètre passé en référence est "validation" qui
vaut true pour dire que l'on fait une
validation.
WebForm_DoPostBackWithOptions effectue
donc une validation côté client puis soumet le formulaire au
serveur. Celui-ci va pouvoir réaliser une validation
côté serveur en appelant un code comme celui-ci:
Listing 7-12: Validation côté client
Code C#
protected void Button1_Click(object sender, EventArgs e) { if (!this.IsValid) { } }
Code VB
Protected Sub Button1_Click(ByVal sender As Object, ByVal e As EventArgs) If Not Me.IsValid Then End If End Sub
La ligne 3 montre alors que l'événement OnSubmit est intercepté par la méthode WebFormOnSubmit définie ligne 30. Si une méthode ValidationOnSubmit est définie, alors on la lance. Elle renvoi le résultat de la validation. Selon que true ou false est renvoyé, alors le formulaire est ou non renvoyé au serveur.
Si la validation échoue il ne reste plus qu'à afficher un message. C'est le rôle de la balise <span> à la ligne 39.
Un test similaire au précédent montre la aussi un fonctionnement similaire à la validation. Une fois encore c'est WebForm_DoPostBackWithOptions qui est sollicité, mais avec le paramètre actionUrl qui contient l'url de la page suivante.
Le reste est clair, le script se contente de remplacer la valeur de la propriété action du formulaire.