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.
- Modèle : les données de domaine de l’application (fichier cs ou vb).
- Vue : l’interface utilisateur (fichier cshtml ou vbhtml contenant du code HTML).
- 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 :
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
;
}
}
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 :
[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 :
@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>
@{
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 :
@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>
@{
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 :
@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 :
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
(
));
}
}
@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.
@{
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 :
@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 :
- Parser l’URL par rapport au pattern pour identifier le contrôleur et l’action ;
- 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 ;
- Identifier les méthodes grâce au contrôleur et à l’action trouver dans l’étape 1 ;
- Filtrer les méthodes avec les attributs GET ou POST ;
- 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.
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▲
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 |
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 |
routes.
MapRoute
(
name:
"Sample3"
,
url:
"Accueil/IndexTemplate/{i}"
,
defaults:
new
{
controller =
"Home"
,
action =
"IndexTemplate"
}
);
Nous modifions la méthode IndexTemplate du contrôleur HomeController :
[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 :
[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 |
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 :
routes.
MapMvcAttributeRoutes
(
);
Nous créons un nouveau contrôleur :
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 :
[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 :
- Authorization Filters : vérifie les droits d’accès ;
- Action Filters : exécute des logiques avant et après l’exécution des actions ;
- Result Filters : exécute des logiques avant et après le résultat des actions ;
- 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 :
protected
override
void
OnActionExecuting
(
ActionExecutingContext filterContext)
{
// Do Somethings
}
ou bien créer un attribut :
public
class
MyCustomActionFilter :
ActionFilterAttribute
{
public
override
void
OnActionExecuting
(
ActionExecutingContext filterContext)
{
// Do somethings
}
}
[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.
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 :
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 :
$.
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 :
@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 :
[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 :
[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.