Tutoriel : Introduction aux contrôles templates pour Asp.Net 2.0 en C#

Nico-pyright(c)

Cet article constitue une introduction à la création de contrôles templates pour Asp.Net en C#. Il se présentera sous la forme d'un tutoriel pour créer un bouton paramétrable, de style « Veuillez patienter… ».

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

- Titre : Tutoriel : Introduction aux contrôles templates pour Asp.Net 2.0 en C#

- Auteur : Nico-pyright(c)

- Parution : 8 avril 2008

- Mise à jour : 8 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ésentation du contrôle type « Bouton veuillez patienter »

- 3.Les contrôles templates

- 3.1.L'exemple du Repeater

- 3.2.Créer son propre contrôle

- 4.Création du contrôle bouton veuillez patienter

- 4.1.Création du contrôle

- 4.2.Exemples d'utilisation

- 4.2.1.Sans validation

- 4.2.1.Avec validation

- 4.2.1.1.Validation sans groupe

- 4.2.1.2.Validation avec groupe de validation

- 4.3.Téléchargement

- 5.Conclusion

- Remerciements

- Contact

1.Introduction

À travers ce tutoriel, nous allons expliquer les fondements de la création d'un contrôle template pour Asp.Net 2.0. et nous allons les utiliser pour créer un contrôle du type « bouton veuillez patienter ». Tout au long de cet article, je vais utiliser Visual C# 2008.

D'une manière générale, les contrôles serveur d'Asp.Net ont une apparence par défaut, paramétrables grâce à des propriétés ou des feuilles de styles.
Un contrôle template (ou contrôle modèle) offre une complète personnalisation de son apparence en permettant de spécifier un ensemble d'éléments HTML ou de contrôles serveur qui lui servira de rendu.

2.Présentation du contrôle type « Bouton veuillez patienter »

Lors d'un postpack, il peut arriver qu'on ait des traitements côté serveur plus ou moins long à effectuer, comme une validation de formulaire via un web-services, un ensemble de tâches pour un paramétrage… Bref, une tâche assez longue qui peut perturber un utilisateur qui verrait son navigateur bloqué, sans page de confirmation créée rapidement.

Ce que j'appelle un bouton « veuillez patienter » est un bouton qui effectue ce postback et qui affiche un message pour informer l'utilisateur que la tache est un peu longue, et qu'il peut aller se chercher un café.

Après avoir un peu cherché sur le net dans les différents tutoriels qui traitent de ce sujet, j'ai souvent vu des articles qui présentaient la réécriture complète d'un bouton, avec une propriété paramétrable qui affiche un texte dans un label très moche, ce que je trouve somme toute très limité.
Ce dont j'aurai besoin, c'est d'un contrôle où je maitrise complètement le rendu, en fonction des conditions inhérentes à la page du moment.

Ce qu'il me faut, c'est la possibilité d'afficher un bouton, et lorsque je clique dessus apparaisse un rendu html que j'aurai préalablement décrit dans ce contrôle.
De dire : ah ba tiens, pour cette page, je veux afficher « veuillez patienter » en rouge sur fond gris.
Et puis pouvoir réutiliser plus loin ce contrôle en affichant cette fois-ci « attention, nous vérifions les informations saisies, cela peut durer de 1 à 5 minutes », dans un cadre vert aux bordures jaunes, couleur bleue sur fond gris.
Bref, vous l'aurez compris… : la présentation que je veux, où je veux.

C'est ici qu'interviennent les contrôles templates.

3.Les contrôles templates

3.1.L'exemple du Repeater

Appelés aussi contrôles « modèles », ce sont des contrôles où l'on a la possibilité de définir nous même le rendu visuel du contrôle.

Un contrôle très connu de contrôle template est le repeater. Il s'agit d'un contrôle de liste lié aux données permettant une présentation personnalisée, grâce à l'application d'un modèle spécifié à chacun des éléments figurant dans la liste.
Son utilisation par exemple, permet d'afficher une liste de données (implémentant IEnumerable) dans notre modèle :

<asp:Repeater id="Repeater1" runat="server" DataSource='<%#new string[] { "Nico-pyright", "Cardi", "Dev01", "..."} %>'>
<HeaderTemplate>
<div style="border:solid 1px black">Liste des utilisateurs  :
</HeaderTemplate>
<ItemTemplate>
<div style="padding-left:10px"><%# Container.DataItem%></div>
</ItemTemplate>
<FooterTemplate>
</div>
</FooterTemplate>
</asp:Repeater>

Dans cet exemple de repeater, on voit bien qu'on est maitre de la présentation visuelle du tableau de chaine que l'on a blindé au repeater. Le repeater fournit la fonctionnalité de « répéter » suivant une source de données, et nous fournissons la présentation.

Ce qu'on veut faire pour notre bouton « veuillez patienter » suit exactement le même principe. Une fonctionnalité définie, immuable : à savoir la fonctionnalité d'un bouton. Et une présentation qui reste au bon vouloir de l'utilisateur.

Notez bien qu'il y a une différence entre un contrôle template qui permet de définir complètement notre rendu visuel et l'utilisation d'une feuille de styles, qui permet d'adapter légèrement la mise en page d'un contrôle.
Par exemple, l'utilisation de styles va permettre de modifier la propriété BackColor d'un contrôle, tandis qu'un template va nous permettre de rajouter une table html, une présentation complexe, ou d'autres contrôles.

3.2.Créer son propre contrôle

Nous avons remarqué dans l'exemple du Repeater, qu'il est possible d'intervenir à plusieurs niveaux, grâce aux trois templates ci-dessus (il y en a d'autres pour le repeater).
Il s'agit des HeaderTemplate, ItemTemplate et FooterTemplate. La fonctionnalité principale d'un contrôle template réside dans l'utilisation de ces différents templates.

Chacun de ces templates est de type ITemplate.

Cette interface définit la méthode InstantiateIn.
Asp.Net va parser le contenu de la balise template de notre contrôle et va appeler la méthode InstantiateIn à chaque template trouvé. Le contrôle crée à chaque fois l'arbre des contrôles que représente le contenu du template.

Tout le principe réside dans cette implémentation.
Lors de l'appel à CreateChildControls, on va utiliser le template pour l'ajouter à l'arbre des contrôles de notre contrôle, grâce à la méthode InstantiateIn.
Il sera également possible de définir un rendu par défaut si aucun template n'est utilisé.

Nous allons voir ce fonctionnement en détail dans l'exemple qui suit : la création d'un contrôle de type « bouton veuillez patienter ».

4.Création du contrôle bouton veuillez patienter

L'objectif est d'avoir un contrôle qui puisse se définir par exemple ainsi :

<Test:PleaseWaitButton ID="PWB" runat="server">
<PleaseWaitMessageTemplate>
<p style="background-color: #eaf2d9;border:1px solid #cccc99;color:green;width:500px;text-align:center">
Veuillez patienter quelques instants, <br/>
nous vérifions les informations que vous avez saisies ...<br />
<asp:Image runat="server" ImageUrl="wait.gif" />
</p>
</PleaseWaitMessageTemplate>
</Test:PleaseWaitButton>

Et qui lors d'un clic peut afficher quelque chose comme ça :

4.1.Création du contrôle

Ici notre contrôle s'appelle PleaseWaitButton. On lui définit son rendu visuel dans le template PleaseWaitMessageTemplate.
En l'occurrence, on affiche un texte pour patienter, et une image style barre de progression infinie.

Nous allons donc créer une classe qui va hériter de Control. Cette classe implémentera aussi INamingContainer.
Sachez simplement que par l'intermédiaire de cette interface, on va pouvoir identifier les contrôles en fournissant un espace de noms à tous les contrôles serveur qu'il contient.

[ParseChildren(true)]
[DefaultProperty("Text")]
public class PleaseWaitButton : Control, INamingContainer
{
}

La classe doit avoir l'attribut ParseChildren(true) (hérité si on dérive de WebControl) afin de dire au parseur d'interpréter le contenu en tant que propriété et non en tant que contrôles enfants.

Cette classe contiendra bien entendu une propriété template, de type ITemplate. L'attribut TemplateContainer permettra de savoir quel type de contrôle sera utilisé lors d'un databinding du genre :

<%# Container.Item %>

Non utilisé dans cet exemple, cet attribut est ici à titre explicatif.
L'attribut PersistenceMode indique que la propriété persiste dans le contrôle serveur ASP.NET en tant que balise imbriquée.

private ITemplate _pleaseWaitMessageTemplate = null;

[Browsable(false), DefaultValue(null), PersistenceMode(PersistenceMode.InnerProperty), TemplateContainer(typeof(TemplateItem))]
public ITemplate PleaseWaitMessageTemplate
{
get { return _pleaseWaitMessageTemplate; }
set { _pleaseWaitMessageTemplate = value; }
}

C'est la méthode CreateChildControls qui contient toute la logique de la création du contrôle template.
On commence par créer un Panel caché, auquel on ajoute le template (chargé et parsé grâce à InstantiateIn) s'il est défini, ou un contrôle litteral avec un texte par défaut s'il ne l'est pas.
Ensuite, on ajoute le bouton, auquel on affecte la propriété Text et on associe un handler de clic.
On ajoute tout ça à la collection de contrôles du contrôle template.
Notez que le template instancié (TemplateItem) est une classe qui implémente INamingContainer et qui doit dériver de Control, directement ou indirectement. Il est assez classique de faire dériver notre élément de template de la classe Panel. Ici, nous dériverons directement de Control.

[ToolboxItem(false)]
public class TemplateItem : Control, INamingContainer
{
}

protected override void CreateChildControls()
{
Controls.Clear();
// On crée un panel caché
Panel panelMessage = new Panel();
panelMessage.ID = "_panel";
panelMessage.Attributes["style"] = "display:none";
if (PleaseWaitMessageTemplate != null)
{
// si un template est défini, on l'utilise
TemplateItem templateItem = new TemplateItem();
PleaseWaitMessageTemplate.InstantiateIn(templateItem);
panelMessage.Controls.Add(templateItem);
}
else
{
// sinon, on crée un message par défaut tout moche
panelMessage.Controls.Add(new LiteralControl("Veuillez patienter ..."));
}
// on crée le bouton avec la propriété Text ou la chaine "OK" si elle n'est pas définie
// et on associe un handler de clic
Button boutonValidation = new Button();
boutonValidation.ID = "_button";
boutonValidation.Text = Text;
boutonValidation.Clic += b_Click;

// on ajoute le panel et le bouton
Controls.Add(panelMessage);
Controls.Add(boutonValidation);
}

Pour fonctionner comme un bouton classique, notre contrôle va publier une propriété Text qui aura également une valeur par défaut, car je n'aime pas les boutons sans texte.
On pourra également associer une méthode pour un traitement particulier lors du clic sur le bouton.
Notre contrôle devra également s'insérer correctement dans un cycle de validation de la page asp.net. Ainsi, nous gérerons la validation et nous exposerons une propriété ValidationGroup.

[Bindable(true), Category("Behavior"), Description("Le groupe de validation")]
public string ValidationGroup
{
get { return (string)ViewState["ValidationGroup"] ?? string.Empty; }
set { ViewState["ValidationGroup"] = value; }
}

[Bindable(true), Category("Appearance"), DefaultValue("OK"), Description("Le texte du bouton")]
public string Text
{
get { return (string)ViewState["Text"] ?? "OK"; }
set { ViewState["Text"] = value; }
}

private event EventHandler _clickHandler;
public event EventHandler Click
{
add { _clickHandler += value; }
remove { _clickHandler -= value; }
}

Pour pouvoir parcourir les sous-contrôles de ce contrôle, il faudra surcharger la propriété Controls. Nous appellerons la méthode EnsureChildControls qui appelle CreateChildControls si cela n'a pas déjà été fait.

public override ControlCollection Controls
{
get { EnsureChildControls(); return base.Controls; }
}

Lors du clic sur le bouton, nous allons dans un premier temps demander la validation de la page puis déclencher l'événement de l'utilisateur, s'il est défini.

private void b_Click(object sender, EventArgs e)
{
// on déclenche la validation manuelle
if (!string.IsNullOrEmpty(ValidationGroup))
Page.Validate(ValidationGroup);
else
Page.Validate();
// on lève l'événement du clic, s'il y en a un
if (_clickHandler != null)
_clickHandler(sender, e);
}

La validation est une étape cruciale du cycle de vie de la page asp.net. Notre contrôle se doit de la respecter. Pour demander l'évaluation de cette validation, nous avons besoin d'appeler explicitement la méthode Page.Validate().
Si le bouton fait partie d'un groupe de validation, l'appel à Page.Validate devra passer en paramètre le nom du groupe de validation. Page.Validate() est une validation côté serveur, c'est-à-dire sensible à la méthode OnServerValidate d'un CustomValidator par exemple.

Il est également important de prendre en charge la validation côté client, pour des validators comme le RequiredFieldValidator. Pour ce faire, nous devons appeler explicitement côté client une méthode : Page_ClientValidate().
C'est également à ce moment-là que nous devrons gérer la visibilité de notre message pour patienter.
Pour ce faire, nous allons définir une méthode javascript que nous allons appeler par l'intermédiaire de la propriété OnClientClick du bouton.

Mais avant toute chose, nous avons besoin d'affecter un ID à notre panel, pour pouvoir le manipuler côté javascript :

protected override void OnPreRender(EventArgs e)
{
// on cherche le panel pour récupérer son ClientID
// on cherche le bouton pour utiliser le ClientID du panel dans une fonction javascript associée à la propriété OnClientClick
// cette opération ne peut pas être faite dans le CreateChildControls, car cela serait trop tôt
Panel panel = (Panel)FindControl("_panel");
Button button = (Button)FindControl("_button");
button.OnClientClick = string.Format("checkForm('{0}')", panel.ClientID);
base.OnPreRender(e);
}

On commence donc par rechercher le panel et on lui affecte un ID. Ensuite, il ne reste plus qu'à rechercher le bouton et à affecter la propriété OnClientClick avec le ClientID du panel.
Notez qu'on ne peut pas faire ce traitement dans le CreateChildControls car l'utilisation du ClientID trop tôt provoquerait des erreurs d'identification des contrôles.

La validation côté client et les affichages/masquages du message pour patienter nécessitent du javascript.
Nous allons donc utiliser Page.ClientScript.RegisterStartupScript pour enregistrer notre script et nos méthodes javascript.
Le principe est le suivant :

- la méthode checkForm est appelée lors du clic client sur le bouton ;

- on appelle la validation côté client grâce à Page_ClientValidate (si le bouton fait partie d'un groupe de validation, on devra passer ce groupe en paramètre). On interceptera une erreur si jamais il n'y a aucune validation ;

- si la validation est bonne, on affiche le panel (représenté par un div) et donc le message pour patienter. Si elle n'est pas bonne, on le masque.

protected override void Render(HtmlTextWriter writer)
{
// création des scripts côté client :
// s’il y a un groupe de validation, on appelle la fonction de validation client avec le groupe en paramètre, sinon sans paramètre
string validationGroupParameters = string.IsNullOrEmpty(ValidationGroup) ? string.Empty : string.Format("'{0}'", ValidationGroup);

// si la validation est OK, on affiche le panel (et donc le message d'attente)
// si elle n'est pas OK, on le cache et on retourne faux
string script = @"function getObj(id)
{
var o;
if (document.getElementById)
{
o = document.getElementById(id).style;
}
else if (document.layers)
{
o = document.layers[id];
}
else if (document.all)
{
o = document.all[id].style;
}
return o;
}
function setDisplay(id)
{
var o = getObj(id);
if (o)
{
o.display = 'block';
}

function unsetDisplay(id)
{
var o = getObj(id);
if (o)
{
o.display = 'none';
}
}
function checkForm(divWaiting)
{
try
{
if (!Page_ClientValidate(" + validationGroupParameters + @"))
{
unsetDisplay(divWaiting);
return false;
}
}
catch (e) {}
setDisplay(divWaiting);
}";
Page.ClientScript.RegisterStartupScript(GetType(), "javascriptButton", script, true);

base.Render(writer);
}

4.2.Exemples d'utilisation

Nous aurons deux cas de figure, avec ou sans validation.

4.2.1.Sans validation

Il suffira d'utiliser le contrôle de cette façon par exemple :

<Test:PleaseWaitButton ID="PWB" runat="server" OnClick="ClickButton">
<PleaseWaitMessageTemplate>
<p style="background-color: #eaf2d9;border:1px solid #cccc99;color:green;width:500px;text-align:center">
Veuillez patienter quelques instants, <br/>
nous vérifions les informations que vous avez saisies ...<br />
<asp:Image runat="server" ImageUrl="wait.gif" />
</p>
</PleaseWaitMessageTemplate>
</Test:PleaseWaitButton>

Avec dans le code behind, quelque chose comme ça pour simuler un long traitement.

protected void ClickButton(object sender, EventArgs e)
{
Thread.Sleep(2000);
}

4.2.1.Avec validation

Pour les exemples de validation, j'utiliserai un RequiredFieldValidator et un CustomValidator.

4.2.1.1.Validation sans groupe

On définit nos validators sans groupe de validation, le contrôle template reste inchangé :

<asp:TextBox runat="server" ID="LeTextBox" />
<asp:RequiredFieldValidator runat="server" ControlToValidate="LeTextBox"
ErrorMessage="Le champ doit être renseigné" Display="dynamic" />
<asp:CustomValidator runat="server" OnServerValidate="ValidateFunction" Display="dynamic"
ErrorMessage="Le champ doit être égal à ABC" />

<Test:PleaseWaitButton ID="PWB" runat="server" OnClick="ClickButton">
<PleaseWaitMessageTemplate>
<p style="background-color: #eaf2d9;border:1px solid #cccc99;color:green;width:500px;text-align:center">
Veuillez patienter quelques instants, <br/>
nous vérifions les informations que vous avez saisies ...<br />
<asp:Image runat="server" ImageUrl="wait.gif" />
</p>
</PleaseWaitMessageTemplate>
</Test:PleaseWaitButton>

Et dans le code behind :

protected void ClickButton(object sender, EventArgs e)
{
if (Page.IsValid)
{
Thread.Sleep(2000);
Response.Write("<br/>Informations OK<br/>");
}
}

protected void ValidateFunction(object source, ServerValidateEventArgs args)
{
args.IsValid = LeTextBox.Text == "ABC";
}

4.2.1.2.Validation avec groupe de validation

Pour la validation avec un groupe de validation, cela fonctionne de la même façon, on renseigne juste les propriétés ValidationGroup :

<asp:TextBox runat="server" ID="LeTextBox" />
<asp:RequiredFieldValidator runat="server" ControlToValidate="LeTextBox"
ErrorMessage="Le champ doit être renseigné" Display="dynamic" ValidationGroup="g" />
<asp:CustomValidator runat="server" OnServerValidate="ValidateFunction" Display="dynamic"
ErrorMessage="Le champ doit être égal à ABC" ValidationGroup="g" />

<Test:PleaseWaitButton ID="PWB" runat="server" OnClick="ClickButton" ValidationGroup="g">
<PleaseWaitMessageTemplate>
<p style="background-color: #eaf2d9;border:1px solid #cccc99;color:green;width:500px;text-align:center">
Veuillez patienter quelques instants, <br/>
nous vérifions les informations que vous avez saisies ...<br />
<asp:Image runat="server" ImageUrl="wait.gif" />
</p>
</PleaseWaitMessageTemplate>
</Test:PleaseWaitButton>

4.3.Téléchargement

Vous pouvez télécharger l'exemple à cette adresse : Télécharger la version .rar ici (17 ko), Télécharger la version .zip ici (21 ko).

5.Conclusion

Grâce à ce tutoriel, nous avons vu une introduction à la création de contrôles templates. Nous l'avons ensuite appliqué pour la réalisation d'un contrôle de type « bouton veuillez patienter ».
Nous avons vu également les éléments primordiaux qui permettent d'assurer la phase de validation de notre contrôle, côté serveur et côté client.

J'espère que vous avez trouvé cet article intéressant et que le contrôle template pourra vous être utile. Ne négligez pas la validation qui est un point critique, mais terriblement utile. Connaitre et tirer parti des mécanismes de validation peut être un atout non négligeable.

Remerciements

Je remercie Louis-Guillaume Morand, Djé ainsi que l'équipe Dotnet 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.