Developpez.com - Microsoft DotNET
X

Choisissez d'abord la catégorieensuite la rubrique :


Créer ses propres contrôles ASP.NET en C#

Date de publication : 10 décembre 2008

Par Rémy Mainil (Ma Page)
 

Cet article a pour but de fournir les bases pour la création de ses propres contrôles serveur ASP.NET en C#.

I. Un contrôle "Hello World"
II. Rendre notre contrôle personnalisable
III. Le code HTML sans erreur avec HtmlTextWriter
IV. La classe WebControl
V. Les contrôles composites
VI. Création d'un Textbox
VII. Conclusion


I. Un contrôle "Hello World"

Plutôt qu'un long discours, entrons directement dans le vif du sujet et lançons nous dans la création de notre premier contrôle serveur.
Voici son code :

using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI;

namespace MonControle
{
    class HWControl : Control
    {
        protected override void Render(HtmlTextWriter writer)
        {
            writer.Write("<h1>Hello World !!!</h1>");
        }
    }
}
Et nous pouvons utiliser ce contrôle comme ceci :

<%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Default.aspx.cs" Inherits="_Default" %>
<%@ Register TagPrefix="MyCtl" Namespace="Dvp.Controls" Assembly="MesControles" %>

<html>
    <body>
    <form runat="server">
       <MyCtl:HWControl id="HWCtl" runat="server"/>
    </form>
    </body>
</html>
En affichant notre page, nous avons bien "Hello World" qui s'affiche à l'écran.
Sur ce petit exemple tout simple, nous pouvons déjà relever les éléments suivants :

  1. Notre contrôle hérite de la classe Control. Cette classe constitue la base de tous les contrôles serveur ASP.NET
  2. La génération du code HTML qui devra être produit par notre contrôle se fait en surchargeant la méthode Render. Cette méthode est appelée lorsque la page est invitée à produire son code HTML.

II. Rendre notre contrôle personnalisable

Voyons maintenant comment personnaliser notre contrôle en lui ajoutant différentes propriétés. L'objectif de ces propriétés est de donner la possibilité de modifier le comportement ou l'apparence de notre contrôle sans devoir toucher à son code. Ajoutons donc une propriété Text permettant de spécifier le texte qui sera affiché par notre contrôle, une propriété ForeColor permettant de changer la couleur de la police du texte affiché par notre contrôle et pour finir une propriété FontSize qui, vous l'aurez compris, permettra de changer la taille de la police de notre texte.
Ces différentes propriétés sont accessible de deux manières : via le code ou sous forme d'attributs dans la balise de notre contrôle.
Mettons cela en pratique via le code suivant :

using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI;
using System.Drawing;
using System.Web.UI.WebControls;

namespace Dvp.Controls
{
    public class HWControl : Control
    {
        private string _text = "Hello World !!!";

        public string Text
        {
            get { return _text; }
            set { _text = value; }
        }

        private Color _foreColor = Color.Black;

        public Color ForeColor
        {
            get { return _foreColor; }
            set { _foreColor = value; }
        }

        private Unit _fontSize = Unit.Pixel(12);

        public Unit FontSize
        {
            get { return _fontSize; }
            set { _fontSize = value; }
        }
        
        protected override void Render(HtmlTextWriter writer)
        {
            writer.Write("<span style='color:" + ColorTranslator.ToHtml(_foreColor) + "; font-size:" + _fontSize.ToString() + ";'>" + _text + "</span>");
        }
    }
}
Et voici comment spécifier les propriétés du contrôles par les attributs ou via le code :

<%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Default.aspx.cs" Inherits="_Default" %>
<%@ Register TagPrefix="MyCtl" Namespace="Dvp.Controls" Assembly="MesControles" %>

<html>
    <body>
    <form runat="server">
       <MyCtl:HWControl id="HWCtl" runat="server" Text="Vive Developpez.com !" FontSize="24px" ForeColor="Red" />
       <MyCtl:HWControl ID="HWCtl2" runat="server" />
    </form>
    </body>
</html>

public partial class _Default : System.Web.UI.Page 
{
    protected void Page_Load(object sender, EventArgs e)
    {
        HWCtl2.Text = "ASP.NET c'est magique !";
        HWCtl2.FontSize = Unit.Pixel(46);
        HWCtl2.ForeColor = Color.Bisque;
    }    
}
info Il est intéressant de remarquer qu'ASP.NET s'occupe de convertir automatiquement les valeurs fournies en attributs dans le type correspondant à la propriété du contrôle.
warning Attention à bien spécifier une valeur par défaut pour les différentes propriétés du contrôle afin que celui-ci soit utilisable même si aucune valeur ne leur est attribuée.
Il est également possible pour un contrôle, d'exposer un objet comme propriété. On affecte alors des valeurs aux propriétés de cet objet ce qui offre l'avantage de pouvoir structurer en une classe un ensemble de propriétés communes à plusieurs contrôles comme par exemple, les propriétés de style.
Deux méthodes sont alors utilisables pour attribuer des valeurs à ces propriétés. La première consiste à préfixer les propriétés de l'objet par le nom de la propriété objet du contrôle suivi d'un tiret.
Par exemple, nous avons dans un contrôle une propriété objet nommée ObjectProp et l'objet qu'elle présente contient une propriété PropA. Via cette syntaxe, nous pouvons accéder à la propriété PropA de la manière suivante :

<MyCtl:MonControl ObjectProp-PropA="valeur" ...
La seconde méthode consiste à utiliser des éléments imbriqués pour définir ces propriétés. Cette méthode nécessite cependant de préfixer la classe de notre contrôle par l'attribut [ParseChildren(true)] afin que les éléments imbriqués soient considérés comme des propriétés et non commes des contrôles enfants.
Via cette méthode, on accède à la propriété PropA de la manière suivante :

<MyCtl:MonControl ....>
    <ObjectProp PropA="valeur" />
</MyCtl:MonControl>

III. Le code HTML sans erreur avec HtmlTextWriter

Vous l'aurez sans doute remarqué, mais le simple fait d'ajouter quelques propriétés à notre contrôle rend le code HTML à générer plus complexe. Imaginez ce que cela deviendra si l'on ajoute encore de nombreuses propriétés : une fameuse source d'erreurs.
C'est là que les services de la classe HtmlTextWriter viennent à notre rescousse.
Les plus observateurs auront remarqué que le paramètre de la méthode Render que nous surchargeons pour gérer le code HTML produit par notre contrôle est du type HtmlTextWriter. Cette classe offre différentes méthodes permettant de générer des balises HTML bien formées et d'y attacher des attributs ainsi que des attributs de style.
Parmis les méthodes de cette classe, retenons les méthodes RenderBeginTag(HtmlTextWriterTag Tag) et RenderEndTag() qui se chargent respectivement de produire la balise ouvrante et la balise fermante correspondante de n'importe quel tag HTML. Retenons également la méthode AddStyleAttribute(HtmlTextWriterStyle key, string value) et la méthode AddAttribute(HtmlTextWriterAttribute key, string value) qui permettent respectivement d'ajouter un attribut de style et un attribut à la balise HTML générée par la première instruction RenderBeginTag rencontrée.
Modifions notre contrôle pour utiliser les méthodes de la classe HtmlTextWriter :

protected override void Render(HtmlTextWriter writer)
        {
            writer.AddStyleAttribute(HtmlTextWriterStyle.FontSize, _fontSize.ToString());
            writer.AddStyleAttribute(HtmlTextWriterStyle.Color, ColorTranslator.ToHtml(_foreColor));
            writer.RenderBeginTag(HtmlTextWriterTag.Span);
            writer.Write(_text);
            writer.RenderEndTag();
        }
Le résultat obtenu est identique à celui obtenu précédemment mais le code est beaucoup plus lisible et facile à comprendre, donc à maintenir.

Fort des connaissances acquises nous pourrions envisager de modifier notre contrôle pour regrouper dans une propriété objet les différents attributs de style que nous pourrions attribuer à notre contrôle. Notre tâche va même s'en trouver fortemment simplifiée car la classe Style fournie dans le framework rempli déjà ce rôle. Voici comment l'utiliser dans notre contrôle :

public class HWControl : Control
    {
        private string _text = "Hello World !!!";

        public string Text
        {
            get { return _text; }
            set { _text = value; }
        }

        private Style _style = new Style();
        
        public Style Style
        {
            get { return _style; }
        }
        
        protected override void Render(HtmlTextWriter writer)
        {
            _style.AddAttributesToRender(writer);
            writer.RenderBeginTag(HtmlTextWriterTag.Span);
            writer.Write(_text);
            writer.RenderEndTag();
        }
    }
La méthode AddAttributesToRender de l'objet Style admet un paramètre du type HtmlTextWriter et permet d'appliquer automatiquement tous les attributs de style qui auront été définis et ce via des appels successifs à la méthode AddStyleAttribute. Ces attributs de style sont appliqués à la première balise créée par la méthode RenderBeginTag de l'objet HtmlTextWriter rencontrée.
Grâce à la classe Style et aux propriétés objets, nous avons donc à notre disposition un mécanisme rapide pour permettre la modification du style de notre contrôle.


IV. La classe WebControl

Une autre classe peut servir de base pour la création de nos contrôles : la classe WebControl.
Cette classe offre les avantages suivants :

Modifions notre contrôle pour hériter de cette classe :

public class HWControl : WebControl
    {
        private string _text = "Hello World !!!";

        public string Text
        {
            get { return _text; }
            set { _text = value; }
        }

        public HWControl() : base(HtmlTextWriterTag.Span) { }

        protected override void RenderContents(HtmlTextWriter writer)
        {
            writer.Write(_text);
        }
    }
On peut remarquer la présence d'un constructeur public qui invoque le constructeur de la classe de base en lui passant le type de contrôle (via une valeur de la classe HtmlTextWriterTag ou via un string (on aurait pu passer "span"). Cet appel au constructeur de la classe de base fait que les balises ouvrantes et fermantes du type passé sont créées automatiquement.
On surcharge alors la méthode RenderContents plutôt que Render. Il ne nous reste plus en effet qu'à créer le contenu situé entre les balises ouvrante et fermante.
Voyons comment utiliser ce contrôle :

<MyCtl:HWControl id="HWCtl" runat="server" Text="Vive Developpez.com !" ForeColor="Red" Font-Size="XX-Large" />

V. Les contrôles composites

Créer un contrôle complexe peut vite s'avérer fastidieux. Plutôt que de se fatiguer à s'occuper de la présentation de tous les éléments de notre contrôle, nous pouvons utiliser la composition de contrôle.
Au lieu de prendre en charge l'intégralité de la génération du code HTML, les contrôles composites présentent leur interface en se reposant sur d'autres contrôles serveur.

La classe Control dont nous héritons pour nos contrôles, présente une propriété Controls du type ControlCollection. Cette collection contiendra tous les contrôles enfants de notre contrôle.
Commençons par écrire le code de base de notre contrôle composite :

using System;
using System.Web;
using System.Web.UI;
using System.Drawing;
using System.Web.UI.WebControls;

namespace Dvp.Controls
{
    public class MonPremierControleComposite : Control
    {
        
    }
}
Voyons maintenant comment lui ajouter des contrôles enfants de façon déclarative :

<%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Default.aspx.cs" Inherits="_Default" %>
<%@ Register TagPrefix="MyCtl" Namespace="Dvp.Controls" Assembly="MesControles" %>

<html>
    <body>
    <form runat="server">
        <MyCtl:MonPremierControle runat="server">
            <asp:button runat="server" Text="Click !" />
            Ceci est un contrôle composite !
        </MyCtl:MonPremierControle>
    </form>
    </body>
</html>
L'exemple suivant montre comment ajouter les mêmes contrôles via le code du contrôle :

using System;
using System.Web;
using System.Web.UI;
using System.Drawing;
using System.Web.UI.WebControls;

namespace Dvp.Controls
{
    public class MonPremierControle : Control
    {
        protected override void CreateChildControls()
        {
            Button _btn = new Button();
            _btn.Text = "Click !!";
            this.Controls.Add(_btn);

            LiteralControl _label = new LiteralControl("Mon premier contrôle composite");
            this.Controls.Add(_label);
        }
    }
}
Que l'on procède de façon déclarative ou via le code revient au final au même. Les différents contrôles sont ajoutés à la collection Controls, et lorsque la méthode Render est appelée sur notre contrôle, elle se charge d'effectuer l'appel à la méthode Render de chacun des contrôles inclus dans la collection afin qu'ils s'occupent de leur présentation, et ainsi de suite de façon récursive.

info Dans le cadre de contrôles ajoutés de façon déclarative, ASP.NET crée automatiquement un contrôle de type "LiteralControl" pour tous les blocs de texte rencontrés.
L'intérêt des contrôles composites n'est évidemment pas de simplement regrouper un ensemble de contrôles sous un seul élément mais plutôt d'offrir un mécanisme simple permettant de réaliser des contrôles complexes en se reposant sur des contrôles existant, tout en décidant de la façon dont ils seront présentés.

Dans un contrôle composite, la méthode CreateChildControls a pour rôle de créer les différents contrôles et de les ajouter à la collection Controls du contrôle parent. Cette méthode peut être invoquée à différents moments du cycle de création d'une page (au contraire des méthodes OnLoad, OnInit, ... qui sont toujours appelées à un moment déterminé) mais ne sera appelée qu'une seule fois à moins de positionner explicitement la propriété ChildControlsCreated à false. Toutefois, si vous souhaitez forcer la création des contrôles enfant à un moment précis, cela peut être fait par un appel de la méthode EnsureChildControls().
La composition de contrôles permet donc de créer rapidement des contrôles complexes en délégant une partie du rendu aux contrôles enfants.
Voici un exemple de contrôle composite qui va afficher une grille de textboxes :

namespace Dvp.Controls
{
    public class MonPremierControle : Control
    {
        private int _nbRow = 4;
        private int _nbCol = 4;

        protected override void CreateChildControls()
        {
            Table _table = new Table();
            for (int i = 0; i &lt; _nbRow; i++)
            {
                TableRow _row = new TableRow();
                for (int j = 0; j &lt; _nbCol; j++)
                {
                    TableCell _cell = new TableCell();
                    TextBox _textbox = new TextBox();
                    _textbox.Text = "Cell" +  i + "," + j;
                    _cell.Controls.Add(_textbox);
                    _row.Cells.Add(_cell);
                }
                _table.Rows.Add(_row);
            }

            this.Controls.Add(_table);
        }

        public int NbRow
        {
            get { return _nbRow; }
            set { _nbRow = value; }
        }

        public int NbCol
        {
            get { return _nbCol; }
            set { _nbCol = value; }
        }
    }
}
En quelques lignes, nous obtenons une table de textboxes sans même avoir du écrire une seule balise HTML.


VI. Création d'un Textbox

A travers les différentes sections précédentes, nous avons vu comment créer et "dessiner" un contrôle serveur ASP.NET
Il est temps maintenant de voir comment gérer les interactions avec nos contrôles.
Au cours de cette section, nous allons voir comment gérer le PostBack au niveau de notre contrôle ainsi que comment utiliser le ViewState pour maintenir de l'information.

Commençons par créer la base de notre contrôle.

public class MonPremierControle : WebControl
    {
        public MonPremierControle() : base("input")
        {}

        protected override void AddAttributesToRender(HtmlTextWriter writer)
        {
            base.AddAttributesToRender(writer);
            writer.AddAttribute(HtmlTextWriterAttribute.Type, "input");
            writer.AddAttribute(HtmlTextWriterAttribute.Name, this.UniqueID);
        }
    }
On se sert du constructeur de base en lui passant le type de contrôle que l'on souhaite créer. Ensuite, on surcharge la méthode AddAttributesToRender afin de préciser le type de notre contrôle (ici, input) et son attribut Name. L'attribut Name est obligatoire pour pouvoir utiliser les données du PostBack.

Ce que l'on peut directement remarquer si on utilise notre contrôle tel quel, c'est que lors d'un postback, le contenu est réinitialisé.
Nous allons donc commencer par ajouter un attribut value à notre contrôle afin de pouvoir en spécifier le contenu. Nous allons ensuite nous occuper de récolter les données PostBack afin de pouvoir conserver la valeur de notre contrôle entre deux aller-retour sur le serveur.
Pour cela, nous allons implémenter l'interface IPostBackDataHandler dans notre contrôle.
Voici donc à quoi ressemble maintenant notre contrôle :

public class MonPremierControle : WebControl, IPostBackDataHandler
    {
        private string _value = null;
        
        public MonPremierControle() : base("input")
        {}

        protected override void AddAttributesToRender(HtmlTextWriter writer)
        {
            base.AddAttributesToRender(writer);
            writer.AddAttribute(HtmlTextWriterAttribute.Type, "input");
            writer.AddAttribute(HtmlTextWriterAttribute.Name, this.UniqueID);
            if (!string.IsNullOrEmpty(_value))
            {
                writer.AddAttribute(HtmlTextWriterAttribute.Value, _value);
            }
        }

        public string Value
        {
            get { return _value; }
            set { _value = value; }
        }

        #region IPostBackDataHandler Membres

        public bool LoadPostData(string postDataKey, System.Collections.Specialized.NameValueCollection postCollection)
        {
            _value = postCollection[postDataKey];
            return false;
        }

        public void RaisePostDataChangedEvent()
        {
            
        }

        #endregion
    }
La méthode qui nous intéresse ici est la méthode LoadPostData. En premier paramètre, cette méthode reçoit la clé à utiliser pour récupérer les données de PostBack de notre contrôle dans la collection postCollection. Cette collection, quant à elle, contient toutes les données PostBack des contrôles présents dans la page.
La méthode RaisePostDataChangedEvent pour sa part, n'est appelée que si la méthode LoadPostData renvoie true. C'est dans cette méthode que nous pouvons faire déclencher des événements relatifs aux données postback de notre contrôle (par exemple, lorsque le texte change).
Il serait légitime de se demander pourquoi ne pas directement publier les événements dans la méthode LoadPostData. En fait, la méthode RaisePostDataChangedEvent n'est appelée que lorsque la méthode LoadPostData a été appelée pour tous les contrôles. Cela garantit donc que tous les contrôles on effectivement reçu leur données postback au moment ou les événements sont déclenchés.
Nous allons maintenant utiliser la méthode RaisePostDataChangeEvent afin de publier un événement signalant que le texte à changé.
Notre contrôle ressemble à ceci :

public class MonPremierControle : WebControl, IPostBackDataHandler
    {
        public MonPremierControle() : base("input")
        {}

        protected override void AddAttributesToRender(HtmlTextWriter writer)
        {
            base.AddAttributesToRender(writer);
            writer.AddAttribute(HtmlTextWriterAttribute.Type, "input");
            writer.AddAttribute(HtmlTextWriterAttribute.Name, this.UniqueID);
            if (!string.IsNullOrEmpty(Text))
            {
                writer.AddAttribute(HtmlTextWriterAttribute.Value, this.Text);
            }
        }

        #region IPostBackDataHandler Membres

        public bool LoadPostData(string postDataKey, System.Collections.Specialized.NameValueCollection postCollection)
        {
            bool _fireEvent = false;
            if(this.Text != postCollection[postDataKey])
            {
                _fireEvent = true;
            }
            this.Text = postCollection[postDataKey];
            return _fireEvent;
        }

        public void RaisePostDataChangedEvent()
        {
            if (TextChanged != null)
            {
                TextChanged(this, EventArgs.Empty);
            }
        }

        #endregion

        public event EventHandler TextChanged;

        public string Text
        {
            get
            {
                if (ViewState["text"] != null)
                {
                    return (string)ViewState["text"];
                }

                return string.Empty;
            }
            set { ViewState["text"] = value; }
        }
    }
Pour ne déclencher l'événement que si le texte est effectivement modifié, il nous faut conserver la valeur précédent le postback et la comparer à la valeur postée.
Nous utilisons pour ce faire le ViewState via la propriété Text. Nous pouvons ainsi comparer les deux valeurs avant de déclencher l'événement si un changement a bien été détecté.
Le ViewState, pour faire simple, est un mécanisme qui permet de stocker dans la page même une série d'informations. Ces informations sont codées et sérialisées sous la forme d'un champ caché de la page. Il suffit de visualiser le code source d'une page ASP.NET depuis un navigateur pour y trouver un champ de type hidden dont l'id est __VIEWSTATE.

Par défaut, seuls les contrôle de type "button" déclenchent un PostBack mais il est possible de reproduire ce comportement pour n'importe quel contrôle et ce de façon assez simple. Premièrement, il faut implémenter l'interface IPostBackEventHandler et donc la méthode RaisePostBackEvent(string args). Deuxièmement, il faut ajouter au rendu HTML de notre contrôle le code JavaScript nécessaire pour provoquer le PostBack. On utilise pour cela la méthode GetPostBackEventReference.
Cette méthode attend deux paramètres. Le premier est le contrôle à la source du PostBack, le second est l'argument qui sera transmis lors du PostBack. Voyons comment tout cela s'organise en créant un contrôle contenant une date que nous pouvons incrémenter ou décrémenter d'un jour grâce à deux liens.

namespace Dvp.Controls
{
    public class MonPremierControle : Control, IPostBackEventHandler
    {
        public DateTime Date
        {
            get
            {
                if (ViewState["Date"] != null)
                {
                    return (DateTime)ViewState["Date"];
                }
                return DateTime.Now;
            }

            set { ViewState["Date"] = value; }
        }

        #region IPostBackEventHandler Membres

        public void RaisePostBackEvent(string eventArgument)
        {
            if (eventArgument.Equals("Ajout"))
            {
                Date = Date.AddDays(1);
            }

            if (eventArgument.Equals("Retrait"))
            {
                Date = Date.AddDays(-1);
            }
        }

        #endregion

        protected override void Render(HtmlTextWriter writer)
        {
            writer.Write("Nous sommes le : " + Date.ToShortDateString());
            writer.Write("  <a href=\"javascript:" + Page.ClientScript.GetPostBackEventReference(this, "Ajout") + "\">Ajouter un jour</a>" );
            writer.Write("  <a href=\"javascript:" + Page.ClientScript.GetPostBackEventReference(this, "Retrait") + "\">Retirer un jour</a>");
        }
    }
}
Notre contrôle, une fois rendu, se compose de la date du jour et de deux liens auxquels nous faisons déclencher des PostBack lorsqu'ils sont cliqués par l'utilisateur. Il nous est dès lors possible, via ce mécanisme, de faire déclencher un PostBack à partir de n'importe quel événement javascript.


VII. Conclusion

A travers différents exemples nous venons de voir les fondements de la création de contrôles sous ASP.NET. Nous avons vu comment interférer sur la façon dont le contrôle est "dessiné" ainsi que sur ses propriétés. Nous avons également vu comment créer des contrôles complexes en se reposant sur des contrôles existant. Pour terminer, nous avons aussi vu comment gérer les données de PostBack et comment faire générer un PostBack à notre contrôle.

Dans un prochain article, nous verrons comment créer des contrôles utilisant des templates pour leur rendu et comment créer des contrôles Bindables.



Valid XHTML 1.1!Valid CSS!

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 © 2008 Rémy MAINIL. 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.

Responsable bénévole de la rubrique Microsoft DotNET : Hinault Romaric -