Accueil Nos publications Blog [ASP.NET MVC 6] View Component

[ASP.NET MVC 6] View Component

ASP.NET logoLa notion de View Component est nouvelle avec ASP.NET MVC 6. Dans ce billet, je vous propose une introduction à ce concept, et notamment un comparatif par rapport à la notion de Child Action que les développeurs ASP.NET MVC pouvaient utiliser jusqu’à maintenant.

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

ASP.NET MVC 5 et les Child Actions

Tout d’abord, beaucoup de développeurs ASP.NET MVC ne connaissent pas le mécanisme des childs actions, ou l’utilise de manière directe ou détournée sans savoir à quels concepts théoriques cela est rattaché.

Avant de parler de child action, revenons sur un autre concept d’ASP.NET MVC, les vues partielles. Pour mémoire, une vue partielle est tout simplement une vue qui n’utilise pas de Layout. Généralement elle permet de diviser le contenu d’une vue trop complexe en plusieurs sous-vues aux domaines fonctionnels différents. Elle permet également de créer des éléments visuels réutilisables.

Ainsi, on utilise généralement des vues partielles pour les éléments suivants :

  • Un menu statique ;
  • Un formulaire ou une partie d’un formulaire ;
  • La représentation d’un modèle (ex. une vue partiellement typée pour afficher une adresse postale).

Exemple, avec le modèle de projet ASP.NET MVC 5. le fichier Views/Shared/_LoginPartial.cshtml est une vue partielle.

@using Microsoft.AspNet.Identity
@if (Request.IsAuthenticated)
{
    using (Html.BeginForm("LogOff", "Account", FormMethod.Post, new { id = "logoutForm", @class = "navbar-right" }))
    {
    @Html.AntiForgeryToken()

    <ul class="nav navbar-nav navbar-right">
        <li>
            @Html.ActionLink("Hello " + User.Identity.GetUserName() + "!", "Index", "Manage", routeValues: null, htmlAttributes: new { title = "Manage" })
        </li>
        <li><a href="javascript:document.getElementById('logoutForm').submit()">Log off</a></li>
    </ul>
    }
}
else
{
    <ul class="nav navbar-nav navbar-right">
        <li>@Html.ActionLink("Register", "Register", "Account", routeValues: null, htmlAttributes: new { id = "registerLink" })</li>
        <li>@Html.ActionLink("Log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })</li>
    </ul>
}

Elle est appelée depuis une div dans le fichier Views/Shared/_Layout.cshtml.

<div class="navbar-collapse collapse">
    <ul class="nav navbar-nav">
        <li>@Html.ActionLink("Home", "Index", "Home")</li>
        <li>@Html.ActionLink("About", "About", "Home")</li>
        <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
    </ul>
    @Html.Partial("_LoginPartial")
</div>

La child action est une vue partielle qui est en mesure de pré-calculer son propre modèle. Cela signifie que l’appelant d’une child action n’est pas obligé de lui fournir un modèle en entrée. Elle sera autonome dans son exécution.

Les applications d’une child action se trouvent souvent parmi les suivantes :

  • Un menu dynamique – les actions affichées peuvent alors être conditionnées selon les droits de l’utilisateur courant ;
  • Une sidebar, qui peut contenir une liste d’articles récents ;
  • Une div de login, qui contient un formulaire de connexion si l’utilisateur courant est anonyme ou alors un récapitulatif de l’identité de l’utilisateur courant.

Avant ASP.NET MVC 6, les développeurs de Microsoft avaient choisi d’implémenter cette fonctionnalité sous la forme d’un couple action-controller classique. L’exécution de l’action se faisant alors d’une manière similaire – en aspect – à l’exécution d’une action classique.

Je peux transformer l’exemple précédent en le faisant devenir une child action. Ici, je vais enlever le peu de logique qui était présent dans la vue et je vais la passer dans le traitement de l’action.

public class PartialLoginViewModel
{
    public bool IsAuthenticated { get; set; }
    public string UserName { get; set; }
}

public class CommonController : Controller
{
    public PartialViewResult LoginPartial()
    {
        var partialLoginViewModel = new PartialLoginViewModel()
        {
            IsAuthenticated = Request.IsAuthenticated,
            UserName = User.Identity.GetUserName()
        };

        return PartialView("_PartialView", partialLoginViewModel);
    }
}

La vue devient la suivante.

@model WebApplication20.Controllers.PartialLoginViewModel

@if (Model.IsAuthenticated)
{
    using (Html.BeginForm("LogOff", "Account", FormMethod.Post, new { id = "logoutForm", @class = "navbar-right" }))
    {
    @Html.AntiForgeryToken()

    <ul class="nav navbar-nav navbar-right">
        <li>
            @Html.ActionLink("Hello " + Model.UserName + "!", "Index", "Manage", routeValues: null, htmlAttributes: new { title = "Manage" })
        </li>
        <li><a href="javascript:document.getElementById('logoutForm').submit()">Log off</a></li>
    </ul>
    }
}
else
{
    <ul class="nav navbar-nav navbar-right">
        <li>@Html.ActionLink("Register", "Register", "Account", routeValues: null, htmlAttributes: new { id = "registerLink" })</li>
        <li>@Html.ActionLink("Log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })</li>
    </ul>
}

Et l’appel depuis le fichier _Layout.cshtml utilise maintenant la méthode Html.Action.

<div class="navbar-collapse collapse">
    <ul class="nav navbar-nav">
        <li>@Html.ActionLink("Home", "Index", "Home")</li>
        <li>@Html.ActionLink("About", "About", "Home")</li>
        <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
    </ul>
    @Html.Action("Common", "LoginPartial")
</div>

Techniquement, tout le cycle de recherche de route, instanciation de contrôleur et exécution de l’action est joué de la même manière. Cependant, en pratique le contexte d’exécution est significativement différent et cela peut entrainer des erreurs de compréhension chez les développeurs.

En effet, les child actions possèdent plusieurs limitations techniques, incompréhensibles au regard des actions MVC traditionnelles. Par exemple, il n’est pas possible de créer une child action tagguée avec le mot clé async. Il n’est pas possible de faire une child action qui utilise un type de résultat RedirectResult – le code compile mais crash à l’exécution.

ASP.NET MVC 6 et les View Components

La notion de View Components apparue avec ASP.NET MVC se veut être la réponse aux problématiques que j’ai énoncée dans cette première partie. Ainsi, il s’agit tout simplement du remplaçant des childs actions, mais dont la conception, pensée radicalement différemment du modèle historique, ne devrait pas perturber les développeurs.

Pour faire simple, on peut écrire un View Component ASP.NET MVC 6 comme on écrit une child action ASP.NET MVC 5. Le View Component est uniquement plus cohérent dans sa forme, là où la child action est complexe à identifier par rapport à une action classique.

Un View Component se compose de deux éléments : une classe qui contient sa logique, et une vue qui décrit l’affichage du composant.

Pour écrire la classe, il existe plusieurs manières de faire que j’ai listées ci-dessous. Attention, dans tous les cas, les mêmes règles de base dans l’écriture d’un contrôleur s’appliquent également ici. Cela signifie que la classe doit être publique et non abstraite.

  • Dériver de la classe ViewComponent ;
  • Décorer le composant avec l’attribut ViewComponentAttribute, ou dériver d’une classe elle-même décorée par l’attribut ViewComponentAttribute ;
  • Suffixer le nom du composant par le terme ViewComponent (ex. MenuViewComponent).

Comme il est possible de créer un View Component qui ne dérive pas de la classe ViewComponent, il est ainsi possible d’en créer un sous la forme d’un POCO. Bien évidemment, pour faciliter l’écriture d’un tel composant et pour bénéficier de nombreuses méthodes utilitaires, l’approche recommandée reste celle où l’on dérive de la classe ViewComponent.

Une fois la classe créée, l’implémentation du traitement se fait au sein d’une méthode Invoke ou InvokeAsync. Le choix de l’une ou l’autre méthode est à faire par le développeur selon son besoin (dépendance sur une autre brique applicative par exemple).

public class MenuViewComponent : ViewComponent
{
    private readonly IMonService _monService;

    public MenuViewComponent(IMonService monService)
    {
        _monService = monService;
    }

    public IViewComponentResult Invoke(int param1, bool param2)
    {
        var menuViewModel = new MenuViewModel();

        return View(menuViewModel);
    }
}

Les méthodes Invoke et InvokeAsync prennent un nombre non déterminé d’arguments. Là encore, le développeur est libre de choisir les arguments dont il a besoin pour son traitement. ASP.NET MVC se charge alors automatiquement de faire correspondre les appels passés lors de l’appel au composant jusqu’à la méthode Invoke.

Le type de retour de cette méthode est une instance de IViewComponentResult. Comme dans l’écriture d’un contrôleur qui dérive de la classe Controller, le fait de dériver de la classe ViewComponent donne accès à plusieurs méthodes utilitaires. Par exemple, la méthode View permet de faire appel aux moteurs de vues afin de localiser la vue la plus adaptée, et de lui passer un modèle. C’est cette méthode View qui va nous donner un résultat de type IViewComponentResult.

Notez que, comme tous les éléments utilisables avec ASP.NET 5, il est possible de recevoir des services par injection dans le constructeur du View Component.

La vue qui décrit l’affichage d’un View Component doit, par défaut, être placée dans un répertoire Components sous le dossier Views/Shared. Ensuite, sous ce dossier Components, il est nécessaire de créer un dossier du nom du View Component. Un peu comme pour les contrôleurs, le suffixe ViewComponent est ignoré lorsque vous faîtes référence à une View Component. Dans le cadre de notre exemple, il est suffisant d’écrire Menu et non MenuViewComponent.

Par défaut, la vue qui sera recherchée par ASP.NET MVC pour afficher votre View Component s’appelle Default.cshmtl. Ainsi, dans le cas de notre exemple, le chemin complet vers cette vue devient donc Views/Shared/Components/Menu/Default.cshtml.

@model MenuViewModel

<div>
    <ul>
        @*
            foreach(var item in Model)
            {
                <li><!-- mon menu --></li>
            }
        *@
    </ul>
</div>

Pour faire appel au composant, il faut utiliser les instructions @Component.Invoke et @Component.InvokeAsync, qui prennent tous les deux un nombre non défini d’arguments. Encore une fois, il n’est pas nécessaire de suffixer le nom du composant avec le terme ViewComponent lors de l’appel à ces méthodes.

<div class="navbar-collapse collapse">
    @Component.Invoke("Menu", 1, true)
    @await Html.PartialAsync("_LoginPartial")
</div>

Ci-dessous, une variante en faisant l’appel de façon asynchrone.

<div class="navbar-collapse collapse">
    @await Component.InvokeAsync("Menu", 1, true)
    @await Html.PartialAsync("_LoginPartial")
</div>

Tout comme le rendu de vues partielles, il existe également des variantes de méthodes dans les cas où l’appel se fait au sein d’un bloc de code et non dans une instruction unique.

<div class="navbar-collapse collapse">
    @{
        await Component.RenderInvokeAsync("Menu", 1, true);
    }
    @await Html.PartialAsync("_LoginPartial")
</div>

Enfin, des variantes sont également proposées pour référencer le composant directement par son type via une méthode générique et non par son nom.

<div class="navbar-collapse collapse">
    @{
        await Component.RenderInvokeAsync<MenuViewComponent>(1, true);
    }
    @await Html.PartialAsync("_LoginPartial")
</div>

Conclusion

ASP.NET MVC permettait déjà la création de composants réutilisables. Cependant, la conception de cette fonctionnalité laissait à désirer, et le représenter sous la forme d’action n’était peut-être pas le choix le plus adapté et le plus clair pour les développeurs. Les View Components sont dédiés à ce besoin, de plus ils apportent de nouvelles fonctionnalités telles que l’injection et le support de l’invocation asynchrone.

A bientôt