Accueil Nos publications Blog [ASP.NET Core] Ecriture de TagHelpers

[ASP.NET Core] Ecriture de TagHelpers

ASP.NET logoDans ce billet, je vous propose de parler à nouveau du concept des TagHelpers. Aujourd’hui, je souhaite me pencher sur l’écriture de vos propres TagHelpers. Il est probable que le développement d’une application ASP.NET Core, les TagHelpers puissent facilement se substituer à ce qui était fait plus difficilement jusqu’à présent avec des HTML Helpers.

Attention, cet article a été écrit à partir de la beta8 d’ASP.NET Core. Les APIs décrites ici sont encore sujettes à changement d’ici la release finale de la plate-forme.

La création d’un TagHelper personnalisé passe par l’écriture d’une classe qui hérite d’un TagHelper existant ou de la classe abstraite TagHelper. Il est également possible de ne pas hériter de cette classe mais plutôt d’implémenter directement l’interface ITagHelper, mais cette façon de faire n’a pas d’intérêt particulier pour la majorité des cas.

Ces deux éléments, la classe abstraite TagHelper et l’interface ITagHelper, proviennent du paquet Microsoft.AspNet.Razor.Runtime. Celui-ci est automatiquement tiré dès lors qu’une référence vers Microsoft.AspNet.Mvc. Il n’est pas nécessaire de référencer le paquet Microsoft.AspNet.Mvc.TagHelpers. Ce dernier contient en fait un ensemble de TagHelpers basiques, mais il ne contient aucun élément nécessaire à l’écriture d’un TagHelper personnalisé.

public class IfTagHelper : TagHelper
{
    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        base.Process(context, output);
    }
}

N’oubliez pas que, pour que le TagHelper soit utilisable, il faut qu’il soit chargé par Razor. Il est donc nécessaire d’ajouter un _ViewImports.cshtml au projet et d’y insérer le contenu ci-dessous. Ici, j’ai fait le choix de découvrir et charger automatiquement tous les TagHelpers de mon assembly WebApplication54.

@addTagHelper "*, WebApplication54"

En l’état, mon TagHelper est automatiquement lié à un tag HTML if. Ce nom est automatiquement déduit à partir du nom de la classe que je viens de créer. Razor va tout simplement prendre tout ce qui est situé avant TagHelper dans le nom de la classe.

Il y a en fait deux types de TagHelpers. Soit ce tag HTML existe et est standard, et dans ce cas je vais étendre son fonctionnement de base, soit il s’agit d’un tag totalement nouveau et qui n’existe pas dans les standards HTML.

Pour mon exemple, je vais donc commencer par prendre ce second exemple en créant un tag sur un élément if. Il est possible de préciser explicitement le nom du tag grâce à l’attribut HtmlTargetElement.

[HtmlTargetElement("if")]
public class IfTagHelper : TagHelper
{
    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        base.Process(context, output);
    }
}

L’implémentation du TagHelper doit ensuite se faire en complétant le corps de la méthode Process. Comme pour la plupart des éléments d’ASP.NET Core, il est également possible d’écrire cette implémentation de manière asynchrone ne complétant la méthode ProcessAsync.

En l’état, si je crée une page Index.cshtml avec le contenu ci-dessous, la méthode Process est bien appelée mais le Tag n’est pas transformé. Il est donc téléchargé tel quel dans le navigateur du client.

<div>
    <if>
        <!-- TODO -->
    </if>
</div>

Notez au passage que la méthode Process est appelée deux fois. Une première fois lorsque l’ouverture du Tag est rencontrée et une seconde fois lorsque sa fermeture est rencontrée.

La méthode Process reçoit deux éléments en paramètres. L’un d’eux, la classe TagHelperOutput permet de contrôler finement ce qui va être émis dans le flux HTML par notre TagHelper.

En changeant la valeur de la propriété TagName par la constante null, je peux simplement annuler l’émission du tag if en sortie. Ainsi, il ne sera jamais téléchargé dans la page HTML reçue par le navigateur d’un client qui consulte mon site.

[HtmlTargetElement("if")]
public class IfTagHelper : TagHelper
{
    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = null;

        base.Process(context, output);
    }
}

Comme tous les éléments faisant partie d’ASP.NET Core, il est possible de recevoir des paramètres via le constructeur de la classe. Ici, j’ai choisi de recevoir une instance de l’interface IHostingEnvironment.

[HtmlTargetElement("if")]
public class IfTagHelper : TagHelper
{
    private readonly IHostingEnvironment _hostingEnvironment;

    public IfTagHelper(IHostingEnvironment hostingEnvironment)
    {
        _hostingEnvironment = hostingEnvironment;
    }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = null;

        base.Process(context, output);
    }
}

Je peux ensuite ajouter une propriété Condition de type booléen. Celle-ci me permet de savoir si je dois émettre ou non le contenu de mon tag dans le flux HTML en sortie. La méthode SuppressOutput me permet de simplement effacer le contenu et d’annuler le fait qu’il soit automatiquement écrit sur le flux en sortie.

[HtmlTargetElement("if")]
public class IfTagHelper : TagHelper
{
    private readonly IHostingEnvironment _hostingEnvironment;

    public bool Condition { get; set; }

    public IfTagHelper(IHostingEnvironment hostingEnvironment)
    {
        _hostingEnvironment = hostingEnvironment;
    }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = null;

        if(!Condition)
        {
            output.SuppressOutput();
        }

        base.Process(context, output);
    }
}

L’usage de mon TagHelper est alors le suivant.

<div>
    <if condition="true">
        <!-- TODO -->
    </if>
</div>

Grâce à l’attribut HtmlAttributeName, je peux spécifier un nom d’attribut qui serait éventuellement différent du nom de la propriété.

[HtmlTargetElement("if")]
public class IfTagHelper : TagHelper
{
    private readonly IHostingEnvironment _hostingEnvironment;

    [HtmlAttributeName("its")]
    public bool Condition { get; set; }

    public IfTagHelper(IHostingEnvironment hostingEnvironment)
    {
        _hostingEnvironment = hostingEnvironment;
    }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = null;

        if(!Condition)
        {
            output.SuppressOutput();
        }

        base.Process(context, output);
    }
}

L’usage de mon TagHelper devient alors le suivant.

<div>
    <if its="true">
        <!-- TODO -->
    </if>
</div>

Il est également possible d’émettre une structure HTML plus complexe à partir d’une TagHelper. Dans l’exemple suivant, j’ai écrit un TagHelper qui permet d’afficher rapidement quelques informations sur l’utilisateur actuellement connecté. Comme pour les HTML Helpers classiques d’ASP.NET MVC, la classe TagBuilder est alors utilisable pour générer du contenu HTML.

Dans l’exemple suivant, j’ai également précisé via la propriété TagStructure que mon tag ne peut être utilisé que sous une forme où il ne contient pas de tag enfant. Cela me permet de m’assurer que le développeur qui consomme mon tag ne l’utilise pas en y encapsulant d’autres balises HTML, car c’est un scénario que je ne supporte pas dans mon implémentation de la méthode Process.

[HtmlTargetElement("user", TagStructure = TagStructure.WithoutEndTag)]
public class UserTagHelper : TagHelper
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly IStringLocalizer<UserTagHelper> _userTagHelperStringLocalizer;

    public UserTagHelper(IHttpContextAccessor httpContextAccessor,
                         IStringLocalizer<UserTagHelper> userTagHelperStringLocalizer)
    {
        _httpContextAccessor = httpContextAccessor;
        _userTagHelperStringLocalizer = userTagHelperStringLocalizer;
    }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName= null;

        var tagBuilder = new TagBuilder("span");

        var userAuthenticationFeature = _httpContextAccessor.HttpContext.Features.Get<IUserAuthenticationFeature>();
        var user = userAuthenticationFeature.User;

        var userName = $"{_userTagHelperStringLocalizer["Hello"]} {user.FirstName} {user.LastName}";

        tagBuilder.InnerHtml.Append(userName);

        output.Content.SetContent(tagBuilder);

        base.Process(context, output);
    }
}

L’usage de mon Tag serait alors le suivant.

<div>
    <user />
</div>

Et le rendu chez le client, serait le suivant.

<div>
    <span>Bonjour Léonard Labat</span>
</div>

Pour générer des éléments plus complexes (liste radio, select, etc.), il est également possible de recevoir par injection une implémentation de l’interface IHtmlGenerator.

Le TagHelper ci-dessous est une illustration de la modification d’un tag existant. Ici, j’ai écrit un petit mécanisme qui me permet de modifier la source d’une image. Mon TagHelper ne se déclenchera que si les attributs hash-src et hash-src-salt sont présents sur un tag img. Pour cela, j’utilise la propriété Attributes de l’attribut HtmlTargetElement. La liste des attributs nécessaires au fonctionnement de mon TagHelper se spécifient alors sous la forme d’une chaîne de caractères où la virgule est le séparateur.

Notez également qu’il n’est pas nécessaire d’ajouter une propriété correspondant à l’attribut hash-src. Seuls les attributs pour lesquels je veux récupérer une valeur nécessitent une propriété sur la classe de mon TagHelper.

Notez que je peux manipuler la collection Attributes de l’instance de TagHelperOutput pour retirer mes attributs personnalisés mais également pour modifier le contenu de l’attribut standard src.

[HtmlTargetElement("img", Attributes = "hash-src, hash-src-salt")]
public class HashImageSrcTagHelper : TagHelper
{
    [HtmlAttributeName("hash-src-salt")]
    public string Salt { get; set; }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.Attributes.RemoveAll("hash-src-salt");
        output.Attributes.RemoveAll("hash-src");

        using (var sha = SHA256.Create())
        {
            var bytes  = Encoding.UTF8.GetBytes(output.Attributes["src"] + Salt);
            output.Attributes["src"] = Convert.ToBase64String(bytes);
        }

        base.Process(context, output);
    }
}

Le contenu de ma vue Index.cshtml.

<div>
    <img src="test" hash-src hash-src-salt="@Guid.NewGuid().ToString()" />
</div>

Et enfin le contenu téléchargé chez le client.

<div>
    <img src="TWljcm9zb2Z0LkFzcE5ldC5SYXpvci5SdW50aW1lLlRhZ0hlbHBlcnMuVGFnSGVscGVyQXR0cmlidXRlZWQwNGYwOTAtYjA4OS00N2MwLThlN2EtZWZmN2JkY2M1NWUz" />
</div>

Voila qui conclut mon cycle de billets sur les TagHelpers. A mon sens, il s’agit vraiment de la nouvelle fonctionnalité d’ASP.NET MVC 6 qui devrait marquer un tournant dans l’écriture de vues avec Razor.