• 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

TP : Presque prêts pour le resto

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

Je vous le dis tout de suite, nous ne sommes pas encore tout à fait prêts à finaliser notre application de choix de restaurant. Il nous manque quelques petites choses mais nous avons déjà pas mal de bases. Avec tout ceci, nous allons pouvoir déjà avancer significativement notre application. Eh oui, il y a plein de contrôleurs et de vues à réaliser, l’occasion pour vous de vous entraîner avec ce nouveau TP.

Nettoyage

Notre superbe projet... avec toutes nos bidouilles dedans pour illustrer les précédents concepts… c’est cracra ! C’est l’heure du nettoyage de printemps, afin de pouvoir repartir comme il faut et attaquer sereinement notre TP.

Dans le fichier RouteConfig.cs, nous devons avoir uniquement la route par défaut, avec le contrôleur Accueil  par défaut :

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Accueil", action = "Index", id = UrlParameter.Optional }
    );
}

Dans le répertoire contrôleur, nous devons avoir un contrôleur Accueil  qui ne fait que renvoyer sur la vue d’accueil :

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

Puis nous devons également avoir un contrôleur Restaurant , qui permet de renvoyer la liste de tous les restaurants, puis de créer et de modifier un restaurant :

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);
    }

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

    [HttpPost]
    public ActionResult CreerRestaurant(Resto resto)
    {
        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");
    }

    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();
    }

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

Le modèle doit être composé des fichiers suivants :

  • BddContext

  • Dal

  • DalEnDur

  • IDal

  • Resto

  • Sondage

  • Utilisateur

  • Vote

comme pour le précédent TP.
La différence réside dans le Resto  qui a des nouveaux attributs et ne doit pas posséder de propriété Email  (sauf si vous le souhaitez bien sûr) :

[Table("Restos")]
public class Resto
{
    public int Id { get; set; }
    [Required(ErrorMessage = "Le nom du restaurant doit être saisi")]
    public string Nom { get; set; }
    [Display(Name="Téléphone")]
    [RegularExpression(@"^0[0-9]{9}$", ErrorMessage="Le numéro de téléphone est incorrect")]
    public string Telephone { get; set; }
}

Si vous avez choisi de ne pas garder l’email, vous devez bien sûr supprimer l’attribut AuMoinsUnDesDeuxAttribute .

Dans les vues, nous avons un répertoire Accueil et un répertoire Restaurant. Dans le répertoire Accueil, nous avons juste une vue Index avec rien de spécial dedans. Dans le répertoire Restaurant, nous avons une vue CreerRestaurant.cshtml qui contient le formulaire de création de restaurant. Il y a notamment les scripts jQuery et le CSS dans la balise <head>  :

<link type="text/css" href="~/Content/Site.css" rel="stylesheet" />
<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>

Puis le formulaire dans la balise body :

@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>
}

Il y a ensuite la vue Index.cshtml qui contient la liste des restaurants :

<table>
    <tr>
        <th>Nom</th>
        <th>Téléphone</th>
        <th>Modifier</th>
    </tr>
    @foreach (var resto in Model)
    {
    <tr>
        <td>@resto.Nom</td>
        <td>@resto.Telephone</td>
        <td>@Html.ActionLink("Modifier " + resto.Nom, "ModifierRestaurant", new { id = resto.Id })</td>
    </tr>
    }
</table>

Avec un petit peu de CSS dans la balise <head>  :

<style type="text/css">
    table {
        border-collapse: collapse;
    }

    td, th {
        border: 1px solid black;
    }
</style>

Puis nous avons la vue ModifierRestaurant.cshtml qui contient le même JavaScript/CSS que la vue CreerRestaurant , avec quasiment le même formulaire :

@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)
            @Html.ValidationMessageFor(model => model.Telephone)
        </div>
        <br />
        <input type="submit" value="Modifier" />
    </fieldset>
}

Le répertoire des vues contient également un répertoire Shared que nous n’avons pas touché.

Ça y est, notre projet est prêt pour votre intervention.

Instructions

Vous l’aurez compris, vous allez réaliser les différentes vues et les différents contrôleurs de notre application, enfin… pas tous non plus, je vous en garde pour plus tard. ^^

Vous allez dans un premier temps améliorer un peu la vue d’accueil, et lui rajouter un bouton permettant de créer un sondage, puis deux liens pour aller naviguer sur la création d’un restaurant et pour aller consulter la liste des restaurants dans le but de modifier ceux existants. Quelque chose qui ressemblera à ça (enfin, vous avez le devoir de faire quelque chose de plus joli que moi !) :

Vue d'accueil à réaliser
Vue d'accueil à réaliser

Nous avons déjà fait les vues et les contrôleurs pour ajouter et modifier les restaurants. Vous aurez juste à tester que tout ça fonctionne en conditions réelles. :)

La création de sondage redirige vers un contrôleur que j’ai appelé Vote, et qui a pour but d’afficher la liste des restaurants que l’utilisateur va pouvoir cocher afin d’indiquer son choix :

Vue de choix d'un restaurant
Vue de choix d'un restaurant

Note : l’URL doit être unique et porter l’id du sondage (ici, l’id est 1) afin de pouvoir être comuniquée aux autres votants avec qui on souhaite aller au restaurant. Il vous faudra d’ailleurs faire un contrôle pour vérifier si l’utilisateur a déjà voté afin de l’empêcher de voter deux fois ; si c’est le cas vous pouvez par exemple le rediriger vers la page de résultats.
Bien sûr, le fait de valider son choix insère toutes les données en base.

Vous allez devoir forcer l’utilisateur à faire un choix, donc à sélectionner au moins un restaurant. À mon avis ce n’est fonctionnellement pas une super bonne idée, car il vaut mieux permettre à un utilisateur d’être potentiellement indécis, mais ici je veux que vous fassiez une validation personnalisée, méchant que je suis ! :p Elle doit bien sûr être faite côté serveur mais également côté client. Si la partie cliente est trop compliquée, vous pouvez ne pas la faire car je reconnais que si on ne sait pas trop faire de Javascript avec jQuery, ce n’est pas vraiment évident ; surtout que dans ce tutoriel je ne vous apprends pas à faire de jQuery, donc je ne peux même pas râler.
Si vous vous le sentez, vous pouvez soit faire en sorte que la validation ressemble à ça (relativement facile) :

Validation des choix, mode facile
Validation des choix, mode facile

Soit (un peu plus dur) :

Validation des choix, mode difficile
Validation des choix, mode difficile

Je ne vous demande pas (encore) de réaliser un système d’authentification pour permettre de différencier les utilisateurs. Sauf que vous allez avoir du mal à simuler plusieurs votes si on ne peut pas se connecter à plusieurs. Je vous propose donc une petite bidouille temporaire permettant d’identifier un utilisateur à partir du nom du navigateur.
Par exemple, si j’utilise Internet Explorer pour naviguer sur mon site, alors la propriété Request.Browser.Browser va valoir la chaîne de caractères « IE ». Si je navigue sur le site avec Chrome, alors cette propriété vaudra « Chrome », etc.
Nous pouvons donc écrire une petite méthode temporaire qui nous crée ou renvoie l’utilisateur à partir du nom du navigateur. Remplacez la méthode :

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

de la DAL, par :

public Utilisateur ObtenirUtilisateur(string idStr)
{
    switch (idStr)
    {
        case "Chrome":
            return CreeOuRecupere("Nico", "1234");
        case "IE":
            return CreeOuRecupere("Jérémie", "1234");
        case "Firefox":
            return CreeOuRecupere("Delphine", "1234");
        default:
            return CreeOuRecupere("Timéo", "1234");
    }
}

private Utilisateur CreeOuRecupere(string nom, string motDePasse)
{
    Utilisateur utilisateur = Authentifier(nom, motDePasse);
    if (utilisateur == null)
    {
        int id = AjouterUtilisateur(nom, motDePasse);
        return ObtenirUtilisateur(id);
    }
    return utilisateur;
}

N’hésitez pas à mettre les valeurs de votre choix, avec les navigateurs de vos choix.
De même, vous devrez changer la méthode :

public bool ADejaVote(int idSondage, string idStr)
{
    int id;
    if (int.TryParse(idStr, out id))
    {
        Sondage sondage = bdd.Sondages.First(s => s.Id == idSondage);
        if (sondage.Votes == null)
            return false;
        return sondage.Votes.Any(v => v.Utilisateur != null && v.Utilisateur.Id == id);
    }
    return false;
}

par :

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

afin de pouvoir savoir si un navigateur a déjà voté.

Il ne reste plus qu’à réaliser le contrôleur et la vue permettant d’obtenir les résultats du vote, de manière à avoir :

La vue de résultat du vote
La vue de résultat du vote

Bien sûr, cette page n’est pas consultable tant que nous n’avons pas voté…

Si vous vous le sentez, n’hésitez pas à vous lancer et à sauter le chapitre suivant. Cependant, il y a quelques points un peu compliqués que je vais détailler de ce pas…

Quelques détails supplémentaires

Les choses un peu touchy se trouvent dans la page qui permet de valider son vote. Il y a déjà les cases à cocher. Comme nous l’avions vu, les cases à cocher sont générées à partir du helper Html.CheckBox  (et son pote Html.CheckBoxFor ) et doivent être liés à un booléen afin de pouvoir bénéficier du binding de modèle.
Or, nous, nous avons à notre disposition une liste de restaurants… donc pas de booléen. Le plus simple pour utiliser le binding de modèle est de créer un view-model contenant l’identifiant du restaurant (pour le retrouver de manière unique), le nom et le numéro de téléphone du restaurant, ainsi qu’un booléen permettant de savoir s’il a été coché ou non. Bref, un view-model de ce genre :

public class RestaurantCheckBoxViewModel
{
    public int Id { get; set; }
    public string NomEtTelephone { get; set; }
    public bool EstSelectionne { get; set; }
}

Lui-même porté par un autre view-model qui sera lié à la vue. Pourquoi un autre view-model ? Parce que dans mon cas, j’ai choisi d’implémenter une validation personnalisée en utilisant l’interface IValidatableObject . C’est donc tout naturellement ce view-model qui devra l’implémenter :

public class RestaurantVoteViewModel : IValidatableObject
{
    public List<RestaurantCheckBoxViewModel> ListeDesResto { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        // à faire !
    }
}

Si vous ne souhaitez pas utiliser cette solution pour créer un validateur personnalisé, vous pouvez vous passer de cette classe.
Je vous laisse choisir votre système pour implémenter votre validation serveur, mais je peux vous donner dès à présent ma solution pour valider la liste des restaurants côté client. En effet, comme je vous l’ai dit, il peut être normal de ne pas avoir toutes les notions jQuery pour réussir à faire ce genre de validations. Je ne vous en voudrais pas si vous copiez un peu sur ce que j’ai fait si vous ne maîtrisez pas jQuery. (Par contre, si vous êtes un cador de jQuery, vous n’avez rien à lire ici, ouste :D) :

<script type="text/javascript">
jQuery.validator.addMethod("verifListe", function (value, element, params) {
    var nombreCoche = $('input:checked[data-val-verifListe]').length;
    if (nombreCoche == 0) {
        $('span[data-valmsg-for=ListeDesResto]').text(params.message).removeClass("field-validation-valid").addClass("field-validation-error");
    }
    else {
        $('span[data-valmsg-for=ListeDesResto]').text('');
    }
    return nombreCoche != 0;
});

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

J’ai appelé ma méthode de vérification verifListe et j’utilise un sélecteur jQuery pour récupérer toutes les cases qui sont cochées et qui ont un attribut data-val-verifListe. Et si aucune des cases n'est cochée, alors j’utilise le champ de validation pour afficher mon message d’erreur.

Voilà, c’est tout ce que vous obtiendrez de moi pour l’instant. Il est temps de vous lancer dans ce TP qui n’est pas trop compliqué, mais qui demande quand même d’avoir compris tous les principes que je vous ai exposé précédemment.

Correction

Alors, c’était comment ? Facile, pas facile ? Les doigts dans le nez j’imagine…
Il est temps pour moi de vous prouver que vous n’avez pas été les seuls à travailler et que j’ai également pris le temps de faire ce TP. ^^

Je pense que ce n’était globalement pas difficile, à part peut-être la partie sur les validations personnalisées ; mais il fallait bien un peu de challenge, sinon vous ne l’auriez pas tenté. Allez, passons à la correction.

Commençons par la vue d’accueil, elle devait afficher simplement un bouton pour créer un sondage, ainsi qu’un lien vers les deux actions permettant d’ajouter un restaurant et d’en modifier :

<p>Prêts à choisir un restaurant ?</p>
@using (Html.BeginForm())
{
    <input type="submit" value="Créer un sondage" />
}
<ul>
    <li>@Html.ActionLink("Ajouter un restaurant", "CreerRestaurant", "Restaurant")</li>
    <li>@Html.ActionLink("Modifier les restaurants", "Index", "Restaurant")</li>
</ul>

Bien sûr, on utilise le helper ActionLink  pour permettre de créer un lien hypertexte vers une action d’un contrôleur. Le contrôleur Accueil  ne fait pas grand-chose :

public class AccueilController : Controller
{
    private IDal dal;

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

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

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

    [HttpPost]
    [ActionName("Index")]
    public ActionResult IndexPost()
    {
        int idSondage = dal.CreerUnSondage();
        return RedirectToAction("Index", "Vote", new { id = idSondage });
    }
}

J’en ai profité pour refactoriser un peu le contrôleur pour inclure notre injection de dépendance du pauvre. Sinon, pas grand chose dans ce contrôleur, il y a juste de quoi répondre à la soumission du formulaire permettant de créer le sondage.

Cette action effectue juste un appel à la méthode de la DAL, puis fait passer l’identifiant du sondage fraîchement créé au contrôleur gérant les votes.

Passons alors au contrôleur Vote . La première chose à faire est de créer les view-models que je vous ai présentés dans le chapitre précédent :

public class RestaurantCheckBoxViewModel
{
    public int Id { get; set; }
    public string NomEtTelephone { get; set; }
    public bool EstSelectionne { get; set; }
}

et

public class RestaurantVoteViewModel : IValidatableObject
{
    public List<RestaurantCheckBoxViewModel> ListeDesResto { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (!ListeDesResto.Any(r => r.EstSelectionne))
            yield return new ValidationResult("Vous devez choisir au moins un restaurant", new[] { "ListeDesResto"});
    }
}

Comme je vous l’ai dit, j’ai choisi d’implémenter la validation personnalisée en implémentant IValidatableObject . J’ai fait ce choix car cette implémentation est fortement liée à la structure de mon view-model, et notamment à sa propriété EstSelectionne . Si j’avais créé un attribut, il aurait également été lié à ce view-model, ce qui lui fait perdre de sa réutilisabilité. Donc, dans ce cas, autant se passer de l’attribut vu qu’il ne pourra pas être réutilisable ailleurs que dans ce projet.

Ici, ma méthode Validate  renvoie une erreur si jamais aucun restaurant n’est sélectionné. Et cette erreur est associée à la propriété ListeDesResto . Ceci impliquera donc que je devrai positionner un helper de validation associé à cette propriété du modèle. Vous le verrez dans la vue.

Le contrôleur est le suivant :

public class VoteController : Controller
{
    private IDal dal;

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

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

    public ActionResult Index(int id)
    {
        RestaurantVoteViewModel viewModel = new RestaurantVoteViewModel
            {
                ListeDesResto = dal.ObtientTousLesRestaurants().Select(r => new RestaurantCheckBoxViewModel { Id = r.Id, NomEtTelephone = string.Format("{0} ({1})", r.Nom, r.Telephone)}).ToList()
            };
        if (dal.ADejaVote(id, Request.Browser.Browser))
        {
            return RedirectToAction("AfficheResultat", new { id = id });
        }
        return View(viewModel);
    }

    [HttpPost]
    public ActionResult Index(RestaurantVoteViewModel viewModel, int id)
    {
        if (!ModelState.IsValid)
            return View(viewModel);
        Utilisateur utilisateur = dal.ObtenirUtilisateur(Request.Browser.Browser);
        if (utilisateur == null)
            return new HttpUnauthorizedResult();
        foreach (RestaurantCheckBoxViewModel restaurantCheckBoxViewModel in viewModel.ListeDesResto.Where(r => r.EstSelectionne))
        {
            dal.AjouterVote(id, restaurantCheckBoxViewModel.Id, utilisateur.Id);
        }
        return RedirectToAction("AfficheResultat", new { id = id });
    }

    public ActionResult AfficheResultat(int id)
    {
        if (!dal.ADejaVote(id, Request.Browser.Browser))
        {
            return RedirectToAction("Index", new { id = id });
        }
        List<Resultats> resultats = dal.ObtenirLesResultats(id);
        return View(resultats.OrderByDescending(r => r.NombreDeVotes).ToList());
    }
}

Dans l’action Index, nous construisons donc le view-model à partir de la liste des restaurants. Puis nous vérifions que l’utilisateur n’a pas déjà voté. Comme expliqué, on utilise (temporairement) la propriété Request.Browser.Browser  pour identifier un utilisateur de manière unique.
Ensuite, il y a l’action Index en POST appelée lors de la soumission du formulaire. Nous vérifions bien sûr que le modèle soit bien valide, puis nous vérifions également que nous récupérons bien un utilisateur. Si ce n’est pas le cas, je renvoie une erreur 401. Bien sûr, ici cela n’arrivera jamais, mais c’est en prévision d’un futur mécanisme d’authentification… Et puis, j’ajoute les restaurants choisis aux votes du sondage.
Vous voyez également l’action d’affichage des résultats, qui va permettre de récupérer les résultats et de les renvoyer à la vue d’affichage que je présenterai après la vue de vote, que voici :

@model ChoixResto.ViewModels.RestaurantVoteViewModel
<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
    <link type="text/css" href="~/Content/Site.css" rel="stylesheet" />
    <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>

    <script type="text/javascript">
        jQuery.validator.addMethod("verifListe", function (value, element, params) {
            var nombreCoche = $('input:checked[data-val-verifListe]').length;
            if (nombreCoche == 0) {
                $('span[data-valmsg-for=ListeDesResto]').text(params.message).removeClass("field-validation-valid").addClass("field-validation-error");
            }
            else {
                $('span[data-valmsg-for=ListeDesResto]').text('');
            }
            return nombreCoche != 0;
        });

        jQuery.validator.unobtrusive.adapters.add
            ("verifListe", function (options) {
                options.params.message = options.message;
                options.rules["verifListe"] = options.params;
                options.messages["verifListe"] = options.message;
            });
    </script>
</head>
<body>
    <p>Cochez les restaurants où vous voulez bien aller. Attention, le vote est définitif !</p>
    @Html.ValidationMessageFor(m => m.ListeDesResto)
    @using (Html.BeginForm())
    {
        for (int i = 0; i < Model.ListeDesResto.Count; i++)
        {
            <div>
                @Html.CheckBoxFor(m => m.ListeDesResto[i].EstSelectionne, new { data_val = "true", data_val_verifListe = "Vous devez choisir au moins un restaurant" })
                @Html.LabelFor(m => m.ListeDesResto[i].EstSelectionne, Model.ListeDesResto[i].NomEtTelephone)
                @*@Html.ValidationMessageFor(m => m.ListeDesResto[i].EstSelectionne)*@
                @Html.HiddenFor(m => m.ListeDesResto[i].Id)
                @Html.HiddenFor(m => m.ListeDesResto[i].NomEtTelephone)
            </div>
        }
        <input type="submit" value="Valider le choix" style="margin-top: 20px;" />
    }
</body>
</html>

C’est la plus complexe. Vous pouvez voir dans la balise <head>  les différentes inclusions CSS et jQuery dont nous avons besoin pour faire les validations. Je passe pour l’instant sur la méthode de validation personnalisée, je la détaillerai un peu plus loin.

Dans le reste de la vue, je commence par positionner l’emplacement pour le message d’erreur de ma validation côté serveur, portée par la propriété ListeDesResto  de mon view-model :

@Html.ValidationMessageFor(m => m.ListeDesResto)

Ceci a son importance car il faut un endroit pour afficher l’erreur dans le cas où l’utilisateur désactive le Javascript et dans le cas (improbable) où nous serions trop paresseux pour faire la validation côté client.
Ensuite, nous affichons les différents choix dans les cases à cocher grâce au helper :

@Html.CheckBoxFor(m => m.ListeDesResto[i].EstSelectionne, new { data_val = "true", data_val_verifListe = "Vous devez choisir au moins un restaurant"})

J’en profite pour générer les attributs qui vont me servir pour la validation client. Notez que le label est associé au même élément que la case à cocher de manière à ce que l’on puisse cocher/décocher la case en cliquant également sur le label.
Si vous souhaitez faire en sorte que l’erreur de validation s’affiche à côté de chaque case à cocher, il faut positionner le ValidationMessage sur la même propriété du modèle et ajouter la ligne que j’ai commentée (je reparlerai des commentaires dans la prochaine partie) :

@Html.ValidationMessageFor(m => m.ListeDesResto[i].EstSelectionne)

Avec ce message de validation, pour effectuer la validation cliente il suffit de compter le nombre de cases à cocher et de renvoyer false pour que les messages s’affichent. Sinon, pour n’avoir qu’un seul et unique message – comme ce que j’ai fait – il faut effectuer des choses en plus pour afficher le message d’erreur dans le champ associé à la ListeDesResto .

Continuons le formulaire et constatons que j’ai rajouté des champs cachés afin de fournir les éléments au binding de modèle pour qu’il puisse reconstituer notre view-model lorsque nous soumettrons le formulaire.
Ainsi, quand le binding de modèle va analyser le contenu de la requête POST, il trouvera pour chaque restaurant un id (champ de type hidden ), le nom et le téléphone (champ de type hidden , qui ici ne sert à rien) et s’il est sélectionné ou pas (grâce à la case à cocher).

Parlons maintenant un peu de la validation cliente. Voici le Javascript que j’ai réalisé :

<script type="text/javascript">
    jQuery.validator.addMethod("verifListe", function (value, element, params) {
        var nombreCoche = $('input:checked[data-val-verifListe]').length;
        if (nombreCoche == 0) {
            $('span[data-valmsg-for=ListeDesResto]').text(params.message).removeClass("field-validation-valid").addClass("field-validation-error");
        }
        else {
            $('span[data-valmsg-for=ListeDesResto]').text('');
        }
        return nombreCoche != 0;
    });

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

Le principe est de récupérer le nombre de cases cochées en utilisant un sélecteur jQuery qui parcourt tous les input cochés (input:checked ) possédant l’attribut témoin de leur prise en charge par le mécanisme de validation : data-val-verifListe .
Si j’utilise le helper de validation associé à la propriété EstSelectionne  qui me génère autant de messages d’erreur qu’il y a de cases, alors j’ai juste à renvoyer vrai si le nombre d’éléments cochés est différent de 0. Typiquement cela serait :

<script type="text/javascript">
    jQuery.validator.addMethod("verifListe", function (value, element, params) {
        var nombreCoche = $('input:checked[data-val-verifListe]').length;
        return nombreCoche != 0;
    });

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

Sauf que j’ai choisi de ne pas afficher X fois le message mais plutôt une unique fois dans le champ associé à la ListeDesResto , le même qui est utilisé pour la validation côté serveur. Pour le sélectionner, je peux utiliser le sélecteur suivant :

$('span[data-valmsg-for=ListeDesResto]')

qui me permet d’accéder au span  qui possède un attribut data-valmsg-for  qui vaut ListeDesResto . Pour trouver ceci, j’ai bien sûr dû regarder dans le code source de la page. Ainsi le helper :

@Html.ValidationMessageFor(m => m.ListeDesResto)

me génère :

<span class="field-validation-valid" data-valmsg-for="ListeDesResto" data-valmsg-replace="true"></span>

Une fois que le sélecteur a renvoyé mon élément, je peux lui affecter la valeur du message d’erreur que j’ai passé en paramètre, grâce à la méthode text(…). Puis pour qu’il s’affiche en rouge, je lui change sa classe CSS et je passe de field-validation-valid  à field-validation-error  qui me met notamment les messages d’erreurs en rouge. Si jamais au moins une des cases est cochée, alors j’efface le message d’erreur en affectant une valeur vide au contrôle HTML.
Et voilà pour la validation.

Je reconnais que ce n’est pas forcément évident de jouer avec ça, mais c’est un bon exercice et j’espère que vous avez pu le réussir, ou à défaut j’espère que vous avez compris ma correction.

Il ne reste plus que la vue qui affiche les résultats. Elle est toute simple :

@model List<ChoixResto.Models.Resultats>

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Voir les résultats</title>
    <style type="text/css">
        table {
            border-collapse: collapse;
        }

        td, th {
            border: 1px solid black;
        }
    </style>
</head>
<body>
    <p>Résultats du sondage :</p>
    <table>
        <tr>
            <th>Nom</th>
            <th>Téléphone</th>
            <th>Nombre de votes</th>
        </tr>
        @foreach (var resto in Model)
        {
        <tr>
            <td>@resto.Nom</td>
            <td>@resto.Telephone</td>
            <td>@resto.NombreDeVotes</td>
        </tr>
        }
    </table>
</body>
</html>

Il y a juste à afficher les résultats, en utilisant une vue fortement typée où le modèle est une liste de Resultats. Aucun piège.

C’en est fini de ce TP.
J’espère qu’il vous a bien servi à assimiler les notions liées aux contrôleurs, aux vues et à la validation des données. C’est vraiment la base de tout développement ASP.NET MVC.

Et les tests ?

Vous avez sûrement testé que tout fonctionnait en démarrant votre navigateur, en créant plusieurs restaurants et en validant votre vote. Puis vous avez démarré un (ou plusieurs) autres navigateurs pour simuler un deuxième utilisateur votant à partir de l’URL du sondage.
(D’ailleurs, vous vous êtes rendus compte que l’utilisateur qui a voté en premier était obligé de rafraîchir sa page pour voir le vote de l’autre utilisateur, nous corrigerons ça en temps et en heure…)

Mais avez-vous pensé à réaliser les tests automatisés de notre nouveau contrôleur ? Et des nouvelles actions de notre contrôleur d’accueil ?

Si oui, alors félicitations. Ces tests vont vous assurer que tout continue à bien fonctionner au fur et à mesure des évolutions et des éventuelles corrections.
Si non, alors vous savez ce qu’il vous reste à faire. :lol:

Je vous propose ici les quelques tests que j’ai réalisé histoire de voir si vous avez grosso-modo la même chose. Déjà, nous devons tester l’action Index  du contrôleur Accueil , lorsqu’elle est appelée en POST :

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

        RedirectToRouteResult resultat = (RedirectToRouteResult)controller.IndexPost();

        Assert.AreEqual("Index", resultat.RouteValues["action"]);
        Assert.AreEqual("Vote", resultat.RouteValues["controller"]);
        List<Resultats> resultats = dal.ObtenirLesResultats(1);
        Assert.IsNotNull(resultats);
    }
}

Un test comme celui-ci n’a pas dû vous poser trop de problèmes.
Maintenant, nous devons tester le contrôleur Vote  et ses différentes actions. Pour cela, j’ai créé une nouvelle classe de tests que j’ai appelé VoteControllerTests . La première chose que j’ai faite, c’est me créer un petit jeu de données au début de chaque test, sachant que j’utilise bien sûr la DalEnDur  comme bouchon :

[TestClass]
public class VoteControllerTests
{
    private IDal dal;
    private int idSondage;

    [TestInitialize]
    public void Init()
    {
        dal = new DalEnDur();
        idSondage = dal.CreerUnSondage();
    }

    [TestCleanup]
    public void Clean()
    {
        dal.Dispose();
    }
}

Ainsi, avant chaque test, je suis sûr d’avoir un sondage. J’ai d’ailleurs toujours mes trois restaurants initialisés dans le constructeur de la DalEnDur , où j’aurais pu créer également mon sondage, mais n’étant lié qu’aux tests du vote cela n’a pas d’intérêt.

Avant de passer aux tests en eux-mêmes, il va falloir faire joujou avec Moq, notre fidèle compagnon de bouchonnage. En effet, dans notre contrôleur, nous utilisons un truc un peu particulier pour simuler le nom d’un utilisateur : l’accès au nom du navigateur :

Utilisateur utilisateur = dal.ObtenirUtilisateur(Request.Browser.Browser);

Non seulement c’est moche, mais en plus cela nous oblige à bouchonner cette dépendance. Eh oui, l’objet Request c’est typiquement un objet rempli par ASP.NET et qui n’a pas de raison d’être dans un contexte de tests automatiques.
Cette mocheté va quand même avoir un effet positif : nous permettre de comprendre un peu comment est créé ce fameux objet Request  et plus précisément, comment est créé le contexte HTTP, via la classe HttpContextBase .

Celui-ci est affecté en fait via la propriété ControllerContext  de tout contrôleur. Ainsi, c’est de cette façon que nous allons pouvoir lui affecter un faux contexte HTTP.

Par exemple, pour bouchonner la propriété Browser, je vais pouvoir faire :

Mock<ControllerContext> controllerContext = new Mock<ControllerContext>();
controllerContext.Setup(p => p.HttpContext.Request.Browser.Browser).Returns("1");

VoteController controleur = new VoteController(dal);
controleur.ControllerContext = controllerContext.Object;

Ici, lorsque la propriété Browser sera utilisée, alors nous aurons la chaîne « 1 » à la place, permettant ensuite de le convertir en entier et d’obtenir l’identifiant de l’utilisateur. Ce bouchonnage devant être fait pour chaque méthode de test, je l’ajoute dans la méthode d’initialisation :

[TestClass]
public class VoteControllerTests
{
    private IDal dal;
    private int idSondage;
    private VoteController controleur;

    [TestInitialize]
    public void Init()
    {
        dal = new DalEnDur();
        idSondage = dal.CreerUnSondage();

        Mock<ControllerContext> controllerContext = new Mock<ControllerContext>();
        controllerContext.Setup(p => p.HttpContext.Request.Browser.Browser).Returns("1");

        controleur = new VoteController(dal);
        controleur.ControllerContext = controllerContext.Object;
    }

    [TestCleanup]
    public void Clean()
    {
        dal.Dispose();
    }
}

Restent les tests en eux-mêmes.
On commence par tester l’action Index avec deux tests qui vont permettre de vérifier que l’on obtient bien un view-model cohérent, d’abord sans avoir créé d’utilisateur (c’est-à-dire que le bouchonnage fournira un id qui n’existe pas dans la liste des utilisateurs), puis avec un utilisateur n’ayant pas voté :

[TestMethod]
public void Index_AvecSondageNormalMaisSansUtilisateur_RenvoiLeBonViewModelEtAfficheLaVue()
{
    ViewResult view = (ViewResult)controleur.Index(idSondage);

    RestaurantVoteViewModel viewModel = (RestaurantVoteViewModel)view.Model;
    Assert.AreEqual(3, viewModel.ListeDesResto.Count);
    Assert.AreEqual(1, viewModel.ListeDesResto[0].Id);
    Assert.IsFalse(viewModel.ListeDesResto[0].EstSelectionne);
    Assert.AreEqual("Resto pinambour (0102030405)", viewModel.ListeDesResto[0].NomEtTelephone);
}

[TestMethod]
public void Index_AvecSondageNormalAvecUtilisateurNayantPasVote_RenvoiLeBonViewModelEtAfficheLaVue()
{
    dal.AjouterUtilisateur("Nico", "1234");
    dal.AjouterUtilisateur("Jérémie", "1234");

    ViewResult view = (ViewResult)controleur.Index(idSondage);

    RestaurantVoteViewModel viewModel = (RestaurantVoteViewModel)view.Model;
    Assert.AreEqual(3, viewModel.ListeDesResto.Count);
    Assert.AreEqual(1, viewModel.ListeDesResto[0].Id);
    Assert.IsFalse(viewModel.ListeDesResto[0].EstSelectionne);
    Assert.AreEqual("Resto pinambour (0102030405)", viewModel.ListeDesResto[0].NomEtTelephone);
}

Ces deux tests sont très classiques et n’apportent pas de nouveauté. Testons également le cas où l’utilisateur a déjà réalisé un vote et qu’il doit être redirigé vers l’affichage des résultats :

[TestMethod]
public void Index_AvecSondageNormalMaisDejaVote_EffectueLeRedirectToAction()
{
    dal.AjouterUtilisateur("Nico", "1234");
    dal.AjouterUtilisateur("Jérémie", "1234");
    dal.AjouterVote(idSondage, 1, 1);

    RedirectToRouteResult resultat = (RedirectToRouteResult)controleur.Index(idSondage);

    Assert.AreEqual("AfficheResultat", resultat.RouteValues["action"]);
    Assert.AreEqual(idSondage, resultat.RouteValues["id"]);
}

Passons ensuite à l’action Index mais en POST. Nous pouvons vérifier qu’un view-model invalide renvoie bien la vue par défaut avec le même view-model :

[TestMethod]
public void IndexPost_AvecViewModelInvalide_RenvoiLeBonViewModelEtAfficheLaVue()
{
    RestaurantVoteViewModel viewModel = new RestaurantVoteViewModel
    {
        ListeDesResto = new List<RestaurantCheckBoxViewModel>
            {
                new RestaurantCheckBoxViewModel { EstSelectionne = false, Id = 2, NomEtTelephone = "Resto pinière (0102030405)"},
                new RestaurantCheckBoxViewModel { EstSelectionne = false, Id = 3, NomEtTelephone = "Resto toro (0102030405)"},
            }
    };
    controleur.ValideLeModele(viewModel);

    ViewResult view = (ViewResult)controleur.Index(viewModel, idSondage);

    viewModel = (RestaurantVoteViewModel)view.Model;
    Assert.AreEqual(2, viewModel.ListeDesResto.Count);
    Assert.AreEqual(2, viewModel.ListeDesResto[0].Id);
    Assert.IsFalse(viewModel.ListeDesResto[0].EstSelectionne);
    Assert.AreEqual("Resto pinière (0102030405)", viewModel.ListeDesResto[0].NomEtTelephone);
}

Ça aussi, c’est de l’archi-classique-déjà-vu. Cette même action renvoie également un code d’erreur 401 si jamais le view-model est valide mais que l’utilisateur n’est pas trouvé. Nous pouvons le tester ainsi :

[TestMethod]
public void IndexPost_AvecViewModelValideMaisPasDutilisateur_RenvoiUneHttpUnauthorizedResult()
{
    RestaurantVoteViewModel viewModel = new RestaurantVoteViewModel
    {
        ListeDesResto = new List<RestaurantCheckBoxViewModel>
            {
                new RestaurantCheckBoxViewModel { EstSelectionne = true, Id = 2, NomEtTelephone = "Resto pinière (0102030405)"},
                new RestaurantCheckBoxViewModel { EstSelectionne = false, Id = 3, NomEtTelephone = "Resto toro (0102030405)"},
            }
    };
    controleur.ValideLeModele(viewModel);

    HttpUnauthorizedResult view = (HttpUnauthorizedResult)controleur.Index(viewModel, idSondage);

    Assert.AreEqual(401, view.StatusCode);
}

Enfin, nous pouvons vérifier que tout se passe bien avec un view-model valide et un utilisateur présent :

[TestMethod]
public void IndexPost_AvecViewModelValideEtUtilisateur_AppelleBienAjoutVoteEtRenvoiBonneAction()
{
    Mock<IDal> mock = new Mock<IDal>();
    mock.Setup(m => m.ObtenirUtilisateur("1")).Returns(new Utilisateur { Id = 1, Prenom = "Nico" });

    Mock<ControllerContext> controllerContext = new Mock<ControllerContext>();
    controllerContext.Setup(p => p.HttpContext.Request.Browser.Browser).Returns("1");
    controleur = new VoteController(mock.Object);
    controleur.ControllerContext = controllerContext.Object;

    RestaurantVoteViewModel viewModel = new RestaurantVoteViewModel
    {
        ListeDesResto = new List<RestaurantCheckBoxViewModel>
                {
                    new RestaurantCheckBoxViewModel { EstSelectionne = true, Id = 2, NomEtTelephone = "Resto pinière (0102030405)"},
                    new RestaurantCheckBoxViewModel { EstSelectionne = false, Id = 3, NomEtTelephone = "Resto toro (0102030405)"},
                }
    };
    controleur.ValideLeModele(viewModel);

    RedirectToRouteResult resultat = (RedirectToRouteResult)controleur.Index(viewModel, idSondage);

    mock.Verify(m => m.AjouterVote(idSondage, 2, 1));
    Assert.AreEqual("AfficheResultat", resultat.RouteValues["action"]);
    Assert.AreEqual(idSondage, resultat.RouteValues["id"]);
}

Notez ici que j’ai rajouté un petit plus pour vous montrer la puissance de Moq. En bouchonnant la Dal avec Moq, je peux vérifier qu’une méthode a bien été appelée. Ici en l’occurrence, je veux vérifier que la méthode AjouterVote  de la DAL a bien été appelée. Cela se fait avec la méthode Verify . Par contre, je suis obligé au début du test de refaire toutes mes initialisations car elles sont différentes de celles faites dans la méthode d’initialisation.

Enfin, il reste à tester la méthode d’affichage des résultats, sans avoir voté au préalable et en ayant correctement voté :

[TestMethod]
public void AfficheResultat_SansAvoirVote_RenvoiVersIndex()
{
    RedirectToRouteResult resultat = (RedirectToRouteResult)controleur.AfficheResultat(idSondage);

    Assert.AreEqual("Index", resultat.RouteValues["action"]);
    Assert.AreEqual(idSondage, resultat.RouteValues["id"]);
}

[TestMethod]
public void AfficheResultat_AvecVote_RenvoiLesResultats()
{
    dal.AjouterUtilisateur("Nico", "1234");
    dal.AjouterUtilisateur("Jérémie", "1234");
    dal.AjouterVote(idSondage, 1, 1);

    ViewResult view = (ViewResult)controleur.AfficheResultat(idSondage);

    List<Resultats> model = (List<Resultats>)view.Model;
    Assert.AreEqual(1, model.Count);
    Assert.AreEqual("Resto pinambour", model[0].Nom);
    Assert.AreEqual(1, model[0].NombreDeVotes);
    Assert.AreEqual("0102030405", model[0].Telephone);
}

Voilà pour ce petit aperçu de mes tests. Ils peuvent être plus exhaustifs que ça alors n’hésitez pas à les enrichir du mieux que vous pouvez.
Il est important de toujours prendre l’habitude d’écrire un test automatisé permettant de valider un développement. Vous vous remercierez plus tard quand vous aurez besoin de revenir sur du code… ou alors vos collègues vous béniront intérieurement de leur avoir laissé des tests en bon état. :)

Conclusion générale de la partie

Ça y est, nous connaissons sur le bout des doigts les fondements de MVC, à savoir le modèle, la vue et le contrôleur. Nous avons vu que chaque élement de MVC compose un tout conséquent et très puissant. À ceci viennent se greffer d’autres éléments qui font partie intégrante du cœur de la plateforme, comme les routes. Nous allons découvrir dans la partie suivante que d’autres éléments annexes vont venir enrichir encore nos applications web et nous faire découvrir toute la puissance et la souplesse d’ASP.NET MVC.
J’ai également insisté sur les tests qui sont très importants dans toute application qui se respecte et qui sont un élément trop souvent négligé.

Vous avez également pu contribuer à notre superbe application fil rouge et voir comment tirer parti de MVC pour réaliser une application web cohérente.

À ce stade du cours, vous êtes déjà opérationnels et capables de réaliser vos applications web. Et pour que vous en soyez convaincus, testez-vous avec le dernier quiz et la dernière activité de ce cours ! Vous pourrez ensuite vous perfectionner à l'aide des Annexes de la partie suivante, mais avant ça...

Bon courage, vous tenez le bon bout ! :D

Example of certificate of achievement
Example of certificate of achievement