Les nouveautés d'ASP.NET MVC 3

| Nicolas Esprit | Philippe Vialatte |

Lancé fin 2009 comme une alternative à ASP.NET Web Forms, ASP.NET MVC est récemment entré en version 3. Dans cet article, nous allons revenir sur les nouveautés que cette version 3 apporte.

Commentez cet article : Commentez

- Titre : Les nouveautés d'ASP.NET MVC 3

- Auteurs : Nicolas Esprit, Philippe Vialatte

- Parution : 15 février 2011

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2011 developpez. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts. Droits de diffusion permanents accordés à Developpez LLC.

Sommaire

- 0. Introduction

- 0-A. Qu'est-ce qu'ASP .NET MVC

- 0-B. Différences entre Web Forms et MVC

- I. Améliorations dans Visual Studio 2010

- I-A. Mise à jour des templates de projet

- I-B. Support de NuGet

- I-C. Fenêtre de création de nouveau projet

- I-D. Fenêtre de création d'une nouvelle vue

- I-E. Le moteur Razor

- I-F. Amélioration du scaffolding des vues

- II. Améliorations pour la Validation

- II-A. Gestion des DataAnnotations

- II-B. Nouveaux attributs de validation

- II-B-1. CompareAttribute

- II-B-2. RemoteAttribute

- II-C. Contrôle plus fin de la validation de requêtes via AllowHTML

- II-D. IValidatableObject

- II-E. IClientValidatable

- III. Global Action Filters

- IV. Meilleur support de l'injection de dépendances

- IV-A. Interface IDependencyResolver

- IV-B. Utilisation du DependencyResolver

- IV-C. Nouvelles interfaces IControllerActivator et IViewPageActivator

- V. Meilleure gestion de la génération et du rendu HTML

- V-A. Nouvelle classe AdditionalMetadataAttribute

- V-B. Nouvelle méthode Html.Raw

- V-C. Changements dans la méthode Html.ValidationMessage

- V-D. Modifications apportées aux méthodes des Helpers

- V-E. Surcharge des méthodes LabelFor et LabelForModel

- VI. Modifications dans les types d'Action Result

- VI-A. HttpNotFoundResult

- VI-B. HttpStatusCodeResult

- VI-C. Redirections permanentes avec RedirectResult

- VII. Meilleur support JavaScript et Ajax

- VII-A. JavaScript discret pour Ajax

- VII-B. jQuery discret pour la validation

- VII-C. Précisions sur l'activation du JavaScript discret et la validation côté client

- VII-D. Nouvelle classe JsonValueProviderFactory

- VIII. Amélioration du cache de sortie

- IX. Améliorations diverses

- IX-A. Sessionless Controllers

- IX-B. Support de code s'exécutant avant celui de la vue (_viewstart.cshtml)

- IX-C. IMetadataAware

- IX-D. ViewBag

- X. Upgrader ses projets ASP.NET MVC 2 vers MVC 3

- Liens

- Conclusion

- Remerciements

0 - Introduction

En guise d'introduction, nous allons voir rapidement l'historique du Framework MVC, et aborder une question que certains se posent encore. Quelle est la différence fondamentale entre ASP.NET MVC et ASP.NET Web Forms ?

0-A - Qu'est-ce qu'ASP .NET MVC

Le patron de conception Modèle-Vue-Contrôleur (en abrégé MVC) est un patron de conception architectural, qui organise l'interface utilisateur d'une application en trois composants :

- un modèle (contenant aussi bien des données que des opérations) ;

- une vue (responsable de la présentation aux utilisateurs) ;

- un contrôleur, dont le rôle est de gérer les événements et la synchronisation entre la Vue et le Modèle.

MVC a été mis au point en 1979 par Trygve Reenskaug, qui travaillait alors sur SmallTalk.

ASP.NET MVC est donc un Framework de développement d'application Web, basé sur ce patron de conception.

Le Contrôleur est une classe exposant un certain nombre de fonctions (ou Actions) renvoyant un objet de type ActionResult. Une Action sera toujours le point d'entrée dans ASP.NET MVC, et pourra renvoyer, au choix, une page, des données dans différents formats (binaire, JSON, etc.), voire rien du tout. Les contrôleurs sont stockés dans le répertoire Controllers de l'application Web, et utilisent comme convention de nommage <Objet>Controller.

Dans le cadre d'ASP.NET MVC, une Vue est l'équivalent d'une page Web, utilisant la liaison des données pour afficher des informations. Les vues sont, par définition, stockées dans le répertoire Views de l'application Web. Toutes les vues qui seront renvoyées par un contrôleur <Objet>Controller seront stockées dans le répertoire Views/<Objet>.

Enfin, le modèle est un ensemble de classes " standards " dont le but est de gérer un ensemble de données, lesquelles seront ensuite affichées à l'utilisateur. C'est la partie " métier " de l'application, qui va gérer l'accès aux données, la validation, etc.

0-B - Différences entre Web Forms et MVC

En termes d'architecture, d'abord, les deux Framework partent de deux patrons d'architectures différents.

Si MVC dérive du patron MVC, Web Forms de son côté, est une application du patron de conception contrôleur de page.

Le plus gros point différenciateur entre Web Forms et MVC est situé sur la cible initiale de chacun de ces Framework.

Historiquement, ASP.NET Web Forms fut développé pour faciliter la migration des développeurs du monde « stateful » du développement d'applications Windows au monde « stateless » du Web. Pour parvenir à cela, un certain nombre de mécanismes (ViewState, PostBack, contrôles serveurs, cycle de vie complexe de la page) ont été mis en place, de façon à « cacher » aux développeurs les mécanismes de communication entre le client et le serveur.

De l'autre côté, MVC utilise exclusivement du binding pour communiquer entre un contrôleur et une vue, n'a pas de ViewState, et est par essence stateless. Le cycle de vie, en contrepartie, est plus léger, et le découpage des contrôleurs favorise la conception pilotée par le domaine (DDD, ou Domain Driven Design). De plus, MVC a été pensé pour favoriser les tests unitaires à tous les niveaux, et expose de nombreux points d'extension.

De nombreux évangélistes ayant débattu du sujet depuis 2009 et ASP.NET MVC 1, on ne reviendra pas sur la meilleure des deux implémentations (quoique les auteurs sont convaincus de la supériorité de MVC sur Web Forms).

I - Améliorations dans Visual Studio 2010

I-A - Mise à jour des templates de projet

Les templates des projets Visual Studio 2010 pour la nouvelle version d'ASP.NET MVC incluent désormais les dernières versions de jQuery, et du plugin de validation jQuery.

Projet ASP.NET MVC 2

Projet ASP.NET MVC 3

Plus précisément nous avons la version 1.4.4 de jQuery et la version 1.7 du plugin de validation jQuery. Comme on peut le constater sur le comparatif MVC 2 / MVC 3 ci-dessus, une nouvelle bibliothèque est présente par défaut dans le template du projet. Il s'agit de la version 1.8.7 de jQuery UI. Cette bibliothèque propose un ensemble de composants pour vos interfaces graphiques et rencontre un certain succès. Pour plus d'informations : http://jqueryui.com.

Mis à part ces ajouts, le template du projet garde la même organisation :

- le dossier App_Data possède le même rôle que pour les sites ASP.NET Web Forms et permet de stocker physiquement des données ;

- le dossier Content contient en général les fichiers statiques. Il est recommandé d'y ajouter les fichiers tels que les feuilles de style CSS et les images ;

- le dossier Controllers correspond à l'emplacement recommandé pour placer ses contrôleurs ;

- le dossier Models est utilisé pour les classes qui représentent le modèle de l'application. Il est également recommandé d'y placer le code implémentant l'accès aux données. Bien entendu, ces classes sont souvent positionnées dans une librairie indépendante ;

- le dossier Scripts pour stocker les fichiers de scripts utilisés par l'application Web ;

- le dossier Views qui est l'emplacement recommandé pour stocker les Views. Cela comprend les pages .aspx, les UserControls .ascx ainsi que les MasterPages .master.

I-B - Support de NuGet

NuGet, anciennement NuPack, est installé avec ASP.NET MVC 3 afin de vous permettre de tirer parti de cet outil et utiliser des librairies ou extensions MVC dans votre projet. Mais qu'est-ce que NuGet ? NuGet est un gestionnaire gratuit de packages open source. Il vous permet facilement de trouver, installer et utiliser toutes sortes de bibliothèques .Net dans vos projets. Il fonctionne avec tous les types de projets .NET y compris bien sûr les projets ASP.NET Web Forms et ASP.NET MVC.

Nuget permet aux développeurs qui maintiennent des projets open source comme StructureMap, NUnit, NHibernate ou encore ELMAH, d'emballer ces librairies dans un package et de les publier dans une galerie en ligne afin de les rendre accessibles à tout le monde. L'avantage également est que NuGet est maintenant intégré à Visual Studio 2010, via le NuGet Package Manager ou via ligne de commande.

NuGet faisant l'objet d'un article plus détaillé en cours d'écriture nous illustrerons ici simplement comment ajouter un package à son projet. Pour notre exemple, nous rajouterons la librairie de code de Developpez.com. Pour ce faire, un clic droit sur notre projet ASP.NET MVC puis "Add Library Package Reference". Ceci affichera la fenêtre de sélection du NuGet Package Manager comme ci-dessous.

Après sélection du projet désiré, un simple clic sur install téléchargera et ajoutera une référence à la librairie Developpez à votre projet. Notez qu'une fenêtre console permet de faire la même chose en ligne de commande via PowerShell comme ci-dessous :

Notez qu'en plus de télécharger et installer automatiquement la librairie, NuGet récupèrera également toutes les dépendances. On peut dire que cela va grandement faciliter la tâche aux développeurs. La liste de packages, consultable sur le site officiel, contient à ce jour 800 packages et continue de grossir.

I-C - Fenêtre de création de nouveau projet

Lorsque vous créez un nouveau projet, la boîte de dialogue vous permet désormais de spécifier le moteur d'affichage ainsi que le modèle de projet ASP.NET MVC. Pour le moteur d'affichage nous avons le choix entre le nouveau moteur Razor (qui fera l'objet d'un prochain article) ou le moteur ASPX standard comme on peut le voir ci-dessous :

Notez qu'avec cette nouvelle version il est également possible de modifier la liste des modèles et des moteurs d'affichage répertoriés dans la boîte de dialogue. Pour ce faire une modification est nécessaire dans la base de registre.

Les modèles de projet par défaut sont les suivants :

- Empty : contient un ensemble minimal de fichiers pour un projet ASP.NET MVC, y compris la structure de répertoires par défaut pour les projets ASP.NET MVC, un fichier qui contient une feuille de style Site.css et un répertoire qui contient les fichiers de script par défaut ;

- Internet Application : contient la structure du modèle Empty, ainsi qu'un exemple d'utilisation du Membership Provider en ASP.NET MVC.

I-D - Fenêtre de création d'une nouvelle vue

Lorsque vous ajoutez une vue fortement typée, la boîte de dialogue filtre désormais bien plus les types non applicables que dans les versions précédentes, comme les nombreuses classes de base du Framework .NET. En outre, la liste est maintenant triée par le nom de classe et non par le nom qualifié complet du type, ce qui rend plus facile de trouver les types.

I-E - Le moteur Razor

Le moteur Razor est une des plus importantes et sympathiques nouveautés apportées par ASP.NET MVC 3. Un prochain article en cours de rédaction présentera plus en détail Razor ainsi que sa syntaxe, mais pour résumer rapidement ses avantages :

- il est compact, expressif et fluide. Il réduit drastiquement le code nécessaire à écrire et en facilite la compréhension ;

- il est facile à apprendre car nécessite peu de concepts à maîtriser ;

- ce n'est pas un nième langage. Microsoft a voulu permettre aux développeurs C# et VB.NET de pouvoir construire du HTML facilement ;

- il possède l'Intellisense ;

- il permet de mettre en place des tests unitaires sans avoir besoin d'un contrôleur ou d'un serveur Web.

A noter que Visual Studio 2010 propose désormais l'IntelliSense ainsi que la coloration syntaxique pour Razor comme on peut le voir sur l'image ci-dessous :

I-F - Amélioration du scaffolding des vues

Les templates T4 utilisés pour le scaffolding des vues (échafaudage en français) avec la boîte de dialogue "Ajouter une nouvelle Vue" génèrent maintenant des vues qui utilisent Html.EditorFor au lieu des helpers tels que Html.TextBoxFor. Ce changement vous permet éventuellement d'annoter des modèles avec des métadonnées (en utilisant des attributs d'annotation de donnée) afin de mieux personnaliser le rendu de votre interface utilisateur à l'exécution.

Lors de l'ajout d'une vue, le scaffolding prend également en charge une meilleure détection ainsi qu'une meilleure utilisation des informations fournies par les clés primaires sur les modèles (y compris le support des conventions de nommage comme ID, ProductID, etc.). Par exemple : la boîte de dialogue "Ajouter une nouvelle Vue" utilise cette information pour s'assurer que la valeur de la clé primaire n'est pas échafaudée comme un champ de formulaire éditable, et que les liens entre les vues sont autogénérés correctement avec les informations de la clé primaire.

Les templates par défaut pour l'édition et la création incluent maintenant des références aux scripts jQuery nécessaires à la validation. Le scaffolding des formulaires des vues prend désormais en charge la validation côté client par défaut. La validation côté client avec ASP.NET MVC 3 est également réalisée par une approche discrète du JavaScript (unobstrusive JavaScript en anglais), rendant la création de pages plus rapide et propre comme indiqué dans le paragraphe sur le sujet plus bas dans cet article.

Même si cela ne fait pas partie de la release d'ASP.NET MVC 3, il est intéressant de souligner la sortie du package NuGet MvcScaffolding écrit conjointement par Steve Sanderson et Scott Hanselman. Ce package vous permettra un gain de productivité si vous souhaitez automatiser les tâches courantes ou un moyen rapide de prendre en main ASP.NET MVC si vous êtes débutant. Pour plus d'informations à ce sujet vous pouvez consulter le billet de Steve Anderson.

II - Améliorations pour la Validation

La version 2 du Framework MVC, avait déjà vu l'apparition d'un ensemble de fonctionnalités assez puissantes de la validation côté client. La version 3 continue à améliorer ce mécanisme, en le rendant encore plus puissant et souple.

II-A - Gestion des DataAnnotations

Le modèle de validation ne prenait pas en compte, du temps de la version 2, toutes les nouveautés apportées par l'espace de noms DataAnnotations. En effet, par un souci de compatibilité, la version 2 du Framework MVC était compilée en mode de compatibilité .Net 3.5, ce qui l'empêchait de profiter de toutes les nouveautés de cet espace de nom.

La version 3 étant compilée avec une dépendance sur le Framework .NET 4.0, il est désormais possible d'utiliser l'intégralité des DataAnnotations.

Les nouveaux attributs disponibles sont les suivants :

- AssociationAttribute : représente un attribut utilisé pour spécifier qu'une propriété d'entité participe à une association ;

- ConcurrencyCheckAttribute : spécifie le type de données de la colonne utilisée pour les contrôles d'accès concurrentiel optimiste ;

- CustomValidationAttribute : spécifie une méthode de validation personnalisée à appeler au moment de l'exécution ;

- DisplayAttribute : fournit un attribut à usage général qui vous permet de spécifier les chaînes localisables pour les types et membres de classes partielles d'entité ;

- EditableAttribute : indique si un champ de données est modifiable ;

- EnumDataTypeAttribute : permet le mappage d'une énumération .NET Framework à une colonne de données ;

- FilterUIHintAttribute : représente un attribut utilisé pour spécifier le comportement de filtrage pour une colonne ;

- KeyAttribute : dénote une ou plusieurs propriétés qui identifient une entité de manière unique ;

- TimestampAttribute : spécifie le type de données de la colonne en tant que version de colonne ;

- ValidationContext : décrit le contexte dans lequel un contrôle de validation est exécuté ;

- ValidationResult : représente un conteneur pour les résultats d'une demande de validation ;

- Validator : définit un helper qui peut être utilisé pour valider des objets, des propriétés et des méthodes lorsqu'il est inclus dans leurs attributs ValidationAttribute associés.

Ceci va nous permettre, par exemple, d'utiliser DisplayAttribute en lieu et place de DisplayNameAttribute. En version 2, localiser une application pouvait se révéler un peu compliqué, les attributs adéquats n'étant pas disponibles. En version 3, pour gérer la localisation d'un champ, il suffit de faire :

[Display(Name="PrenomName", Description="PrenomDesc",  ResourceType=typeof(ModelResources))]
public string Prenom { get; set; }

Ce code va automatiquement aller chercher, dans ModelResources , le champ PrenomName qui correspond à la culture courante, et l'utiliser pour générer le label de nos champs en relation avec le prénom.

II-B - Nouveaux attributs de validation

Deux nouveaux attributs ont fait leur apparition, de façon à faciliter certains scénarios de validation courants.

II-B-1 - CompareAttribute

L'idée derrière CompareAttribute est très simple. Cet attribut permet de vérifier qu'une propriété a la même valeur qu'une des autres propriétés du modèle en cours de validation.

Les scénarios d'application vont être la confirmation d'une adresse e-mail ou d'un mot de passe.

Par exemple, si, sur un formulaire d'inscription, on veut que l'utilisateur confirme son adresse e-mail, il nous suffira de produire le code suivant :

[Required]
[DataType(DataType.EmailAddress)]
public string Email { get; set; }

[Required]
[Display(Name = "Confirmez votre adresse e-mail")]
[Compare("Email", ErrorMessage = "Les deux adresses e-mail sont différentes")]
[DataType(DataType.EmailAddress)]
public string Emailconfirmation { get; set; }

L'attribut CompareAttribute étant supporté par la validation côté client, on verra le message apparaître directement lors de la saisie, sans avoir besoin d'un aller-retour avec le serveur.

II-B-2 - RemoteAttribute

Autant la validation côté client avec MVC 2 était simple à mettre en place, autant dès qu'il fallait vérifier une donnée côté serveur...on se retrouvait à devoir mettre les mains dans le cambouis.

Avec MVC V3, valider une donnée côté serveur devient d'une simplicité étonnante.

En effet, il suffit de décorer la propriété à valider avec l'attribut RemoteAttribute, et de le configurer pour que la validation côté client se fasse avec un appel à une de nos actions. La configuration en elle-même est simplissime, il nous suffit de renseigner, au niveau de l'attribut :

- l'action à appeler ;

- le contrôleur exposant l'action ;

- éventuellement, un message d'erreur, que ce soit sous la forme d'un texte, ou d'une ressource.

Par exemple, supposons que je veuille vérifier qu'un login n'existe pas, et que je souhaite utiliser un fichier de ressources pour gérer le message d'erreur, il me suffira d'ajouter au modèle :

[Required]
[Remote("CheckLogin", "User", ErrorMessageResourceType = typeof(ModelResources), ErrorMessageResourceName = "DoublonLogin")]
public string Login {get ; set ;}

Et d'ajouter, dans mon fichier UserController, une action renvoyant un résultat au format JSON :

public ActionResult CheckLogin(string login)
{
var manager = new UserManager();
return Json(!manager.ContainsLogin(login), JsonRequestBehavior.AllowGet);
}

Désormais, lorsque l'utilisateur quitte le champ texte Login, l'appel à l'action CheckLogin est effectué, et un message apparait si le compte utilisateur existe déjà.

II-C - Contrôle plus fin de la validation de requêtes via AllowHTML

Que ce soit avec Web Forms ou MVC, le comportement par défaut d'ASP.NET, lorsque l'on ajoute des balises HTML dans des champs, est de rejeter l'entrée utilisateur avec le message « A potentially dangerous Request.Form value was detected from the client ».

En effet, le Framework va valider les requêtes utilisateur, de façon à empêcher les attaques de style XSS (Cross-site scripting) en n'acceptant pas de balises, qui pourraient contenir un script malicieux.

Lorsque l'on veut permettre à un utilisateur de rentrer des tags HTML (pour utiliser un éditeur HTML, par exemple), la seule solution avec MVC V2 était de désactiver l'attribut ValidateInput au niveau de l'action. Supposons que nous voulions laisser à un utilisateur la possibilité d'ajouter un commentaire avec un éditeur HTML, il nous faudra donc produire le code suivant :

[HttpPost]
[ValidateInput(false)]
public ActionResult Addcomment(CommentViewModel comment){
// sauvegarde de notre nouveau commentaire
}

L'inconvénient de cette méthode étant qu'elle désactive la validation sur l'ensemble du modèle, et pas uniquement sur les propriétés dans lesquelles on veut lui laisser stocker du code HTML. Dans notre cas, l'utilisateur va donc pouvoir, à la main, ajouter des balises HTML dans *tous* les champs de notre vue.

Avec MVC 3, il est désormais possible de désactiver la validation de requêtes au niveau d'une propriété, dans le modèle. On pourra donc, au niveau de notre objet CommentViewModel, faire ceci :

public class CommentViewModel{
public string Titre {get ; set ;}
[AllowHTML]
public string Body {get ; set ;}
}

Le second point positif étant évidemment que cette modification est faite au niveau du modèle, et plus de la vue.

II-D - IValidatableObject

L'interface IValidatableObject permet de gérer des scénarios de validations plus complexes.

Implémenter cette interface va nous permettre de rajouter, une fois que toutes les propriétés de l'objet ont été validées, une validation logique de l'ensemble de l'objet.

Si on repart de notre exemple précédent de validation du login de l'utilisateur. Autant la validation côté client va très bien fonctionner si l'utilisateur a activé Javascript, autant il va quand même falloir vérifier côté serveur que les données sont effectivement valides.

Pour cela, on va simplement modifier notre classe Utilisateur pour lui faire implémenter IValidatableObject, et implémenter la méthode Validate :

public class User : IValidatableObject
{
[Required]
[Remote("CheckLogin", "User", ErrorMessageResourceType = typeof(ModelResources), ErrorMessageResourceName = "DoublonLogin")))]
public string Login {get ; set ;}
public IEnumerable<ValidationResult> Validate(ValidationContext context)
{
var manager = new UserManager();
if (manager.ContainsLogin(login))
yield return new ValidationResult("Ce login existe déjà");
}
}

Il nous suffira ensuite, dans le contrôleur, de tester que le modèle est valide pour récupérer l'erreur sur le login :

public ActionResult Create (User nouvelUtilisateur)
{
if (!ModelState.IsValid)
return View();
// reste de la fonction
}

II-E - IClientValidatable

Pour avoir une validation à la fois côté client et côté serveur, il existe désormais un mécanisme supplémentaire, qui peut être utilisé pour ajouter de nouveaux attributs de validation, dans la veine de CompareAttribute.

Supposons que l'on veuille implémenter une validation personnalisée côté client et serveur. Notre cas métier étant, par exemple, que l'on veuille vérifier que le mot de passe d'un utilisateur est suffisamment complexe (qu'il fait au moins 8 caractères, contient des caractères en minuscule, en majuscule et non alpha).

On va, de plus, vouloir bénéficier du support de la validation par Javascript discret (unobtrusive Javascript) par JQuery.

On va commencer par définir un nouvel attribut de validation, qui va implémenter IClientValidatable.

public class LoginAttribute : RegularExpressionAttribute, IClientValidatable
{
public LoginAttribute()
: base("^.*(?=.{8,})(?=.*[a-z])(?=.*[A-Z])(?=.*[\d\W]).*$ ")
{}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
yield return new ModelClientValidationRule
{
ErrorMessage = this.ErrorMessage,
ValidationType = "login"
};
}
}

Ce que fait ce code est assez simple.

Comme on va utiliser une expression régulière, on va étendre RegularExpressionAttribute.

On va ensuite, dans GetClientValidationRule, définir que la validation du login va utiliser le type de validation « login ».

Ce type n'est actuellement pas défini dans les règles de JQuery. Il nous suffit de l'ajouter de la façon suivante :

jQuery.validator.unobtrusive.adapters.add("login", function (rule) {
var message = rule.Message;

return function (value, context) {
var loginPattern = ^.*(?=.{8,})(?=.*[a-z])(?=.*[A-Z])(?=.*[\d\W]).*$ ;
return loginPattern.test(value);
};
});

A noter, ceci ne fonctionnera, évidemment, que si le Javascript est bien en mode discret.

III - Global Action Filters

Un filtre d'action est un attribut que vous pouvez appliquer sur une action d'un contrôleur ou sur un contrôleur entier et qui modifie la façon dont l'action est exécutée (pour plus d'informations à ce sujet consultez l'article developpez). Avec ASP.NET MVC 1 et 2 vous pouvez spécifier des filtres sur les contrôleurs, et donc sur toutes ses actions, en utilisant une syntaxe d'attribut comme ci-dessous :

[MyActionFilter]
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}

public ActionResult About()
{
return View();
}
}

Les développeurs souhaitent souvent appliquer une logique de filtre sur tous les contrôleurs au sein d'une application. ASP.NET MVC 3 permet maintenant de spécifier un filtre qui doit s'appliquer globalement à tous les contrôleurs d'une application. Vous pouvez maintenant le faire en les ajoutant à la collection GlobalFilters. Une méthode RegisterGlobalFilters est maintenant incluse par défaut dans le fichier Global.asax afin de fournir un endroit pratique pour réaliser cela :

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new MyActionFilterAttribute());
}

protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();

RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}

Cette nouveauté est très pratique si vous voulez qu'elle s'applique à tous les contrôleurs et les actions de votre application. Mais que faire si vous voulez qu'elle ne s'applique que sous certaines conditions ? Supposons que vous ayez besoin d'afficher une information par exemple lorsque le débogage est activé, mais lorsque vous déployez en production vous ne voulez pas que vos utilisateurs la voient. On utilisera pour cela un fournisseur de filtre personnalisé. Votre classe doit mettre en oeuvre l'interface IFilterProvider. Le code ci-dessous illustre cet exemple :

public class DebugFilterProvider : IFilterProvider
{
private readonly List<Func<ControllerContext, object>> _conditions = new List<Func<ControllerContext, object>>();
public void Add(Func<ControllerContext, object> condition)
{
_conditions.Add(condition);
}

public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
{
return from condition in _conditions
select condition(controllerContext) into filter
where filter != null
select new Filter(filter, FilterScope.Global);
}
}

La méthode GetFilters boucle sur toutes les conditions, qui est en fait une liste générique qui contient un délégué, et si ce délégué n'est pas null, alors il va créer un nouvel objet filtre et le renvoyer. Ainsi dans votre fichier Global.asax, vous pouvez ajouter ce code afin que ce filtre fonctionne uniquement lorsque le débogage est activé :

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
var provider = new DebugFilterProvider();
provider.Add(c => c.HttpContext.IsDebuggingEnabled ? new MyActionFilterAttribute() : null);
FilterProviders.Providers.Add(provider);
}

Les Global Action Filters ainsi que l'utilisation de conditions offrent de nombreuses possibilités. On peut par exemple imaginer la déclaration d'un ActionFilter dans le Global.asax pour toutes les actions nommées "Index" dans notre application. Cette nouveauté MVC 3 en réjouira plus d'un !

IV - Meilleur support de l'injection de dépendances

L'injection de dépendances, et les avantages que cette technique apporte, devraient normalement être un concept connu de la plupart des développeurs.

Pour un bref rappel, cette technique consiste en la délégation de la résolution d'une dépendance par un composant à l'exécution, et non plus à la compilation. Ceci permet de diminuer le couplage entre les différentes entités de l'application, et donc, par exemple, de faciliter les tests unitaires.

Cette technique et les logiciels qui s'y rapportent ont déjà été évoqués dans cet article : Injection de dépendances en .NET.

Dans les versions précédentes du Framework MVC, il n'était pas particulièrement plus aisé de mettre en place l'injection de dépendances qu'avec Web Forms (ou Winform), ce qui était navrant, vu que MVC est très orienté test unitaire, et que l'injection de dépendance est très utilisée dans ce cadre.

Il était en effet nécessaire, pour pouvoir injecter nos services, d'ajouter une certaine quantité de code de « plomberie » de façon à pouvoir gérer ce scénario de façon relativement transparente.

La version 3 corrige cette lacune, et l'injection de dépendance fait maintenant partie des fonctionnalités fondamentales du Framework. En effet, le Framework inclut dorénavant une localisation unique dans laquelle les dépendances à résoudre vont être cherchées. Cette fonctionnalité se rapproche du patron de conception Service Locator.

Attention, MVC3 ne fournit pas de Framework d'injection de dépendances, mais offre des points d'extensions pour permettre de « brancher » facilement un Framework existant.

IV-A - Interface IDependencyResolver

Cette nouvelle interface ajoute un niveau d'indirection au-dessus des autres Frameworks, dans le but de standardiser leur configuration.

L'interface IDependencyResolver est tout ce qu'il y a de plus simple. Elle se présente en effet ainsi :

namespace System.Web.Mvc {
public interface IDependencyResolver {
object GetService(Type serviceType);
IEnumerable<object> GetServices(Type serviceType);
}
}

Pour que MVC 3 utilise un Framework d'injection de dépendances, il nous suffit donc de créer (ou de réutiliser, il y a de fortes chances pour que la plupart de ces Frameworks proposent une classe de ce type très vite) une classe implémentant l'interface, et d'appeler, au niveau de la méthode Application_Start, la méthode DependencyResolver.SetResolver.

Nous allons, pour l'exemple, utiliser la classe StructureMapDependencyResolver décrite par Steve Smith :

public class StructureMapDependencyResolver : IDependencyResolver
{
public StructureMapDependencyResolver(IContainer container)
{
_container = container;
}

public object GetService(Type serviceType)
{
if (serviceType.IsAbstract || serviceType.IsInterface)
{
return _container.TryGetInstance(serviceType);
}
return _container.GetInstance(serviceType);
}

public IEnumerable<object> GetServices(Type serviceType)
{
return _container.GetAllInstances<object>()
.Where(s => s.GetType() == serviceType);
}

private readonly IContainer _container;
}

Maintenant que nous avons notre classe StructureMapDependencyResolver, il ne nous reste plus qu'à ajouter cette ligne :

DependencyResolver.SetResolver(new StructureMapDependencyResolver(new Container())) ;

pour bénéficier de l'injection de dépendance de façon quasiment transparente.

IV-B - Utilisation du DependencyResolver

En effet, pour revenir à nos exemples précédents, nous allons légèrement refactoriser notre code, de façon à ce que notre classe UserManager soit injectée depuis le DependencyResolver.

public class UserManager
{
private readonly UserRepository _repository;
public bool ContainsLogin (string login)
{
return _repository.Any(user => user.Login.Equals(login));
}
}

On va donc commencer par définir notre interface IUserManager :

public inteface IUserManager{
public bool ContainsLogin();
}

Puis modifier la classe UserManager pour lui faire implémenter l'interface IUserManager :

public class UserManager : IUserManager
{
// le reste ne change pas
}

On va ensuite, configurer notre conteneur, de la façon suivante :

IContainer container = new Container(x =>
{
x.For<IUserManager>().Use<UserManager>();
});
DependencyResolver.SetResolver(new StructureMapDependencyResolver(container));

Notre conteneur est prêt, il nous faut maintenant modifier notre classe UserController, de façon à passer en paramètre une interface IUserManager, et modifier la méthode CheckLogin, qui va utiliser cet objet.

public class UserController : Controller
{
private readonly IUserManager _manager;

public UserController(IUserManager manager)
{
_manager = manager;
}

public ActionResult CheckLogin(string login)
{
return Json(!_manager.ContainsLogin(login), JsonRequestBehavior.AllowGet);
}

// reste de la classe
}

Et c'est tout ! Au moment où le contrôleur est appelé, le Framework va appeler le DependencyResolver, et récupérer le type concret UserManager.

Dans le cas que l'on vient de voir, le Framework nous aide, en ajoutant les appels nécessaires de façon silencieuse. Il y a cependant des cas où l'on voudra appeler directement le DependencyResolver, ce qui se fait simplement en appelant la méthode GetService<T> du DependencyResolver actif :

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (DependencyResolver.Current.GetService<IUserManager>().ContainsLogin(Login))
yield return new ValidationResult("Ce login existe déjà");
}

IV-C - Nouvelles interfaces IControllerActivator et IViewPageActivator

Toujours dans l'idée d'ajouter des possibilités d'extensions du Framework, deux nouvelles interfaces ont été ajoutées. L'idée de ces interfaces est de permettre aux développeurs de gérer très précisément la façon dont un contrôleur (ou une vue pour IViewPageActivator) est instancié grâce à l'injection de dépendances.

Ces deux interfaces sont relativement simples, vu qu'elles demandent simplement d'implémenter une méthode Create.

public interface IControllerActivator
{
IController Create(RequestContext requestContext, Type controllerType);
}

public interface IViewPageActivator
{
object Create(ControllerContext controllerContext, Type type);
}

Implémenter ces classes nous permettra de contrôler le scénario de résolution de dépendance en nous basant sur le DependencyResolver. On pourrait éventuellement vouloir ajouter des logs ou tout autre scénario alternatif...

On pourrait par exemple faire ceci :

public class StructureMapControllerActivator : IControllerActivator
{
public IController Create(RequestContext requestContext, Type controllerType){
// logger l'appel à Create
return (IController)DependencyResolver.Current.GetService<controllerType>();
}
}

public class StructureMapViewPageActivator : IViewPageActivator
{
public object Create(ControllerContext controllerContext, Type type){
// logger l'appel à Create
return DependencyResolver.Current.GetService<type>();
}
}

Il nous reste néanmoins à enregistrer nos activateurs dans le DependencyResolver, de la façon suivante :

IContainer container = new Container(x =>
{
x.For<IViewPageActivator>().Use<StructureMapViewPageActivator>();
x.For<IControllerActivator>().Use<StructureMapControllerActivator>();
});
DependencyResolver.SetResolver(new StructureMapDependencyResolver(container));

V - Meilleure gestion de la génération et du rendu HTML

V-A - Nouvelle classe AdditionalMetadataAttribute

Vous pouvez utiliser la classe AdditionalMetadataAttribute pour remplir le dictionnaire ModelMetadata.AdditionalValues pour une propriété de modèle. Par exemple, supposons qu'on ait un modèle de vue dont certaines propriétés ne doivent être visibles que pour un administrateur. Ce modèle peut être annoté avec le nouvel attribut en utilisant "AdminUniquement" comme clé et "true" comme valeur, comme on peut le voir dans l'exemple ci-dessous :

public class ProductViewModel {
[AdditionalMetadata("AdminUniquement", true)]
public string Name {get; set;}
}

Cette classe permet donc facilement d'ajouter des attributs accessibles lors du rendu. Charge au développeur d'interpréter ces métadonnées supplémentaires et de les utiliser.

V-B - Nouvelle méthode Html.Raw

Par défaut, le moteur Razor encode en HTML toutes les valeurs. Pour pallier cela, MVC 3 introduit une nouvelle méthode : Html.Raw(). Pour illustrer ceci, supposons qu'on stocke dans une variable string du code HTML et qu'on souhaite l'afficher ensuite. Lors du rendu le code HTML sera interprété. Par exemple le code ci-dessous :

@{
string mvcLink = "<a href=\"http://asp.net/mvc\" title=\"ASP.NET MVC Website\">http://asp.net/mvc</a>";
}
<p>To learn more about ASP.NET MVC visit @mvcLink.</p>

<p>To learn more about ASP.NET MVC visit @Html.Raw(mvcLink)</p>

Affichera le résultat ci-dessous. On peut voir que le contenu de la variable "mvcLink" est encodé en HTML, tandis que grâce à l'appel à la nouvelle méthode Html.Raw le contenu de la variable est bien interprété et le contenu de la variable "mvcLink" est rendu sous forme de lien HTML.

V-C - Changements dans la méthode Html.ValidationMessage

Le framework ASP.NET MVC fournit deux Helpers pour la validation, Html.ValidationMessage et Html.ValidationSummary, qui sont utilisés pour afficher des messages d'erreur lors de la validation. Le Helper Html.ValidationMessage a été modifié avec MVC 3 afin d'afficher désormais le premier message d'erreur utile plutôt que de simplement afficher la première erreur. Durant le binding du modèle, le dictionnaire ModelState peut être alimenté par plusieurs sources avec des messages d'erreur à propos d'une propriété, y compris à partir du modèle lui-même (s'il implémente l'interface IValidatableObject), de la validation des attributs appliqués à la propriété, ainsi que des exceptions rencontrées lors de l'accès à la propriété.

Lorsque la méthode Html.ValidationMessage affiche un message de validation, elle ne prend désormais plus en compte les entrées dans le dictionnaire ModelState qui incluent une exception, parce que ce sont généralement des messages d'erreur qui ne sont pas destinés à l'utilisateur. Au lieu de cela, la méthode recherche le premier message de validation qui n'est pas associé à une exception et affiche ce message. Si aucun message non associé à une exception n'est trouvé, alors elle prendra le premier message qui vient et affichera le message d'erreur générique associé à l'exception. Bref, cette modification apportée par MVC 3 n'aura pas d'impact dans votre manière de coder, mais d'un point de vue utilisateur elle optimisera l'affichage de message d'erreur User-Friendly.

V-D - Modifications apportées aux méthodes des Helpers

Les méthodes des Helpers vous permettent de spécifier en attribut une paire nom/valeur en utilisant un objet anonyme comme on peut le voir ci-dessous :

Html.TextBox("Nom", "Valeur", new {titre = "Titre"})

Cette approche ne vous permet pas d'utiliser des traits d'union dans le nom des attributs, puisque le trait d'union ne peut être utilisé pour le nom d'une propriété en ASP.NET. Cependant, il faut prendre en compte l'arrivée prochaine d'HTML 5 ! Et cette nouvelle version du langage HTML utilisera beaucoup les traits d'union pour les propriétés utilisant "data-" comme préfixe. Dans un même temps, les underscores ne peuvent être utilisés dans les noms d'attributs HTML mais sont valides pour les noms de propriétés en ASP.NET. Tenant compte de cela, et pour permettre l'utilisation des traits d'union en prévision de l'arrivée de HTML5, ASP.NET MVC 3 vous permet maintenant de spécifier les attributs à l'aide d'un objet anonyme en utilisant des underscores afin qu'ils soient transformés en traits d'union lors du rendu HTML. L'exemple ci-dessous illustre cette modification :

Html.TextBox("Nom", "Valeur", new {data_required = "true"})

Le code ci-dessus verra donc son underscore (sur l'attribut "data_required") transformé en trait d'union lors de l'exécution du Helper. Nous aurons donc au final le rendu HTML suivant :

<input data-required="true" id="Nom" name="Nom" type="textbox" value="Valeur" />

Nous vous recommandons de lire ces deux articles sur les différents Helpers Html : partie 1, partie 2.

V-E - Surcharge des méthodes LabelFor et LabelForModel

ASP.NET MVC 3 inclut des surcharges des méthodes LabelForModel et LabelFor. Ces nouvelles surcharges vous permettent de spécifier ou remplacer le texte du Label.

VI - Modifications dans les types d'Action Result

Comme mentionné dans l'introduction, chaque Action d'un contrôleur doit renvoyer un objet de type ActionResult. Au fur et à mesure de l'évolution du Framework, de nouveaux ActionResult sont ajoutés, souvent pour simplifier l'écriture du code.

Dans la version 3 de MVC, deux nouveaux types, HttpNotFoundResult et HttpStatusCodeResult ont fait leur apparition. De plus, un des anciens types d'ActionResult, RedirectResult, a été étendu.

VI-A - HttpNotFoundResult

Ce nouveau type a pour but de simplifier la gestion d'une page n'existant pas, ou un Id non valide.

Imaginons que notre application MVC possède une Action View sur le contrôleur UserController, permettant de voir les détails d'un utilisateur.

Si l'on veut, lorsqu'un utilisateur entre un Id qui n'est pas valide, le rediriger vers une page avec un status code de 404, on peut désormais faire :

public ActionResult View(int id) {
if (!UserRepository.Exists(id)) {
return new HttpNotFoundResult("Cet utilisateur n'existe pas");
}
return View(GetUserInformation(id));
}

A noter, une méthode raccourcie, HttpNotFound(), a aussi été ajoutée.

VI-B - HttpStatusCodeResult

HttpStatusCodeResult permet de gérer l'envoi, à l'utilisateur, du status code de notre choix.

La gestion d'un status code spécifique est à réserver à des cas d'optimisation pour les moteurs de recherche, où l'on va vouloir utiliser des status codes bien spécifiques pour indiquer, par exemple, que la ressource n'est définitivement plus disponible.

On pourrait, par exemple, utiliser cet ActionResult pour indiquer, en fonction du contexte fonctionnel, qu'une ressource n'est plus disponible (statut 410), que l'accès à la page est interdit (code 430), ou tout autre code.

Par exemple, supposons que l'on ait une action sur une page donnée permettant à l'utilisateur de récupérer son mot de passe, mais que cette action ne soit accessible que pendant un temps donné après que l'utilisateur ait demandé un nouveau mot de passe, on pourrait désormais l'implémenter ainsi :

public ActionResult GenerateNewPassword(int id) {
if (!UserRepository.Exists(id)) {
return new HttpNotFoundResult("Cet utilisateur n'existe pas");
}
var user = UserRepository.LoadById(id);
if (user.DateNewPassWordRequested < DateTime.Now.AddHours(-2))         return new HttpStatusCodeResult(410);
return View(GetUserInformation(id));
}

VI-C - Redirections permanentes avec RedirectResult

La classe RedirectResult possède désormais un nouveau paramètre dans son constructeur.

Ce paramètre booléen permet de spécifier une redirection permanente (en termes de code de retour, on passe de 302 à 301).

Pour l'utiliser directement, il nous faut faire :

public ActionResult Index(){    return new RedirectResult("http://www.developpez.com", true);}

Ceci dit, le cas général ne sera pas d'appeler directement RedirectResult, mais plutôt d'utiliser une des méthodes de redirection de la classe Controller, laquelle expose :

- RedirectPermanent: permet une redirection permanente sur une Url donnée ;

- RedirectToRoutePermanent: permet une redirection permanente sur une route ;

- RedirectToActionPermanent : permet une redirection permanente sur une Url donnée.

VII - Meilleur support JavaScript et Ajax

VII-A - JavaScript discret pour Ajax

Le JavaScript a une réputation de langage de script limité et mal fait, inadapté à des développements massifs. Cette réputation est due à une longue accumulation de mauvaises pratiques mais aussi à une implémentation différente d'un navigateur à l'autre. La récente émergence de standards appliqués aux navigateurs, de Framework JavaScript (jQuery, Prototype, Archetype...) et les premiers débogueurs de bonne qualité rendent possible la production d'un JavaScript organisé et évolutif. Le JavaScript discret peut être vu comme une partie du mouvement des standards Web. Il est courant d'observer un code présenté ainsi :

<input type="text" name="date" onchange="validateDate(this);" />

L'appel JavaScript est inclus au sein du HTML. Cela rend plus difficile la lecture et la maintenance du code (ce qui vaut au JavaScript sa mauvaise réputation de code instable et difficile à maintenir), mais surtout, cela mélange la couche relevant proprement des données du document et la couche relevant du comportement de l'interface. Il est pourtant possible de faire la même chose de la manière suivante : on réalise du pur HTML d'une part, et on attache un événement depuis un fichier JavaScript d'autre part. Ce qui donne, en HTML :

<input type="text" name="date" id="datefield" />

Et dans le fichier JavaScript :

document.getElementById( "datefield" ).addEventListener( 'change', function(){ do_something(); }, false );

Cette manière de procéder est appelée discrète, on parle alors de JavaScript discret ou d'Unobtrusive JavaScript en anglais, car l'association avec l'action JavaScript n'est plus incluse dans le HTML mais se réalise par l'assignation d'un événement sur un élément du DOM. Le code JavaScript se rattache alors sur le DOM par la sélection de l'attribut id de l'élément HTML.

Avec ASP.NET MVC 1.0, les helpers Ajax étaient implémentés comme des méthodes d'extension de la classe AjaxHelper (et disponibles via la propriété Ajax de votre vue). Deux catégories d'aides helpers Ajax étaient fournies : Ajax links et Ajax forms. Ces deux catégories font fondamentalement la même chose : faire une demande asynchrone, et faire quelque chose avec le résultat lorsque vous avez terminé (y compris savoir si vous avez reçu une réponse de type succès ou échec en provenance du serveur).

Avec ASP.NET MVC 3, le moteur d'exécution a été mis à jour pour supporter JavaScript discret. Un consommateur a également été créé pour ces attributs JavaScript discret qui utilisent jQuery pour effectuer les requêtes Ajax.

Les méthodes d'extension AjaxHelper (ActionLink et RouteLink pour les liens Ajax, BeginForm et BeginRouteForm pour les formulaires Ajax) ont des API qui sont très similaires à leurs homologues HtmlHelper. La principale différence est que les versions AjaxHelper prennent toutes un paramètre supplémentaire sous la forme de la classe AjaxOptions :

public class AjaxOptions {
public string Confirm { get; set; }
public string HttpMethod { get; set; }
public InsertionMode InsertionMode { get; set; }
public int LoadingElementDuration { get; set; }
public string LoadingElementId { get; set; }
public string OnBegin { get; set; }
public string OnComplete { get; set; }
public string OnFailure { get; set; }
public string OnSuccess { get; set; }
public string UpdateTargetId { get; set; }
public string Url { get; set; }
}

Les propriétés ci-dessus sont toutes utilisées pour stipuler exactement ce que vous voulez que votre appel Ajax fasse.

Lorsque le mode discret est désactivé, MVC génèrera le JavaScript en mode inline sur vos balises a et form qui dépendent de la bibliothèque ASP.NET Ajax Library (MicrosoftAjax.js) et la bibliothèque MVC Ajax qui utilise ASP.NET Ajax (MicrosoftMvcAjax.js) :

<form
action="/ajax/callback"
id="form0"
method="post"
onclick="Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));"
onsubmit="Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, loadingElementId: 'loading', updateTargetId: 'updateme' });">

Ce comportement est identique à celui des versions 1 et 2 d'ASP.NET MVC.

Lorsque le mode discret est activé pour le Javascript avec ASP.NET MVC 3, le code HTML qui sera généré lors du rendu sera sensiblement différent, comme on peut le voir ci-dessous :

<form
action="/ajax/callback"
data-ajax="true"
data-ajax-loading="#loading"
data-ajax-mode="replace"
data-ajax-update="#updateme"
method="post">

La première chose que vous remarquerez est que le code HTML est très lisible (ce qui est très utile pour le débogage). L'utilisation de noms et de valeurs simples augmente la lisibilité. Il est également important de souligner la taille réduite du code HTML généré en mode JavaScript discret. Le code est compatible avec HTML 5, en utilisant la syntaxe d'attribut extensible, préfixé par "data-". Les sélecteurs CSS sont également utilisés pour une flexibilité accrue côté client pour le choix du chargement et de la mise à jour des éléments.

Avec ASP.NET MVC 3, un seul flag est nécessaire pour activer le mode discret du JavaScript, ce qui active à la fois le JavaScript discret et la validation discrète côté client. Le mode discret du JavaScript est désactivé par défaut afin de fournir une compatibilité ascendante avec les projets mis à niveau à partir des anciennes versions d'ASP.NET MVC. Cependant, pour les projets créés directement avec la version 3, donc via le template de projet, le mode discret est activé par défaut.

Pour activer ou non le mode discret du JavaScript pour une application ASP.NET MVC, on peut utiliser le fichier Web.config :

<configuration>
<appSettings>
<add key="UnobtrusiveJavaScriptEnabled" value="true"/>
</appSettings>
</configuration>

L'activation ou la désactivation sont également possibles via le code-behind :

HtmlHelper.UnobtrusiveJavaScriptEnabled = true;

L'utilisation du code-behind pour activer ou désactiver le mode discret est contextuelle. Plus précisément si vous réalisez ceci dans votre fichier Global.asax, le changement de mode sera appliqué à l'ensemble de l'application. A contrario si vous l'utilisez dans un contrôleur, le changement de mode ne concernera que l'action en cours.

En plus d'activer le mode discret, vous aurez également besoin d'inclure deux fichiers de script : la bibliothèque jQuery (~/Scripts/jquery-1.4.1.js) et le plugin pour MVC d'Ajax discret avec jQuery (~/Scripts/jquery.unobtrusive-ajax.js). Une remarque importante : si vous oubliez d'inclure l'un ou l'autre script, vous ne verrez pas d'erreur lors de l'utilisation de la requête Ajax, celle-ci va tout simplement se comporter comme une requête non-Ajax, donc en synchrone.

Le tableau ci-dessous liste la correspondance entre les membres de la classe AjaxOptions et les attributs de donnée HTML 5 :

Membre de AjaxOptions

Attribut HTML 5

Confirm

data-ajax-confirm

HttpMethod

data-ajax-method

InsertionMode

data-ajax-mode

LoadingElementDuration

data-ajax-loading-duration

LoadingElementId

data-ajax-loading

OnBegin

data-ajax-begin

OnComplete

data-ajax-complete

OnFailure

data-ajax-failure

OnSuccess

data-ajax-success

UpdateTargetId

data-ajax-update

Url

data-ajax-url

VII-B - jQuery discret pour la validation

Pour rappel le système de validation côté client depuis ASP.NET MVC 2.0 a été découplé du système de validation côté serveur par l'utilisation de JSON. Par rapport à la validation côté serveur, la validation côté client nécessite une meilleure compréhension de la programmation JavaScript.

Comme nous venons de le voir, une des améliorations ASP.NET MVC V3 concerne les helpers Ajax et Validation qui permettent une approche discrète du JavaScript. Le JavaScript discret permet d'éviter l'injection inline de code JavaScript dans une balise HTML, et permet ainsi une séparation plus nette en utilisant la convention "data" d'HTML 5. Cela rend votre code HTML plus court et plus propre, et rend plus facile la permutation ou la personnalisation des librairies JavaScript. Les Helpers ASP.NET MVC 3 pour la validation utilisent maintenant le plugin jQueryValidate par défaut. Dans les précédentes versions vous aviez besoin d'appeler explicitement Html.EnableClientValidation() afin de permettre la validation côté client. Ce qui n'est plus nécessaire avec l'arrivée de MVC 3 : la validation côté client, en utilisant une approche discrète, est maintenant activée par défaut.

Pour plus de détails à ce sujet, nous vous recommandons la lecture de ces très bons articles :

- Experience ASP.NET MVC 3 - the Unobtrusive jQuery-Based Ajax Support

- ASP.NET MVC 3 unobtrusive validation

VII-C - Précisions sur l'activation du JavaScript discret et la validation côté client

Vous pouvez activer ou désactiver la validation client et le JavaScript discret globalement à l'aide des membres statiques de la classe HtmlHelper, comme dans l'exemple suivant :

HtmlHelper.ClientValidationEnabled = true;
HtmlHelper.UnobtrusiveJavaScriptEnabled = true;

Comme indiqué précédemment, les nouveaux projets ASP.NET MVC 3 ont le mode discret du JavaScript activé par défaut. Vous pouvez également activer ou désactiver ces fonctionnalités dans le fichier Web.config en utilisant les paramètres suivants :

<configuration>
<appSettings>
<add key="ClientValidationEnabled" value="true"/>
<add key="UnobtrusiveJavaScriptEnabled" value="true"/>
</appSettings>
</configuration>

Parce que vous pouvez activer ces fonctionnalités par défaut, de nouvelles surcharges ont été introduites à la classe HtmlHelper qui vous permettent de remplacer les paramètres par défaut, comme illustré dans les exemples suivants :

public void EnableClientValidation();
public void EnableClientValidation(bool enabled);
public void EnableUnobtrusiveJavaScript();
public void EnableUnobtrusiveJavaScript(bool enabled);

Pour la compatibilité ascendante, ces deux fonctionnalités sont désactivées par défaut.

VII-D - Nouvelle classe JsonValueProviderFactory

Quiconque a été impliqué dans un projet ASP.NET MVC avec utilisation importante d'Ajax aura probablement remarqué que quelque chose manquait toujours. Imaginez le scénario suivant :

<% using (Html.BeginForm()) { %>
<p>
<%=Html.LabelFor(m => m.Username)%>
<%=Html.TextBoxFor(m => m.Username)%>
</p>
<p>
<%=Html.LabelFor(m => m.Password)%>
<%=Html.TextBoxFor(m => m.Password)%>
</p>
<%} %>
<script type="text/JavaScript">
$("form").submit(function (evt) {
// extract values to submit
var form = $(this),
username = form.find("[name=Username]").val(),
password = form.find("[name=Password]").val(),
json = JSON.stringify({
Username: username,
Password: password
});

$.ajax({
url: form.attr("action"),
type: 'POST',
contentType: 'application/json; charset=utf-8',
dataType: 'json',
data: json,
success: handleLogin
});

// stop form submitting
evt.preventDefault();
});
</script>

Nous utilisons donc JSON pour envoyer le username et le password au serveur. Ce qui exécute un POST à l'action suivante :

[HttpPost]
public ActionResult Index(LoginRequest request)
{
// do login
return Json(new
{
Success = true
});
}

Avec ASP.NET MVC 2 le code suivant ne fonctionnerait pas immédiatement. Le binder du modèle par défaut en MVC 2 utilise les paramètres de la requête pour binder les propriétés du modèle, mais dans ce cas, il n'y en a pas puisque le contenu Ajax est le corps de la requête. Pour répondre à ce genre de problème dans MVC 2, nous devons fournir un binder personnalisé pour le modèle qui sait comment traiter les requêtes JSON :

public class JsonModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
HttpRequestBase request = controllerContext.HttpContext.Request;
string jsonStringData = new StreamReader(request.InputStream).ReadToEnd();
return JsonConvert.DeserializeObject(jsonStringData, bindingContext.ModelType);
}
}

Puis nous modifions notre action pour lui indiquer d'utiliser ce binder personnalisé :

[HttpPost]
public ActionResult Index([ModelBinder(typeof(JsonModelBinder))]LoginRequest request)
{
// do login
return Json(new
{
Success = true
});
}

ASP.NET MCV3 comble désormais cette lacune grâce à la nouvelle classe JsonValueProviderFactory. Celle-ci opère à un niveau supérieur à celui du binder du modèle. Cette classe, lorsqu'une requête JSON est reçue, tire les valeurs hors du corps JSON comme des paires clé/valeur. Ce qui signifie qu'elles sont à la disposition du binder du modèle dont le binder par défaut. Bref, plus besoin comme en MVC 2 d'un binder personnalisé !

Grâce à JsonValueProviderFactory fournie par MVC 3, le code de notre action sera simplifié comme ceci :

[HttpPost]
public ActionResult Index(LoginRequest request)
{
// do login
return Json(new
{
Success = true
});
}

Pour vérifier simplement la différence de traitement de la requête JSON par MVC 3 avec ou sans JsonValueProviderFactory, vous pouvez utiliser ce code dans l'Application_Start du Global.asax :

var jsonValueProviderFactory = ValueProviderFactories.Factories.OfType<JsonValueProviderFactory>().First();
ValueProviderFactories.Factories.Remove(jsonValueProviderFactory);

Vous remarquez alors que le paramètre request de type LoginRequest verra toutes ses propriétés non renseignées avec le JsonValueProviderFactory désactivé.

VIII - Amélioration du cache de sortie

Le cache de sortie est une fonctionnalité de la plateforme ASP.NET qui existe depuis la version 1.0.

Le principe est simple, à savoir qu'une page ASP.NET va mettre en cache tout ou partie du code HTML généré par une page Web, ce qui permettra d'accélérer les accès ultérieurs à la page, en diminuant le temps de rendu.

Cette fonctionnalité était bien entendu disponible dans les versions antérieures du Framework MVC, grâce à l'attribut OutputCache, mais avec une limitation non négligeable. En effet, elle est basée sur la philosophie de rendu spécifique de Web Forms, et, en conséquence, embarquait les limitations suivantes :

- pas de cache partiel possible : en effet, le cache de sortie cachant des contrôles serveur, qui ne sont (normalement) pas utilisés en MVC, il fallait se résoudre à soit cacher l'ensemble de la page, soit ne rien cacher du tout ;

- contournement des filtres d'action : l'attribut OutputCache étant appelé en tout début de cycle de vie, il contournait la plupart des filtres d'action, ce qui pouvait produire des comportements non voulus.

Avec la version 3, il est désormais possible de cacher le résultat d'actions appelées par RenderAction depuis la page principale.

Par exemple, supposons que nous voulions afficher à un utilisateur une zone lui souhaitant la bienvenue.

Pour cela, on va commencer par créer, dans le répertoire Share, une nouvelle vue Meteo.cshtml:

<h2>Meteo</h2>
Dans la ville de @ViewBag.Ville, il fait @ViewBag.Temperature degrés.
<br />
<img src="@ViewBag.ImgSource" alt="Alternate Text" />

On va ensuite ajouter, dans notre contrôleur HomeController, une Action spécifique Meteo (en admettant qu'on ait une fonction GetCurrentMeteo, qui, pour une zone géographique, renvoie un objet météo) :

[ChildActionOnly]
public ActionResult Meteo(int zoneGeographique)
{
var meteo = GetCurrentMeteo(zoneGeographique);
ViewBag.Ville = meteo.Ville;
ViewBag.Temperature= meteo.Temperature;
ViewBag.ImgSource = meteo.ImgSource;

return PartialView();
}

Et, dans la page d'accueil, on va afficher cette page de météo de la façon suivante (en admettant qu'on ait mis dans le ViewBag la zone géographique de l'utilisateur) :

@{Html.RenderAction("Meteo", "Home", new { zoneGeographique = ViewBag.zoneGeographique });}

Finalement, on va activer le cache de sortie. Il nous suffira pour cela, dans le contrôleur, d'ajouter l'attribut OutputCache sur l'action Meteo, de le configurer pour varier par Zone, avec une expiration à 60 secondes, de la façon suivante :

[ChildActionOnly]
[OutputCache(Duration=60)]
public ActionResult Meteo(int zoneGeographique)
{

Et pendant la prochaine minute, aucun appel ne sera fait à Meteo.

On a aussi la possibilité de lier le rafraichissement des données au paramètre passé à l'action de la façon suivante :

[ChildActionOnly]
[OutputCache(Duration=600, VaryByParam = "zoneGeographique")]
public ActionResult Meteo(int zoneGeographique)
{

Le code ci-dessus permettra de ne rafraichir la vue qu'une fois toutes les 10 minutes, ou au changement de zone géographique.

IX - Améliorations diverses

IX-A - Sessionless Controllers

Lorsqu'on développe un site Web à haute performance, il est nécessaire d'utiliser une ferme Web ainsi que du Load Balancing. On doit ainsi centraliser le stockage de la session dans une base de données. Mais que faire lorsqu'on souhaite simplement se passer complètement de la Session, ou l'utiliser uniquement en lecture seule ? ASP.NET MVC 3 introduit un nouvel attribut pour les contrôleurs : SessionStateAttribute. Il est maintenant possible de décorer un contrôleur avec cet attribut afin de désactiver la session, l'utiliser uniquement en lecture seule, ou normalement en écriture. L'exemple ci-dessous sera plus parlant :

[SessionState(SessionStateBehavior.Disabled)]
public class HomeController : Controller
{
public ActionResult Index()
{
TempData["CurrentTime"] = DateTime.Now;
return View();
}
}

L'attribut SessionStateAttribute est positionné sur Disabled dans le code précédent, ainsi l'appel à l'action Index déclenchera une exception. Pour rappel TempData utilise la Session pour le stockage.

Notez qu'il est également possible d'appliquer cet attribut en une fois à tous les contrôleurs. Pour ce faire on peut utiliser la nouveauté apparue avec MVC 3 et décrite plus haut : les Global Action Filters, comme ci-dessous.

public class MvcApplication : System.Web.HttpApplication
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
filters.Add(new SessionStateAttribute(SessionStateBehavior.Disabled));
}

protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();

RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
...
}

Voici la liste des valeurs possibles pour le type Enum SessionStateBehavior :

- Disabled : la Session n'est pas active pour exécuter cette requête ;

- Default : on utilise la logique par défaut d'ASP.NET pour déterminer le comportement de la Session pour cette requête ;

- ReadOnly : la Session n'est active qu'en lecture seule pour cette requête ;

- Required : la Session est active en lecture / écriture pour cette requête.

IX-B - Support de code s'exécutant avant celui de la vue (_viewstart.cshtml)

Afin de respecter le principe DRY, pour "Don't Repeat Yourself", les vues Razor supportent maintenant une portion de code qui sera exécutée avant celle de la vue mais aussi appliquée à toutes les vues d'une application ASP.NET MVC. Cette portion de code peut être appliquée dans un fichier spécial nommé _ViewStart.cshtml pour le langage C# et _ViewStart.vbhtml pour VB.NET. Ce nouveau fichier devient un emplacement idéal pour placer tout ce qui peut être commun aux vues.

Par défaut ce fichier est très court et contient simplement la définition du Layout. Le code est exécuté avant celui de la vue requêtée :

@{
Layout = "~/Views/Shared/_Layout.cshtml";
}

Ce fichier a également un périmètre, il est utilisé uniquement par les vues qui sont situées dans le même répertoire et sous-répertoires que celui-ci. Les vues situées dans un autre répertoire ne sont donc pas affectées par le fichier _ViewStart. Comme on peut le voir sur l'image ci-dessous, le fichier _ViewStart.cshtml est localisé par défaut dans le répertoire Views. Il couvre donc les répertoires Home et Shared. Notez toutefois qu'il ne peut être appliqué aux vues avec accès restreint (comme c'est le cas pour _Layout.cshtml).

Bien entendu il est possible d'ajouter plus de fonctionnalités au fichier _ViewStart.cshtml si besoin. Cependant, cela ne concerne que les traitements qui ne peuvent être facilement appliqués aux vues par les pages Layout.

IX-C - IMetadataAware

La nouvelle interface IMetadataAware vous permet d'écrire des attributs qui simplifient la façon dont vous pouvez contribuer au processus de création du ModelMetadata. Avant l'arrivée d'ASP.NET MVC 3 et la disponibilité de cette interface, il était nécessaire d'écrire un fournisseur de métadonnées personnalisé afin d'avoir un attribut pour fournir des métadonnées supplémentaires.

Cette interface est consommée par la classe AssociatedMetadataProvider, ainsi le support pour l'interface IMetadataAware est automatiquement hérité par toutes les classes qui dérivent de cette classe (notamment, la classe DataAnnotationsModelMetadataProvider).

Pour plus d'informations à ce sujet, vous pouvez consulter cet article en deux parties : partie un, partie deux.

IX-D - ViewBag

Si vous utilisez ASP.NET MVC et ViewData, vous devriez être assez à l'aise avec la nouveauté ViewBag introduite dans ASP.NET MVC 3. ViewBag n'est rien de plus qu'un emballage pratique pour rendre plus facile le stockage et la récupération des paires clé / valeur dans le magasin de stockage utilisé par ViewData.

Par exemple, lorsque vous enregistrez un message dans ViewData au sein d'un contrôleur en utilisant ViewData["MonMessage"]="Mon message", vous pouvez afficher le message dans votre vue en utilisant @ViewBag.Message. A l'inverse vous pouvez également enregistrer le message au sein d'un contrôleur avec ViewBag.MonMessage="Mon Message", et vous pouvez le récupérer dans la vue en utilisant @ViewData["MonMessage"]. ViewData et ViewBag représentent donc les mêmes données. ViewBag est simplement un wrapper dynamique autour des mêmes données, vous permettant d'y accéder d'une manière plus commode. Voici un exemple d'un contrôleur utilisant ViewBag pour stocker les données :

public ActionResult Index()
{
ViewBag.Date = DateTime.Now;

ViewBag.BilletDeBlog = new BilletDeBlog
{
Titre = "Mon billet",
Corps = "Bla bla bla bla.",
Auteur = "Moi"
};

ViewBag.Tags = new[]
{
"ASP.NET MVC 3",
"jQuery",
"ASP.NET 4.0",
"Visual Studio 2010"
};

return View();
}

Vous pouvez accéder à ces éléments stockés dans le ViewBag de la même façon dans la vue :

@{
ViewBag.Title = "Mon Blog";
}

<h2>@ViewBag.BilletDeBlog.Titre</h2>
@Html.Raw(ViewBag.BilletDeBlog.Corps)
<p>Date : @ViewBag.Date.ToShortDateString() </p>
<ul>
@foreach (var tag in ViewBag.Tags)
{
<li>@tag</li>
}
</ul>

Ce qui donnera le résultat suivant :

Un des gros avantages de l'utilisation de ViewBag, c'est que nous n'avons pas besoin de caster ViewBag.Date en DateTime ou ViewBag.BilletDeBlog en BilletDeBlog. Cela réduit le bruit dans la vue et améliore la lisibilité du code (en plus du gain apporté par le nouveau moteur Razor).

Précision : il n'y a pas de prise en charge IntelliSense pour ViewBag. Lorsque vous utilisez ViewBag dans une vue, vous n'obtiendrez pas une liste de propriétés que vous avez spécifiées dans le contrôleur. ViewBag est de type dynamique et il est logique qu'on ne puisse avoir l'Intellisense sur les types dynamiques. Ce qu'il faut retenir c'est que ViewBag est simplement une manière différente d'accéder à l'espace de stockage utilisé par ViewData.

X - Upgrader ses projets ASP.NET MVC 2 vers MVC 3

Nous parlons de toutes les nouveautés de la version 3 du Framework MVC, mais qu'en est-il des projets utilisant la version 2 et qui voudraient bénéficier des avantages de la dernière version ? En effet, comme vu précédemment, les modifications apportées touchent de nombreux éléments de la solution (version de JQuery, bibliothèques utilisées, etc.).

La question se pose donc de savoir comment faire pour facilement mettre à jour les projets de V2 en V3. Heureusement, un utilitaire est disponible sur CodePlex pour traiter ce problème: ASP.NET MVC 3 Application Upgrader.

Ce projet permet la conversion automatique d'un projet MVC 2, avec comme cible ASP.NET 4, dans une solution au format Visual Studio 2010. Les projets dans une autre configuration auront à être mis à jour au préalable.

Pour montrer le fonctionnement de l'application, nous allons convertir un projet (celui du challenge Azure) en MVC 3. Je vais donc lancer l'application et sélectionner mon fichier de solution :

L'application va me lister tous les éléments de la solution sur lesquels une action va éventuellement être effectuée, à savoir :

- fichier de projet ;

- web.config ;

- solution.

On verra aussi toutes les modifications que le projet va subir durant la conversion. L'outil commençant par une sauvegarde du projet, il ne risque pas de vous faire perdre le travail en cas de problème, n'hésitez donc pas à vous en servir.

Si les fichiers sont en lecture seule, la conversion va échouer. Pensez, si vous avez un contrôleur de code source qui verrouille les fichiers, à les extraire au préalable.

Liens

- Blog de Brad Wilson

- Blog de David Hayden

- Blog de Scott Guthrie

- Blog de Scott Hanselman

- Release Notes officielles ASP.NET MVC 3

Conclusion

Avec cette nouvelle version, ASP.NET MVC passe un cap de maturité. La plupart des nombreuses petites améliorations apportées vont faciliter la vie des développeurs, tandis que le moteur Razor va changer leur façon de coder. Nous espérons que les développeurs ASP.NET Web Forms sauront apprécier mieux MVC avec la version 3 et que cela démocratisera son utilisation.

Remerciements

Nous remercions Claude LELOUP et Jean-Michel Ormes pour la relecture orthographique ainsi que Thomas Levesque pour ses remarques.