• 30 hours
  • Medium

Free online content available in this course.

course.header.alt.is_certifying

You can get support and mentoring from a private teacher via videoconference on this course.

Got it!

Last updated on 11/23/17

Valider les données

Log in or subscribe for free to enjoy all this course has to offer!

Validation de modèle

Même s’il a trait au modèle, j’ai choisi d’écrire ce chapitre au niveau du contrôleur car la plupart des choses sont faites dans le contrôleur et que nous avions besoin de savoir comment récupérer les données postées via le contrôleur. Je m’excuse par avance si vous trouvez que je fais un écart disgracieux au plan de ce cours.

Maintenant que nous savons récupérer un modèle modifié suite à une soumission de formulaire, il est temps de vérifier que notre utilisateur n’a pas fait n’importe quoi. Et oui, difficile de faire confiance à un utilisateur lambda. Qui me dit par exemple qu’il ne va pas oublier de saisir un nom de restaurant alors que celui-ci est obligatoire ?
Eh oui, rappelez-vous, sur notre modèle nous avions dit que le nom du restaurant devait être obligatoire :

[Table("Restos")]
public class Resto
{
    public int Id { get; set; }
    [Required]
    public string Nom { get; set; }
    [Display(Name="Téléphone")]
    public string Telephone { get; set; }
}

C’est lui, c’est l’attribut Required  qui est là pour ça. Vous me direz, nous pourrions très bien envisager de faire un petit contrôle dans notre action, du genre :

[HttpPost]
public ActionResult ModifierRestaurant(Resto resto)
{
    if (string.IsNullOrWhiteSpace(resto.Nom))
    {
        ViewBag.MessageErreur = "Le nom du restaurant doit être rempli";
        return View(resto);
    }
    using (IDal dal = new Dal())
    {
        dal.ModifierRestaurant(resto.Id, resto.Nom, resto.Telephone);
        return RedirectToAction("Index");
    }
}

Et puis rajouter dans la vue un petit message du genre :

<div>
    @Html.LabelFor(model => model.Nom)
    @Html.TextBoxFor(model => model.Nom)
    <span style="color:red">@ViewBag.MessageErreur</span>
</div>

Ce qui nous donnerait :

Validation simple de formulaire
Validation simple de formulaire

Et pourquoi pas… Cela fonctionnerait…
Sauf que !!!! Pas besoin de faire tout cela, ASP.NET MVC sait le faire pour nous. :) Chouette.

Il suffit de regarder le contenu de la propriété IsValid  de l’objet ModelState . Par exemple, si je ne saisis pas de valeur dans le champ nom, alors IsValid  vaudra false . Nous pourrons donc changer le code précédent par :

[HttpPost]
public ActionResult ModifierRestaurant(Resto resto)
{
    if (!ModelState.IsValid)
    {
        ViewBag.MessageErreur = ModelState["Nom"].Errors[0].ErrorMessage;
        return View(resto);
    }
    using (IDal dal = new Dal())
    {
        dal.ModifierRestaurant(resto.Id, resto.Nom, resto.Telephone);
        return RedirectToAction("Index");
    }
}

En fait, tout le résultat de la validation est dans l’objet ModelState . Cet objet contient également un dictionnaire avec nos trois propriétés de notre Resto  : Id , Nom  et Téléphone . Seul celui correspondant au nom possède une erreur et nous retrouvons le message d’erreur qui lui est associé dans la collection Errors. Ce message s’affiche donc dans notre vue :

Validation de formulaire par la propriété ModelState
Validation de formulaire par la propriété ModelState

Ce message d’erreur est créé automatiquement par le framework de validation à partir de ce que nous avons dans le modèle :

[Required]
public string Nom { get; set; }

Si nous souhaitons modifier le message d’erreur, nous pouvons l’indiquer dans l’attribut :

[Required(ErrorMessage = "Le nom du restaurant doit être saisi")]
public string Nom { get; set; }

Par contre, c’est assez contraignant de devoir trouver le message d’erreur dans le ModelState , de le passer dans le ViewBag  et de créer une balise pour l’afficher dans notre vue. Heureusement, il existe une solution plus élégante pour le faire : utiliser le helper Html.ValidationMessage  et plus particulièrement son équivalent typé. Il vous suffit simplement de remplacer notre <span>  par cet appel de helper :

@Html.ValidationMessageFor(model => model.Nom)

Et tout est automatique. Plus besoin d’aller chercher le message d’erreur, ni de remplir le ViewBag… Bref, il n’y a plus rien à faire dans le contrôleur à part vérifier que le modèle est valide :

[HttpPost]
public ActionResult ModifierRestaurant(Resto resto)
{
    if (!ModelState.IsValid)
        return View(resto);
    using (IDal dal = new Dal())
    {
        dal.ModifierRestaurant(resto.Id, resto.Nom, resto.Telephone);
        return RedirectToAction("Index");
    }
}

Et dans la vue, nous avons au final :

@using (Html.BeginForm())
{
    <fieldset>
        <legend>Modifier un restaurant</legend>

        <div>
            @Html.LabelFor(model => model.Nom)
            @Html.TextBoxFor(model => model.Nom)
            @Html.ValidationMessageFor(model => model.Nom)
        </div>
        <div>
            @Html.LabelFor(model => model.Telephone)
            @Html.TextBoxFor(model => model.Telephone)
        </div>
        <br />
        <input type="submit" value="Modifier" />
    </fieldset>
}

Et le résultat est le même, au détail près que la couleur n’est pas rouge :

Validation via le framework de validation
Validation via le framework de validation

Si nous regardons le HTML généré pour ce message d’erreur, nous avons :

<span class="field-validation-error" data-valmsg-for="Nom" data-valmsg-replace="true">Le nom du restaurant doit &#234;tre saisi</span>

Le nom de l’attribut class est celui qui est utilisé par défaut. Il est possible d’utiliser cet attribut pour rajouter la couleur rouge. D’ailleurs, ce style est créé par défaut lorsque vous créez un projet ASP.NET MVC, il se situe à cet emplacement /Content/Site.css.
Il s’agit d’une feuille de style tout bête créée par Visual Studio. Nous pouvons l’ajouter à notre vue, dans la section <head>  :

<link type="text/css" href="../../Content/Site.css" rel="stylesheet" />

Nous pouvons désormais voir que notre page a un nouveau look, grâce à la feuille de style :

Validation avec du style
Validation avec du style

Et voilà, une bonne petite validation qui fonctionne. ^^

Et c’est LA validation, the only one. C’est-à-dire que c’est celle qui est indispensable et qui se fait côté serveur, par le contrôleur, et qui permet de valider complètement les règles métiers de notre application. On parle de validation côté serveur parce que c’est bien le serveur web (et donc ASP.NET MVC) qui reçoit une requête avec la soumission du formulaire et qui peut en vérifier le contenu avant de décider si elle est correcte ou pas.

<style type="text/css">
    .field-validation-error { color : #f00;}
	.validation-summary-errors { color : #f00; font-weight: bold};
	.input-validation-error { border: 2px solide #f00;background-color : #fee}
	input[type="checkbox"].input-validation-error { outline: 2px solid #f00;}
</style>

Validation coté client

Mais il existe une autre validation, celle qui est jolie et qui fait gagner du temps : la validation côté client. Rappelez-vous dans notre introduction au HTML, nous avions parlé de JavaScript et nous avions notamment montré comment faire une mini validation côté client. Il s’agit en fait d’une méthode JavaScript qui est exécutée par le navigateur au moment de la soumission du formulaire et qui empêche cette soumission si jamais nos champs ne sont pas valides.
Cette validation a un fort intérêt parce qu’elle empêche la page d’être envoyée au serveur si jamais il y a un problème, ce qui fait gagner du temps à nos utilisateurs. Par contre, elle ne peut suffire d’elle-même car un utilisateur a la possibilité de désactiver le JavaScript dans son navigateur.

Je vous rassure tout de suite, nous n’allons pas écrire le même genre de JavaScript que nous avions écrit dans la toute première partie car… ASP.NET MVC a déjà tout prévu. ;) Le roi de la simplicité. Il vous suffit d’inclure des scripts déjà prêts à l’emploi et qui en plus, ont été générés dans notre solution. Rajoutez-donc dans la balise <head>  de notre vue les scripts suivants :

<script type="text/javascript" src="../../Scripts/jquery-1.10.2.js"></script>
<script type="text/javascript" src="../../Scripts/jquery.validate-vsdoc.js"></script>
<script type="text/javascript" src="../../Scripts/jquery.validate.unobtrusive.js"></script>

Réaffichez la page de modification d’un restaurant pour tenter de valider notre formulaire sans que le nom ne soit rempli. Impossible ! Vous pouvez constater qu’il n’y a aucun aller-retour serveur et que la page n’est pas postée.

C’est bien sûr grâce aux scripts jQuery que nous avons inclus mais également grâce au fait que les hepers HTML génèrent des attributs complémentaires sur nos balises, comme nous avons vu plus haut.

C’est grâce à ces conventions de nommage que les scripts jQuery de validation sont capables de trouver les bons éléments à valider, ainsi que les bons messages d’erreurs à afficher.

Notez que ce principe de validation fonctionne avec l’attribut Required  que nous avons utilisé sur le nom du restaurant, mais également avec plein d’autres attributs qui permettent de valider plein de choses différentes. Pour limiter la taille maximale du nom du restaurant, nous pouvons par exemple ajouter l’attribut StringLength  :

[StringLength(80)]
public string Nom { get; set; }

Ainsi, vous ne pourrez pas avoir un nom dont la taille est supérieure à 80 caractères. Regardez ce que génère le helper :

<input data-val="true" data-val-length="Le champ Nom doit être une chaîne dont la longueur maximale est de 80." data-val-length-max="80" data-val-required="Le nom du restaurant doit être saisi" id="Nom" name="Nom" type="text" value="Resto pinambour" />

Encore des attributs supplémentaires qui sont générés par le helper et qui sont utilisés par jQuery pour faire notre validation côté client. Rappelez-vous que la même validation doit être faite également côté serveur, au cas où le JavaScript aurait été désactivé, en vérifiant la propriété ModelState.IsValid .
Vous pouvez supprimer le StringLength que nous n’allons pas réutiliser.

Vous pouvez faire des validations encore plus compliquées grâce aux expressions régulières. Cela pourrait servir par exemple à vérifier qu’une adresse email est bien formée. Nous n’en avons pas dans notre modèle, mais nous avons un numéro de téléphone. Grâce aux expressions régulières, nous pouvons vérifier que celui-ci est un numéro de téléphone correct. Pour nous simplifier la vie, nous dirons qu’un numéro de téléphone commence par 0 et est suivi de 9 chiffres (pas d’espaces, pas de / ni de tirets) :

[Table("Restos")]
public class Resto
{
    […]
    [RegularExpression(@"^0[0-9]{9}$")]
    public string Telephone { get; set; }
}

N’oubliez pas de rajouter le message de validation pour le téléphone dans la vue :

@Html.ValidationMessageFor(model => model.Telephone)

Et voilà, lorsque nous saisissons un numéro de téléphone incorrect, nous obtenons un message d’erreur :

Validation par expression régulière
Validation par expression régulière

Bon, OK, le message d’erreur n’est pas vraiment très sympathique… Mais vous savez comment le changer. Il suffit de remplir la propriété ErrorMessage  :

[RegularExpression(@"^0[0-9]{9}$", ErrorMessage="Le numéro de téléphone est incorrect")]

Ce qui donne :

Utilisation d'un message d'erreur personnalisé
Utilisation d'un message d'erreur personnalisé
protected override void Seed(BddContext context)
{
    context.Restos.Add(new Resto { Id = 1, Nom = "Resto pinambour", Telephone = "0102030405" });
    context.Restos.Add(new Resto { Id = 2, Nom = "Resto pinière", Telephone = "0102030405" });
    context.Restos.Add(new Resto { Id = 3, Nom = "Resto toro", Telephone = "0102030405" });

    base.Seed(context);
}

Il existe encore d’autres attributs de validation, voyons en encore un. Si nous voulons par exemple qu’un champ ne soit valide que si sa valeur est comprise entre deux autres, nous pourrons utiliser l’attribut Range . Idéale pour l’âge de notre utilisateur par exemple… Bon, OK, nous n’avons pas cette propriété, mais nous aurions pu. :p

public class Utilisateur
{
    […]
    [Range(18, 120)]
    public int Age { get; set; }
}

Je vous citerai encore sans l’illustrer l’attribut Compare  qui permet de s’assurer que deux champs sont identiques. Cela peut vous servir par exemple dans un formulaire d’inscription, afin de vérifier que l’utilisateur ne s’est pas trompé dans son adresse email en la lui faisant saisir deux fois.

Nous verrons dans le chapitre sur l’Ajax comment réaliser une validation côté client qui nécessite d’exécuter du code sur le serveur.

Validation avancée

Si malgré tout cela vous n’arrivez pas à couvrir tous les scénarios validation que vous souhaitez, il vous reste d’autres options. La première, simple, est de faire vos validations côté serveur, dans votre contrôleur ; un peu comme ce que nous avions fait au tout début, avant de connaître le mécanisme de validation.
Imaginons par exemple que vous ayez fait un formulaire permettant de créer un nouveau restaurant… un imaginaire tout à fait relatif puisque nous allons le faire. :lol: Il pourrait être judicieux avant d’ajouter ce nouveau restaurant, de vérifier qu’il n’existe pas déjà. Pour cela, nous pouvons utiliser la méthode dans notre DAL qui va vérifier si un restaurant existe :

public interface IDal : IDisposable
{
    bool RestaurantExiste(string nom);
    […]
}

Ensuite, il nous faut créer deux méthodes dans le contrôleur (GET et POST) :

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

[HttpPost]
public ActionResult CreerRestaurant(Resto resto)
{
    using (IDal dal = new Dal())
    {
        if (dal.RestaurantExiste(resto.Nom))
        {
            ModelState.AddModelError("Nom", "Ce nom de restaurant existe déjà");
            return View(resto);
        }
        if (!ModelState.IsValid)
            return View(resto);
        dal.CreerRestaurant(resto.Nom, resto.Telephone);
        return RedirectToAction("Index");
    }
}

ainsi que la vue associée CreerRestaurant.cshtml :

@model ChoixResto.Models.Resto

[...]

@using (Html.BeginForm())
{
    <fieldset>
        <legend>Ajouter un restaurant</legend>

        <div>
            @Html.LabelFor(model => model.Nom)
            @Html.TextBoxFor(model => model.Nom)
            @Html.ValidationMessageFor(model => model.Nom)
        </div>
        <div>
            @Html.LabelFor(model => model.Telephone)
            @Html.TextBoxFor(model => model.Telephone)
            @Html.ValidationMessageFor(model => model.Telephone)
        </div>
        <br />
        <input type="submit" value="Ajouter" />
    </fieldset>
}

On utilise la méthode AddModelError  pour indiquer qu’il y a un problème avec la propriété Nom  du restaurant. Nous n’oublions bien sûr pas de vérifier que tous les champs sont valides avec ModelState.IsValid . Ainsi, lorsque le restaurant existe, nous pourrons avoir le résultat suivant :

Validation personnalisée
Validation personnalisée

C’est une solution intéressante qui permet de faire des choses que ne nous permettent pas les scénarios de base prévus par ASP.NET MVC et qui permet éventuellement de tester la cohérence de plusieurs champs, chose que nous ne pouvons pas vraiment faire autrement. Imaginons par exemple que vous ayez un formulaire client où vous devez offrir la possibilité de saisir un numéro de téléphone fixe ou un numéro de téléphone portable ou les deux, et qu’il faille au moins un numéro de téléphone. On ne peut pas envisager d’utiliser l’attribut Required  sur les champs car le résultat est conditionné par ce qu’il y a dans les deux champs. Voilà un exemple de scénario typique où nous pouvons utiliser ce genre de validation.
Sauf que si nous devons faire ce test dans l’action qui permet de créer, puis dans l’action qui permet de modifier, et peut-être ailleurs, nous allons dupliquer plein de code…

Il y a plusieurs solutions pour résoudre ce problème. La première est de faire en sorte que le modèle porte sa propre validation, validation qui doit être compatible avec le mécanisme de validation du framework ASP.NET MVC. Cette solution c’est l’implémentation de l’interface IValidatableObject . Elle impose l’implémentation d’une méthode de validation :

public class Resto : IValidatableObject
{
    public int Id { get; set; }
    public string Nom { get; set; }
    public string Telephone { get; set; }
    public string Email { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (string.IsNullOrWhiteSpace(Telephone) && string.IsNullOrWhiteSpace(Email))
            yield return new ValidationResult("Vous devez saisir au moins un moyen de contacter le restaurant", new [] {"Telephone", "Email"});
        // etc.
    }
}

Pour l’illustrer, j’ai enlevé les attributs de la classe pour qu’elle soit plus facile à lire. Et plutôt que d’avoir le choix entre un téléphone fixe ou portable, vous aurez le choix entre un téléphone et un email. J’ai donc ajouté une propriété Email  (que vous pourrez supprimer par la suite) dans la classe Resto du modèle. Le principe c’est de faire les validations sur le modèle et de renvoyer les éventuelles erreurs qui peuvent se produire.
En retour, cette classe s’insère parfaitement dans le mécanisme de validation et s’il y a un souci, alors ModelState.IsValid  vaudra false . Et nous aurons :

Validation personnalisée avec l'interface IValidatableObject
Validation personnalisée avec l'interface IValidatableObject

Cette méthode a l’avantage de pouvoir être partagée entre les contrôleurs qui doivent valider le modèle, ce qui est plutôt pratique. Bien sûr, cette validation se faisant côté serveur, il vous faut la transposer également côté client si vous voulez que ce même contrôle soit fait par le navigateur avant de poster la page…
Pour ce faire, nous devons nous intéresser dans un premier temps à cette fameuse validation client. Regardons déjà comment est structuré un champ qui doit être validé, prenons le champ obligatoire Nom que nous avons déjà vu :

<input data-val="true" data-val-length="Le champ Nom doit être une chaîne dont la longueur maximale est de 80." data-val-length-max="80" data-val-required="Le nom du restaurant doit être saisi" id="Nom" name="Nom" type="text" value="" />

Vous vous en doutez, le framework de validation d’ASP.NET MVC fonctionne grâce à des conventions. Et ces conventions s’expriment grâce aux attributs des balises. Nous pouvons voir notamment un attribut data-val  à true . Il veut dire que le champ est soumis à validation. Puis nous voyons un data-val-length  et un data-val-required . En fait, cela permet d’indiquer que le champ devra être validé par la méthode length  et par la méthode required . Le contenu de cet attribut est justement le message d’erreur qui est ensuite affiché.
De même, nous pouvons nous douter que data-val-length-max  indique la valeur maximale de la longueur de la chaîne et que cette valeur, 80, est passée en paramètre de la méthode length .

Si nous voulons créer une nouvelle méthode personnalisée pour valider un champ, nous devons donc utiliser ce type de formalisme afin de nous insérer dans le mécanisme de validation. La validation côté client est faite par le framework de validation de jQuery, nous allons donc devoir écrire une méthode à qui nous passons deux noms de champs en paramètres et qui doit être exécutée par le framework de validation. Cela se fait ainsi :

<script type="text/javascript">
    jQuery.validator.unobtrusive.adapters.add
        ("verifcontact", ["parametre1", "parametre2"], function (options) {
            options.rules["verifcontact"] = options.params;
            options.messages["verifcontact"] = options.message;
        });
</script>

J’ajoute ainsi aux validateurs une méthode verifcontact  qui accepte un tableau de deux paramètres, nommés parametre1  et parametre2 . Le framework de validation extrait les valeurs des attributs de la balise HTML et les mets dans le paramètre options . Options.params  contient donc le tableau de paramètres et options.message contient le message d’erreur. Ensuite, j’ai juste à faire passer les paramètres au mécanisme de validation afin de pouvoir les retrouver dans la méthode de validation suivante :

<script type="text/javascript">
    jQuery.validator.addMethod("verifcontact",
    function (value, element, params) {
        var tel = $("#" + params.parametre1).val();
        var email = $("#" + params.parametre2).val();
        return tel != '' || email != '';
    });
</script>

Si vous connaissez un peu la syntaxe de jQuery, ceci ne devrait pas vous poser de problème. Le principe est d’ajouter la méthode verifcontact  dans le mécanisme de validation. Je récupère ensuite la valeur des deux champs que je passe en paramètres et je vérifie s’il y en a au moins un de rempli. Renvoyer false me permettra d’indiquer que la validation a échoué, true indiquera bien sûr que la validation est OK.
Ce JavaScript n’est pas vraiment sécurisé ni optimisé, mais le but est que vous compreniez assez facilement ce que je fais.

Il ne reste plus qu’à écrire le HTML avec les attributs qui vont bien. Il faut réussir à obtenir le HTML suivant :

<input data-val="true" data-val-regex="Le numéro de téléphone est incorrect" data-val-regex-pattern="^0[0-9]{9}$" data-val-verifcontact="Vous devez saisir au moins un moyen de contacter le restaurant" data-val-verifcontact-parametre1="Telephone" data-val-verifcontact-parametre2="Email" id="Telephone" name="Telephone" type="text" value="" />

<input data-val="true" data-val-verifcontact="Vous devez saisir au moins un moyen de contacter le restaurant" data-val-verifcontact-parametre1="Telephone" data-val-verifcontact-parametre2="Email" id="Email" name="Email" type="text" value="" />

Étant donné que notre champ téléphone est déjà soumis à une validation, il possède déjà un attribut data-val  qui vaut true . Il me reste les autres attributs à générer. Nous avons l’attribut :

data-val-verifcontact="Vous devez saisir au moins un moyen de contacter le restaurant"

puis les deux paramètres :

data-val-verifcontact-parametre1="Telephone" data-val-verifcontact-parametre2="Email"

Ce HTML respecte bien les conventions que nous avons vues, à savoir verifcontact  qui est le nom de la méthode, préfixé par data-val . Les paramètres suivent le même principe. Je mets bien sûr comme valeur de ces attributs les noms des champs que je souhaite valider.
Nous avons déjà vu que pour ajouter des attributs, il fallait construire un objet anonyme et le passer en paramètre du helper. Ce qui donne :

@Html.TextBoxFor(model => model.Telephone, new { data_val_verifcontact = "Vous devez saisir au moins un moyen de contacter le restaurant", data_val_verifcontact_parametre1 = "Telephone", data_val_verifcontact_parametre2 = "Email" })

@Html.TextBoxFor(model => model.Email, new { data_val = "true", data_val_verifcontact = "Vous devez saisir au moins un moyen de contacter le restaurant", data_val_verifcontact_parametre1 = "Telephone", data_val_verifcontact_parametre2 = "Email"})

N’oublions pas pour le champ Email de rajouter data-val à true car ce champ-là ne le possède pas déjà.
Je vous remets le JavaScript complet, qui doit bien sûr se mettre dans la balise <head>  :

<script type="text/javascript">
    jQuery.validator.addMethod("verifcontact",
    function (value, element, params) {
        var tel = $("#" + params.parametre1).val();
        var email = $("#" + params.parametre2).val();
        return tel != '' || email != '';
    });

    jQuery.validator.unobtrusive.adapters.add
        ("verifcontact", ["parametre1", "parametre2"], function (options) {
            options.rules["verifcontact"] = options.params;
            options.messages["verifcontact"] = options.message;
        });
</script>

Et voilà, la validation cliente est en place. Maintenant, nous sommes capables de valider ces champs sans aller-retour serveur.
N’oubliez quand même pas qu’il est indispensable de garder la validation serveur au cas où l’utilisateur désactiverait le JavaScript sur son navigateur.

C’est bien cette technique, mais c’est un peu casse-pieds de devoir générer les champs dans le HTML. Heureusement, il y a l’autre solution pour faire des validations personnalisées. Cette solution permet également d’être réutilisable, mais elle se veut être plus générique et même transposable éventuellement dans plusieurs applications.
En effet, nous allons créer un nouvel attribut, au même titre que ceux déjà existants comme Required… Parfait pour étendre les quelques validations déjà existantes du framework de validation.

Pour ce faire, il faut créer une nouvelle classe qui va dériver de ValidationAttribute . C’est typiquement le genre de classes qu’il faudrait créer dans une assembly à part. Là, étant donné qu’il s’agit d’un exemple que nous supprimerons bientôt, vous pouvez la créer dans votre solution :

public class AuMoinsUnDesDeuxAttribute : ValidationAttribute
{
    public string Parametre1 { get; set; }
    public string Parametre2 { get; set; }

    public AuMoinsUnDesDeuxAttribute() : base("Vous devez saisir au moins un moyen de contacter le restaurant")
    {
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        PropertyInfo[] proprietes = validationContext.ObjectType.GetProperties();
        PropertyInfo info1 = proprietes.FirstOrDefault(p => p.Name == Parametre1);
        PropertyInfo info2 = proprietes.FirstOrDefault(p => p.Name == Parametre2);

        string valeur1 = info1.GetValue(validationContext.ObjectInstance) as string;
        string valeur2 = info2.GetValue(validationContext.ObjectInstance) as string;

        if (string.IsNullOrWhiteSpace(valeur1) && string.IsNullOrWhiteSpace(valeur2))
            return new ValidationResult(ErrorMessage);
        return ValidationResult.Success;
    }
}

Ouh là… c’est quoi ce truc complexe… On dirait de la réflexion ?

Eh oui, c’est bien ça.
Le principe, c’est que nous devons substituer la méthode IsValid  afin de fournir notre propre logique de validation. La seule feinte c’est d’offrir la possibilité de passer des paramètres à cet attribut (un peu comme le message d’erreur, ou la taille de la chaîne). Ici, nous devons passer deux chaînes de caractères représentant les champs que nous souhaitons valider. En l’occurrence, ce seront les champs Telephone  et Email  bien sûr. La réflexion intervient pour connaître la valeur de ces deux propriétés dans l’objet de contexte. Nous cherchons à connaître la valeur de la propriété Resto.Telephone  ainsi que la valeur de la propriété Resto.Email . C’est justement ce que fait ce code. Une fois qu’on a les deux valeurs, nous n’avons plus qu’à vérifier qu’au moins une des deux est saisie. Fastoche !
Nous pouvons voir que nous passons une valeur par défaut au message d’erreur en utilisant le constructeur de la classe de base et que nous l’utilisons pour renvoyer le message d’erreur en cas de validation incorrecte.

Ensuite, il n’y a plus qu’à utiliser l’attribut, comme on aurait pu le faire avec ceux que nous connaissons déjà :

public class Resto
{
    public int Id { get; set; }
    public string Nom { get; set; }
    [AuMoinsUnDesDeux(Parametre1 = "Telephone", Parametre2 = "Email", ErrorMessage = "Vous devez saisir au moins un moyen de contacter le restaurant")]
    public string Telephone { get; set; }
    [AuMoinsUnDesDeux(Parametre1 = "Telephone", Parametre2 = "Email", ErrorMessage = "Vous devez saisir au moins un moyen de contacter le restaurant")]
    public string Email { get; set; }
}

J’ai bien sûr retiré de la classe Resto  la méthode de validation que nous avions faite avant. Côté HTML, c’est pareil j’ai supprimé les attributs complémentaires que j’avais positionnés sur les champs. Le téléphone et l’email sont donc tout simplement :

<div>
    @Html.LabelFor(model => model.Telephone)
    @Html.TextBoxFor(model => model.Telephone)
    @Html.ValidationMessageFor(model => model.Telephone)
</div>
<div>
    @Html.LabelFor(model => model.Email)
    @Html.TextBoxFor(model => model.Email)
    @Html.ValidationMessageFor(model => model.Email)
</div>

Et voilà, il n’y a plus qu’à tester.

Validation personnalisée grâce à un attribut
Validation personnalisée grâce à un attribut

Parfait tout ça, sauf que… il manque la validation côté client ! Encore une fois. Mais il ne manque pas grand-chose car nous avons écrit tout le JavaScript précédemment. Il manque juste de quoi générer correctement le HTML. Et pour que ce soit un maximum réutilisable, nous allons faire en sorte que ce soit l’attribut qui génère le bon HTML. Pour cela, notre attribut doit implémenter l’interface IClientValidatable  :

public class AuMoinsUnDesDeuxAttribute : ValidationAttribute, IClientValidatable
{
    public string Parametre1 { get; set; }
    public string Parametre2 { get; set; }

    public AuMoinsUnDesDeuxAttribute() : base("Vous devez saisir au moins un moyen de contacter le restaurant")
    {
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        PropertyInfo[] proprietes = validationContext.ObjectType.GetProperties();
        PropertyInfo info1 = proprietes.FirstOrDefault(p => p.Name == Parametre1);
        PropertyInfo info2 = proprietes.FirstOrDefault(p => p.Name == Parametre2);

        string valeur1 = info1.GetValue(validationContext.ObjectInstance) as string;
        string valeur2 = info2.GetValue(validationContext.ObjectInstance) as string;

        if (string.IsNullOrWhiteSpace(valeur1) && string.IsNullOrWhiteSpace(valeur2))
            return new ValidationResult(ErrorMessage);
        return ValidationResult.Success;
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        ModelClientValidationRule regle = new ModelClientValidationRule();
        regle.ValidationType = "verifcontact";
        regle.ErrorMessage = ErrorMessage;
        regle.ValidationParameters.Add("parametre1", Parametre1);
        regle.ValidationParameters.Add("parametre2", Parametre2);
        return new List<ModelClientValidationRule> { regle };
    }
}

Cette interface nous impose d’implémenter la méthode GetClientValidationRules  et nous offre l’opportunité de spécifier les éléments dont nous aurons besoin pour nos règles de validation. Ici, en l’occurrence je crée une nouvelle règle qui contient le nom de la méthode JavaScript du framework de validation de jQuery à appeler : verifcontact . Nous en profitons pour indiquer le message d’erreur et les deux paramètres de notre attribut.
Tout ceci va permettre de générer correctement le bon HTML, simplement grâce à notre attribut. Si vous recompilez et que vous réaffichez la vue, alors maintenant le simple fait d’avoir posé l’attribut sur les propriétés Telephone et Email du modèle, combiné aux helpers

@Html.TextBoxFor(model => model.Telephone)
@Html.TextBoxFor(model => model.Email)

produira le HTML suivant :

<input data-val="true" data-val-regex="Le numéro de téléphone est incorrect" data-val-regex-pattern="^0[0-9]{9}$" data-val-verifcontact="Vous devez saisir au moins un moyen de contacter le restaurant" data-val-verifcontact-parametre1="Telephone" data-val-verifcontact-parametre2="Email" id="Telephone" name="Telephone" type="text" value="" />
<input data-val="true" data-val-verifcontact="Vous devez saisir au moins un moyen de contacter le restaurant" data-val-verifcontact-parametre1="Telephone" data-val-verifcontact-parametre2="Email" id="Email" name="Email" type="text" value="" />

Ce qui est parfait pour la méthode JavaScript de validation que nous avons écrite. :)

Et voilà pour ces deux solutions permettant de créer des validations personnalisées. À vous de choisir celle qui convient le mieux à vos besoins.

Afficher la bonne vue

Nous avons déjà vu que c’était la méthode View()  et la méthode PartialView()  qui étaient responsables du choix de la vue que le contrôleur allait afficher. Sans paramètres, ces vues affichent la vue par défaut, conformément aux conventions. Il est également possible de choisir une autre vue en indiquant son nom ou son emplacement en paramètre de la vue.

Nous avons également aperçu l’utilisation de la méthode RedirectToAction. Comme son nom le suggère, cette méthode effectue une redirection et exécute ensuite l’action passée en paramètre. Nous l’avons utilisée lorsque nous avons fait la création de restaurant :

[HttpPost]
public ActionResult CreerRestaurant(Resto resto)
{
    […]
    if (!ModelState.IsValid)
        return View(resto);
    dal.CreerRestaurant(resto.Nom, resto.Telephone);
    return RedirectToAction("Index");
}

Concrètement, au niveau HTTP, après le POST de la page, le serveur renvoie un code HTTP 302 qui indique une redirection temporaire, puis exécute l’action Index, comme si nous avions navigué sur l’URL /Restaurant/Index.

Cette méthode a également une petite sœur qui s’appelle RedirectToActionPermanent . Elle fait la même chose sauf qu’au lieu de renvoyer un code HTTP 302, elle renvoie le 301 qui indique une redirection permanente. Cette méthode est plutôt à utiliser lorsque la ressource n’a vraiment plus de raison d’être à cet emplacement.

Dans le même genre, nous avons à notre disposition les méthodes Redirect  et RedirectPermanent  qui renvoient respectivement le code 302 et le code 301. Cette méthode permet notamment de renvoyer vers un site externe (mais également vers une page interne) :

public ActionResult AfficheOpenClassRooms(string id) 
{
    return Redirect("http://fr.openclassrooms.com/");
}

Toujours dans le même genre, nous avons le couple de méthodes RedirectToRoute  et RedirectToRoutePermanent . Elles font à peu près comme RedirectToAction  mais sont un peu plus flexibles dans la mesure où on lui passe directement des routes :

public ActionResult RetourAccueil(string id) 
{
    return RedirectToRoute(new { controller = "Accueil", action = "index" });
}

Il est également possible de renvoyer le code HTTP 404, indiquant que la ressource est introuvable en utilisant la méthode HttpNotFound . Vous vous souvenez de la méthode permettant d’afficher un restaurant en vue de le modifier :

public ActionResult ModifierRestaurant(int? id)
{
    if (id.HasValue)
    {
        Resto restaurant = dal.ObtientTousLesRestaurants().FirstOrDefault(r => r.Id == id.Value);
        if (restaurant == null)
            return View("Error");
        return View(restaurant);
    }
    else
        return View("Error");
}

Il y a plusieurs tests qui permettent de vérifier que l’id passé en paramètre existe bien (oui, rien n’empêche de saisir n’importe quoi dans l’URL, id inexistant voire une chaîne). Dans le cas où il est introuvable, alors nous renvoyions vers la vue partagée Error. Il pourrait être pertinent de renvoyer plutôt un code 404 dans le cas où l’id n’a pas de valeur, afin d’indiquer que la page n’existe pas. Dans ce cas, on utilisera la méthode HttpNotFound  :

public ActionResult ModifierRestaurant(int? id)
{
    if (id.HasValue)
    {
        Resto restaurant = dal.ObtientTousLesRestaurants().FirstOrDefault(r => r.Id == id.Value);
        if (restaurant == null)
            return View("Error");
        return View(restaurant);
    }
    else
        return HttpNotFound();
}

D’autres méthodes permettent de générer du contenu. C’est le cas par exemple de la méthode Content()  qui permet de renvoyer une chaîne de caractères :

public ActionResult AfficheChaine()
{
    return Content("Pas de HTML, juste une chaine");
}

Ceci est l’équivalent de ce que nous avions fait dans la partie d’avant au tout début de l’étude des contrôleurs. Nous avions changé le type de retour ActionResult  en string. La méthode Content()  permet de renvoyer une chaîne tout en gardant un type dérivé d’ActionResult .

Dans le même genre, nous pouvons également renvoyer du JavaScript ou du JSON.

En général, on se sert du JSON pour sérialiser ou désérialiser un objet afin de le faire transiter entre deux systèmes hétérogènes. Par exemple, je pourrai fournir la représentation JSON d’un restaurant de cette façon :

public ActionResult AfficheJson()
{
    Resto resto = new Resto { Id = 1, Nom = "Resto pinambour" };
    return Json(resto, JsonRequestBehavior.AllowGet);
}

Ce qui me renverra le JSON suivant :

{"Id":1,"Nom":"Resto pinambour","Telephone":null}

Vous voyez qu’avec cette méthode, on peut renvoyer autre chose que du HTML. Il est également possible de faire en sorte de renvoyer des fichiers afin que l’utilisateur puisse télécharger du contenu. Cela se passe avec la méthode File() . Par exemple, si je rajoute un fichier dans le répertoire App_Data, je pourrais faire en sorte que l’utilisateur le télécharge en faisant :

public ActionResult ObtientFichier()
{
    string fichier = Server.MapPath("~/App_Data/MonFichier.txt" );
    return File(fichier, "application/octet-stream", "MonFichier.txt");
}

Toutes ces méthodes sont des helpers qui ont pour but de renvoyer une classe qui dérive de ActionResult  avec pour objectif de renvoyer le contenu désiré.
De la même façon, vous pouvez renvoyer une instance de la classe EmptyResult  pour afficher une page vide, ce qui est bien inutile 21 :

public ActionResult RenvoiDuVide()
{
    return EmptyResult();
}

Mais il est également possible de renvoyer directement des objets, sans passer par des méthodes helpers, afin de renvoyer du contenu différent.

Par exemple, nous pouvons renvoyer un code HTTP d’erreur 401, indiquant que l’accès à la ressource est non autorisé, grâce à la classe HttpUnauthorizedResult  :

public ActionResult AccesAuthentifie()
{
    if (HttpContext.User.Identity.IsAuthenticated)
        return View();
    return new HttpUnauthorizedResult();
}

voire renvoyer n’importe quel code HTTP avec HttpStatusCodeResult  :

public ActionResult AccesAuthentifie()
{
    if (HttpContext.User.Identity.IsAuthenticated)
        return View();
    return new HttpStatusCodeResult(401);
}

Avec tout ça, vous devriez être capables de renvoyer le contenu que vous souhaitez à votre utilisateur.

Tester un contrôleur

Je le vois dans vos yeux : vous avez été tristes de ne pas avoir pu écrire des tests automatisés pour vos vues. Rassurez-vous, avec les contrôleurs sonne le retour du testing… Ahhh, chouette. :p Grâce au découpage MVC, il devient plus facile d’écrire des tests automatisés. Et c’est également le cas avec les contrôleurs.
Alors, qu’est-ce qu’un test de contrôleur doit faire ?
Plusieurs choses, il doit :

  • vérifier que la bonne vue (ou action) est retournée par l’action du contrôleur ;

  • vérifier que le view-model est celui qu’on attend ;

  • vérifier que les données passées à la vue sont bien présentes.

Voilà ce que je vous propose d’illustrer. Prenons par exemple notre contrôleur d’accueil qui est tout simple :

public class AccueilController : Controller
{
    public ActionResult Index()
    {
        return View();
    }
}

Ce que nous souhaitons vérifier ici c’est qu’en instanciant le contrôleur et en appelant sa méthode index , alors nous récupérons la vue par défaut. Pour cela, créons une nouvelle classe de test que nous appelons par exemple AccueilControllerTests . Le principe est d’instancier le contrôleur, d’appeler la méthode Index  et de comparer le résultat. Rappelez-vous, notre action retourne un ActionResult .
En fait, la méthode View()  renvoi un ViewResult  qui est une classe dérivant d’ActionResult  et cette classe possède une propriété ViewName  qui est le nom de la vue. Voici le test permettant de vérifier ceci :

[TestMethod]
public void AccueilController_Index_RenvoiVueParDefaut()
{
    AccueilController controller = new AccueilController();

    ViewResult resultat = (ViewResult)controller.Index();

    Assert.AreEqual(string.Empty, resultat.ViewName);
}

La vue par défaut porte un nom vide étant donné que nous ne retournons rien en paramètre de la méthode View() .

Nous pouvons également vérifier que les bonnes informations sont correctement passées à la vue. Imaginons que nous ayons une action qui permette d’afficher la date et qui permette d’afficher un petit message :

public ActionResult AfficheDate(string id)
{
    ViewBag.Message = "Bonjour " + id + " !";
    ViewData["Date"] = new DateTime(2012, 4, 28);
    return View("Index");
}

Nous pourrons tester cette action avec le test suivant :

[TestMethod]
public void AccueilController_AfficheDate_RenvoiVueIndexEtViewData()
{
    AccueilController controller = new AccueilController();

    ViewResult resultat = (ViewResult)controller.AfficheDate("Nicolas");

    Assert.AreEqual("Index", resultat.ViewName);
    Assert.AreEqual(new DateTime(2012, 4, 28), resultat.ViewData["date"]);
    Assert.AreEqual("Bonjour Nicolas !", resultat.ViewBag.Message);
}

Nous notons que cette fois-ci, nous obtenons le nom de la vue dans la propriété ViewName. De même, les différents messages sont accessibles via les objets ViewData  ou ViewBag .

Vous pouvez supprimer ce test et l’action qui affiche la date, nous ne nous en servirons plus.

Bon, pour le contrôleur Accueil , c’est plutôt simple car il n’a aucune dépendance. Là où ça se corse, c’est pour le contrôleur Restaurant  car celui-ci utilise la base de données.
Ici, nous ne souhaitons tester que le contrôleur car nous écrivons un test unitaire. Nous avons déjà écrits des tests pour vérifier que le modèle était bon, nous n’avons donc pas besoin de le retester. De plus, nous ne voulons pas avoir à ré-écraser la base de données à chaque fois. Nous devons donc bouchonner la DAL.
Vous allez me dire : Moq ! Et je vous répondrai pourquoi pas… Sauf que nous pouvons utiliser d’autres techniques pour bouchonner cette DAL. Nous pourrions par exemple écrire une fausse DAL qui implémenterait la même interface mais qui utiliserait des données en mémoire plutôt que des données persistées en base. Cela permettrait entre autre d’accélérer considérablement le temps d’exécution de nos tests.
Ajoutons donc la classe DalEnDur  dans notre projet de test. Cette classe doit implémenter bien sûr l’interface IDal  :

public class DalEnDur : IDal
{
    private List<Resto> listeDesRestaurants;
    private List<Utilisateur> listeDesUtilisateurs;
    private List<Sondage> listeDessondages;

    public DalEnDur()
    {
        listeDesRestaurants = new List<Resto>
        {
            new Resto { Id = 1, Nom = "Resto pinambour", Telephone = "0102030405"},
            new Resto { Id = 2, Nom = "Resto pinière", Telephone = "0102030405"},
            new Resto { Id = 3, Nom = "Resto toro", Telephone = "0102030405"},
        };
        listeDesUtilisateurs = new List<Utilisateur>();
        listeDessondages = new List<Sondage>();
    }

    public List<Resto> ObtientTousLesRestaurants()
    {
        return listeDesRestaurants;
    }

    public void CreerRestaurant(string nom, string telephone)
    {
        int id = listeDesRestaurants.Count == 0 ? 1 : listeDesRestaurants.Max(r => r.Id) + 1;
        listeDesRestaurants.Add(new Resto { Id = id, Nom = nom, Telephone = telephone });
    }

    public void ModifierRestaurant(int id, string nom, string telephone)
    {
        Resto resto = listeDesRestaurants.FirstOrDefault(r => r.Id == id);
        if (resto != null)
        {
            resto.Nom = nom;
            resto.Telephone = telephone;
        }
    }

    public bool RestaurantExiste(string nom)
    {
        return listeDesRestaurants.Any(resto => string.Compare(resto.Nom, nom, StringComparison.CurrentCultureIgnoreCase) == 0);
    }

    public int AjouterUtilisateur(string nom, string motDePasse)
    {
        int id = listeDesUtilisateurs.Count == 0 ? 1 : listeDesUtilisateurs.Max(u => u.Id) + 1;
        listeDesUtilisateurs.Add(new Utilisateur { Id = id, Prenom = nom, MotDePasse = motDePasse });
        return id;
    }

    public Utilisateur Authentifier(string nom, string motDePasse)
    {
        return listeDesUtilisateurs.FirstOrDefault(u => u.Prenom == nom && u.MotDePasse == motDePasse);
    }

    public Utilisateur ObtenirUtilisateur(int id)
    {
        return listeDesUtilisateurs.FirstOrDefault(u => u.Id == id);
    }

    public Utilisateur ObtenirUtilisateur(string idStr)
    {
        int id;
        if (int.TryParse(idStr, out id))
            return ObtenirUtilisateur(id);
        return null;
    }

    public int CreerUnSondage()
    {
        int id = listeDessondages.Count == 0 ? 1 : listeDessondages.Max(s => s.Id) + 1;
        listeDessondages.Add(new Sondage { Id = id, Date = DateTime.Now, Votes = new List<Vote>() });
        return id;
    }

    public void AjouterVote(int idSondage, int idResto, int idUtilisateur)
    {
        Vote vote = new Vote
        {
            Resto = listeDesRestaurants.First(r => r.Id == idResto),
            Utilisateur = listeDesUtilisateurs.First(u => u.Id == idUtilisateur)
        };
        Sondage sondage = listeDessondages.First(s => s.Id == idSondage);
        sondage.Votes.Add(vote);
    }

    public bool ADejaVote(int idSondage, string idStr)
    {
        Utilisateur utilisateur = ObtenirUtilisateur(idStr);
        if (utilisateur == null)
            return false;
        Sondage sondage = listeDessondages.First(s => s.Id == idSondage);
        return sondage.Votes.Any(v => v.Utilisateur.Id == utilisateur.Id);
    }

    public List<Resultats> ObtenirLesResultats(int idSondage)
    {
        List<Resto> restaurants = ObtientTousLesRestaurants();
        List<Resultats> resultats = new List<Resultats>();
        Sondage sondage = listeDessondages.First(s => s.Id == idSondage);
        foreach (IGrouping<int, Vote> grouping in sondage.Votes.GroupBy(v => v.Resto.Id))
        {
            int idRestaurant = grouping.Key;
            Resto resto = restaurants.First(r => r.Id == idRestaurant);
            int nombreDeVotes = grouping.Count();
            resultats.Add(new Resultats { Nom = resto.Nom, Telephone = resto.Telephone, NombreDeVotes = nombreDeVotes });
        }
        return resultats;
    }

    public void Dispose()
    {
        listeDesRestaurants = new List<Resto>();
        listeDesUtilisateurs = new List<Utilisateur>();
        listeDessondages = new List<Sondage>();
    }
}

La classe est un peu longue parce qu’il y a plein de méthodes, mais en fait elle ressemble beaucoup à la DAL que nous avons déjà, à ceci près qu’il n’y a pas du tout d’utilisation d’Entity Framework et que tout est stocké dans des listes en mémoire.

Sauf que… comment utiliser cette DalEnDur  sans modifier le code de notre contrôleur et perturber le fonctionnement de notre application web ? Parce que ça serait quand même pas mal si nous pouvions utiliser la DalEnDur  dans nos tests automatisés et que ce soit la DAL normale qui soit utilisée lorsque l’on navigue sur notre application. Or pour l’instant c’est impossible ; regardez le code du contrôleur :

public class RestaurantController : Controller
{
    public ActionResult Index()
    {
        using (IDal dal = new Dal())
        {
            List<Resto> listeDesRestaurants = dal.ObtientTousLesRestaurants();
            return View(listeDesRestaurants);
        }
    }

    […]
}

La DAL est instanciée directement dans le corps de l’action. Et c’est pareil pour les autres actions que j’ai masquées ici.
Nous allons donc devoir refactoriser ce code de manière à ce que la dépendance à la DAL soit moins forte. Pour cela, nous pouvons utiliser un principe d’injection de dépendance ultra simpliste mais qui va nous aider ici à avoir un couplage faible entre le contrôleur et le modèle.

Il suffit d’offrir la possibilité d’instancier la DAL en dehors du contrôleur et de passer cette instance dans le constructeur de notre contrôleur. 

public class RestaurantController : Controller
{
    private IDal dal;

    public RestaurantController(IDal dalIoc)
    {
        dal = dalIoc;
    }

    public ActionResult Index()
    {
        List<Resto> listeDesRestaurants = dal.ObtientTousLesRestaurants();
        return View(listeDesRestaurants);
    }

    […]
}

Notez que maintenant, il n’y a plus d’using  dans le corps de l’action Index  ; il faudra donc faire pareil pour les autres actions.

Ainsi, nous pourrons écrire un test qui va instancier la fausse DAL et la passer au contrôleur via son constructeur. Pour le montrer, créez donc une nouvelle classe de tests, dédiée au contrôleur Restaurant , que nous pouvons appeler RestaurantControllerTests  :

[TestMethod]
public void RestaurantController_Index_LeControleurEstOk()
{
    using (IDal dal = new DalEnDur())
    {
        RestaurantController controller = new RestaurantController(dal);

        ViewResult resultat = (ViewResult)controller.Index();

        List<Resto> modele = (List<Resto>)resultat.Model;
        Assert.AreEqual("Resto pinambour", modele[0].Nom);
    }
}

Vous voyez comme c’est simple ici d’utiliser la DalEnDur  pour les tests. Reste un souci. L’application MVC n’est plus fonctionnelle car un contrôleur a besoin d’avoir un constructeur par défaut et doit également fournir une implémentation pour la DAL normale. Cela peut se résoudre simplement de cette façon :

public class RestaurantController : Controller
{
    private IDal dal;

    public RestaurantController() : this(new Dal())
    {
    }

    public RestaurantController(IDal dalIoc)
    {
        dal = dalIoc;
    }

    public ActionResult Index()
    {
        List<Resto> listeDesRestaurants = dal.ObtientTousLesRestaurants();
        return View(listeDesRestaurants);
    }

    […]
}

Ainsi, lorsque le contrôleur est instancié par ASP.NET MVC, alors on utilise la classe DAL. Lorsqu’il est instancié par les tests, il utilise la DalEnDur  que nous passons en paramètres du constructeur.

Et voilà, le problème de dépendance est résolu. Nous pouvons nous replonger dans nos tests et vérifier que l’action qui permet de modifier un restaurant est tout à fait fonctionnelle. Prenons l’action qui répond à la requête POST, à savoir :

[HttpPost]
public ActionResult ModifierRestaurant(Resto resto)
{
    if (!ModelState.IsValid)
        return View(resto);
    dal.ModifierRestaurant(resto.Id, resto.Nom, resto.Telephone);
    return RedirectToAction("Index");
}

Ce que nous voulons vérifier ici, c’est qu’en passant un Resto  incorrect alors nous obtenons la vue qui réaffiche le restaurant à modifier. Et lorsque le Resto  est correct, alors on est bien redirigé vers l’action Index . Mais il y a une petite subtilité… En effet, la vérification du modèle se fait lors du binding de modèle qui est fait avant d’instancier l’action du contrôleur, chose sur laquelle nous n’avons pas la main. Du coup, ceci implique que ModelState.IsValid  vaudra toujours true .
Ceci est gênant car nous aimerions bien pouvoir vérifier que lorsque le modèle n’est pas valide, alors nous obtenons la vue par défaut avec le bon modèle. Pour cela, nous allons devoir simuler le fait que le modèle n’est pas valide en ajoutant à la propriété ModelState  une erreur. Cela se fait au début de la méthode de test, qui sert à préparer les données :

[TestMethod]
public void RestaurantController_ModifierRestaurantAvecRestoInvalide_RenvoiVueParDefaut()
{
    using (IDal dal = new DalEnDur())
    {
        RestaurantController controller = new RestaurantController(dal);
        controller.ModelState.AddModelError("Nom", "Le nom du restaurant doit être saisi");

        ViewResult resultat = (ViewResult)controller.ModifierRestaurant(new Resto { Id = 1, Nom = null, Telephone = "0102030405" });

        Assert.AreEqual(string.Empty, resultat.ViewName);
        Assert.IsFalse(resultat.ViewData.ModelState.IsValid);
    }
}

Ainsi, en appelant la méthode AddModelError , nous rendons la propriété IsValid  à false , ce qui permet de vérifier que nous obtenons bien ce que nous souhaitons.

Vous me direz sûrement que ce n’est pas terrible. Nous passons un Resto  en paramètre du contrôleur avec un Nom non renseigné alors qu’il est obligatoire et nous sommes obligés de simuler nous-mêmes le fait que le modèle est invalide.
En fait, si vous y réfléchissez, vous vous poserez la question suivante :

Qu’est-ce que mon test cherche à vérifier ? Que le binding de modèle et que la validation fonctionne ?

Ceci est inutile à tester car le binding de modèle fonctionne ! C’est un composant du framework ASP.NET MVC, alors il fonctionne. Il y a sûrement des tests unitaires écrits par les ingénieurs de Microsoft qui valident son fonctionnement.

Non, notre test doit vérifier que lorsque le modèle est invalide, alors la vue renvoyée est bien celle par défaut. C’est exactement ce que fait ce test.

Je reconnais par contre que ce n’est pas très pratique de devoir préparer les différentes erreurs que nous allons rencontrer et qu’il est plus facile de construire un Resto  invalide. Nous pouvons simplifier notre tâche en simulant cette fameuse validation de binding de modèle. Créons une petite méthode d’extension qui va nous permettre de valider notre modèle. Pour cela, vous pouvez ajouter une classe statique dans votre projet de test, que nous appellerons ControllerExtensions  :

public static class ControllerExtensions
{
    public static void ValideLeModele<T>(this Controller controller, T modele)
    {
        controller.ModelState.Clear();

        ValidationContext context = new ValidationContext(modele, null, null);
        List<ValidationResult> validationResults = new List<ValidationResult>();
        Validator.TryValidateObject(modele, context, validationResults, true);

        foreach (ValidationResult result in validationResults)
        {
            foreach (string memberName in result.MemberNames)
            {
                controller.ModelState.AddModelError(memberName, result.ErrorMessage);
            }
        }
    }
}

Elle s’occupe de valider le modèle en appelant la méthode de validation du framework : TryValidateObject . S’il y a des erreurs, alors elle les ajoute automatiquement à la propriété ModelState .

Ainsi, nous pourrons créer un test qui valide le modèle et qui vérifie ce qui se passe lorsque celui-ci est invalide :

[TestMethod]
public void RestaurantController_ModifierRestaurantAvecRestoInvalideEtBindingDeModele_RenvoiVueParDefaut()
{
    RestaurantController controller = new RestaurantController(new DalEnDur());
    Resto resto = new Resto { Id = 1, Nom = null, Telephone = "0102030405" };
    controller.ValideLeModele(resto);

    ViewResult resultat = (ViewResult)controller.ModifierRestaurant(resto);

    Assert.AreEqual(string.Empty, resultat.ViewName);
    Assert.IsFalse(resultat.ViewData.ModelState.IsValid);
}

Il suffit d’appeler la méthode de validation avant d’invoquer l’action du contrôleur que nous souhaitons tester.
Et voilà.

Sur le même principe, nous pouvons vérifier ce qu’il se passe lorsque le modèle est valide :

[TestMethod]
public void RestaurantController_ModifierRestaurantAvecRestoValide_CreerRestaurantEtRenvoiVueIndex()
{
    using (IDal dal = new DalEnDur())
    {
        RestaurantController controller = new RestaurantController(dal);
        Resto resto = new Resto { Id = 1, Nom = "Resto mate", Telephone = "0102030405" };
        controller.ValideLeModele(resto);

        RedirectToRouteResult resultat = (RedirectToRouteResult)controller.ModifierRestaurant(resto);

        Assert.AreEqual("Index", resultat.RouteValues["action"]);
        Resto restoTrouve = dal.ObtientTousLesRestaurants().First();
        Assert.AreEqual("Resto mate", restoTrouve.Nom);
    }
}

Nous pouvons voir qu’un RedirectToAction  renvoi un RedirectToRouteResult  et que nous pouvons accéder à l’action en utilisant la propriété RouteValues . À noter que ce dictionnaire possède également le nom du contrôleur, mais étant donné qu’il n’est pas utilisé ici, car c’est le même que l’action appelante, il n’existera pas dans le dictionnaire.

Je m’arrête là dans les tests des contrôleurs car je sais que vous avez déjà saisi l’importance de tester automatiquement tout ce que l’on peut. Vous avez désormais les clés pour tester vos contrôleurs et vous assurer que ceux-ci fonctionnent et qu’au fur et à mesure, vous ne créiez pas de régressions.

Remarquez qu’on peut encore se simplifier les tests avec des helpers que l’on peut trouver sur le net. Il y a d’ailleurs tout un projet visant à faciliter le développement ASP.NET MVC, qui contient notamment des helpers de test. Vous pouvez aller y jeter un œil si vous êtes anglophone : http://mvccontrib.codeplex.com/wikipage?title=TestHelper

En résumé

  • Les contrôleurs servent à traiter les actions des utilisateurs et à déterminer quelle est la vue à renvoyer au navigateur.

  • Il est très facile de passer des paramètres à un contrôleur grâce au mécanisme qui fait correspondre des éléments de l’URL et des paramètres de méthodes.

  • Le binding de modèle est un élément très puissant qui permet de transformer des éléments de formulaire HTML soumis au contrôleur en objets complexes du modèle.

  • Les formulaires soumis au contrôleur peuvent (et doivent !) être validés avant d’être traités, c’est le rôle du framework de validation d’ASP.NET MVC.

  • Le framework permet également de réaliser une validation côté client grâce à l’aide de la bibliothèque jQuery.

  • Pour tester facilement un contrôleur qui possède des dépendances, on peut utiliser un mécanisme d’inversion de contrôle pour bouchonner cette dépendance.

Example of certificate of achievement
Example of certificate of achievement