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.
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 :
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▲
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.