Accueil Nos publications Blog Comment créer une extension HtmlHelper qui permet de créer un containeur HTML ?

Comment créer une extension HtmlHelper qui permet de créer un containeur HTML ?

ASP.NETASP.net MVC est un framework extensible, de nombreuses possibilités permettent de l’adapter à votre façon de travailler.

Combien d’entre nous se sont un jour dit : « c’est dommage il manque un helper à Razor qui me permettrait d’ajouter facilement un fieldset (ou tout autre contrôle html manquant), vivement que Microsoft y pense ! ». Et pourquoi ne nous le ferions pas nous même ? Après tout ASP.net MVC nous propose tous les outils nécessaires pour le faire ! non ?

Commençons par le basique !

La création d’une extension, c’est avant tout la création d’une méthode d’extension pour la classe HtmlHelper de l’espace de noms System.Web.Mvc.

Voici un exemple de principe :


public static HtmlString HtmlComment(this HtmlHelper html, string text) {
    return new HtmlString(string.Format("<!-- {0} -->", text));
}

Le mot clé this dans la signature de cette méthode indique au compilateur que nous souhaitons étendre les possibilités de la classe HtmlHelper. Pour en savoir plus, n’hésitez pas à consulter MSDN.

Qui s’utilisera dans notre vue de cette manière :

@Html.HtmlComment("voici un commentaire html")

Et donnera le résultat suivant :

<!-- voici un commentaire html -->

Voilà… tout simplement.

Allons un peu plus loin !

Alors bien sûr cet exemple ne sert à rien, mais il permet de vous exposer simplement le principe !

Allons maintenant un peu plus loin… imaginons que nous voulions créer un helper qui puisse ajouter ce contrôle

avec sa légende et son tag fermant !

J’aimerai obtenir quelque chose comme cela :


<fieldset class="testFieldset" id="fiedset1">
   <legend>ma légende</legend>
   du contenu...
</fieldset>

Et idéalement, de pouvoir l’obtenir en tapant le code suivant :


@using (Html.BeginFieldset(legend: "ma légende", id: "fiedset1", cssClass: "testFieldset"))
{
    <text>
        du contenu...
    </text>
}

Pourquoi ce code ? Et bien il ressemble à un comportement que je connais : la création d’un formulaire.


@using (Html.BeginForm()) {
   // du code
}

On comprend bien dans cette exemple que tout ce qui se trouve à l’intérieur des accolades se retrouvera à l’intérieur des balises ouvrante et fermante de notre contrôle.

Pour cela, nous allons ajouter une méthode d’extension à HtmlHelper comme nous l’avons fait précédemment, mais plutôt que de retourner un objet HtmlString, nous allons instancier un objet personnalisé qui implémente l’interface IDisposable.
Pourquoi implémenter IDisposable ?
Bien, c’est très simple : son comportement est très appréciable puisque au moment de détruire l’objet, à la sortie du using, la méthode Dispose est automatiquement appelée et c’est justement à ce moment que nous voulons fermer la balise.


public static MvcFieldset BeginFieldset(
         this HtmlHelper html,
         string legend = "",
         string id = "",
         string cssClass = "",
         object htmlAttributes = null)
{
    MvcFieldset result = new MvcFieldset(html.ViewContext)
    {
        Id = id,
        Legend = legend,
        CssClass = cssClass,
        HtmlAttributes = new RouteValueDictionary(htmlAttributes)
    };
    result.Render();
    return result;
}

Voici un exemple d’extension assez complet puisqu’on peut lui donner l’id, la class CSS, la légende et des attributs html.

Allons voir maintenant ce que contient la classe MvcFieldset !

Pour commencer, elle contient les 4 propriétés dont nous avons besoin :


public string Id { get; set; }
public string Legend { get; set; }
public string CssClass { get; set; }
public IDictionary<string, object> HtmlAttributes { get; set; }

le constructeur :


public MvcFieldset(ViewContext viewContext)
{
    _writer = viewContext.Writer;
    tagBuilder = new TagBuilder("fieldset");
}

qui récupère le writer du ViewContext courant et instancie un TagBuilder pour notre fieldset.

Puis une méthode Render pour créer, générer, le code html


public virtual void Render()
{
    if (!string.IsNullOrEmpty(Id))
    {
        tagBuilder.GenerateId(Id);
    }

    if (!string.IsNullOrEmpty(CssClass))
    {
        tagBuilder.AddCssClass(CssClass);
    }

    if (HtmlAttributes != null)
    {
        tagBuilder.MergeAttributes<string, object>(HtmlAttributes);
    }

    _writer.WriteLine(tagBuilder.ToString(TagRenderMode.StartTag));

    RenderLegend();
}

private void RenderLegend()
{
    if (!string.IsNullOrEmpty(Legend))
    {
        TagBuilder legendTb = new TagBuilder("legend");
        _writer.Write(legendTb.ToString(TagRenderMode.StartTag));
        _writer.Write(Legend);
        _writer.Write(legendTb.ToString(TagRenderMode.EndTag));
    }
}

Puis pour terminer la méthode Dispose de l’interface IDisposable et une version locale « protégée » pour éviter qu’elle soit appelée plusieurs fois.


public void Dispose()
{
    this.Dispose(true);
    GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing) {
    if (!this._disposed) {
        this._disposed = true;

        this._writer.WriteLine(tagBuilder.ToString(TagRenderMode.EndTag));
    }
}

L’appel à GC.SuppressFinalize(false) n’est pas nécessaire mais indique au framework qu’il n’y a aucune ressource système à supprimer de la mémoire, il ne faut donc pas appeler le destructeur de la classe.

Voilà, nous avons terminé, nous pouvons utiliser notre helper pour créer facilement des fieldsets.

Je vous invite à vous monter votre propre bibliothèque de helpers : vous verrez qu’on a vite fait de faciliter la lecture des vues en augmentant la réutilisabilité.

Voici donc l’intégralité du code de l’article :


    public class MvcFieldset : IDisposable
    {
        private bool _disposed = false;
        private readonly TextWriter _writer;
        private TagBuilder tagBuilder;

        public string Id { get; set; }
        public string Legend { get; set; }
        public string CssClass { get; set; }
        public IDictionary<string, object> HtmlAttributes { get; set; }

        public MvcFieldset(ViewContext viewContext)
        {
            _writer = viewContext.Writer;
            tagBuilder = new TagBuilder("fieldset");
        }

        public virtual void Render()
        {
            if (!string.IsNullOrEmpty(Id))
            {
                tagBuilder.GenerateId(Id);
            }

            if (!string.IsNullOrEmpty(CssClass))
            {
                tagBuilder.AddCssClass(CssClass);
            }

            if (HtmlAttributes != null)
            {
                tagBuilder.MergeAttributes<string, object>(HtmlAttributes);
            }

            _writer.WriteLine(tagBuilder.ToString(TagRenderMode.StartTag));

            RenderLegend();
        }

        private void RenderLegend()
        {
            if (!string.IsNullOrEmpty(Legend))
            {
                TagBuilder legendTb = new TagBuilder("legend");
                _writer.Write(legendTb.ToString(TagRenderMode.StartTag));
                _writer.Write(Legend);
                _writer.Write(legendTb.ToString(TagRenderMode.EndTag));
            }
        }

        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!this._disposed)
            {
                this._disposed = true;

                this._writer.WriteLine(tagBuilder.ToString(TagRenderMode.EndTag));
            }
        }

        public void EndFieldset()
        {
            this.Dispose(true);
        }
    }

    public static class HtmlHelpers
    {
        public static MvcFieldset BeginFieldset(
                  this HtmlHelper html,
                  string legend = "",
                  string id = "",
                  string cssClass = "",
                  object htmlAttributes = null)
        {
            MvcFieldset result = new MvcFieldset(html.ViewContext)
            {
                Id = id,
                Legend = legend,
                CssClass = cssClass,
                HtmlAttributes = new RouteValueDictionary(htmlAttributes)
            };
            result.Render();
            return result;
        }

        public static void EndFieldset(this HtmlHelper html)
        {
            html.ViewContext.Writer.Write("</fieldset>");
        }
    }