La première étape est de créer une nouvelle classe héritant de WebControl. Nous décidons de l'appeler PercentageControl.

[ToolboxData("<{0}:PercentageControl runat=server></{0}:PercentageControl>")]
public class PercentageControl : WebControl
{
}

Il s'agit d'un contrôle "pourcentage" qui est censé nous donner un pourcentage. Nous décidons donc de lui rajouter une propriété renvoyant le pourcentage. Nous définissons également, par attribut, que la "default property" est cette propriété. Nous allons également garder en ViewState cette valeur si elle existe.
 
[DefaultProperty("Value")]
[ToolboxData("<{0}:PercentageControl runat=server></{0}:PercentageControl>")]
public
 class PercentageControl : WebControl
{
    [Bindable(true)]
    [Category("Custom Category")]
    [DefaultValue("")]
    [Description("The percentage value.")]
    [Localizable(false)]
    public double? Value
    {
        get
        {
            var value = this.ViewState["Value"];
            if (value != null)
            {
                return (double)value;
            }
            return default(double?);
        }
        set { this.ViewState["Value"] = value; }
    }
}
 
Nous avons maintenant notre propriété mais nous n'avons encore aucun HTML généré... L'idée est donc maintenant d'implémenter le code C# qui après processing affichera du code HTML.
 
protected override HtmlTextWriterTag TagKey
{
  get { return HtmlTextWriterTag.Input; }
}
protected override void AddAttributesToRender(HtmlTextWriter writer)
{
   base.AddAttributesToRender(writer);
   writer.AddAttribute(HtmlTextWriterAttribute.Type, "text");
   writer.AddAttribute(HtmlTextWriterAttribute.Name, this.UniqueID);
   writer.AddAttribute(HtmlTextWriterAttribute.Value, this.Value.HasValue 
                                                      ? this.Value.ToString() 
                                                      : string.Empty);
}
 
Si nous exécutions ce code dans une page HTML, nous verrions donc une textbox. Mais, si nous ajoutions un bouton ou tout autre contrôle provoquant un PostBack, nous verrions que le contrôle serait chaque fois remis à la valeur enregistrée par défaut. Pour garder le contrôle à jour et pour mettre à jour la propriété "Value", il faut implémenter l'interface IPostBackDataHandler et enregistrer notre contrôle comme supportant le "postback". Via ce mécanisme, nous pouvons par exemple lancer un event pour annoncer que notre pourcentage a été modifié lors du postback.
 
protected override void OnPreRender(System.EventArgs e)
{
   base.OnPreRender(e);
   this.Page.RegisterRequiresPostBack(this);
}

#region IPostBackDataHandler Members
public bool LoadPostData(string postDataKey, NameValueCollection postCollection)
{
   var result = postCollection[this.UniqueID];
   if (string.IsNullOrEmpty(result))
   {
       this.Value = default(double?);
       return !this.Value.HasValue;
   }
   double newValue;
   if (double.TryParse(result, out newValue))
   {
      bool hasChanged = this.Value != newValue;
      this.Value = newValue;
      return hasChanged;
   }
   return false;
}

public void RaisePostDataChangedEvent()
{
}
#endregion
 
Nous obtenons un contrôle fonctionnel. Il ne nous reste plus qu'à l'utiliser. Afin d'améliorer un petit peu le look and feel pour nous développeurs, nous pouvons ajouter une icône qui sera visible dans la toolbox de Visual Studio. Nous ajoutons une image en "embedded resource" et nous ajoutons un attribut sur notre classe qui pointe vers ce fichier.
 
[ToolboxBitmap(typeof(PercentageControl), "Percentage.bmp")]
 
 
Voici le fichier final:  
 
[DefaultProperty("Value")]
[ToolboxData("<{0}:PercentageControl runat=server></{0}:PercentageControl>")]
[ToolboxBitmap(typeof(PercentageControl), "Percentage.bmp")]
public class PercentageControl : WebControlIPostBackDataHandler
{
    [Bindable(true)]
    [Category("Custom Category")]
    [DefaultValue("")]
    [Description("The percentage value.")]
    [Localizable(false)]
    public double? Value
    {
        get
        {
            var value = this.ViewState["Value"];
            if (value != null)
            {
                return (double)value;
            }
            return default(double?);
        }
        set { this.ViewState["Value"] = value; }
    }
    protected override HtmlTextWriterTag TagKey
    {
        get { return HtmlTextWriterTag.Input; }
    }
    protected override void AddAttributesToRender(HtmlTextWriter writer)
    {
        base.AddAttributesToRender(writer);
        writer.AddAttribute(HtmlTextWriterAttribute.Type, "text");
        writer.AddAttribute(HtmlTextWriterAttribute.Name, this.UniqueID);
        writer.AddAttribute(HtmlTextWriterAttribute.Value, this.Value.HasValue 
                                                            ? this.Value.ToString() 
                                                            : string.Empty);
    }
    protected override void OnPreRender(System.EventArgs e)
    {
        base.OnPreRender(e);
        this.Page.RegisterRequiresPostBack(this);
    }
    #region IPostBackDataHandler Members
    public bool LoadPostData(string postDataKey, NameValueCollection postCollection)
    {
        var result = postCollection[this.UniqueID];
        if (string.IsNullOrEmpty(result))
        {
            this.Value = default(double?);
            return !this.Value.HasValue;
        }
        double newValue;
        if (double.TryParse(result, out newValue))
        {
            bool hasChanged = this.Value != newValue;
            this.Value = newValue;
            return hasChanged;
        }
        return false;
    }
    public void RaisePostDataChangedEvent()
    {
    }
    #endregion
}
 
Nous pouvons dès à présent le référencer et l'utiliser dans notre code ASP.NET.
 
<div>
    <Custom:PercentageControl runat="server" Value="200" />
    <asp:Button Text="Click" runat="server" />
</div>