Cet article fait partie d’une série consacrée à l’injection de dépendance dans MVC 3 :

Ca fait long temps que je n’ai pas écrit sur ASP.NET MVC 3 et vu que ma série concernant l’injection de dépendance n’est pas terminée, j’en profite pour vous parler de vues/moteurs de vues dans MVC 3.

Le code source de cet article se trouve ici vs2010solution_43E39653.

Tout d’abord commençons avec un petit rappel concernant les moteurs de vue.

IViewEngine/IView

Depuis la version 1 de MVC nous avons la possibilité d’enregistrer nos moteurs de vues dans une collection statique ViewEngines.Engines. L’ordre d’enregistrement est très important car le premier moteur qui arrive à afficher la vue gagne.

Nouveauté MVC 3 : Les méthodes FindView et FindPartialView de la classe ViewEngineCollection ont été mis à jour pour prendre en charge la nouvelle fonctionnalité d’injection de dépendance. Le Framework MVC vérifie les moteurs de vues enregistrés dans la collection statique ViewEngines.Engines mais également tous les moteurs enregistrés dans le DependencyResolver grâce à la méthode DependencyResolver.Current.GetAllInstances<IViewEngine>(). Les moteurs de vues enregistrés dans DependencyResolver sont prioritaires par rapport à ceux enregistrés dans la collection statique.

Si vous avez un moteur de vues customisé :

   1: public class CustomViewEngine : IViewEngine {
   2:     // votre implémentation de IViewEngine 
   3: }

vous pouvez l’enregistrer dorénavant de cette manière :

   1: var container = new UnityContainer();
   2: container.RegisterType<IViewEngine, CustomViewEngine>("CustomViewEngine");
   3: DependencyResolver.SetResolver(new UnityMvcServiceLocator(container));

La responsabilité des moteurs de vues est de générer les vues. Dans le cas des moteurs Razor ou de WebForms les classes sont générées à partir de la source de la vue (fichier .aspx ou .cshtml). Ces classes dérive implicitement des classes de bases dont le moteur de vues est dépendant (ViewPage, ViewMasterPage, ViewUserControl ou WebViewPage).

ViewPage

Avant MVC 3 les classes générées pour les vues ne supportaient pas l’injection de dépendance car leur création était trop ancrée dans l’implémentation des moteurs de vues.

Nouveauté MVC 3 : Une mise à jour a été fait dans les moteurs de vues pour supporter l’injection de dépendance. La création de vues est donc basée sur le DependencyResolver qui lorsqu’il n’arrive pas à localiser une vue concrète passe la main au fonctionnement standard Activator.CreateInstance() comme c’était le cas dans les versions précédentes.

L’injection de dépendance se passe via les propriétés et non le constructeur comme dans d’autres cas. C’est à cause d’une limitation technique dans ASP.NET et la manière dont les classes de vues sont générées à partir du markup.

Pour illustrer le fonctionnement de l’injection de dépendance nous allons créer un petit exemple. Le but de l’exemple et de vous démontrer ce qu’il est possible de faire et non de dire que c’est de cette manière là qu’il faut le faire car il faut faire très attention avec ce gendre d’artéfacts.

Etape 1 : Création de la classe dérivée qui contiendra le service qu’on veut utiliser dans la vue

   1: public class InjectedViewPage : WebViewPage
   2: {
   3:     [Dependency]
   4:     public IOrderService OrderService { get; set; }
   5:  
   6:     public override void Execute()
   7:     {
   8:     }
   9: }

Nous dérivons tout simplement de la classe WebViewPage et ajoutons une propriété par laquelle l’implémentation concrète de la dépendance IOrderService sera injectée. L’attribut [Dependency] est utilisé par notre containeur DI Unity afin d’identifier les propriétés où l’injection doit être faite.

Etape 2 : Ajouter une action dans le contrôleur qui renvoie la vue avec le service injecté.

   1: public ActionResult TestInjectedView()
   2: {
   3:     return View();
   4: }

Cela est très simple. Juste une action en plus. Remarquez que je ne passe aucune donnée à la vue.

Etape 3 : Ajouter la vue

   1: @inherits UI.InjectedViewPage
   2: @{
   3:     ViewBag.Title = "TestInjectedView";
   4: }
   5:  
   6: <h2>TestInjectedView</h2>
   7: <p>
   8: Resultat de l'appel au service @@IOrderService avec un paramètre = 10<br /><br />
   9: @OrderService.CalculateVAT(10);
  10: </p>

A la ligne 1 : nous indiquons que nous voulons que la vue hérite de notre class InjectedViewPage.

A la ligne 9 : nous utilisons tout simplement le service qui a été injecté par le DependencyResolver.

Nous pouvons maintenant tester notre application en accédant à l’url (dans mon cas) http://localhost/Home/TestInjectedView :

InjectedViewPageResult

Comme vous pouvez constater l’injection du service c’est correctement passée car on affiche un résultat correct.

Attention : vous devez absolument utiliser le DependencyResolver pour que cela fonctionne.

   1: container.RegisterType<IControllerActivator, UnityControllerActivator>();
   2: DependencyResolver.SetResolver(new UnityDependencyResolver(container));

Version améliorée avec IViewPageActivator

Ce qui mérite d’être mentionné c’est l’introduction d’une nouvelle classe de base BuildManagerCompiledView et du nouveau service IViewPageActivator.

BuildManagerCompiledView  peut être utile aux développeurs qui créent les moteurs de vues utilisant le BuildManager afin de convertir les fichiers de markup dans des classes.

IViewPageActivator instancie les classes de vues qui ont été compilées à partir des fichiers de markup. IViewPageActivator est localisable via DependencyResolver. Son enregistrement est unique et il n’y a pas de point d’enregistrement statique.

En voici là signature :

   1: public interface IViewPageActivator {
   2:     object Create(ControllerContext controllerContext, Type type);
   3: }

La logique a été mise à jour pour consulter DependencyResolver en appelant GetSerivce(typeof(IViewPageActivator)) et utiliser le service s’il est présent. S’il n’y a pas d’instance de IViewPageActivator, DependencyResolver créera un type concret en appelant GetService(viewPageType). Si cela ne réussit pas alors le comportement de MVC 2 sera utilisé avec Activator.CreateInstance.

Pour résumer, l’interface IViewPageActivator a été introduite pour laisser plus de contrôle aux développeurs sur la manière dont le pages sont instanciées avec l’injection de dépendance. Comme vous pouvez les constater sur les images ci-dessous, deux classes en été modifiées WebFormView et RazorView qui possèdent un paramètre supplémentaire dans le constructeur IViewPageActivator qui permet l’injection de dépendance.

WebFormView :

WebFormView

RazorView

RazorView

Un petit exemple illustre cette nouvelle fonctionnalité :

Etape 1 : Création d’une classe implémentant IViewPageActivator

Nous allons l’appeler UnityViewPageActivator car Unity sera utilisé pour fournir les dépendances nécessaires.

   1: public class UnityViewPageActivator : IViewPageActivator
   2: {
   3:     public object Create(ControllerContext controllerContext, Type type)
   4:     {
   5:         return DependencyResolver.Current.GetService(type);
   6:     }
   7: }

Rien de magique, nous implémentant la méthode Create de l’interface IViewPageActivator qui sera appelée lors de la création des vues. Nous utilisons DependencyResolver comme un service locator à l’intérieur de la méthode qui lui même utilise Unity pour résoudre les services nécessaires.

Etape 2 : Modification de Global.asax pour enregistrer notre classe d’implémentation

Ici, rien de spécial non plus. L’enregistrement est très simple :

   1: // View Page activator
   2: container.RegisterType<IViewPageActivator, UnityViewPageActivator>();
   3: DependencyResolver.SetResolver(new UnityDependencyResolver(container));

Il n’y a pas de modification à apporter à la classe InjectedViewPage ni à la vue TestInjectedViewPage.cshtml. Quand on lance l’application nous allons obtenir le même résultat que dans l’exemple suivant. Dans ce cas d’utilisation de IViewPageActivator très simple il n’y a aucune différence par rapport à l’exemple précédent. Nous n’avons effectuer aucune opération supplémentaire dans la méthode Create à part de retourner une instance de vue avec DependencyResolver. Dans des cas plus compliqués vous pouvez imaginer prendre la main sur la création des vues et effectuer dans traitements. Personnellement je ne vois pas concrètement ce que ça pourrait être mais si vous avez des idées n’hésitez pas à me faire un retour là dessus.

Conclusion

Comme vous pouvez le constater, dans ASP.NET MVC 3 vous pouvez dorénavant bénéficier de l’injection de dépendances dans des vues. C’est bien… par contre la vraie question est, à quoi cela peut servir ?

Injecter un service dans une vue, autant utiliser un service locator directement ce qui est une mauvaise pratique en soi! On peut potentiellement utiliser une service qui accède à une base de données par exemple, faire en gros tout et n’importe quoi. Si on utilise un service dans une vue alors potentiellement il y a de la logique qui est déportée vers la vue. Au bout d’un temps cela peut être un enfer comme j’en ai beaucoup vu dans les applications ASP.NET classiques où la logique métier s’est mélangée à la présentation dans la vue. Quid de la testabilité ? Alors que c’est beaucoup plus propre de faire comme avant et de fournir un modèle de vue bien formaté. Il faut donc faire attention lorsqu’on utilise cette extension du Framework MVC. Personnellement je ne l’utilise pas cette fonctionnalité car j’estime qu’elle encourage les mauvaises pratiques mais c’est bien de savoir qu’elle existe ! Avez vous des cas concret où cela serait utile ?

La prochaine fois je ferai un post sur l’injection de dépendance dans les filtres.

// Thomas