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

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

7 - Architecture des événements clients

Les événements immédiats

Les événements différés

Les contrôles de validation

Renvoi inter page

 

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 événements immédiats

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édiat

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

Les événements différés

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.

Les contrôles de validation

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>&nbsp;
        <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&amp;t=632965033199531250" 
	type="text/javascript"></script>
  26:   
  27:  <script src="/Chap1/WebResource.axd?d=9R1zbTAEwx52hU5ylMm04Kba0z9H9nzafh1ovFYTGLc1&amp;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" />&nbsp;
  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" />&nbsp;&nbsp;
        <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.

Renvoi inter page

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.