[ASP.NET MVC] Ces petites choses de Razor que l’on ignore … (3/4)
Dans ce troisième billet, nous allons utiliser les éléments découverts précédemment afin de personnaliser notre utilisation des template Razor dans un contexte ASP.NET MVC. |
Usage personnalisé de Razor pour ASP.NET MVC
Etendre la classe de base des template
Généralement, la personnalisation la plus courante de Razor dans un contexte ASP.NET MVC consiste à ajouter des méthodes d’extension sur la classe HtmlHelper
ou sur la classe UrlHelper
.
namespace System.Web.Mvc.Html
{
public static class HtmlExtensions
{
public static MvcHtmlString CalendarFor<TModel>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, DateTime>> expression)
{
throw new NotImplementedException();
}
}
}
La méthode d’extension ajoutée sur la classe HtmlHelper
se comporte alors comme un contrôle que nous pourrions choisir d’utiliser dans une vue.
@Html.CalendarFor(m => m.MaDate)
Je reparlerai de l’écriture d’une méthode d’extension dont le but est de générer un contrôle HTML dans le prochain billet de ce blog. Il y a beaucoup de choses intéressantes à dire à ce sujet mais ce n’est pas le point technique que je veux aborder ici.
En termes de conception, ajouter une méthode d’extension sur la classe HtmlHelper
est discutable si cette méthode n’a pas pour but de générer du HTML. Prenons l’exemple d’une méthode chargée de vérifier la valeur d’un switch de configuration. Il est tout à fait possible d’écrire la méthode ci-dessous.
@if (Html.IsSwitchEnabled("MySwitch"))
{
<p>Le switch est activé !</p>
}
Or, conceptuellement parlant, enrichir la classe HtmlHelper
dans ce scénario précis est discutable. La méthode devrait plutôt être directement disponible dans la vue, sans passer par l’intermédiaire de la propriété Html
. L’exemple ci-dessous illustre ce premier cas de figure.
@if (IsSwitchEnabled("MySwitch"))
{
<p>Le switch est activé !</p>
}
Ou alors, nous pouvons aussi imaginer avoir une propriété Configuration
, d’un type propre à notre application, qui contiendrait la méthode IsSwitchEnabled
. L’exemple ci-dessous illustre ce second cas de figure.
@if (Configuration.IsSwitchEnabled("MySwitch"))
{
<p>Le switch est activé !</p>
}
Grâce à ce que nous avons découvert dans les deux précédents billets de cette série, ce genre de modification est très simple à mettre en place. Nous avons distingué deux cas de figure :
- L’ajout d’une méthode directement accessible dans le code d’une vue ;
- L’ajout d’une propriété directement accessible dans le code d’une vue.
Ces deux cas de figure peuvent être implémentés en personnalisant la classe de base utilisée par les vues. Dans une application ASP.NET MVC, cette classe de base se nomme WebViewPage<TModel>
, l’argument TModel
étant par défaut de type dynamic
si aucune instruction @model
n’est présente dans la vue.
En dérivant de la classe WebViewPage<TModel>
, il ne reste plus qu’à créer la méthode IsSwitchEnabled
. Dans l’exemple ci-dessous, je fais appel à une implémentation de IConfigurationService
, qui représente mon gestionnaire de configuration. Pour la récupérer, je ne peux pas compter sur l’injection par le constructeur car, Razor serait incapable d’instancier dynamiquement les classes qu’il génère pour mes vues, je fais donc appel au DependencyResolver
. Ce n’est pas parfait, mais, si je veux contourner ce problème et récupérer des instances pour mes vues depuis un conteneur IoC, il faudrait réinventer la roue. L’inconvénient d’une telle approche se situe dans l’écriture de tests unitaires de vues, j’y reviendrai dans le prochain billet.
public abstract class CustomWebViewPage<TModel> : WebViewPage<TModel>
{
private readonly IConfigurationService _configurationService;
protected CustomWebViewPage()
{
_configurationService = DependencyResolver.Current.GetService<IConfigurationService>();
}
protected bool IsSwitchEnabled(string switchName)
{
return _configurationService.IsSwitchEnabled(switchName);
}
}
Il faut ensuite aller dans le fichier web.config de l’application pour remplacer le type à utiliser comme classe de base pour les vues ; souvenez-vous, il s’agit de la propriété pageBaseType
du nœud pages. Dans mon cas, puisque nous devons renseigner le nom complet de notre type, cela correspond à WebApplication3.CustomWebViewPage
.
<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<pages pageBaseType="WebApplication3.CustomWebViewPage">
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Optimization"/>
<add namespace="System.Web.Routing" />
<add namespace="WebApplication3" />
</namespaces>
</pages>
</system.web.webPages.razor>
- Attention, si le type de la classe de base à utiliser pour les vues est issu d’une autre assembly, il faudra bien faire attention à fournir le nom vraiment complet du type (avec l’information sur le nom de l’assembly en question, son public key token si besoin, etc.).
- Attention également aux fichiers de configuration en cascade ! Si vous choisissez de modifier la configuration de Razor, prenez garde à la portée de la configuration : soit vous le faites en global et assumez que toutes les vues de l’application sont impactées par vos modifications, soit vous le faites plus localement pour un contrôleur ou pour une area par exemple. Vous utiliserez alors un fichier de configuration allégé ne contenant que le nœud de configuration de Razor placé uniquement au niveau du répertoire désiré.
Après avoir effectué une compilation de l’application, vous pouvez retourner dans une des vues et constater que la méthode IsSwitchEnabled
est bel et bien disponible et proposée par l’IntelliSense.
Vous remarquerez également qu’au runtime, Razor fait bien appel à votre méthode et utilise sa valeur de retour et donc, que le rendu est bien celui attendu.
Un peu plus tôt dans ce billet, j’évoquais la possibilité d’enrichir la classe de base d’une propriété permettant d’accéder à un objet qui représente toute la configuration de l’application. Je ne vais pas rentrer dans le détail de cette implémentation, mais vous comprendrez bien qu’il s’agit uniquement d’ajouter une propriété sur notre classe CustomerWebViewPage
comme nous venons de le faire pour la méthode IsSwitchEnabled
.
Enrichir la collection d’espaces de noms
Dans la première partie de ce billet, j’ai présenté une méthode d’extension utilisable dans nos vues par le biais de l’objet HtmlHelper
. Pour pouvoir faire appel à cette méthode, il y a deux possibilités :
- Soit en plaçant la méthode d’extension dans un espace de noms propre à MVC et donc, déjà référencé par Razor. C’est la solution utilisée dans la première partie de ce billet et que je privilégie généralement dans mes projets de développement. Pour mémoire, par défaut, la liste des espaces de noms présents est la suivante.
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Optimization"/>
<add namespace="System.Web.Routing" />
</namespaces>
- Soit en plaçant la méthode d’extension dans un espace de noms personnalisé qu’il est alors nécessaire de référencer dans les vues via une instruction
@using
. Dans le cas de mon exemple, si la méthode d’extensionCalendarFor
avait été déclarée dans l’espace de nomsWebApplication3
, il faudrait que j’ajoute l’instruction suivante en entête des vues qui auraient besoin de faire usage de mon contrôle calendrier.
@using WebApplication3
Placer du code dans des espaces de noms différents de l’emplacement physique d’un fichier est une pratique structurelle discutable (l’article https://codebetter.com/patricksmacchia/2009/02/15/re-factoring-re-structuring-and-the-cost-of-levelizing/ est particulièrement intéressant sur ce sujet). Cependant, utiliser la seconde méthode peut compliquer l’écriture de vues pour les développeurs connaissant mal la structure organisationnelle du code, et donc l’emplacement de tel ou tel contrôle, et n’utilisant pas d’outil tel que Resharper.
S’il s’agit d’un contrôle, ou d’une bibliothèque de contrôles, utilisée dans de nombreuses vues, il est possible de factoriser cette directive @using
en la déclarant plus globalement au niveau du nœud de configuration de Razor dans le web.config. Souvenez-vous du précédent billet, cette collection d’espaces de noms est automatiquement recopiée dans le code source généré par Razor puis CodeDOM.
Ainsi, ma collection d’espace de noms dans mon fichier web.config ressemble alors à la suivante. Je peux tranquillement enlever toutes les utilisations de la directive @using WebApplication3
, celle-ci n’est plus nécessaire.
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Optimization"/>
<add namespace="System.Web.Routing" />
<add namespace="WebApplication3" />
</namespaces>
La pré compilation des vues
Par défaut, le fonctionnement de la compilation d’une vue Razor est assez proche que celui utilisé par des vues WebForms. Cela signifie qu’il transforme et compile les vues d’une application ASP.NET MVC le plus tard possible, on parle alors de compilation Just In Time (JIT). « Le plus tard possible » signifiant lorsqu’une réponse utilisant la vue en question doit être retournée à un client. Une fois cette étape de compilation terminée, son résultat est placé en cache dans le répertoire temporaire du site IIS, et les prochains clients accédant à cette ressource ne provoqueront pas une nouvelle compilation.
Une telle approche a deux grands inconvénients, à savoir :
- Les éventuelles erreurs de syntaxe ou de manipulation d’un modèle qui se seraient glissées inopinément dans une vue ne peuvent être découvertes que lorsqu’un utilisateur navigue sur une action liée à cette vue, ce qui lève alors une erreur HTTP 500.
- Compiler une vue Razor, c’est faire appel au moteur de composition pour générer une classe spécifique puis compiler cette classe. Ces deux étapes consomment beaucoup de ressources serveur par rapport à un simple rendu d’une vue déjà compilée, et impliquent un temps d’attente prolongé pour le client. Ce délai pouvant être de quelques secondes, selon le contexte et la configuration du serveur qui héberge le site.
Pour améliorer l’expérience des visiteurs d’une application ASP.NET MVC, mais aussi pour détecter plus facilement les erreurs présentes dans une vue, il est possible de forcer la pré compilation des vues écrites avec Razor. Le principe est simple, il faut finalement mimer ce que fait ASP.NET au runtime pour compiler la vue afin de le déporter au niveau du processus de compilation classique de Visual Studio.
Cette étape de compilation peut se faire en appelant le programme aspnet_compiler.exe
, présent dans le répertoire du Framework (du type C:\Windows\Microsoft.NET\Framework64\v4.0.30319
). Cet appel doit être combiné au switch –p indiquant le répertoire physique de l’application à compiler ainsi que le switch –v indiquant le répertoire virtuel de l’application en question.
Pour illustrer cet usage, j’ai volontairement glissé une erreur dans une vue Razor de l’un de mes projets. La sortie de l’utilitaire en cas d’erreur est donc visible dans la capture ci-dessous.
En corrigeant mon erreur, la sortie devient la suivante.
Visual Studio permet d’automatiser cette exécution de aspnet_compiler
en éditant manuellement le fichier .csproj de l’application MVC concernée. Le document XML représentant le projet contient un nœud nommé MvcBuildViews
dont la valeur par défaut est false
. Il suffit de modifier cet élément afin de faire passer cette valeur à true
. A partir de là, en compilant le projet dans Visual Studio, l’erreur générée par aspnet_compiler
apparaît directement dans la fenêtre Error List
.
Bien évidemment, le temps de compilation des vues sera maintenant répercuté sur le poste de chaque développeur de l’équipe. Une solution hybride consisterait à compiler les vues, ni au runtime, ni sur le poste de chaque développeur, mais lors du processus de compilation de l’intégration continue.
Si vous choisissez d’implémenter ce cas de figure, laissez la valeur du nœud MvcBuildViews
du fichier .csproj
à false
. Puis, dans la définition de build chargée de compiler le projet, il faut passer le commutateur /p :MvcBuildViews=true
comme argument à MSBuild.
Après avoir lancé une build, nous obtenons bien la même erreur dans le rapport de génération.
A bientôt !