1.Introduction

A 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 post-pack, 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 tache 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 :

 
Sélectionnez
<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 bindé 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éfini 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 :

 
Sélectionnez
<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 click puisse afficher quelque chose comme ca :

Image non disponible

4.1.Création du contrôle

Ici notre contrôle s'appelle PleaseWaitButton. On lui défini 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 serveurs qu'il contient.

 
Sélectionnez
[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 :

 
Sélectionnez
<%# Container.Item %>

Non utilisée 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.

 
Sélectionnez
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 click.
On ajoute tout ca à 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.

 
Sélectionnez
[ToolboxItem(false)]
public class TemplateItem : Control, INamingContainer
{
}
 
Sélectionnez
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 click
	Button boutonValidation = new Button();
	boutonValidation.ID = "_button";
	boutonValidation.Text = Text;
	boutonValidation.Click += 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 click 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.

 
Sélectionnez
[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.

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

Lors du click 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.

 
Sélectionnez
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 click, 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 parti 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 :

 
Sélectionnez
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 etre 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 click 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.
 
Sélectionnez
protected override void Render(HtmlTextWriter writer)
{
	// création des scripts côté client :
	// si 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 2 cas de figures, avec ou sans validation.

4.2.1.Sans validation

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

 
Sélectionnez
<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 ca pour simuler un long traitement.

 
Sélectionnez
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é :

 
Sélectionnez
<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 :

 
Sélectionnez
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 :

 
Sélectionnez
<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 tutorial, nous avons vu une introduction à la création de contrôle template. 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 tutorial, dans le source, dans la programmation ou pour toutes informations, n'hésitez pas à me contacter par mail, ou par le forum.