Apprendre les fonctionnalités de base du framework ASP.NET MVC

Le but de ce tutoriel est de donner un aperçu des fonctionnalités de base offertes par la technologie ASP.NET MVC.

ASP.NET MVC est un framework de développement d’application web développé par Microsoft et implémentant le patron de conception MVC.

Commentez Donner une note à l'article (5)   

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Le patron de conception MVC

Le patron de conception Modèle-Vue-Contrôleur sépare une application en trois composants principaux : le modèle, la vue et le contrôleur.

E:\Privé\Doc\mvc.png
  1. Modèle : les données de domaine de l’application (fichier cs ou vb).
  2. Vue : l’interface utilisateur (fichier cshtml ou vbhtml contenant du code HTML).
  3. Contrôleurs : gestion des évènements utilisateurs, de l’affichage et du contenu des vues (fichier cs ou vb dont le nom suit la convention nameController, contenant une classe héritant de Controller).

II. Factorisation de la vue

ASP.NET MVC nous offre plusieurs solutions pour factoriser le code au niveau de la vue et ainsi éviter de réécrire plusieurs fois le même code.

II-A. Layout page

La layout page qui est l’équivalent de la master page dans ASP.NET WebForms, permet de regrouper le code commun à l’ensemble des pages tel que l’entête, le pied de page ou encore les menus.

D’un point de vue technique, une layout page est juste une vue qui contient une autre vue et qui n’a pas vocation à être affichée directement.

Lorsque vous créez un projet ASP.NET MVC avec Visual Studio, il contient par défaut une layout page : MyMvcProject\Views\Shared\_Layout.cshtml.

Le underscore au début du nom est une convention qui permet de différencier les layout pages des vues classiques. Cela n’a aucune incidence technique telle que l’interdiction de l’accès direct à cette vue par URL, car de toute façon ASP.NET MVC oblige à passer par le système routage pour afficher n’importe quelle vue.

Il est tout à fait possible de créer une imbrication de layout pages pour par exemple afficher un sous-menu commun à un groupe de pages.

II-B. Vue partielle

La vue partielle est une vue qui est contenue dans une autre vue. Au niveau technique, il n’y a strictement aucune différence entre une vue et une vue partielle, n’importe quelle vue peut devenir une vue partielle si elle est appelée dans une autre vue.

Imaginons que nous voulions créer une page pour enregistrer des employés. Notre page doit contenir les champs nom et prénom ainsi qu’une partie adresse que nous allons mettre dans une vue partielle.

Le code des classes du modèle :

MyMvcProject\Models\Employee.cs
Sélectionnez
public class Employee
{
    public int Id { get; set; }
    [Display(Name = "Last name :")]
    public string LastName { get; set; }
    [Display(Name = "First name :")]
    public string FirstName { get; set; }
    public Address Address { get; set; }
}
MyMvcProject\Models\Address.cs
Sélectionnez
public class Address
{
    [Display(Name = "Street :")]
    public string Street { get; set; }
    public string StreetName { get; set; }
    [Display(Name = "Zip code :")]
    public string ZipCode { get; set; }
    [Display(Name = "City :")]
    public string City { get; set; }
}

Le code du contrôleur :

MyMvcProject\Controllers\HomeController.cs
Sélectionnez
[HttpGet]
public ActionResult IndexPartialView1()
{
    var employee = new Employee
    {
        Address = new Address()
    };
    return View(employee);
}
            
[HttpPost]
public ActionResult IndexPartialView1(Employee employee)
{
    return View(employee);
}

Le code des vues :

MyMvcProject\Views\Home\AddressPartialView1.cshtml
Sélectionnez
@model MyMvcProject.Models.Address
            
<div class="row">
    <div class="col-md-2">
        @Html.LabelFor(m => m.Street)
    </div>
    <div class="col-md-4">
        @Html.TextBoxFor(m => m.Street, new { @class = "form-control" })
    </div>
</div>
<div class="row">
    <div class="col-md-2">
        @Html.LabelFor(m => m.City)
    </div>
    <div class="col-md-4">
        @Html.TextBoxFor(m => m.City, new { @class = "form-control" })
    </div>
</div>
<div class="row">
    <div class="col-md-2">
        @Html.LabelFor(m => m.ZipCode)
    </div>
    <div class="col-md-4">
        @Html.TextBoxFor(m => m.ZipCode, new { @class = "form-control" })
    </div>
</div>
MyMvcProject\Views\Home\IndexPartialView1.cshtml
Sélectionnez
@{
    ViewBag.Title = "Test Vue Partielle 1";
}
@model MyMvcProject.Models.Employee
            
@using (Html.BeginForm("IndexPartialView1", "Home"))
{
    <div class="row">
        @Html.HiddenFor(m => m.Id)
        <div class="col-md-2">
            @Html.LabelFor(m => m.FirstName)
        </div>
        <div class="col-md-4">
            @Html.TextBoxFor(m => m.FirstName, new {@class = "form-control"})
        </div>
    </div>
    <div class="row">
        <div class="col-md-2">
            @Html.LabelFor(m => m.LastName)
        </div>
        <div class="col-md-4">
            @Html.TextBoxFor(m => m.LastName, new {@class = "form-control"})
        </div>
    </div>
    @Html.Partial("AddressPartialView1", Model.Address)
    <div class="row">
        <div class="col-md-2">
            <input type="submit" value="Save" class="btn btn-info" />
        </div>
    </div>
}

Si vous cliquez sur le bouton, l’application va générer une exception, car l’objet Address est null. En effet, si nous allons voir dans le code HTML, nous pouvons remarquer que les balises name générées pour la vue partielle sont par exemple de la forme name=Street au lieu de name=Address.Street.

Ceci est un point important lorsqu’on utilise les vues partielles, on change de contexte.

Pour que cela fonctionne, il faut changer le modèle de la vue partielle en Employee et l’appeler de cette façon :

MyMvcProject\Views\Home\AdressPartialView2.cshtml
Sélectionnez
@model MyMvcProject.Models.Employee
            
<div class="row">
    <div class="col-md-2">
        @Html.LabelFor(m => m.Address.Street)
    </div>
    <div class="col-md-4">
        @Html.TextBoxFor(m => m.Address.Street, new { @class = "form-control" })
    </div>
</div>
<div class="row">
    <div class="col-md-2">
        @Html.LabelFor(m => m.Address.City)
    </div>
    <div class="col-md-4">
        @Html.TextBoxFor(m => m.Address.City, new { @class = "form-control" })
    </div>
</div>
<div class="row">
    <div class="col-md-2">
        @Html.LabelFor(m => m.Address.ZipCode)
    </div>
    <div class="col-md-4">
        @Html.TextBoxFor(m => m.Address.ZipCode, new { @class = "form-control" })
    </div>
</div>
MyMvcProject\Views\Home\IndexPartial2.cshtml
Sélectionnez
@{
    ViewBag.Title = "Test Vue Partielle 2";
}
@model MyMvcProject.Models.Employee
            
@using (Html.BeginForm("IndexPartialView2", "Home"))
{
    <div class="row">
        @Html.HiddenFor(m => m.Id)
        <div class="col-md-2">
            @Html.LabelFor(m => m.FirstName)
        </div>
        <div class="col-md-4">
            @Html.TextBoxFor(m => m.FirstName, new {@class = "form-control"})
        </div>
    </div>
    <div class="row">
        <div class="col-md-2">
            @Html.LabelFor(m => m.LastName)
        </div>
        <div class="col-md-4">
            @Html.TextBoxFor(m => m.LastName, new {@class = "form-control"})
        </div>
    </div>
    @Html.Partial("AddressPartialView2", Model)
    <div class="row">
        <div class="col-md-2">
            <input type="submit" value="Save" class="btn btn-info" />
        </div>
    </div>
}

Cette fois-ci les données sont bien renvoyées au contrôleur. Par contre, la vue partielle est peu réutilisable et elle ne peut pas resservir dans cette vue. En effet, notre vue partielle est maintenant liée à un modèle de type Employee contenant un objet de type Address s’appelant lui-même Address.

II-C. Helper

Les helpers sont des méthodes qui rendent du code HTML.

Il y a deux façons de définir un helper soit directement dans la vue ou bien dans une méthode à part.

Reprenons nos classes précédentes et imaginons que nous souhaitions créer un composant pour la rue qui soit composé de deux champs texte (un pour le numéro et un pour le nom de la rue).

Nous pouvons le créer dans la vue comme ci-dessous :

MyMvcProject\Views\Home\AddressHelper1.cshtml
Sélectionnez
@model MyMvcProject.Models.Employee
            
@helper Street()
{
    <div class="row">
        <div class="col-md-2">
            @Html.TextBoxFor(m => m.Address.Street, new { @class = "form-control" })
        </div>
        <div class="col-md-8">
            @Html.TextBoxFor(m => m.Address.StreetName, new { @class = "form-control" })
        </div>
    </div>
}
            
<div class="row">
    <div class="col-md-2">
        @Html.LabelFor(m => m.Address.Street)
    </div>
    <div class="col-md-4">
        @Street()
    </div>
</div>
<div class="row">
    <div class="col-md-2">
        @Html.LabelFor(m => m.Address.City)
    </div>
    <div class="col-md-4">
        @Html.TextBoxFor(m => m.Address.City, new { @class = "form-control" })
    </div>
</div>
<div class="row">
    <div class="col-md-2">
        @Html.LabelFor(m => m.Address.ZipCode)
    </div>
    <div class="col-md-4">
        @Html.TextBoxFor(m => m.Address.ZipCode, new { @class = "form-control" })
    </div>
</div>

C’est pratique, car nous avons l’Intellisense et la coloration syntaxique HTML. Hélas ce composant n’est pas réutilisable en dehors de notre vue.

À la place nous pouvons définir notre composant dans une méthode :

MyMvcProject\Html\InputExtensions.cs
Sélectionnez
public static class InputExtensions
{
    public static MvcHtmlString StreetFor<TModel>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, string>> expressionStreet, Expression<Func<TModel, string>> expressionStreetName)
    {
        var divStreet = new TagBuilder("div");
        divStreet.AddCssClass("col-md-2");
        divStreet.InnerHtml = htmlHelper.TextBoxFor(expressionStreet, new { @class = "form-control" }).ToString();
            
        var divStreetName = new TagBuilder("div");
        divStreetName.AddCssClass("col-md-8");
        divStreetName.InnerHtml = htmlHelper.TextBoxFor(expressionStreetName, new { @class = "form-control" }).ToString();
            
        var divRow = new TagBuilder("div");
        divRow.AddCssClass("row");
        divRow.InnerHtml = string.Concat(divStreet, divStreetName);
            
        return MvcHtmlString.Create(divRow.ToString());
    }
}
MyMvcProject\Views\Home\AddressHelper2.cshtml
Sélectionnez
@using MyMvcProject.Html
@model MyMvcProject.Models.Employee
            
<div class="row">
    <div class="col-md-2">
        @Html.LabelFor(m => m.Address.Street)
    </div>
    <div class="col-md-4">
        @Html.StreetFor(m => m.Address.Street, m=> m.Address.StreetName)
    </div>
</div>
<div class="row">
    <div class="col-md-2">
        @Html.LabelFor(m => m.Address.City)
    </div>
    <div class="col-md-4">
        @Html.TextBoxFor(m => m.Address.City, new { @class = "form-control" })
    </div>
</div>
<div class="row">
    <div class="col-md-2">
        @Html.LabelFor(m => m.Address.ZipCode)
    </div>
    <div class="col-md-4">
        @Html.TextBoxFor(m => m.Address.ZipCode, new { @class = "form-control" })
    </div>
</div>

Nous définissons une méthode d’extension sur le type HtmlHelper. Ainsi ce nouveau composant est disponible dans n’importe quelle vue, mais en contrepartie nous ne bénéficions plus de l’Intellisense HTML ce qui augmente la possibilité d’erreurs.

II-D. Templates

Les templates sont des vues que l’on va appeler à l’aide des méthodes d’extension DisplayFor et EditorFor.

Si nous appelons la méthode EditorFor avec notre objet Address de la classe Employee, le framework génère une vue par défaut en se basant sur les champs de la classe Address.

MyMvcProject\Views\Home\IndexTemplate.cshtml
Sélectionnez
@{
    ViewBag.Title = "Home Page";
}
@model MyMvcProject.Models.Employee
            
@using (Html.BeginForm("IndexTemplate", "Home"))
{
    @Html.HiddenFor(m => m.Id)
    <div class="row">
        <div class="col-md-2">
            @Html.LabelFor(m => m.FirstName)
        </div>
        <div class="col-md-4">
            @Html.TextBoxFor(m => m.FirstName, new {@class = "form-control"})
        </div>
    </div>
    <div class="row">
        <div class="col-md-2">
            @Html.LabelFor(m => m.LastName)
        </div>
        <div class="col-md-4">
            @Html.TextBoxFor(m => m.LastName, new {@class = "form-control"})
        </div>
    </div>
    @Html.EditorFor(m => m.Address)
    <div class="row">
        <div class="col-md-2">
            <input type="submit" value="Save" class="btn btn-info" />
        </div>
    </div>
}

Il est intéressant de noter qu’à la différence des vues partielles, les balises name sont générées de la façon suivant : name=Address.Street. Le contexte reste le même, ce qui nous arrange, car le ModelBinder pourra reconstituer l’objet lors de la soumission du formulaire.

Il ne reste plus qu’à créer le template que l’on souhaite utiliser à la place du template par défaut. Pour cela, il faut créer une vue Address.cshtml dans MyMvcProject\Views\Shared\EditorTemplates :

MyMvcProject\Views\Shared\EditorTemplates\Address.cshtml
Sélectionnez
@model MyMvcProject.Models.Address
            
<div class="row">
    <div class="col-md-2">
        @Html.LabelFor(m => m.Street)
    </div>
    <div class="col-md-4">
        @Html.TextBoxFor(m => m.Street, new { @class = "form-control" })
    </div>
</div>
<div class="row">
    <div class="col-md-2">
        @Html.LabelFor(m => m.City)
    </div>
    <div class="col-md-4">
        @Html.TextBoxFor(m => m.City, new { @class = "form-control" })
    </div>
</div>
<div class="row">
    <div class="col-md-2">
        @Html.LabelFor(m => m.ZipCode)
    </div>
    <div class="col-md-4">
        @Html.TextBoxFor(m => m.ZipCode, new {@class = "form-control"})
    </div>
</div>

Le framework retrouve tout seul le template adéquat. Nous pouvons spécifier de la même façon un template pour l’affichage en lecture seule en utilisant cette fois-ci le répertoire DisplayTemplates.

Il y a plusieurs façons d’appeler les templates. On peut par exemple déplacer les répertoires DisplayTemplates et EditorTemplates et leur contenu dans le répertoire de la vue. Ils ne seront alors disponibles que pour les vues de ce répertoire.

On peut également préciser le nom du template en argument des méthodes DisplayFor ou EditorFor ou de l’attribut UIHint (à utiliser dans le modèle).

Si elle est possible, je préfère la méthode que j’ai utilisée, car c’est la plus légère en termes de syntaxe.

II-E. Tableau récapitulatif

Fonctionnalité Utilité
Layout Regrouper les éléments tels que l’entête, le pied de page ou le menu
Vue partielle Découper une vue en sous-vues pour améliorer la maintenabilité
Helper Créer des éléments réutilisables tels qu’une zone de texte personnalisée
Templates Réutiliser à travers l’application des templates HTML pour l’affichage d’un certain type

III. Routage

En MVC, le routage est le mécanisme qui permet d’accéder aux vues par l’intermédiaire des actions dans les contrôleurs. ASP.NET MVC 5 introduit en supplément du routage par convention, le routage par attribut. Dans les deux cas, la configuration consiste à enregistrer les chemins menant aux différentes vues dans la table de route.

Voici les étapes du processus de routage :

  1. Parser l’URL par rapport au pattern pour identifier le contrôleur et l’action ;
  2. Récupérer les paramètres dans l’URL et dans le contenu de la requête s’il s’agit d’une requête POST ;
  3. Identifier les méthodes grâce au contrôleur et à l’action trouver dans l’étape 1 ;
  4. Filtrer les méthodes avec les attributs GET ou POST ;
  5. Affecter les paramètres à la méthode sélectionnée. Si les paramètres fournis ne sont pas convertibles vers les types des paramètres de la méthode choisie, alors le moteur de routage signale une erreur 500. Si les paramètres ne sont pas fournis dans l’URL et que les paramètres dans la méthode choisie sont « nullables » ou de type référence, alors ils sont mis à null.

III-A. Routage par convention

La configuration de la table de routage se fait dans le fichier MyMvcProject\App_Start\RouteConfig.cs. La méthode RegisterRoutes est appelée au démarrage de l’application.

MyMvcProject\App_Start\RouteConfig.cs
Sélectionnez
public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
            
    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
}

La 1re ligne spécifie au système de routage d’ignorer les requêtes aux fichiers .axd. Ainsi elles seront traitées par le handler ASP.NET.

III-A-1. Paramètre name

C’est le nom de la route dans la table de routage. Il est utilisé en tant que paramètre dans certaines méthodes du framework. Par exemple, la méthode d’extension RouteUrl.

III-A-2. Paramètre url

C’est le pattern pour identifier les URL attrapées par cette route. Controller et action sont des alias. Id est le nom d’un paramètre dans la méthode correspondante à cette route.

Par exemple, pour une URL du type : http://localhost:65012/nimporteou/nimportequoi/nimportequi, le mécanisme de routage va tenter d’appeler l’action NimporteQuoi du contrôleur NimporteOuController avec en paramètre « NimporteQui ».

III-A-3. Paramètre defaults

C’est la route par défaut si une URL ne correspond pas au pattern.

III-A-4. Paramètre constraints

Ceci permet de spécifier via des expressions régulières des contraintes pour les paramètres.

III-A-5. Exemples

MyMvcProject\App_Start\RouteConfig.cs
Sélectionnez
routes.MapRoute(
    name: "Sample1",
    url: "Accueil/{action}",
    defaults: new {controller = "Home", action = "IndexTemplate"}
);
URL Résultat
/Accueil/IndexTemplate

Controller=Home (utilisation du contrôleur par défaut)

Action=IndexTemplate

/Accueil

Controller=Home (utilisation du contrôleur par défaut)

Action=IndexTemplate (utilisation de l’action par défaut)

/Home/IndexTemplate/20 404 Not Found, car l’URL ne correspond pas au pattern
/Contact/IndexTemplate 404 Not Found, car le contrôleur ContactController n’existe pas
MyMvcProject\App_Start\RouteConfig.cs
Sélectionnez
routes.MapRoute(
    name: "Sample2",
    url: "Accueil",
    defaults: new { controller = "Home", action = "IndexTemplate" }
);
URL Résultat
/ 404 Not Found, car l’URL ne correspond pas au pattern
/Accueil

Controller=Home (utilisation du contrôleur par défaut)

Action=IndexTemplate (utilisation de l’action par défaut)

/Accueil/IndexTemplate/20 404 Not Found, car l’URL ne correspond pas au pattern
/Home/IndexTemplate 404 Not Found, car l’URL ne correspond pas au pattern
MyMvcProject\App_Start\RouteConfig.cs
Sélectionnez
routes.MapRoute(
    name: "Sample3",
    url: "Accueil/IndexTemplate/{i}",
    defaults: new { controller = "Home", action = "IndexTemplate" }
);

Nous modifions la méthode IndexTemplate du contrôleur HomeController :

MyMvcProject\Controllers\HomeController.cs
Sélectionnez
[HttpGet]
public ActionResult IndexTemplate(int i)
{
    var employee = new Employee
    {
         Id = i,
         Address = new Address()
    };
    return View(employee);
}
URL Résultat
/Accueil/IndexTemplate/5

Controller=Home (utilisation du contrôleur par défaut)

Action=IndexTemplate

/Accueil/IndexTemplate 404 Not Found, car l’URL ne correspond pas au pattern
/Accueil/IndexTemplate/five 500 Error System.Int32, car « five » n’est pas convertible en int

Nous modifions la précédente méthode :

MyMvcProject\Controllers\HomeController.cs
Sélectionnez
[HttpGet]
public ActionResult IndexTemplate(int? i)
{
    var employee = new Employee
    {
        Id = i ?? 0,
        Address = new Address()
    };
    return View(employee);
}
URL Résultat
/Accueil/IndexTemplate/5

Controller=Home (utilisation du contrôleur par défaut)

Action=IndexTemplate

i=5

/Accueil/IndexTemplate

Controller=Home (utilisation du contrôleur par défaut)

Action=IndexTemplate

i=null

/Accueil/IndexTemplate/five

Controller=Home (utilisation du contrôleur par défaut)

Action=IndexTemplate

i=null

MyMvcProject\App_Start\RouteConfig.cs
Sélectionnez
routes.MapRoute(
    name: "Sample4",
    url: "Accueil/IndexTemplate/{i}",
    defaults: new { controller = "Home", action = "IndexTemplate" },
    constraints: new { i = @"[123]" }
);
URL Résultat
/Accueil/IndexTemplate/1

Controller=Home (utilisation du contrôleur par défaut)

Action=IndexTemplate

i=5

/Accueil/IndexTemplate/5 404 Not Found, car le paramètre ne respecte pas l’expression régulière

III-B. Routage par attribut

ASP.NET MVC 5 introduit un nouveau mécanisme : le routage par attribut qui peut remplacer ou se combiner avec le routage par convention. Il se configure directement dans le contrôleur.

Tout d’abord, il faut activer le routage par attribut :

MyMvcProject\App_Start\RouteConfig.cs
Sélectionnez
routes.MapMvcAttributeRoutes();

Nous créons un nouveau contrôleur :

MyMvcProject\Controllers\ProductController.cs
Sélectionnez
public class ProductController : Controller
{
    [HttpGet]
    [Route("Produit")]
    public ActionResult IndexTemplate()
    {
        var employee = new Employee
        {
            Address = new Address()
        };
        return View(employee);
    }
            
    [HttpPost]
    [Route("Produit")]
    public ActionResult IndexTemplate(Employee employee)
    {
        return View(employee);
    }
}
URL Résultat
/Produit

Controller=Product

Action=IndexTemplate

/Produit/IndexTemplate 404 Not Found, car l’URL ne correspond pas au pattern

Nous pouvons définir un préfixe et une action par défaut :

MyMvcProject\Controllers\ProductController.cs
Sélectionnez
[RoutePrefix("Produit")]
[Route("{action=Detail1}")]
public class ProductController : Controller
{
    [HttpGet]
    public ActionResult Detail1()
    {
        var product = new Product();
        return View(product);
    }
            
    [HttpPost]
    public ActionResult Detail1(Product product)
    {
        return View(product);
    }
            
    [HttpGet]
    [Route("~/Detail2")]
    public ActionResult Detail2()
    {
        var product = new Product();
        return View(product);
    }
            
    [HttpPost]
    [Route("~/Detail2")] // On ne prend pas en compte le prefixe
    public ActionResult Detail2(Product product)
    {
        return View(product);
    }
            
    [HttpGet]
    [Route("IndexTemplate")]
    public ActionResult IndexTemplate()
    {
        var employee = new Employee
        {
            Address = new Address()
        };
        return View(employee);
    }
            
    [HttpGet]
    [Route("IndexTemplate/{i}")]
    public ActionResult IndexTemplate(int i)
    {
        var employee = new Employee
        {
            Id = i,
            Address = new Address()
        };
        return View(employee);
    }
            
    [HttpPost]
    [Route("IndexTemplate")]
    public ActionResult IndexTemplate(Employee employee)
    {
        return View(employee);
    }
}
URL Résultat
/Produit

Controller=Product

Action=Detail1 (utilisation de l’action par défaut)

/Produit/IndexTemplate

Controller=Product

Action=IndexTemplate

/Produit/Detail2 404 Not Found, car l’URL ne correspond pas au pattern
/Detail2

Controller=Product

Action=Detail2

Production/IndexTemplate/5

Controller=Product

Action=IndexTemplate

i=5

Avec le routage par attribut, nous pouvons spécifier des contraintes et une valeur par défaut pour les paramètres. Par exemple [Route("IndexTemplate/{i=1}")] initialisera le paramètre i à 1 s’il n’est pas fourni dans l’URL.

Voici la liste des contraintes :

Contrainte Description Exemple
alpha Correspond à une chaîne de caractères composée de minuscules ou majuscules (a-z, A-Z) {x:alpha}
bool Correspond à un type booléen {x:bool}
datetime Correspond à un type DateTime {x:datetime}
decimal Correspond à un type décimal {x:decimal}
double Correspond à un type double {x:double}
float Correspond à un type float {x:float}
guid Correspond à un type Guid {x:guid}
int Correspond à un type int {x:int}
length Correspond à une chaîne de caractères d’une taille spécifique ou incluse dans une plage de valeurs {x:length(6)}
{x:length(1,20)}
long Correspond à un type long {x:long}
max Correspond à un entier d’une valeur maximum {x:max(10)}
maxlength Correspond à une chaîne de caractères d’une taille maximum {x:maxlength(10)}
min Correspond à un entier d’une valeur minimum {x:min(10)}
minlength Correspond à une chaîne de caractères d’une taille minimum {x:minlength(10)}
range Correspond à un entier dans une plage de valeur {x:range(10,50)}
regex Correspond à une expression régulière {x:regex(^\d{3}-\d{3}-\d{4}$)}

Je trouve que le routage par attribut permet d’éviter plus facilement les erreurs, car les routes sont configurées au même endroit que le code de l’action qu’elles ciblent.

À l’inverse avec le routage par convention, toute la configuration du routage est à un seul endroit. Ce qui rend les modifications plus simples et n’encombre pas le code des contrôleurs.

IV. Filtres

En ASP.NET MVC, les filtres permettent d’ajouter une logique au niveau contrôleur.

Il y a quatre types de filtres :

  1. Authorization Filters : vérifie les droits d’accès ;
  2. Action Filters : exécute des logiques avant et après l’exécution des actions ;
  3. Result Filters : exécute des logiques avant et après le résultat des actions ;
  4. Exception Filters : exécute une logique s’il y a une exception.

Il existe des filtres par défaut tels que AuthorizeAttribute, OutputCacheAttribute ou HandleErrorAttribute.

Ce sont des attributs que l’on peut appliquer soit au niveau du contrôleur soit au niveau de l’action. Si on l’applique au niveau du contrôleur toutes ses actions seront impactées.

Pour définir un filtre personnalisé, nous pouvons soit le créer directement dans le contrôleur :

MyMvcProject\Controllers\HomeController.cs
Sélectionnez
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
    // Do Somethings
}

ou bien créer un attribut :

MyMvcProject\Filters\MyCustomActionFilter.cs
Sélectionnez
public class MyCustomActionFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        // Do somethings
    }
}
MyMvcProject\Controllers\HomeController.cs
Sélectionnez
[HttpGet]
[MyCustomActionFilter]
public ActionResult IndexTemplate()
{
    var employee = new Employee
    {
        Address = new Address()
    };
    return View(employee);
}

Avec un attribut, nous utilisons la réflexion donc cette solution est moins performante que de le créer dans le contrôleur. Mais en contrepartie, si nous le créons dans le contrôleur le filtre s’applique à toutes les actions et il n’est pas réutilisable dans un autre contrôleur.

V. Bundles

Les bundles permettent de regrouper les fichiers JavaScript et CSS et de les minifier. L’intérêt étant de limiter le nombre et la taille des requêtes HTTP pour récupérer ce type de fichier.

Les bundles se configurent dans le fichier MyMvcProject\App_Start\BundleConfig.cs. Nous pouvons définir des bundles transverses à plusieurs pages (par exemple pour les fichiers jQuery ou bien les fichiers CSS communs à l’ensemble du site) ou bien définir un bundle pour chaque page.

Il existe deux wildcards pour cibler rapidement un groupe de fichier :

  • pour remplacer n’importe quel caractère de 0 à n fois ;
  • {version} pour spécifier n’importe quelle version.

Attention toutefois, il y a des limitations. Le wildcard * n’est seulement permis que dans la dernière partie du chemin, en entête ou en queue et utilisé seulement une fois et pas en combinaison avec {version}.

{version} peut être utilisé plusieurs fois et à n’importe quel endroit du chemin.

VI. Validation

Sur une application web, il y a la validation côté client et la validation côté serveur. La validation côté client sert pour l’expérience utilisateur. Elle permet de lui offrir une interface qui répond rapidement, mais elle n’offre aucune garantie au niveau de la sécurité. En effet, un utilisateur peut facilement la contourner avec les outils de débogage fournis dans les navigateurs.

La validation côté serveur gère quant à elle la sécurité de l’application vis-à-vis des données saisies par le client. Étant donné qu’elle est par définition réalisée sur le serveur, elle entraîne une certaine latence au niveau de l’interface.

Un avantage de la technologie ASP.NET MVC est de permettre de réaliser les validations de base (par exemple l’obligation de remplir un champ) avec un même code pour les deux types de validations. Le côté client étant géré par la bibliothèque validation de jQuery.

VI-A. Validation par défaut

Dans notre modèle précédent, nous pouvons rendre le champ LastName obligatoire en ajoutant l’attribut Required à la propriété LastName de la classe Employee.cs.

Si dans le Web.config les paramètres ClientValidationEnabled et UnobtrusiveJavaScriptEnabled sont à true et que nous avons rajouté les fichiers jQuery et jQuery.validate alors la validation côté client est également prise en charge.

MyMvcProject\Models\Employee.cs
Sélectionnez
public class Employee
{
    [Display(Name = "Identifiant :")]
    public int Id { get; set; }
    [Required(ErrorMessage = "Required")]
    [Display(Name = "Last name :")]
    public string LastName { get; set; }
    [Display(Name = "First name :")]
    public string FirstName { get; set; }
    public Address Address { get; set; }
}

VI-B. Validation personnalisée

Si nous souhaitons réaliser une validation plus spécifique qui n’est pas offerte par les attributs de validation ASP.NET MVC, nous pouvons soit utiliser l’interface IValidatableObject ou bien créer un attribut personnalisé.

VI-B-1. Interface IValidatableObject

Reprenons notre modèle et rajoutons deux propriétés. Nous voulons que la distance dépende de la zone sélectionnée. Par exemple, si la zone sélectionnée est Area 1 alors la distance doit être comprise entre 0 et 10, etc.

Nous allons implémenter l’interface IValidatableObject :

MyMvcProject\Models\Employee.cs
Sélectionnez
public class Employee : IValidatableObject
{
    [Display(Name = "Identifiant :")]
    public int Id { get; set; }
    [Required(ErrorMessage = "Required")]
    [Display(Name = "Last name :")]
    public string LastName { get; set; }
    [Display(Name = "First name :")]
    public string FirstName { get; set; }
    public Address Address { get; set; }
    [Display(Name = "Area :")]
    public int Area { get; set; }
    [Display(Name = "Distance :")]
    public int? Distance { get; set; }
            
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (!Distance.HasValue) yield break;
        switch (Area)
        {
            case 1:
                if (Distance < 1 || Distance > 10)
                {
                    yield return new ValidationResult("Distance must be between 1 and 10");
                }
                break;
            case 2:
                if (Distance < 11 || Distance > 20)
                {
                    yield return new ValidationResult("Distance must be between 11 and 20");
                }
                break;
            default:
                if (Distance < 21)
                {
                    yield return new ValidationResult("Distance must be greater than 20");
                }
                break;
        }
    }
}

Maintenant à chaque fois que le formulaire est soumis, la méthode Validate va être exécutée. Le résultat de cette méthode est stocké dans la propriété ModelState.IsValid (objet accessible dans le contrôleur).

Il nous reste à coder la validation côté client. Pour cela, nous allons créer un fichier customValidation.js pour ajouter une règle de validation personnalisée :

MyMvcProject\Scripts\customValidation.cs
Sélectionnez
$.validator.unobtrusive.adapters.add('mycustomvalidationrule', function (options) {
    options.rules['mycustomvalidationrule'] = options.params;
    options.messages['mycustomvalidationrule'] = options.message;
});
            
$.validator.addMethod("mycustomvalidationrule", function(value, element, params) {
    if (value === '') return true;
            
    var distance = parseInt(value);
    var area = parseInt($("#Area").val());
            
    if (area === 1) {
        return distance > 0 && distance < 11;
    } else if (area === 2) {
        return distance > 10 && distance < 21;
    } else {
        return distance > 20;
    }
});

Il ne faut pas oublier de rajouter l’attribut de validation (data-val-mycustomvalidationrule) sur le champ à contrôler :

MyMvcProject\Views\Home\IndexTemplate.cshtml
Sélectionnez
@Html.TextBoxFor(m => m.Distance, new { @class = "form-control", data_val_mycustomvalidationrule = "Distance is out of range (0 < Area 1 < 11, 10 < Area 2 < 21, 20 < Area 3)" })

Si nous mettons directement le nom de notre attribut avec des tirets, nous allons avoir une erreur à l’exécution. Donc nous écrivons l’attribut avec des underscores qui seront automatiquement remplacés par des tirets lors du rendu HTML.

VI-B-2. Attribut personnalisé

La méthode précédente lie la validation côté serveur à un modèle, ce qui rend impossible sa réutilisation. Si nous souhaitons réutiliser notre validation, il faut créer un attribut personnalisé. Cette méthode utilise la réflexion ce qui la rend moins performante que celle avec la classe IValidatable.

Nous allons d’abord créer notre attribut :

MyMvcProject\Attributes\MyCustomValidationAttribute.cs
Sélectionnez
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class MyCustomValidationAttribute : ValidationAttribute, IClientValidatable
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        int? distance = (int?) value;
        int area = (int) validationContext.ObjectType.GetProperty("Area").GetValue(validationContext.ObjectInstance);
            
        if (!distance.HasValue) return ValidationResult.Success;
        switch (area)
        {
            case 1:
                if (distance < 1 || distance > 10)
                {
                    return new ValidationResult("Distance must be between 1 and 10");
                }
                break;
            case 2:
                if (distance < 11 || distance > 20)
                {
                    return new ValidationResult("Distance must be between 11 and 20");
                }
                break;
            default:
                if (distance < 21)
                {
                    return new ValidationResult("Distance must be greater than 20");
                }
                break;
        }
            
        return ValidationResult.Success;
    }
            
    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        yield return
            new ModelClientValidationRule
            {
                ValidationType = "mycustomvalidationrule",
                ErrorMessage = "Distance is out of range (0 < Area 1 < 11, 10 < Area 2 < 21, 20 < Area 3)"
            };
    }
}

Ensuite, il faut décorer le champ à valider avec cet attribut :

MyMvcProject\Models\Employee.cs
Sélectionnez
[MyCustomValidationAttribute]
public int? Distance { get; set; }

Nous devons toujours coder la validation côté client, mais cette fois-ci nous n’avons pas besoin de rajouter l’attribut de validation sur le champ dans la vue. En effet, ceci est automatiquement réalisé avec la méthode GetClientValidationRules de notre attribut personnalisé.

VII. Remerciements

Je tiens à remercier Guillaume SIGUI et ClaudeLELOUP pour leurs relectures et leurs corrections orthographiques.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2016 Kevin Sousselier. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.