Tutoriel : Comprendre la création de contrôles ASP.NET et leur cycle de vie en C#

Nico-pyright(c)

Cet article a pour but d'améliorer votre compréhension de la création et de l'utilisation de contrôles ASP.NET. Il fera également le tour du cycle de vie d'une page ASP.NET et montrera quelques erreurs qui peuvent apparaitre lorsque l'on ne maitrise pas exactement ces notions.

N'hésitez pas à commenter cet article ! Commentez ♪

- Titre : Tutoriel : Comprendre la création de contrôles ASP.NET et leur cycle de vie en C#

- Auteur : Nico-pyright(c)

- Parution : 25 avril 2008

- Mise à jour : 25 avril 2008

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2007 Nico-pyright(c). Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts. Droits de diffusion permanents accordés à Developpez LLC.

Sommaire

- 1.Introduction

- 2.Préambule

- 2.1.Avertissement

- 2.2.Rappel rapide sur ASP.NET

- 2.3.À qui s'adresse ce tutoriel ?

- 3.Création statique de contrôles, création dynamique

- 3.1.Comment fonctionne la création de contrôles ?

- 3.2.L'arbre des contrôles

- 4.Initialisation et ordre des événements

- 5.Attention avec la création de contrôles dynamiques

- 6.Le cycle de vie de la page ASP.NET

- 6.1.Les principaux événements

- 6.2.Le rattrapage

- 6.3.Les opérations de liaisons de données (DataBinding)

- 6.4.Schéma de synthèse du cycle de vie

- 7.Faire attention à ce que l'arbre des contrôles soit le même avant et après un postback

- 7.1.Un Exemple

- 7.2.L'explication

- 7.3.Un autre exemple

- 8.Retrouver l'identifiant d'un contrôle qui a effectué le postback

- 9.Création précoce d'un identifiant unique

- 10.Conclusion

- Remerciements

- Contact

1.Introduction

Vous êtes un développeur ASP.NET utilisant le C# et vous souhaitez approfondir votre compréhension des mécanismes d'ASP.NET, mieux maitriser la création de vos composants et appréhender correctement le cycle de vie d'une page ASP.NET ?
Alors ce tutoriel est pour vous.

Vous apprendrez dans ce cours les différences entre la création de contrôles « statique » et la création « dynamique ». Vous verrez également les différentes étapes du cycle de vie d'ASP.NET. Enfin, vous découvrirez comment éviter certains pièges inhérents à ce modèle de développement.
Tout au long de ce tutoriel, j'utiliserai le logiciel Visual Studio 2008.

2.Préambule

2.1.Avertissement

Dans ce tutoriel, je vais régulièrement utiliser la méthode Response.Write qui permet d'écrire directement dans le flux html de la page.

C'est une ancienne méthode, relique des vieilles versions d'asp, qui n'a globalement pas intérêt à être utilisée, mais que j'utilise ici dans un souci de clarté du code, afin de réduire la taille de celui-ci.

Ainsi, le code suivant :

Response.Write("<span>blablabla</span>");

Pourra avantageusement être remplacé par :

<asp:Label ID="MonLabel" runat="server"/>

protected override void OnPreRender(EventArgs e)
{
MonLabel.Text = "blablabla";
base.OnPreRender(e);
}

2.2.Rappel rapide sur ASP.NET

ASP.NET est la plateforme orientée objet de Microsoft dédiée au développement d'application web. Elle a été créée pour simplifier et améliorer la création de sites web complexes.
Pouvant être accompagnée d'un outil performant comme Visual Studio, la programmation Web Form, comme on l'appelle aussi, a complètement chamboulé les anciennes façons de créer des applications web, via des scripts CGI, comme pouvait l'être l'ancienne version : ASP.
Celle-ci propose un modèle évolué, qui se rapproche d'un modèle événementiel comme le sont les applications client-lourd (comme les Winforms) et permettant de conserver l'état d'une page, le protocole HTTP ne le permettant pas à la base.

Ainsi, dans cette approche axée sur un modèle événementiel, notre page va passer par un certain nombre d'étapes. C'est ce qu'on appelle le cycle de vie que nous verrons un peu plus en détail plus loin dans cet article.
Pour résumer, lors de l'affichage d'une page, on va passer par une phase d'initialisation (OnInit), une phase d'initialisation par le code utilisateur (OnLoad), une phase de validation et de levé d'événements des contrôles (OnClick, OnSelectedIndexChanged, etc…), une phase de rendu (OnPreRender) et une phase de déchargement (OnUnload).
Plus d'informations sur le cycle de vie au paragraphe 6.

2.3.À qui s'adresse ce tutoriel ?

Débutants ou non-débutants trouver des informations susceptibles de les intéresser. Cependant, certaines sections nécessitent d'avoir quelques connaissances en ASP.NET.

3.Création statique de contrôles, création dynamique

3.1.Comment fonctionne la création de contrôles ?

La déclaration :

<form id="form1" runat="server">
<asp:TextBox runat="server" ID="MonTextBox" Text="Une valeur ..." />
</form>

fait exactement la même chose que le code suivant :

protected override void OnInit(EventArgs e)
{
TextBox monTextBox = new TextBox();
monTextBox.ID = "MonTextBox";
monTextBox.Text = "Une valeur ...";
form1.Controls.Add(monTextBox);
base.OnInit(e);
}

c'est-à-dire qu'on crée le contrôle et qu'on l'ajoute à l'arbre des contrôles.
Pour la première forme, c'est ASP.NET qui crée et ajoute à la volée le contrôle à l'arbre des contrôles, tandis que la seconde forme est faite par nos soins à un certain moment du cycle de vie de la page.
La première forme est appelée une création « statique », la seconde une création « dynamique ». C'est un abus de langage pour dire que la première est créée par le framework ASP.NET alors que dans le second cas, c'est fait par nous.

Exactement la même chose ai-je dit ? En fait pas tout à fait…

Que se passe-t-il quand on a affaire à une construction statique ?

Pour le savoir, créez un nouveau projet Web de type ASP.NET Web Application, ajoutez les déclarations ci-dessus dans la page Default.aspx, exécutez et rendez vous dans le répertoire temporaire d'ASP.NET (chez moi : C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files\root\af7e295c\fa2ab30e)

J'y retrouve les fichiers App_Web_z3tjd2dt.0.cs, App_Web_z3tjd2dt.1.cs et App_Web_z3tjd2dt.dll
Les fichiers .cs sont les classes générées à la volée par ASP.NET et représentent le contenu de notre page aspx.
Vous pouvez visualiser le contenu de ces deux fichiers : App_Web_z3tjd2dt.0.cs et App_Web_z3tjd2dt.1.cs.

Ainsi, on peut noter que dans le fichier App_Web_z3tjd2dt.0.cs :

- on retrouve notre page Default.aspx sous la forme d'une classe : default_aspx ;

- on voit une méthode BuildControlTree qui ajoute les entêtes html, qui appelle BuildControl__control2, qui ajoute la balise body, qui appelle BuildControlform1 et qui ferme la balise body ;

- BuildControl__control2 s'occupe de la partie <head>, title, etc. ;

- BuildControlform1 construit notre formulaire en ajoutant un littéral qui passe à la ligne (\r\n), appelle la méthode BuildControlMonTextBox et rajoute encore un saut de ligne.

N.B. On voit que la classe default_aspx hérite d'une classe, la classe WebApplication3.Default :

App_Web_z3tjd2dt.0.cs

public class default_aspx : global::WebApplication3.Default

On comprend mieux pourquoi le designer déclare les contrôles en protected et non en private, à cause de cet héritage. Si les contrôles étaient déclarés en private, ils ne seraient pas accessibles.

La méthode BuildControlMonTextBox s'occupe quant à elle d'affecter un ID à notre contrôle et la propriété Text.

3.2.L'arbre des contrôles

Pour bien comprendre la création des contrôles, il faut le voir comme un arbre (plus précisément un arbre ordonné).
Ainsi, la page Aspx suivante :

<%@ Page Language="C#" AutoEventWireup="false" CodeBehind="Default.aspx.cs" Inherits="WebApplication3.Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<asp:TextBox runat="server" ID="MonTextBox" Text="Une valeur ..." />
</form>
</body>
</html>

produit l'arbre suivant :

On peut visualiser cet arbre dans la page html, grâce à la petite fonction ci-dessous :

private void AfficheArbre(ControlCollection controls, int profondeur)
{
foreach (Control control in controls)
{
Response.Write(new String('-', profondeur * 4) + "> ");
Response.Write(control.GetType() + " - <b>" + control.ID + "</b><br />");
if (control.Controls != null)
{
AfficheArbre(control.Controls, profondeur + 1);
}
}
}

protected override void OnPreRender(EventArgs e)
{
AfficheArbre(Page.Controls, 0);
base.OnPreRender(e);
}

Cette fonction parcourt récursivement les contrôles de la page et affiche le type du contrôle et son identifiant.
Pour notre page, on aura :

> System.Web.UI.LiteralControl -
> System.Web.UI.HtmlControls.HtmlHead -
----> System.Web.UI.HtmlControls.HtmlTitle -
> System.Web.UI.LiteralControl -
> System.Web.UI.HtmlControls.HtmlForm - form1
----> System.Web.UI.LiteralControl -
----> System.Web.UI.WebControls.TextBox - MonTextBox
----> System.Web.UI.LiteralControl -
> System.Web.UI.LiteralControl -

Toute cette initialisation de contrôles se fait dans la méthode Frameworkinitialize, qui comme nous le dit msdninitialise la page et crée l'arbre de contrôle basé sur l'aspx.

Donc, la différence entre la méthode « statique » et notre méthode « dynamique » réside dans le moment où est construit ce fameux arbre de contrôles.
En statique, cela se fait très tôt, proche du constructeur.
En dynamique, dans cet exemple, cela se fait plus tard, dans le OnInit de la page.

Ceci explique qu'avec une construction statique, dans le OnPreInit nos contrôles soient déjà initialisés.

Cela se vérifie si on ouvre le reflector sur App_Web_bj9gdk3r.dll, on observe une classe default_aspx qui appelle this.__BuildControlTree(this); dans la méthode FrameworkInitialize() et qui enchaine les méthodes BuildControl comme ci-dessus.

Cette différence de « moment » où se construit l'arbre des contrôles peut faire apparaitre quelques problèmes…

4.Initialisation et ordre des événements

Imaginons une page avec un TextBox et un bouton pour valider

<form id="form1" runat="server">
<asp:TextBox runat="server" ID="LeTextBox" />
<asp:Button runat="server" Text="Go" />
</form>

dans le codebehind on initialise la valeur par défaut du textbox et dans le PreRender on affiche la valeur saisie :

protected override void OnInit(EventArgs e)
{
LeTextBox.Text = "par défaut ...";
base.OnInit(e);
}

protected override void OnPreRender(EventArgs e)
{
Response.Write(string.Format("La valeur saisie est : {0}", LeTextBox.Text));
base.OnPreRender(e);
}

On a juste à saisir une valeur, à appuyer sur le bouton et tout se passe bien, car l'initialisation est faite dans le OnInit.

Mettez maintenant l'initialisation dans le OnLoad.

protected override void OnLoad(EventArgs e)
{
LeTextBox.Text = "par défaut ...";
base.OnLoad(e);
}

Cela ne fonctionne plus… Après le postback, la valeur vaut « par défaut », alors qu'on l'a bien modifié.

Que s'est-il passé ?

On y reviendra un peu plus en détail plus tard, mais sachez que toute requête à une page passe par un certain nombre d'étapes. On appelle ça le cycle de vie d'une page.
C'est-à-dire qu'ASP.NET va enchainer un certain nombre d'étapes pour produire le résultat de notre page…

En l'occurrence, les trois méthodes que l'on vient de surcharger sont exécutées dans cet ordre :

OnInit, OnLoad, OnPreRender.

Il faut savoir qu'entre le OnInit et le OnLoad, après un postback, il y a une autre étape qui est exécutée (ce n'est pas la seule, il y en a d'autres) à savoir le chargement des données qui ont été postées lors du clic sur le bouton. C'est la méthode LoadPostBackData.

Lors du premier exemple, on a donc (après le postback) :

Événement

Valeur

OnInit

Text vaut « par défaut »

LoadPostBackData

Text vaut la valeur saisie

OnLoad

Text vaut toujours la valeur saisie

OnPreRender

Text vaut toujours la valeur saisie

Avec le second exemple, on a :

Événement

Valeur

OnInit

Text vaut ""

LoadPostBackData

Text vaut la valeur saisie

OnLoad

Text vaut « par défaut »

OnPreRender

Text vaut toujours « par défaut »

On se rend compte qu'il ne faut pas faire n'importe quoi dans l'ordre des événements. Ainsi, quand on le peut, on aura toujours intérêt à faire les initialisations le plus tôt possible, comme le fait ASP.NET.
Lorsqu'on ne le peut pas, on peut éviter de réaffecter la valeur par défaut lors du postback.
Pour ça, on va faire un test sur IsPostBack (qui nous renvoie true lors qu'on est dans un postback). Ainsi, l'exemple redevient fonctionnel avec :

protected override void OnLoad(EventArgs e)
{
if (!IsPostBack)
LeTextBox.Text = "par défaut ...";
base.OnLoad(e);
}

Cette fois-ci on a (après le postback) :

Événement

Valeur

OnInit

Text vaut ""

LoadPostBackData

Text vaut la valeur saisie

OnLoad

On ne fait rien, car IsPostBack vaut true, Text vaut donc la valeur saisie

OnPreRender

Text vaut toujours la valeur saisie

Vous retrouverez cette explication également dans la FAQ ASP.NET.

Comme on vient de le voir, LoadPostBackData est appelé entre OnInit et OnLoad.
Ainsi, si on fait quelque chose comme ça dans le OnInit :

protected override void OnInit(EventArgs e)
{
if (IsPostBack)
{
Response.Write(string.Format("La valeur saisie est : {0}", LeTextBox.Text));
}
base.OnInit(e);
}

Ça ne peut pas marcher, conformément à ce qu'on vient d'évoquer. LeTextBox n'a pas encore récupéré sa valeur, car LoadPostBackData n'est pas encore passé…
Les données ont malgré tout été envoyées en POST, ce qui fait qu'on peut quand même les récupérer de cette façon :

protected override void OnInit(EventArgs e)
{
if (IsPostBack)
{
Response.Write(string.Format("La valeur saisie est : {0}", Request.Form[LeTextBox.UniqueID]));
}
base.OnInit(e);
}

La collection Request.Form contient les valeurs qui sont passées lors des postback, grâce à l'ID unique de la textbox, on peut récupérer notre valeur. Notez que vous aurez rarement à utiliser cette méthode si vous respectez convenablement le cycle de vie de la page ASP.NET.

5.Attention avec la création de contrôles dynamiques

Prenez par exemple le besoin suivant :
On veut afficher une dropdownlist qui contient la liste des régions et quand on sélectionne une région, une autre dropdown apparait qui contient la liste des départements, et une fois le département choisi, on l'affiche.

Pour que le code reste lisible, on va le simuler avec un filtre sur l'alphabet, comme illustré ci-dessous :

On pourrait se dire : on prend une dropdown qu'on alimente et lors d'un changement, on crée la nouvelle dropdown avec le contenu filtré par rapport à la première valeur, et sur l'événement de changement de cette deuxième liste, on affiche la valeur choisie.
Ce qui donne ceci :

<asp:DropDownList runat="server" ID="GroupLettres" AutoPostBack="true" OnSelectedIndexChanged="DropDownChange">
<asp:ListItem Text="Choisissez ..." Value="" />
<asp:ListItem Text="A --> I" Value="1" />
<asp:ListItem Text="J --> Q" Value="2" />
<asp:ListItem Text="R --> Z" Value="3" />
</asp:DropDownList>

La première DropDownList est créée avec trois possibilités de choix. On note que la propriété AutoPostBack vaut true et on note OnSelectedIndexChanged associé à une méthode.
Ainsi, tout changement de valeur sélectionnée sur la dropdown provoquera un postback (AutoPostBack = true) et l'événement de changement OnSelectedIndexChanged sera levé et la méthode DropDownChange appelée.

protected void DropDownChange(object sender, EventArgs e)
{
DropDownList listLettre = new DropDownList();
int valSelected;
if (int.TryParse(((DropDownList)sender).SelectedValue, out valSelected))
{
string chaine = string.Empty;
switch (valSelected)
{
case 1:
chaine = "ABCDEFGHI";
break;
case 2:
chaine = "JKLMNOPQ";
break;
case 3:
chaine = "RSTUVWXYZ";
break;
}
foreach (Char c in chaine)
{
listLettre.Items.Add(new ListItem(c.ToString(), c.ToString()));
}
listLettre.AutoPostBack = true;
listLettre.SelectedIndexChanged += listLettre_SelectedIndexChanged;
form1.Controls.Add(listLettre);
}
}

void listLettre_SelectedIndexChanged(object sender, EventArgs e)
{
Response.Write(string.Format("Vous avez choisi {0}", ((DropDownList)sender).SelectedValue));
}

Dans la méthode DropDownChange, on crée une nouvelle dropdownlist remplie avec les valeurs de l'alphabet correspondant à la valeur récupérée de la première DropDown.
De la même façon, AutoPostBack est positionnée à true et on associe une méthode à l'événement de changement.

Sauf qu'à l'exécution, on va se rendre compte qu'on ne passe jamais dans l'événement listLettre_SelectedIndexChanged et même… lors du deuxième postback, la deuxième dropdown va disparaitre.

Ceci parce que l'arbre des contrôles doit être entièrement reconstruit à chaque postBack. Ainsi, lorsqu'on ajoute à l'arbre des contrôles un contrôle lors d'un événement particulier (sur un contrôle par exemple), on prend le risque de ne pas le rajouter au postback suivant, ce qui aura pour effet de ne pas binder l'événement par exemple ou de faire disparaitre le contrôle.

Au premier affichage de la page, l'état de l'arbre est celui-ci :

On arrive sur la page la première fois : Ajout du formulaire, ajout de la dropdown GroupLettres.

Choix d'un élément de la dropdown => postback : Ajout du formulaire, ajout de la dropdown GroupLettres, Evenement DropDownChange levé => Ajout de la dropDown listLettre.
Au deuxième affichage de la page, l'état de l'arbre est celui-ci :

Choix d'un élément de la deuxième dropdown => nouveau postback : Ajout du formulaire, ajout de la dropdown GroupLettres. Ici, l'événement DropDownChange n'est pas levé, donc ASP.NET ne connait pas listLettre et est incapable de lever l'événement associé.
On en revient au premier arbre. La deuxième dropdown a disparu et l'événement n'est pas levé.

Comment faire pour résoudre ce problème ?

Et bien il faut ajouter la deuxième dropdown à chaque requête de cette page. Et pour rester conforme à l'énoncé du sujet, on jouera sur la visibilité de la deuxième dropdown.
Ainsi, on surcharge le OnLoad pour créer notre DropDown :

protected override void OnLoad(EventArgs e)
{
DropDownList listLettre = new DropDownList();
listLettre.ID = "ListLettres";
listLettre.AutoPostBack = true;
listLettre.SelectedIndexChanged += listLettre_SelectedIndexChanged;
listLettre.Visible = false;
form1.Controls.Add(listLettre);
base.OnLoad(e);
}

Notez qu'on affecte un ID à notre dropdown pour pouvoir la retrouver plus tard (avec FindControl) et qu'on la masque avec listLettre.Visible = false;
Lors de l'événement de changement de la première dropdown, on remplit la deuxième dropdown et on l'affiche :

protected void DropDownChange(object sender, EventArgs e)
{
int valSelected;
if (int.TryParse(((DropDownList)sender).SelectedValue, out valSelected))
{
string chaine = string.Empty;
DropDownList listLettre = (DropDownList) form1.FindControl("ListLettres");
switch (valSelected)
{
case 1:
chaine = "ABCDEFGHI";
break;
case 2:
chaine = "JKLMNOPQ";
break;
case 3:
chaine = "RSTUVWXYZ";
break;
}
foreach (Char c in chaine)
{
listLettre.Items.Add(new ListItem(c.ToString(), c.ToString()));
}
listLettre.Visible = true;
}
}

ainsi, l'événement de changement de la deuxième dropdownlist pourra être intercepté :

protected void listLettre_SelectedIndexChanged(object sender, EventArgs e)
{
Response.Write(string.Format("Vous avez choisi {0}", ((DropDownList)sender).SelectedValue));
}

Notre exemple est désormais fonctionnel.

Vous me diriez : oui, mais… maintenant, notre création dynamique n'est franchement plus utile !! Et vous auriez raison. On pourrait remplacer tout le contenu de la méthode OnLoad dans le source aspx de cette façon :

<asp:DropDownList runat="server" ID="ListLettres" AutoPostBack="true" OnSelectedIndexChanged="listLettre_SelectedIndexChanged" Visible="false">
</asp:DropDownList>

On a tout intérêt à laisser ASP.NET gérer la création des composants autant que faire se peut. Rappelons-nous, ASP.NET instancie et ajoute à l'arbre les contrôles très tôt, ce qui peut éviter des erreurs d'enchainement des événements.

D'une manière générale, il vaut mieux essayer de construire tous nos contrôles statiquement et jouer ensuite sur la visibilité dans le code behind.

6.Le cycle de vie de la page ASP.NET

Je vous présente ci-dessous les principales étapes du cycle de vie d'une page ASP.NET. Il est primordial de bien connaitre ces étapes afin de pouvoir écrire des applications cohérentes, et déjouer les pièges associés à ce cycle de vie.
Pour une description exhaustive, rendez-vous en fin de chapitre.

6.1.Les principaux événements

Initialisation d'une page par le framework ASP.NET :

Événement

Description et actions associées

FrameworkInitialize

Initialise l'arbre des contrôles, basé sur l'aspx.

DeterminePostbackMode

Détermine si on est dans un PostBack et affecte la variable IsPostBack. Charge également les données GET et POST. Une fois cet événement passé, on peut utiliser Request.Form.

PreInit

Événement valable uniquement pour une page. À ce moment-là, les propriétés mises en design dans l'aspx sont déjà initialisées. C'est l'endroit idéal pour créer des contrôles dynamiquement.

Init

Dans cet événement, on peut également lire les propriétés mises en design, mais on ne peut pas récupérer leurs valeurs si elles ont été modifiées par l'utilisateur lors d'un post back ; ce ne sera possible qu'après LoadPostData(). Ces valeurs seront tout de même accessibles grâce à l'objet Request. Attention, ASP.NET commence par lever les événements OnInit des UserControls récursivement et ensuite lève l'événement de la Page.

TrackViewState

Démarre la surveillance des modifications de l'état d'affichage des contrôles

InitComplete

Événement valable uniquement pour la page. On s'en sert pour des opérations qui nécessitent que tous les éléments soient initialisés.

LoadViewState

Si la page est en post back (IsPostBack == true), ASP.NET déserialize les informations du view state et les appliques aux contrôles.

LoadPostBackData

Événement levé uniquement si IsPostBack == true. ASP.NET renseigne les contrôles avec leurs valeurs grâce au POST DATA.

Initialisation par le code utilisateur :

Événement

Description et actions associées

PreLoad

Événement valable uniquement pour la page, à utiliser avant le chargement récursif de tous les contrôles.

OnLoad

Appelle récursivement OnLoad sur les contrôles enfants. C'est l'endroit idéal pour les databinds. Attention, cette fois-ci ASP.NET commence par le onload de la page et ensuite ceux des usercontrols récursivement.

Validation, événements et fin de chargement

Événement

Description et actions associées

Validation

Ici, les méthodes serveur des validateurs sont appelées.

Événement des contrôles

ASP.NET va lever les événements associés à des contrôles : OnClick, OnSelectedIndexChanged, etc…

LoadComplete

Événement valable uniquement pour la page. On s'en sert pour des actions qui nécessitent que tous les contrôles soient chargés.

Rendu :

Événement

Description et actions associées

PreRender

Avant cet événement, la page appelle EnsureChildControls. À utiliser avant que le contrôle ou la page ne soit définitivement figé.

SaveStateComplete

Arrive au moment de l'enregistrement du viewState, si on modifie un contrôle après cet événement, ce ne sera pas pris en compte.

Render

Méthode qui effectue le rendu de chaque contrôle.

Déchargement :

Événement

Description et actions associées

Unload

Cet événement est utilisé pour libérer les ressources.

6.2.Le rattrapage

Il est à noter que si un contrôle a été créé dynamiquement et ajouté à l'arbre de contrôles, il va rattraper tous les événements qu'il a en retard.
Par exemple, ajoutons un userControl à l'arbre des contrôles dans l'événement OnLoadComplete de la page :

protected override void OnLoadComplete(EventArgs e)
{
Controls.Add(LoadControl("~/UC1.aspx"));
base.OnLoadComplete(e);
}

Nous aurons (pour les événements principaux) dans l'ordre :

Page : OnInit
Page : OnLoad
Page : OnLoadComplete
UserControl : OnInit
UserControl : OnLoad
Page : OnPreRender
UserControl : OnPreRender

Alors que si l'usercontrol avait été déposé dans le designer, on aurait eu :

UserControl : OnInit
Page : OnInit
Page : OnLoad
UserControl : OnLoad
Page : OnLoadComplete
Page : OnPreRender
UserControl : OnPreRender

6.3.Les opérations de liaisons de données (DataBinding)

Pour les opérations de liaisons de données (DataBinding), on observe plusieurs comportements :

- soit on laisse ASP.NET gérer la liaison toute seule, l'événement DataBinding étant appelé par défaut avant le OnPreRender du contrôle lorsque la source d'un contrôle est déclarée (par exemple DataSourceID pour un repeater) ;

- soit on fait le databinding à la main lorsqu'on en a besoin, par exemple pour un contrôle utilisant le scriptlet d'expressions liées # (plus de précisions dans la FAQ ASP.NET). À ce moment-là, l'événement OnDataBinding est appelé au moment de l'appel de la méthode DataBind sur le contrôle.

Par exemple, si on a un repeater qui est alimenté avec un ObjectDataSource de cette façon :

<asp:ObjectDataSource runat="server" ID="MonObjectDataSource" TypeName="test.TestObject" SelectMethod="GetValue" />
<asp:Repeater runat="server" ID="LeRepeater" DataSourceID="MonObjectDataSource">
<ItemTemplate>
<asp:Label runat="server" Text="<%#Container.DataItem %>" />
</ItemTemplate>
</asp:Repeater>

public class TestObject
{
public int[] GetValue()
{
return new int[] { 1, 2, 3, 4 };
}
}

protected override void OnInit(EventArgs e)
{
LeRepeater.DataBinding += LeRepeater_DataBinding;
base.OnInit(e);
}

on aura :

OnLoad
OnPreRender de la page
LeRepeater_DataBinding
OnPreRender du Repeater

Alors que si je veux utiliser le repeater avec une datasource :

<asp:Repeater runat="server" ID="LeRepeater" DataSource="<%#new int[] {1, 2, 3, 4} %>">

Je suis obligé d'appeler explicitement la méthode databind() sur le repeater (ou sur la page), et si je le fais par exemple dans le OnLoad :

protected override void OnLoad(EventArgs e)
{
LeRepeater.DataBind();
base.OnLoad(e);
}

j'aurai :

LeRepeater_DataBinding
OnLoad
OnPreRender de la page
OnPrenRender du Repeater

Notez que si j'avais appelé la méthode DataBind de la page, je serais d'abord passé dans le OnDataBinding de la page.

6.4.Schéma de synthèse du cycle de vie

Vous pouvez retrouver ci-dessous un schéma, conçu par Leon Andrianarivony, qui synthétise l'enchainement des étapes du cycle de vie d'une page ASP.NET.

Cliquez pour agrandir

Notez qu'ASP.NET peut permettre de rajouter quelques événements de confort. Il s'agit de mapping des événements classiques que l'ont peut utiliser lorsque la propriété AutoEventWireup est à true.
Par exemple, la méthode Page_Load sera appelée après la méthode OnLoad.
Ce mapping est un confort d'utilisation pour éviter d'avoir à surcharger les méthodes de la page ; mais c'est aussi une hérésie en termes de performances. Le framework va user de réflexions et de delegates simplement pour nous éviter une surcharge.
Préférez sans hésiter les surcharges et mettez la propriété AutoEventWireup à false.

7.Faire attention à ce que l'arbre des contrôles soit le même avant et après un postback

7.1.Un Exemple

Imaginons une appli qui simule un webmail (très moche, je sais :))
Cette appli web très light est composée d'une seule page.
Dans cette page, on surcharge le OnLoad pour avoir ces étapes :

protected override void OnLoad(EventArgs e)
{
InitMail();
SimulerArriverNouveauxMails();
DisplayMail();
base.OnLoad(e);
}

La méthode InitMail permet d'initialiser les mails non lus, avec un objet en session qui est une liste de trois chaines de caractères :

private void InitMail()
{
if (!IsPostBack)
Session["mails"] = null;
if (Session["mails"] == null)
{
Session["mails"] = new List<string>(new string[] { "mail 1", "mail 2", "mail 3" });
}
}

La méthode SimulerArriverNouveauxMails ne fait rien au premier passage, elle va se déclencher au prochain postback pour rajouter deux mails dans la liste en session :

private void SimulerArriverNouveauxMails()
{
if (IsPostBack && Session["mails"] != null)
{
List<String> listMails = (List<string>) Session["mails"];
if (listMails.Count == 3)
listMails.AddRange(new string[] {"mail 4", "mail 5"});
Session["mails"] = listMails;
}
}

Je l'ai exprès bloqué pour que ça ne dépasse pas les cinq mails.
Et la fonction DisplayMail affiche les mails. Pour ce faire, elle construit pour chaque mail un Panel. Ce panel est composé d'un label qui affiche le libellé du mail et un bouton qui permettra d'aller consulter le mail.

private void DisplayMail()
{
List<String> listMails = (List<string>) Session["mails"];
for (int i = listMails.Count - 1; i >= 0; i--)
{
Panel panelMail = new Panel();
Label labelMail = new Label();
labelMail.Text = listMails[i];
Button buttonMail = new Button();
buttonMail.Text = "Voir le mail";
buttonMail.Clic += buttonMail_Click;
panelMail.Controls.Add(labelMail);
panelMail.Controls.Add(buttonMail);
form1.Controls.Add(panelMail);
}
}

On associe au bouton un handler de clic. Lors du clic, on récupère le bouton courant (sender) et on affiche le message…

protected void buttonMail_Click(object sender, EventArgs e)
{
Panel panelMail = (Panel) ((Control) sender).Parent;
foreach (Control c in panelMail.Controls)
{
if (c is Label)
Response.Write(string.Format("Vous lisez le mail : {0}", ((Label)c).Text));
}
}

À première vue ça a l'air pas trop mal…
Sauf que ça ne marche pas comme on l'attend.
Exécutez la page, il construit nos trois mails et nos trois boutons. Cliquez sur un des boutons, par exemple le bouton du mail 3.
Il rafraichit la page, ajoute les nouveaux mails reçus et nous dit que nous sommes en train de lire le mail numéro 5… Petit souci !

7.2.L'explication

Que s'est-il passé ?

Quand je regarde le code source de ma page au premier lancement, je vois :

<div><span>mail 3</span><input type="submit" name="ctl04" value="Voir le mail" /></div>
<div><span>mail 2</span><input type="submit" name="ctl07" value="Voir le mail" /></div>
<div><span>mail 1</span><input type="submit" name="ctl10" value="Voir le mail" /></div>

À chaque bouton est associé un « name ». Le nom suit une incrémentation en fonction du nombre de contrôles.
Je clique sur « voir le mail » correspondant au mail 3, voici ce qu'il se passe :
On passe dans le OnLoad et on récupère la liste avec trois mails. À cette liste, on rajoute deux mails.
Ce qui fait qu'on va reconstruire cinq boutons avec une incrémentation logique qui devrait être ctl04, ctl07, ctl10, ctl13 et ctl16.
Puis on passe dans l'événement du clic sur le bouton et on affiche le Text du bouton sélectionné, qui était identifié par ctl04 avant le postback. On commence à se douter de ce qui va arriver…

Or, désormais, ctl04 représente le bouton associé au mail 5, vu qu'ils ont été créés dans cet ordre-là. Ce qui fait qu'on va passer dans le OnClick du bouton 5 et on affiche alors le contenu du mail 5, alors qu'on avait cliqué sur le bouton du mail 3…
Voilà ce qui arrive quand l'arbre des contrôles n'est pas le même avant et après le postback.

Le tableau ci-dessous nous résume les étapes

On peut résoudre ceci facilement en fixant un identifiant nous même, sans laisser ASP.NET le gérer à notre place :

private void DisplayMail()
{
List<String> listMails = (List<string>)Session["mails"];
for (int i = listMails.Count - 1; i >= 0; i--)
{
Panel panelMail = new Panel();
Label labelMail = new Label();
labelMail.Text = listMails[i];
Button buttonMail = new Button();
buttonMail.ID = listMails[i];
buttonMail.Text = "Voir le mail";
buttonMail.Clic += buttonMail_Click;
panelMail.Controls.Add(labelMail);
panelMail.Controls.Add(buttonMail);
form1.Controls.Add(panelMail);
}
}

Ainsi, il n'y aura pas de risque de mélange ou d'usurpation d'ID.

7.3.Un autre exemple

Ce problème peut être mis en évidence d'une autre façon, très facilement :
Dans mon onload je crée trois boutons de manière aléatoire (on tire d'abord trois chiffres aléatoires différents les uns des autres et on crée les trois boutons) :

protected override void OnLoad(EventArgs e)
{
int i = 0;
List<int> list = new List<int>();
while (i < 3)
{
Random rnd = new Random();
int val = rnd.Next(0, 15);
while (list.Contains(val))
val = rnd.Next(0, 15);
list.Add(val);
i++;
}
foreach (int val in list)
{
Button b = new Button();
b.Click += bouton_Click;
b.Text = val.ToString();
form1.Controls.Add(b);
}

base.OnLoad(e);
}

et lors du clic, on affiche le bouton cliqué :

protected void bouton_Click(object sender, EventArgs e)
{
Response.Write(string.Format("bouton cliqué : {0}", ((Button)sender).Text));
}

Imaginons que mon tirage aléatoire me donne la première fois :

6 ,10, et 4

Je clique sur 10.
Mon tirage aléatoire me donne cette fois-ci :

14, 3 et 9

C'est donc « bouton cliqué : 3 » que j'observe, alors que j'ai cliqué sur 10…
L'événement est quand même levé alors qu'il s'agit de six boutons différents car les identifiants se retrouvent être les mêmes avant et après le postback, bien que les boutons soient différents.

Lors de la création des boutons, rajoutons :

b.ID = val.ToString();

On clique et l'événement n'est plus levé puisqu'il ne correspond à aucun bouton existant. Enfin un comportement logique, mais on ne passe bien sûr pas dans le clic du bouton.

Dans ce second exemple, le fait d'avoir des boutons compléments différents permet de mieux voir le problème. Mais il peut y avoir des situations du même genre qui seront très difficiles à appréhender et à déboguer si vous n'avez pas cette règle en tête.

8.Retrouver l'identifiant d'un contrôle qui a effectué le postback

Comme indiqué dans la FAQ ASP.NET, on peut retrouver le contrôle qui a effectué le postback grâce à la valeur de

Request.Form["__EVENTTARGET"]

qui contiendra l'identifiant du contrôle qui a fait le postback. Ceci va fonctionner pour les dropdownlist par exemple, mais pas pour les boutons, car le mécanisme de post va effectuer simplement l'envoi du formulaire et Request.Form[« __EVENTTARGET »] vaudra null.
On peut retrouver un bouton de cette façon :

foreach (string ctl in Request.Form)
{
Control c = FindControl(ctl);
if (c is Button)
{
Response.Write(string.Format("Le bouton qui a effectué le postback est : {0}", c.UniqueID));
break;
}
}

ou si on connait l'identifiant d'un bouton particulier qu'on veut tester :

if (Request.Form[MonBouton.ID] != null)
{
Response.Write("MonBouton a été cliqué");
}

NB : Notez qu'en général, on a besoin d'identifier le contrôle qui a généré le postback dans l'événement qui lui est associé. Dans ce cas, on utilise l'objet sender qui est passé en paramètre.

protected void Button_Click(object sender, EventArgs e)
{
Button monBouton = (Button)sender;
// ....
}

On peut utiliser la méthode décrite plus haut lorsqu'on a besoin d'identifier le contrôle qui a effectué le postback avant la levée des événements des contrôles (dans le OnInit ou OnLoad par exemple).

9.Création précoce d'un identifiant unique

Une autre erreur difficile à identifier peut venir d'un déclenchement trop précoce de la création du contrôle

Soit l'exemple suivant :

<asp:Repeater runat="server" ID="LeRepeater" OnItemCreated="ItemCreated" DataSource="<%#new int[] {1, 2, 3, 4} %>">
<ItemTemplate>
<asp:Label runat="server" Text="<%#Container.DataItem %>" />
<asp:Button runat="server" ID="LeBoutton" Text="Go" OnClick="ClickButton" />
<asp:HiddenField runat="server" ID="LeHidden"/>
</ItemTemplate>
</asp:Repeater>

Un repeater qui se répète quatre fois et qui crée un Label, un Button et un champ caché.
L'événement ItemCreated est associé à une méthode :

protected void ItemCreated(Object Sender, RepeaterItemEventArgs e)
{
if (e.Item.ItemType == ListItemType.AlternatingItem || e.Item.ItemType == ListItemType.Item)
{
Button monBouton = (Button)e.Item.FindControl("LeBoutton");
HiddenField hidden = (HiddenField)e.Item.FindControl("LeHidden");
hidden.Value = monBouton.UniqueID;
}
}

protected override void OnLoad(EventArgs e)
{
if (!IsPostBack)
LeRepeater.DataBind();
base.OnLoad(e);
}

protected void ClickButton(object sender, EventArgs e)
{
Response.Write("Je passe dans le clic du bouton");
}

Au moment de la création des éléments du repeater, on fait un FindControl pour trouver le bouton et le champ caché et on affecte la valeur de l'identifiant unique du bouton au champ caché.
Sauf qu'ici, le fait de réclamer trop tôt la propriété UniqueID (valable aussi pour ClientID) pose un problème. En effet, si vous regardez le code html de la page générée, vous verrez :

<span>1</span>
<input type="submit" name="LeBoutton" value="Go" id="LeBoutton" />
<input type="hidden" name="LeHidden" id="LeHidden" value="LeBoutton" />

<span>2</span>
<input type="submit" name="LeBoutton" value="Go" id="LeBoutton" />
<input type="hidden" name="LeHidden" id="LeHidden" value="LeBoutton" />
.....

C'est-à-dire que l'identifiant du bouton sera le même pour tous les contrôles bouton, car ASP.NET n'a pas pu aller jusqu'au bout de sa création.
Conséquence : l'événement du bouton n'est pas levé, car ASP.NET ne saura pas le retrouver.

Cet exemple est très simpliste et je reconnais qu'il n'est pas vraiment réaliste.
On peut toutefois retrouver ce comportement dans un contrôle plus réaliste, disponible dans mon tutoriel d'introduction aux contrôles templates.
Nous avons besoin du clientId d'un contrôle pour gérer son affichage en javascript. Si nous avions utilisé cette propriété dans la méthode CreateChildControls(), le résultat aurait été erroné.
C'est pour cela que cette utilisation est déplacée en l'occurrence dans la surcharge de la méthode OnPreRender.

10.Conclusion

J'espère qu'à travers cet article vous avez pu améliorer votre compréhension des concepts inhérents à la création des contrôles d'une page ASP.NET. Il y a effectivement certaines erreurs qui peuvent découler de la mauvaise compréhension de ces mécanismes ou du cycle de vie de la page ASP.NET.
Bien maitriser ces notions permet d'éviter beaucoup d'erreurs qui, dans certaines situations, pourraient s'avérer difficiles à déboguer.

Remerciements

Je remercie l'équipe Dotnet, notamment Ditch et Cardi, pour leurs relectures attentives du document.

Contact

Si vous constatez une erreur dans le tutoriel, dans le source, dans la programmation ou pour toutes informations, n'hésitez pas à me contacter par mail, ou par le forum.