[ASP.NET MVC] Nouveautés MVC 3 Part 2 - Améliorations pour la Validation

Publié le 16 février 2011 par Nicolasesprit

Suite de la série de billets sur les nouveautés d'ASP.NET MVC 3 écrits conjointement avec Philippe Viallate, MVP et responsable de la rubrique .NET sur Developpez.com. Vous pouvez consulter la première partie : [ASP.NET MVC] Nouveautés MVC 3 Part 1 - Améliorations dans Visual Studio 2010

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

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ée pour valider des objets, des propriétés et des méthodes lorsqu'elle est incluse 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.

Nouveaux attributs de validation

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

CompareAttribute

L'idée derrière CompareAttribute est très simple. Cet attribut permet de vérifier qu'une propriété à 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 ail 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 coté client, on verra le message apparaître directement lors de la saisie, sans avoir besoin d'un aller-retour avec le serveur. 

RemoteAttribute

Autant la validation coté client avec MVC 2 était simple à mettre en place, autant dès qu'il fallait vérifier une donnée coté serveur...on se retrouvait à devoir mettre les mains dans le cambouis. Avec MVC V3, valider une donnée coté 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 coté 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
  • Eventuellement, 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à. 

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 laissions à 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.

IValidatableObject

L'interface IValidatableObject permet de gérer des scenarios 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 coté client va très bien fonctionner si l'utilisateur a activé javascript. Autant il va quand même falloir vérifier coté 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
}

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 coté 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.