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

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

6 - Cas pratiques récurrents

Règles d'alimentation

Règle de synchronisation

Alimenter un composant enfant dans un Datagrid ou un Gridview

Les événements d'un contrôle enfant d'un Gridview ou d'un DataGrid

Lever un événement serveur depuis un script javascript

 

6 - Cas pratiques récurrents

Une page ASP est composée d'un certain nombre de contrôles qui vont réagir entre eux par l'intermédiaire d'événements. Le fonctionnement de cet ensemble dépend étroitement de notre capacité à savoir les exploiter de façon adéquate.

En tant que développeur, les problèmes les plus typiques à résoudre sont:

  • Alimenter les contrôles
  • Persister la sélection ou la saisie
  • Synchroniser deux contrôles
  • Alimenter un contrôle dans un GridView ou un DataGrid

Ils répondent tous à un certain nombre de règles dont la connaissance vous fera gagner beaucoup de temps, le temps que vous consommez à vous débattre avec le fonctionnement de votre page.

Commençons par une classe utilitaire Binome utilisée pour toute la suite afin d'alimenter nos contrôles. La classe Binome expose deux propriétés Libelle et Valeur ainsi qu'une méthode static/Shared qui retourne une collection de Binome.

Listing 6-1: Alimentation des combos:

Code C#

using System.Collections.Generic;
public class Binome
{
    private string _Libelle;
    public string Libelle
    {
        get
        {
            return _Libelle;
        }
        set
        {
            _Libelle = value;
        }
    }

    private string _Valeur;
    public string Valeur
    {
        get
        {
            return _Valeur;
        }
        set
        {
            _Valeur = value;
        }
    }

    public static List<Binome> ObtenirCollection(string prefix)
    {
        List<Binome> Liste = new List<Binome>();

        for (int i = 0; i < 20; i++)
        {
            Binome Element = new Binome();
            Element.Libelle = prefix + i.ToString();
            Element.Valeur=i.ToString();
            Liste.Add(Element);
        }

        return Liste;
    }
}

Code VB

Imports System.Collections.Generic

Public Class Binome
    Private _Libelle As String
    Public Property Libelle() As String
        Get
            Return _Libelle
        End Get
        Set(ByVal value As String)
            _Libelle = value
        End Set
    End Property

    Private _Valeur As String
    Public Property Valeur() As String
        Get
            Return _Valeur
        End Get
        Set(ByVal value As String)
            _Valeur = value

        End Set
    End Property

    Public Shared Function ObtenirCollection(ByVal prefix As String) As List(Of Binome)
        Dim Liste As List(Of Binome) = New List(Of Binome)

        For i As Integer = 0 To 19
            Dim Element As Binome = New Binome()

            Element.Libelle = prefix + i.ToString()
            Element.Valeur = i.ToString()
            Liste.Add(Element)
        Next

        Return Liste
    End Function
End Class

Règles d'alimentation

Les règles d'alimentation permettent de résoudre les questions relatives à l'alimentation d'un contrôle. Nous allons prendre l'exemple d'une liste déroulante, mais tout autre contrôle convient aussi bien.

Listing 6-2: Une combo sur une page

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:dropdownlist  DataTextField="Libelle" DataValueField="Valeur" ID="DropDownList1" runat="server">
        </asp:dropdownlist>
        
        <asp:button ID="Button1" runat="server" Text="Button" OnClick="Button1_Click" /></div>
    </form>
</body>
</html>

Pour faire fonctionner le formulaire on doit ajouter dans l'événement Load ou Init de la page le code suivant:

Listing 6-3: Code behind du listing précédent

Code C#

protected void Page_Load(object sender, EventArgs e){
   if (!this.IsPostBack)
   {
       this.DropDownList1.DataSource = Binome.ObtenirCollection("ddl1_");
       this.DropDownList1.DataBind();
   }
}

Code VB

Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) 
 If Not Me.IsPostBack Then 
   Me.DropDownList1.DataSource = Binome.ObtenirCollection("ddl1_") 
   Me.DropDownList1.DataBind 
 End If 
End Sub

La règle est la suivante:

L'alimentation d'un contrôle se fait lors des événements Load ou Init de la page et uniquement lors du premier chargement de la page.

Lors du premier chargement de la page on alimente la combo. Puisque l'on est après le Init du composant, on copie dans le viewstate le contenu de ce combo.
Ultérieurement on fait une sélection et on poste la page. celle-ci va donc successivement recharger le combo depuis le viewstate, puis restaurer la sélection à l'aide des informations trouvées dans le paquet HTTP POST. Si on alimentait à nouveau le combo dans l'événement Load, alors on perdrait aussitôt la sélection puisqu'elle est restaurée avant.

Dans le cas où rechercher les informations du contrôle n'est pas coûteux, il est possible d'optimiser le viewstate en alimentant le contrôle dans son événement Init. Nous avons vu dans l'article sur le viewstate comment faire.

Evidemment l'alimentation doit se faire à chaque renvoi de la page cette fois, d'où la nouvelle règle:

L'alimentation d'un contrôle lors de l'événement Init du contrôle se fait à chaque appel.

Note:
On parle bien de l'événement Init du contrôle, pas de la page. Souvenez vous du diagramme tracé dans l'article sur le viewstate.

Listing 6-4: Règle d'alimentation dans OnInit

Code C#

protected override void OnInit(EventArgs e){
    this.DropDownList1.DataSource = Binome.ObtenirCollection("ddl1_");
    this.DropDownList1.DataBind();
}

Code VB

Protected OverLoads Sub OnInit(ByVal e As EventArgs) 
   Me.DropDownList1.DataSource = Binome.ObtenirCollection("ddl1_") 
   Me.DropDownList1.DataBind 
End Sub

Règle de synchronisation

Le contenu d'un contrôle peut être lié à une sélection dans un autre composant comme une liste déroulante. Compliquons alors notre exemple avec 2 combos, mais dont le contenu du second dépend de celui du premier.

Listing 6-5: Deux combos

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:dropdownlist AutoPostBack="True OnSelectedIndexChanged="dd1_SelectedIndexChanged" ID="dd1" runat="server"" DataTextField="Libelle"
            DataValueField="Valeur">
        </asp:dropdownlist>
        <asp:dropdownlist ID="ddl2" runat="server" DataTextField="Libelle" DataValueField="Valeur">
        </asp:dropdownlist><br />
        <br />
        <asp:button ID="Button1" runat="server" Text="Button" /></div>
    </form>
</body>
</html>

Code C#

protected void Page_Load(object sender, EventArgs e)
{
    if (!this.IsPostBack)
    {
        // on applique la règle d'alimentation des combos
        this.dd1.DataSource = Binome.ObtenirCollection("ddl1_");
        this.dd1.DataBind();

        this.ddl2.DataSource = Binome.ObtenirCollection("sel" + dd1.SelectedIndex + "_");
        this.ddl2.DataBind();
    }
}

protected void dd1_SelectedIndexChanged(object sender, EventArgs e)
{
    // on applique la règle de synchronisation du combo lié
    this.ddl2.DataSource = Binome.ObtenirCollection("sel" + dd1.SelectedIndex + "_");
    this.ddl2.DataBind();
}

Code VB

Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) 
 If Not Me.IsPostBack Then 
   ' on applique la règle d'alimentation des combos
   Me.dd1.DataSource = Binome.ObtenirCollection("ddl1_") 
   Me.dd1.DataBind 

   Me.ddl2.DataSource = Binome.ObtenirCollection("sel" + dd1.SelectedIndex + "_") 
   Me.ddl2.DataBind 
 End If 
End Sub 

Protected Sub dd1_SelectedIndexChanged(ByVal sender As Object, ByVal e As EventArgs) 
 ' on applique la règle de synchronisation du combo lié
 Me.ddl2.DataSource = Binome.ObtenirCollection("sel" + dd1.SelectedIndex + "_") 
 Me.ddl2.DataBind 
End Sub

La règle illustrée par l'exemple précédent est la suivante:

La synchronisation d'un contrôle lié se fait depuis l'événement levé lorsque son état change.

Alimenter un composant enfant dans un DataGrid ou un Gridview

Un problème classique, un contrôle dans un GridView a besoin d'être alimenté avec le contenu d'une autre table. Typiquement se sera le cas d'une combo qui charge des valeurs de référence.

Il y a plusieurs façons de résoudre ce problème comme créer un contrôle personnalisé qui charge lui même son contenu. Cette solution permet de mettre en place une optimisation éventuelle du viewstate en alimentant le contrôle dans son événement Init.

On peut aussi plus directement remplir le contrôle (une combo dans notre exemple) depuis les événements du GridView:

Listing 6-6: Alimenter une combo dans un GridView

<html xmlns="http://www.w3.org/1999/xhtml">
<body>
    <form id="form1" runat="server">
        <div>
            <asp:gridview OnRowDataBound="GridView1_RowDataBound" ID="GridView1" runat="server" AutoGenerateColumns="False">
                <columns>
                    <asp:boundfield DataField="Libelle" />
                    <asp:templatefield>
                        <edititemtemplate>
                            <asp:textbox ID="TextBox1" runat="server"></asp:textbox>
                        </edititemtemplate>
                        <itemtemplate>
                            <asp:dropdownlist ID="DropDownList1" runat="server" DataTextField="Libelle" DataValueField="Valeur">
                            </asp:dropdownlist>
                        </itemtemplate>
                    </asp:templatefield>
                </columns>
            </asp:gridview>
        </div>
    </form>
</body>
</html>

Code C#

protected void Page_Load(object sender, EventArgs e)
{
    if (!this.IsPostBack)
    {
        // règle d'alimentation
        this.GridView1.DataSource = Binome.ObtenirCollection("grille_");
        this.GridView1.DataBind();
    }
}
protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e)
{
    if (e.Row.RowType == DataControlRowType.DataRow)
    {
        DropDownList Ddl = e.Row.FindControl("DropDownList1") as DropDownList;
        Ddl.DataSource = Binome.ObtenirCollection("ddl_");
        Ddl.DataBind();
    }
} 

Code VB

Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) 
 If Not Me.IsPostBack Then 
   ' règle d'alimentation
   Me.GridView1.DataSource = Binome.ObtenirCollection("grille_") 
   Me.GridView1.DataBind 
 End If 
End Sub 

Protected Sub GridView1_RowDataBound(ByVal sender As Object, ByVal e As GridViewRowEventArgs) 
 If e.Row.RowType = DataControlRowType.DataRow Then 
   Dim Ddl As DropDownList = CType(ConversionHelpers.AsWorkaround(e.Row.FindControl("DropDownList1"), GetType(DropDownList)), DropDownList) 
   Ddl.DataSource = Binome.ObtenirCollection("ddl_") 
   Ddl.DataBind 
 End If 
End Sub 

Il est toujours plus propre d'éviter de placer du code inline dans une page ASPX. Ce n'est pas sa place et contrairement à une légende cela n'est en rien plus rapide à l'exécution.
Le bon endroit est d'intercepter l'événement RowDataBound pour le GridView et DataItemBound pour le DataGrid. Ces événements sont levés au moment où la ligne est liée aux données. On trouvera en bibliographie une comparaison entre ces deux composants [35].

Il est important de comprendre que l'événement est levé pour chaque line du tableau, y compris l'entête. D'où le test de début de code.

Dans le cas d'un DataGrid les événements et le code diffèrent légèrement:

Listing 6-7: Cas du DataGrid

Code C#

if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
{
// alimentation du composant

Code VB

If e.Item.ItemType = ListItemType.Item OrElse e.Item.ItemType = ListItemType.AlternatingItem Then 
' alimentation du composant
End If 

Le test de début se justifie parce que DataItemBound distingue les lignes paires des lignes impaires. Si vous n'en tenez pas compte, vous ne remplirez qu'une ligne sur deux.

Une alternative est celle-ci:

Listing 6-8: Alternative aux codes précédents

<asp:dropdownlist OnInit="Initialisation" ID="DropDownList1" runat="server" 
DataTextField="Libelle" DataValueField="Valeur">
</asp:dropdownlist> 

Code C#

protected void Initialisation(object sender,EventArgs e)
{
    DropDownList Ddl = sender as DropDownList;
    Ddl.DataSource = Binome.ObtenirCollection("ddl_");
    Ddl.DataBind();
} 

Code VB

Protected Sub Initialisation(ByVal sender As Object, ByVal e As EventArgs) 
 Dim Ddl As DropDownList = CType(ConversionHelpers.AsWorkaround(sender, GetType(DropDownList)), DropDownList) 
 Ddl.DataSource = Binome.ObtenirCollection("ddl_") 
 Ddl.DataBind 
End Sub 

Dans tous les cas on obtient ceci:

Vous devez maintenant savoir ce que cette solution implique...

Les événement des contrôles enfants d'un Gridview ou d'un DataGrid

Cette fois le contrôle enfant lève un événement. Nous souhaitons l'intercepter. Nous allons prendre le cas d'un DropDownList.

J'ai depuis toujours l'habitude d'utiliser les événements tels DataItemBound ou RowItemBound pour alimenter mes grilles. L'avantage que j'y trouve est que l'on peut mieux contrôler ce qui s'affiche dans le contrôle qu'en utilisant du code inline. C'est donc tout naturellement que j'ai essayé d'abonner un gestionnaire d'événement à mon combo depuis un des événements précédents... Il se trouve que cela ne fonctionne pas.

J'ai depuis découvert deux solutions à cette difficulté. La première consiste à effectuer l'abonnement depuis DataItemCreated (DataGrid) ou RowCreated (GridView). Cet événement se produit après ItemDataBound/RowDataBound  au moment où la création de la ligne est terminée.

Dans le cas du GridView, mais c'est transposable directement au DataGrid le code est le suivant:

Listing 6-9: Intercepter les événements d'un contrôle enfant

<asp:gridview OnRowCreated="GridView1_RowCreated" OnRowDataBound="GridView1_RowDataBound" 
ID="GridView1" runat="server" AutoGenerateColumns="False">
                <columns>
                    <asp:boundfield DataField="Libelle" />
                    <asp:templatefield>
                        <edititemtemplate>
                        </edititemtemplate>
                        <itemtemplate>
<asp:dropdownlist ID="DropDownList1" AutoPostBack="true" runat="server" DataTextField="Libelle" 
DataValueField="Valeur">
</asp:dropdownlist>
                        </itemtemplate>
                    </asp:templatefield>
                </columns>
            </asp:gridview> 

Code C#

protected void GridView1_RowCreated(object sender, GridViewRowEventArgs e)
{
     if (e.Row.RowType == DataControlRowType.DataRow)
     {
        DropDownList Ddl = e.Row.FindControl("DropDownList1") as DropDownList;
        Ddl.SelectedIndexChanged += new EventHandler(Ddl_SelectedIndexChanged);
    }
}

void Ddl_SelectedIndexChanged(object sender, EventArgs e)
{
    DropDownList Ddl = sender as DropDownList;
    Response.Write(string.Format("Sélectionné: {0}", Ddl.SelectedValue));
}

Code VB

Protected Sub GridView1_RowCreated(ByVal sender As Object, ByVal e As GridViewRowEventArgs) 
 If e.Row.RowType = DataControlRowType.DataRow Then 
   Dim Ddl As DropDownList = CType(ConversionHelpers.AsWorkaround(e.Row.FindControl("DropDownList1"), GetType(DropDownList)), DropDownList) 
   Ddl.SelectedIndexChanged += New EventHandler(Ddl_SelectedIndexChanged) 
 End If 
End Sub

Sub Ddl_SelectedIndexChanged(ByVal sender As Object, ByVal e As EventArgs) 
 Dim Ddl As DropDownList = CType(ConversionHelpers.AsWorkaround(sender, GetType(DropDownList)), DropDownList) 
 Response.Write(String.Format("Sélectionné: {0}", Ddl.SelectedValue)) 
End Sub

Notez l'attribut AutoPostBack qui est à true pour avoir un événement immédiat. Mais ce code marche aussi bien avec un code différé.

Si l'on n'a pas besoin de câbler dynamiquement le gestionnaire d'événement, il existe une autre solution que nous avons en fait déjà vu au chapitre précédent:

Listing 6-10: Autre solution

<asp:gridview OnRowCreated="GridView1_RowCreated" OnRowDataBound="GridView1_RowDataBound"
    ID="GridView1" runat="server" AutoGenerateColumns="False">
    <columns>
        <asp:boundfield DataField="Libelle" />
        <asp:templatefield>
            <itemtemplate>
                <asp:dropdownlist OnSelectedIndexChanged="Ddl_SelectedIndexChanged" ID="DropDownList1" 
			runat="server" AutoPostBack="true" DataTextField="Libelle"
                    	DataValueField="Valeur">
                </asp:dropdownlist>
            </itemtemplate>
        </asp:templatefield>
    </columns>
</asp:gridview> 

On câble le gestionnaire de façon déclarative dans la page ASPX. Le résultat est exactement le même.

On peut aussi avoir besoin de connaître la ligne sur laquelle l'événement s'est produit. Il existe diverses solutions applicables ou non selon le contrôle qui lève l'événement.

Certains contrôles disposent d'un événement Command. Ce sont ceux qui implémentent IButtonControl comme Button, LinkButton, ImageButton, mais aussi GridView et DataList et d'autres encore.

Listing 6-11: Utilisation de l'événement Command

<asp:gridview OnRowCommand="GridView1_RowCommand" ID="GridView1" runat="server" AutoGenerateColumns="False"
    OnRowDataBound="GridView1_RowDataBound">
    <columns>
        <asp:boundfield DataField="Libelle" />
        <asp:templatefield>
            <itemtemplate>
                <asp:button ID="Button2" runat="server" Text="Button" />
            </itemtemplate>
        </asp:templatefield>
    </columns>
</asp:gridview> 

Code C#

protected void Page_Load(object sender, EventArgs e)
{
    if (!this.IsPostBack)
    {
        // règle d'alimentation
        this.GridView1.DataKeyNames = new string[] { "Valeur" };
        this.GridView1.DataSource = Binome.ObtenirCollection("grille_");
        this.GridView1.DataBind();
    }
}

protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e)
{
    if (e.Row.RowType == DataControlRowType.DataRow)
    {
        Button Bouton = e.Row.FindControl("Button2") as Button;
        Bouton.CommandName = "clicBouton";
        Bouton.CommandArgument = GridView1.DataKeys[e.Row.RowIndex].Value as string;
    }

}

protected void GridView1_RowCommand(object sender, GridViewCommandEventArgs e)
{
    if (e.CommandName == "clicBouton")
    {
        Button Bouton = e.CommandSource as Button;
        Response.Write(string.Format("Id de la ligne: {0}, contrôle source: {1}", e.CommandArgument,Bouton.ClientID));
    }
} 

Code VB

Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) 
 If Not Me.IsPostBack Then 
   Me.GridView1.DataKeyNames = New String() {"Valeur"} 
   Me.GridView1.DataSource = Binome.ObtenirCollection("grille_") 
   Me.GridView1.DataBind 
 End If 
End Sub 

Protected Sub GridView1_RowDataBound(ByVal sender As Object, ByVal e As GridViewRowEventArgs) 
 If e.Row.RowType = DataControlRowType.DataRow Then 
   Dim Bouton As Button = CType(ConversionHelpers.AsWorkaround(e.Row.FindControl("Button2"), GetType(Button)), Button) 
   Bouton.CommandName = "clicBouton" 
   Bouton.CommandArgument = CType(ConversionHelpers.AsWorkaround(GridView1.DataKeys(e.Row.RowIndex).Value, GetType(String)), String) 
 End If 
End Sub 

Protected Sub GridView1_RowCommand(ByVal sender As Object, ByVal e As GridViewCommandEventArgs) 
 If e.CommandName = "clicBouton" Then 
   Dim Bouton As Button = CType(ConversionHelpers.AsWorkaround(e.CommandSource, GetType(Button)), Button) 
   Response.Write(String.Format("Id de la ligne: {0}, contrôle source: {1}", e.CommandArgument, Bouton.ClientID)) 
 End If 
End Sub 

Tout commence dans Load. On déclare la propriété Binome.Valeur comme identifiant de chaque ligne.
Dans RowDataBound on alimente les propriétés nécessaires à l'événement Command du bouton. A chaque clic sur le bouton celui-ci sera levé et intercepté par GridView dans son événement RowCommand qui se contente d'affiche un message de passage.

On pourrait également faire l'interception au niveau de la page en surchargeant l'événement OnBubbleEvent. Nous verrons plus en détail cette technique dans la partie consacrée à l'architecture serveur.

Les choses se corsent pour les autres contrôles. Il existe diverses solution, celle présentée à l'avantage d'être toujours possible. Nous prendrons le cas d'un DropDownList:

Listing 6-12: Evénement d'un contrôle enfant:

<html xmlns="http://www.w3.org/1999/xhtml">

<script language="javascript">
function JeVousEcoute(id,controle)
{
    alert(id);
    alert(controle.selectedIndex);
}
</script>

<body>
    <form id="form1" runat="server">
        <asp:gridview OnRowCommand="GridView1_RowCommand" ID="GridView1" runat="server" 
		AutoGenerateColumns="False" OnRowDataBound="GridView1_RowDataBound">
            <columns>
                <asp:boundfield DataField="Libelle" />
                <asp:templatefield>
                    <itemtemplate>
                        <asp:dropdownlist DataTextField="Libelle" DataValueField="Valeur" 
				ID="DropDownList1" runat="server">
                        </asp:dropdownlist>
                    </itemtemplate>
                </asp:templatefield>
            </columns>
        </asp:gridview>
    </form>
</body>
</html> 

Code C#

protected void Page_Load(object sender, EventArgs e)
{
    if (!this.IsPostBack)
    {
        // règle d'alimentation
        this.GridView1.DataKeyNames = new string[] { "Valeur" };
        this.GridView1.DataSource = Binome.ObtenirCollection("grille_");
        this.GridView1.DataBind();
    }
}

protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e)
{
    if (e.Row.RowType == DataControlRowType.DataRow)
    {
        DropDownList Ddl = e.Row.FindControl("DropDownList1") as DropDownList;
        Ddl.DataSource = Binome.ObtenirCollection("ddl_");
        Ddl.DataBind();

        string Modele = "javascript:JeVousEcoute('{0}',this);";
        Ddl.Attributes.Add("onchange",string.Format(Modele,this.GridView1.DataKeys[e.Row.RowIndex].Value));
    }

}

protected void GridView1_RowCommand(object sender, GridViewCommandEventArgs e)
{
    if (e.CommandName == "clicBouton")
    {
        Button Bouton = e.CommandSource as Button;
        Response.Write(string.Format("Id de la ligne: {0}, contrôle source: {1}", e.CommandArgument,Bouton.ClientID));
    }
} 

Code VB

Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) 
 If Not Me.IsPostBack Then 
   Me.GridView1.DataKeyNames = New String() {"Valeur"} 
   Me.GridView1.DataSource = Binome.ObtenirCollection("grille_") 
   Me.GridView1.DataBind 
 End If 
End Sub 

Protected Sub GridView1_RowDataBound(ByVal sender As Object, ByVal e As GridViewRowEventArgs) 
 If e.Row.RowType = DataControlRowType.DataRow Then 
   Dim Ddl As DropDownList = CType(ConversionHelpers.AsWorkaround(e.Row.FindControl("DropDownList1"), GetType(DropDownList)), DropDownList) 
   Ddl.DataSource = Binome.ObtenirCollection("ddl_") 
   Ddl.DataBind 

   Dim Modele As String = "javascript:JeVousEcoute('{0}',this);" 
   Ddl.Attributes.Add("onchange", String.Format(Modele, Me.GridView1.DataKeys(e.Row.RowIndex).Value)) 
 End If 
End Sub 

Protected Sub GridView1_RowCommand(ByVal sender As Object, ByVal e As GridViewCommandEventArgs) 
 If e.CommandName = "clicBouton" Then 
   Dim Bouton As Button = CType(ConversionHelpers.AsWorkaround(e.CommandSource, GetType(Button)), Button) 
   Response.Write(String.Format("Id de la ligne: {0}, contrôle source: {1}", e.CommandArgument, Bouton.ClientID)) 
 End If 
End Sub 

L'idée est simplement de lancer un script javascript sur l'événement OnChange de la combo (fonction JeVousEcoute). C'est ensuite le rôle de ce script de lever un l'événement serveur comme on verra dans le chapitre qui suit.

Le code est assez explicite, remarquez juste comment on déclare l'appel à OnChange et la façon dont on passe les paramètres voulus: l'Id de la ligne et le contrôle qui lève l'événement.

Nous verrons lors de l'étude de la partie serveur de ce tutoriel une autre méthode pour obtenir les même résultats.

Lever un événement serveur depuis un script javascript

On peut avoir besoin de lever un événement depuis un javascript. Sous sa forme la plus simple un événement est un appel à submit(). Mais là où les choses deviennent intéressantes c'est de pouvoir passer des paramètres. On utilise traditionnellement des champs cachés qui sont lus dans le paquet HTTP.
ASP propose en standard une telle architecture basé sur l'appel d'une méthode __doPostBack():

Listing 6-13: Architecture postback

input type="hidden" name="__EVENTTARGET" id="__EVENTTARGET" value="" />
<input type="hidden" name="__EVENTARGUMENT" id="__EVENTARGUMENT" value="" />

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

La méthode attend deux paramètres:

eventTarget ID du contrôle qui est responsable du renvoi de la page
eventArgument Argument complémentaire facultatif utilisé par l'événement

Ceux-ci sont recopiés dans des champs cachés que le paquet HTTP postera et que nous pourrons lire côté serveur.
Examinons un peu la ligne avec le if qui a été ajoutée depuis ASP 2.0[34].

L'idée est de permettre à une fonction javascript d'être exécutée avant que le formulaire soit posté vers le serveur. Normalement on intercepte l'événement onsubmit du formulaire. Malheureusement cela ne fonctionne qu'avec les boutons de type submit et échoue si on lance depuis un script la fonction submit() comme le démontre l'exemple suivant:

Listing 6 - 14: Une fonction qui s'exécute avant l'envoi de la page... mais ne marche pas

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<script language="javascript" type="text/javascript">
<!--

function Button1_onclick() 
{
	document.form1.submit();
}

function maFonction()
{
	alert("hello");
}
// -->
</script>
</head>
<body>
    <form id="form1" runat="server" onsubmit="javascript:maFonction();">
    <input onclick="return Button1_onclick();" id="Button1" type="button" value="button" 
      language="javascript" />
    </form>
</body>
</html>

La nouvelle syntaxe __doPostBack prend plus de précautions et poste la page s'il n'y a pas de méthode onsubmit (premier test) ou bien s'il y a une méthode onsubmit et qu'elle a renvoyée true (deuxième test). Cela évite les bidouilles diverses que l'on faisait auparavant.

Comment utiliser __doPostBack dans un script? Le plus simple est d'examiner un exemple:

Notre exemple démontre deux façons d'utiliser __doPostBack:

  1. Simuler un événement serveur, celui de Button2 par un click sur un lien hypertexte
  2. Générer un événement serveur en cliquant sur Button1

Listing 6- 15: Exemple d'utilisation de __doPostBack

<script language="javascript" type="text/javascript">
<!--
function Traitement(cible,argument) 
{
    __doPostBack(cible,argument);
}
// -->
</script>

<html xmlns="http://www.w3.org/1999/xhtml" >
<body>
    <form id="form1" runat="server">
        <input onclick="javascript:Traitement('Button1','Input');" id="Button1" type="button" 
		value="button1"  />
        <a href="javascript:Traitement('button2','ancre');">Simule un click sur button2</a>&nbsp;<br />
        <br />
        <asp:button ID="Button2" runat="server" OnClick="Button2_Click" Text="Button2" />
    </form>
</body>
</html> 

Code C#

protected override void OnInit(EventArgs e)
{
    Page.ClientScript.GetPostBackEventReference(new System.Web.UI.PostBackOptions(this));
    base.OnInit(e);
}

private void reponseAuRenvoi()
{
    if (! string.IsNullOrEmpty(Request["__EVENTTARGET"]))
    {
        string Cible = Request["__EVENTTARGET"];
        string Argument = Request["__EVENTARGUMENT"];
        Response.Write(string.Format("Cible: {0},Argument: {1}<br/>",Cible,Argument));
    }
}

protected void Page_Load(object sender, EventArgs e)
{
    reponseAuRenvoi();
}

protected void Button2_Click(object sender, EventArgs e)
{
    Response.Write("Button2 cliqué<br/>");
}

protected override void Render(HtmlTextWriter writer)
{
    // l'argument est sensible à la casse
    this.Page.ClientScript.RegisterForEventValidation(this.Button2.UniqueID, "ancre");
    base.Render(writer);
}

Code VB

Protected Overloads Overrides Sub OnInit(ByVal e As EventArgs) 
 Page.ClientScript.GetPostBackEventReference(New System.Web.UI.PostBackOptions(Me)) 
 MyBase.OnInit(e) 
End Sub 

Private Sub reponseAuRenvoi() 
 If Not String.IsNullOrEmpty(Request("__EVENTTARGET")) Then 
   Dim Cible As String = Request("__EVENTTARGET") 
   Dim Argument As String = Request("__EVENTARGUMENT") 
   Response.Write(String.Format("Cible: {0},Argument: {1}<br/>", Cible, Argument)) 
 End If 
End Sub 

Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) 
 reponseAuRenvoi 
End Sub 

Protected Sub Button2_Click(ByVal sender As Object, ByVal e As EventArgs) 
 Response.Write("Button2 cliqué<br/>") 
End Sub 

Protected Overloads Overrides Sub Render(ByVal writer As HtmlTextWriter) 
 Me.Page.ClientScript.RegisterForEventValidation(Me.Button2.UniqueID, "ancre") 
 MyBase.Render(writer) 
End Sub 

Il mérite pas mal d'explications! Mais commençons par le faire fonctionner.

Au démarrage on observe:

Un clic sur Button 1 :

Un clic sur le lien:

Un clic sur Button 2:

Button1 est un contrôle HtmlInputButton qui ne déclenche donc pas nativement un renvoi de la page. Il faut l'aider à l'aide d'un peu de javascript, c'est le boulot de Traitement qui encapsule un appel à __doPostBack.  Un renvoi étant effectué, le code behind lance la méthode reponseAuRenvoi() depuis l'événement Load.
La méthode examine la valeur du champ caché __EVENTTARGET. S'il n'est pas vide, c'est que __doPostBack a été invoqué, on affiche ensuite la valeur des deux champs cachés, c'est la deuxième séquence.

Le lien montre une autre façon d'utiliser __doPostback en se faisant passer pour Button2.
Le fonctionnement est similaire au cas précédent, sauf que le nom de la cible est le nom de Button2.
ASP détecte lit la valeur de __EVENTTARGET et trouve Button2 d'où le premier message. Puisqu'il s'agit d'un contrôle serveur il appelle alors son gestionnaire d'événement Button2_Click qui s'exécute, d'où le second message. L'événement Load se produisant avant la méthode reponseAuRenvoi() est également exécutée.

La dernière séquence montre juste que l'on peut faire une distinction entre le cas précédent et un clic direct sur Button2 puisque dans ce cas __EVENTTARGET ne sera pas renseigné.

Reste une question, d'où viennent le script __doPostback et les champs cachés?
On pourrait les ajouter à la main bien sur, mais il est plus maintenable de se servir de la méthode standard GetPostBackEventReference depuis OnInit. Il existe une variante spécialisée pour le contrôle HyperLink: GetPostackClientHyperLink.

Notez que cet appel n'est pas nécessaire si la page contient au moins un contrôle visible doté d'une propriété AutoPostback à true car il est alors toujours généré.

Le dernier point à élucider est le code dans la méthode Render.
Ce code n'était pas nécessaire avec ASP 1. Une des nouveautés d'ASP 2.0 est le mécanisme de validation des événements dont nous avons parlé dans la première partie de ce tutoriel.

Normalement, __EVENTARGUMENT lors d'un clic sur un Button est vide. C'est ce que le mécanisme de validation enregistre automatiquement.
Notre cas est différent puisque l'on y passe la valeur "ancre". On doit donc enregistrer ce cas en lançant un appel à RegisterForEventValidation.

Note:
__doPostback est en partie remplacé par une fonction offrant de meilleures perspectives en terme d'évolution:  WebForm_DoPostBackWithOptions .

Cette fonction est également générée comme __doPostBack par GetPostBackEventReference() selon les options choisies. Nous en parlerons plus longuement dans la partie suivante du tutoriel.