Liens
MVC
Model |
- Modèle de données
- Accès aux données (bdd, webservice)
|
View |
|
Controller |
- en fonction de l'url, choisit le Model et la View
|
Controller
Convention de routage: /Home/Index
- Classe HomeController
- Méthode Index
Controllers/HomeController.cs
|
[HandleError] // Action filter qui s'appliquera à toutes les actions du controller
public class HomeController : Controller
{
[ActionName("Index2")] // Alias
[HttpPost]
[HandleError] // Action filter
public ActionResult Index(string id) // permet de récupérer l'id passé à l'url
{
// il est possible d'ajouter dynamiquement n'importe quelle propriété à ViewBag
ViewBag.SomeProperty = "Some message";
// retourne la vue contenue dans Views/Home/Index.cshtml
return View();
// passage du modèle à la vue
var model = new Model();
model.SomeProperty = "Some message";
return View(model);
// utilisation du paramètre id
var safe_id = Server.HtmlEncode(id);
var controller = RouteData.Values["controller"];
var action = RouteData.Values["action"];
var id = RouteData.Values["id"];
// renvoie une vue simplifiée contenant seulement un string
return Content("Ok!");
|
MapRoute
|
Les routes sont traités dans leur ordre d'apparition. |
App_Start/RouteConfig.cs
|
// nouvelle route
routes.MapRoute(
name: "New",
url: "new/{name}", // correspond à toutes les url commençant par new
defaults: new { controller = "New", action = "Search", name = UrlParameter.Optional }
);
// route définie par défaut
routes.MapRoute(
name: "Default", // Route name
url: "{controller}/{action}/{id}", // URL with parameters
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
|
Bind
Include, Exclude
|
Permet de contrôler quelles propriétés sont éditables.
Prévient le problème d'overposting ou de mass assignment, où une propriété qui n'est pas dans le formulaire est affectée (via un paramètre dans l'url par exemple) |
PersonController.cs
|
// Include permet de white-lister toutes les propriétés dont les valeurs pourront être affectées
// les propriétés qui ne sont pas listées n'auront pas leurs valeurs affectées même si elle sont présente dans le formulaire
public ActionResult Create([Bind(Include = "Name,Adress")] Person person)
{ }
// Exclude permet de black-lister toutes les propriétés dont les valeurs ne pourront pas être affectées
// les propriétés qui ne sont pas listées pourront être affectées
public ActionResult Create([Bind(Exclude = "Phone")] Person person)
{ }
|
Prefix
|
@Html.ActionLink("Text", "Index", new { id = item.Id })
|
PersonController.cs
|
// un paramètre id est envoyé via ActionLink
// un paramètre personId est attendu par l'action Index
// permet de lier id à personId
public ActionResult Index([Bind(Prefix="id")] int personId)
{ }
|
ActionResult
Type
|
Description
|
Méthode
|
ViewResult PartialViewResult |
La réponse est générée par le view engine. |
View() PartialView()
|
ContentResult |
Retourne un string. |
Content()
|
EmptyResult |
Pas de réponse générée. |
-
|
FileContentResult FilePathResult FileStreamResult |
Retourne le contenu d'un fichier. |
File(Server.Path("~/Dossier/Fichier.ext"), "text/txt")
|
HttpUnauthorizedResult |
Retourne un status HTTP 403. |
-
|
JavaScriptResult |
Retourne un script à exécuter. |
JavaScript
|
JsonResult |
Retourne des données au format JSON. |
Json(new { Prop1 = value1, Prop2 = value2 }, JsonRequestBehavior.AllowGet)
|
RedirectResult |
Redirige le client vers une autre URL. |
Redirect("http://www.domaine.fr")
|
RedirectToRouteResult |
Redirige vers une autre action. |
RedirectToRoute("Default", new { controller = "Home", action = "Index" }) RedirectToAction("Index", "Home", new { Prop = value })
|
Action Filters
Permet d’exécuter du code avant ou après que l'action ou le Result soit exécutée.
Type
|
Description
|
OutputCache |
Met en cache la réponse du controller.
|
ValidateInput |
Désactive la validation de la requête.
|
Authorize |
Restreint l'accès au utilisateurs connectés.
|
ValidateAntoForgeryToken |
Prévient les cross site request forgeries.
|
HandleError |
Permet de spécifier une View à utiliser en cas de unhandled exception.
|
Controllers/PersonsController.cs
|
// met en cache toutes les actions du controller
[OutputCache(Duration = 60)]
public class PersonsController : Controller
{
// met en cache ActionResult produit par Index pendant 60s
// mise en cache différente en fonction de la valeur du paramètre du header X-Requested-With
// mise en cache serveur exclusivement
[OutputCache(Duration = 60, VaryByHeader = "X-Requested-With", Location = OutputCacheLocation.Server)]
public ActionResult Index()
{ }
// utilisation du CacheProfile définit dans Web.config
[OutputCache(CacheProfile = "LongCache")]
public ActionResult Index()
{ }
// il est possible d'isoler une action qui prend du temps dans une ChildAction,
// afin de lui spécifier une durée de cache particulière
[ChildActionOnly]
[OutputCache(Duration = 60)]
public ActionResult DoSomethingThatTakesTime()
{ }
|
Views/Persons/Index.cshtml
|
@Html.Action("DoSomethingThatTakesTime")
|
Web.config
|
<system.web>
<caching>
<outputCacheSettings>
<outputCacheProfiles>
<add name="LongCache" duration="300" />
<add name="ShortCache" duration="10" />
|
Propriété
|
Description
|
Duration |
durée de mise en cache en secondes
|
VaryByParam |
- * : mise en cache pour chaque combinaison de paramètres
- none : mise en cache sans se préoccuper des paramètres
- name : mise en cache pour chaque valeur du paramètre name
|
Location |
mise en cache du côté serveur, client ou les 2
|
VaryByHeader |
mise en cache pour chaque valeur du paramètre du header spécifié
|
VaryByCustom |
à définir dans Global.asax
|
SqlDependency |
mise en cache jusqu'au changement des données dans la bdd
|
Global filters
App_Start/FilterConfig.cs
|
filters.Add(new HandleErrorAttribute());
|
Custom Filters
Filters/MyCustomFilterAttribute.cs
|
public class MyCustomFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
|
View
Views/Home/Index.cshtml
|
@model FullNamespace.Models.MyModelClass
@{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<div>Message: @Model.SomeProperty</div>
<div>
@* Razor comment *@
Message: @ViewBag.SomeProperty
</div>
@* code block *@
@{
var v = "v";
}
@foreach (var item in Model)
{
<p>Item: @item</p>
@* force Text à ne pas être pris en compte comme code C# mais comme texte HTML *@
@:Text
}
@* opération numérique *@
@(Model.SomeProperty / 10)
|
HTML Helpers
Views/Home/Index.cshtml
|
@* lien *@
@Html.ActionLink("Texte du lien", "Action", new { MyProp = value })
@* appel le DisplayTemplates string (car model.Title est de type string) pour l'affichage de Title *@
@Html.DisplayFor(model => model.Title)
|
Layout View
Définit le layout de toutes les vues.
Views/_ViewStart.cshtml
|
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
|
Section
Définit où est placé la section dans le layout.
Views/Shared/_Layout.cshtml
|
@RenderSection("feature", required: false)
|
Remplit la section avec un contenu.
Views/Home/Index.cshtml
|
@section feature
{
Some text to display.
}
|
Partial View
Permet de découper du code HTML pour le réutiliser ou pour simplifier.
Views/Home/Index.cshtml
|
@Html.PartialView('_MyPartialView', myModel)
|
Views/Home/_MyPartialView.cshtml
|
@model Namespace.Models.MyModel
<div>Some text to display.</div>
|
Depuis un controller
Permet de créer une sous-requête. Utile quand on a pas accès au model, dans _Layout.cshtml par exemple.
Views/Shared/_Layout.cshtml
|
@Html.Action("MyAction", "Home")
|
HomeController.cs
|
[ChildActionOnly] // cette action ne sera pas accessible directement en entrant son url /Home/MyAction
public ActionResult MyAction()
{
return PartialView("_MyPartialView", myModel);
|
Model
|
Les attributs de validation influencent aussi Entity Framework. |
|
La validation se fait du côté client en javascript et aussi du côté serveur. |
|
public class MyClass
{
public int Id { get; set; }
// par défaut @Html.LabelFor(model => model.MyProperty) va afficher MyProperty
// DisplayName permet de changer la valeur affichée
[DisplayName("SuperProperty")]
[DisplayFormat(NullDisplayText = "vide")] // valeur à afficher si MyProperty est null
public string MyProperty { get; set; }
[Required] // pour les types nullables, la propriété doit être affectée. Les types valeurs sont requis par défaut
[StringLength(1024)] // longueur max
public string MyProperty { get; set; }
// doit être compris entre 1 et 10
[Range(1, 10, ErrorMessage = "Custom error message")]
public int MyProperty { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime Date { get; set; }
[DataType(DataType.Currency)]
public decimal Montant { get; set; }
}
|
Custom Validation
|
La validation des custum validators se fait uniquement du côté serveur. |
ValidationAttribute
|
[MaxWords(2)]
public string MyProperty { get; set; }
|
MaxWordsAttribute.cs
|
public class MaxWordsAttribute : ValidationAttribute
{
private readonly int _maxWords;
public MaxWordsAttribute(int maxWords)
: base("{0} has too many words.")
{
_maxWords = maxWords;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value != null)
{
var valueAsString = value.ToString();
if (valueAsString.Split(' ').Length > _maxWords)
{
var errorMessage = FormatErrorMessage(validationContext.DisplayName);
return new ValidationResult(errorMessage);
}
}
return ValidationResult.Success;
}
}
|
IValidatableObject
|
Utile pour valider plusieurs propriétés en même temps. |
|
public class MyClass : IValidatableObject
{
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Property1 == "Test" && Property1 == 10)
{
yield return new ValidationResult("Custom error message");
}
}
}
|
|
@* code permettant l'affichage des erreurs IValidatableObject *@
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
|
Arborescence
- App_Start
- BundleConfig.cs
- FilterConfig.cs
- RouteConfig.cs
- Content
- Controllers
- fonts
- Models
- Scripts
- Views
- Home (pour le controller Home)
- Index.cshtml (pour l'action Index)
- Shared (partagés par tous les controllers)
- Web.config
- Global.asax
- Web.config
Configuration
AppSettings
Web.config
|
<appSettings>
<add key="Key1" value="Value1" />
|
|
string v = ConfigurationManager.AppSettings["Key1"];
|
Ordre de traitement des fichiers de configuration
Config
|
Chemin
|
1. Machine config |
C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config\machine.config
|
2. Machine web.config |
C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config\web.config
|
3. Parent's web.config |
|
4. Your web.config |
|
Les configurations les plus basses écrasent les plus hautes.
Langue et Culture
Web.config
|
<system.web>
<!-- utilise la langue définit par l'explorateur web -->
<globalization culture="auto" uiCulture="auto" />
<!-- force la langue à en-US -->
<globalization culture="en-US" uiCulture="en-US" />
|
Paquet NuGet
|
Description
|
jQuery.Validation.Localization |
messages de validation
|
jQuery.UI.i18n |
textes pour le datepicker
|
jQuery.Validation.Globalize jquery-globalize cldrjs |
tests de validation
|
JQuery Globalize 0.1.3
|
jquery-globalize.0.1.3 n'est plus à jour avec jQuery.Validation.Globalize |
Installer jquery-globalize.0.1.3 et jQuery.Validation.Globalize avec NuGet.
App_Start/BundleConfig.cs
|
bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
"~/Scripts/jquery.validate.js",
"~/Scripts/jquery.validate.unobtrusive.js",
// validate globalization
"~/Scripts/jquery.validate.globalize.js"));
// jquery.validate.globalize.js ne peut pas être dans le même bundle que globalize
// car il serait déclarer en premier et causerait l'erreur « Globalize is not defined »
// il faut donc le mettre dans un bundle qui sera déclarer après globalize
bundles.Add(new ScriptBundle("~/bundles/globalize0.1.3").Include(
"~/Scripts/globalize.0.1.3/globalize.js",
"~/Scripts/globalize.0.1.3/cultures/globalize.culture.fr-FR.js"));
));
|
|
@section Scripts {
@Scripts.Render("~/bundles/globalize")
@Scripts.Render("~/bundles/jqueryval")
|
Scripts/jquery.validate.globalize.js
|
(function ($, Globalize) {
// définir la culture
Globalize.culture("fr");
$.validator.methods.date = function (value, element) {
// ne pas passer le format comme dateParseFormat, cette version ne le gère pas
//var val = Globalize.parseDate(value, $.validator.methods.dateGlobalizeOptions.dateParseFormat);
var val = Globalize.parseDate(value);
return this.optional(element) || (val instanceof Date);
};
|
JQuery Globalize 1.3.0
|
cldr-data ne peut pas être installé avec NuGet. |
Installer jQuery.Validation.Globalize avec NuGet. jquery-globalize et cldrjs seront installés comme dépendances.
|
# installer cldr-data
npm install cldr-data
|
App_Start/BundleConfig.cs
|
bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
"~/Scripts/jquery.validate.js",
"~/Scripts/jquery.validate.unobtrusive.js",
// validate globalization
"~/Scripts/jquery.validate.globalize.js"));
// jquery.validate.globalize.js ne peut pas être dans le même bundle que globalize
// car il serait déclarer en premier et causerait l'erreur « Globalize is not defined »
// il faut donc le mettre dans un bundle qui sera déclarer après globalize
bundles.Add(new ScriptBundle("~/bundles/globalize").Include(
"~/Scripts/cldr.js",
"~/Scripts/cldr/*.js",
"~/Scripts/globalize.js",
"~/Scripts/globalize/number.js",
"~/Scripts/globalize/plural.js",
"~/Scripts/globalize/currency.js",
"~/Scripts/globalize/date.js", // date.js doit être en dernier, sinon « validateParameterTypeString is not a function »
"~/Scripts/cldr-data.js" // fichier à créer
));
|
|
@section Scripts {
@Scripts.Render("~/bundles/globalize")
@Scripts.Render("~/bundles/jqueryval")
|
cldr-data.js
|
// fichier à créer pour permettre le chargement des scripts cldr-data
// pour currency et date
$.when(
$.getJSON("/Scripts/cldr-data/supplemental/likelySubtags.json"),
$.getJSON("/Scripts/cldr-data/main/fr/numbers.json"),
$.getJSON("/Scripts/cldr-data/supplemental/numberingSystems.json"),
$.getJSON("/Scripts/cldr-data/supplemental/plurals.json"),
$.getJSON("/Scripts/cldr-data/supplemental/ordinals.json"),
$.getJSON("/Scripts/cldr-data/main/fr/currencies.json"),
$.getJSON("/Scripts/cldr-data/supplemental/currencyData.json"),
$.getJSON("/Scripts/cldr-data/main/fr/ca-gregorian.json"),
$.getJSON("/Scripts/cldr-data/main/fr/timeZoneNames.json"),
$.getJSON("/Scripts/cldr-data/supplemental/timeData.json"),
$.getJSON("/Scripts/cldr-data/supplemental/weekData.json"),
).then(function () {
return [].slice.apply(arguments, [0]).map(function (result) {
return result[0];
});
}).then(Globalize.load).then(function () {
Globalize.locale("fr");
});
|
Resources
Log
Health Monitoring
elmah
Installer Elmah.MVC avec Nuget.
Authentication
Web API Authorize
Individual User Accounts |
login + password. Profils utilisateur sont stockés dans une bdd SQL Server. Permet aussi de s'authentifier avec des comptes Facebook, Twitter, Google et autres.
|
Work or School Accounts |
Active Directory, Azure Active Directory, Office 365
|
Windows Authentication |
Active Directory en intranet
|
Individual User Accounts
Controllers/HomeController.cs
|
[Authorize] // restreint l'accès de toutes les actions du controller aux utilisateurs authentifiés
public class HomeController : Controller
{
[Authorize] // restreins l'accès aux utilisateurs authentifiés
public ContentResult Secret()
// restreins l'accès aux utilisateurs user1 et user2
// et aux utilisateurs du role admin
[Authorize(Users = "user1,user2", Roles = "admin")]
public ContentResult Admin()
// dans le cas où le controller restreint l'accès de toutes les actions aux utilisateurs authentifiés
// autorise l'accès à tous les utilisateurs pour cette action
[AllowAnonymous]
public ContentResult NotASecret()
|
App_Start/Startup.Auth.cs |
configuration
|
Views/Shared/_LoginPartial.cshtml |
Liens Login et Register + message d'accueil
|
Controllers/AccountController.cs |
action login
|
Bdd
Utilise EF pour stocker les profils utilisateurs dans la bdd.
Models\IdentityModels.cs
|
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext()
: base("DefaultConnection", throwIfV1Schema: false)
|
Web.config
|
<connectionStrings>
<add name="DefaultConnection"
connectionString="Data Source=(LocalDb)\MSSQLLocalDB;
AttachDbFilename=|DataDirectory|\aspnet-[PROJECT NAME]-[DATE].mdf;
Initial Catalog=aspnet-[PROJECT NAME]-[DATE];
Integrated Security=True"
providerName="System.Data.SqlClient" />
|
La bdd localdb se trouve cachée dans le dossier App_Data
|
// créer un nouveau role
var roleStore = new RoleStore<IdentityRole>(context);
var roleManager = new RoleManager<IdentityRole>(roleStore);
roleManager.Create(new IdentityRole { Name = "admin" });
// créé un nouvel utilisateur
var userStore = new UserStore<ApplicationUser>(context);
var userManager = new UserManager<ApplicationUser>(userStore);
var user = new ApplicationUser { UserName = "john" };
userManager.Create(user, "password");
// associer le nouvel utilisateur au role admin
userManager.AddToRole(user.Id, "admin");
|
External Logins (OpenID, OAuth)
App_Start/Startup.Auth.cs
|
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
// permettre l'authentification via un compte Microsoft
app.UseMicrosoftAccountAuthentication(
clientId: "",
clientSecret: "");
|
Controllers/AccountController.cs
|
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
// accéder à l'email du compte externe
var result = await AuthenticationManager.AuthenticateAsync(DefaultAuthenticationTypes.ExternalCookie);
// result.Identity.Claims
|
Javascript
Bundle
En release, regroupe et minifie différents fichiers JS dans un seul fichier afin de réduire le nombre de téléchargement.
Global.asax.cs
|
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
BundleConfig.RegisterBundles(BundleTable.Bundles);
|
App_Start/BundleConfig.cs
|
public class BundleConfig
{
public static void RegisterBundles(BundleCollection bundles)
{
// met les fichiers suivants dans le bundle custom
bundles.Add(new ScriptBundle("~/bundles/custom").Include(
"~/Scripts/jquery-{version}.js",
"~/Scripts/jquery.unobtrusive*",
"~/Scripts/jquery.validate*",
"~/Scripts/custom.js"));
|
|
@section scripts {
@Scripts.Render("~/bundles/custom")
}
|
AJAX
|
Installer Microsoft.jQuery.Unobtrusive.Ajax avec NuGet.
Et inclure le script ~/Scripts/jquery.unobtrusive-ajax.js |
Debug
Utiliser IE.
Vider le cache dans IE 11
- Tools → Safety → Delete Browsing History (Ctrl + Shift + Del)
- Tools → Internet Options → Browsing History → Delete browsing history on Exit
CRUD
Create
PersonController.cs
|
// GET: Persons/Create
public ActionResult Create()
{
return View();
}
// POST: Persons/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "Name")] Person person)
{
if (ModelState.IsValid)
{
db.Persons.Add(person);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(person);
}
|
Views/Person/Create.cshtml
|
@model FullNamespace.Person
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
<div class="form-group">
@Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Date, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
}
|
Read / Detail
PersonController.cs
|
// GET: Persons/Details/5
public ActionResult Details(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Person person = db.Persons.Find(id);
if (person == null)
{
return HttpNotFound();
}
return View(person);
}
|
Views/Person/Details.cshtml
|
@model FullNamespace.Person
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Name)
</dt>
<dd>
@Html.DisplayFor(model => model.Name)
</dd>
</dl>
|
Update / Edit
PersonController.cs
|
// GET: Persons/Edit/5
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Person person = db.Persons.Find(id);
if (person == null)
{
return HttpNotFound();
}
return View(person);
}
// POST: Persons/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "Name")] Person person)
{
if (ModelState.IsValid)
{
db.Entry(person).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(person);
}
|
Views/Person/Edit.cshtml
|
@model FullNamespace.Person
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
@Html.HiddenFor(model => model.Id)
<div class="form-group">
@Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
}
|
Delete
PersonController.cs
|
// GET: Persons/Delete/5
public ActionResult Delete(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Person person = db.Persons.Find(id);
if (person == null)
{
return HttpNotFound();
}
return View(person);
}
// POST: Persons/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
Person person = db.Persons.Find(id);
db.Ecritures.Remove(person);
db.SaveChanges();
return RedirectToAction("Index");
}
|
Views/Person/Delete.cshtml
|
@model FullNamespace.Person
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Name)
</dt>
<dd>
@Html.DisplayFor(model => model.Name)
</dd>
</dl>
@using (Html.BeginForm()) {
@Html.AntiForgeryToken()
<div class="form-actions no-color">
<input type="submit" value="Delete" class="btn btn-default" />
</div>
}
|
Tests unitaires
Il faut choisir une source de données pour les tests:
- une bdd de test
- des données statique chargées en mémoire, il faudra modifier l'accès aux données via un service
PersonControllerTest.cs
|
[TestClass]
public class PersonControllerTest
{
[TestMethod]
public void Index()
{
var dataService = new InMemoryDataService();
dataService.Add(TestData.Persons);
var controller = new PersonController(dataService);
// pour éviter l'erreur avec Request.IsAjaxRequest()
controller.ControllerContext = new FakeControllerContext();
ViewResult result = controller.Index() as ViewResult;
var model = result.Model as IEnumerable<Person>;
Assert.IsNotNull(result);
Assert.AreEqual(model.Count(), 10);
}
[TestMethod]
public void Create()
{
var dataService = new InMemoryDataService();
var controller = new PersonController(dataService);
controller.Create(new Person());
Assert.AreEqual(1, dataService.Added.Count);
Assert.AreEqual(true, dataService.Saved);
}
[TestMethod]
public void Create_ModelError()
{
var dataService = new InMemoryDataService();
var controller = new PersonController(dataService);
controller.ModelState.AddModelError("", "Invalid");
controller.Create(new Person());
Assert.AreEqual(0, dataService.Added.Count);
}
}
|
PersonController.cs
|
public class PersonsController : Controller
{
IDataService dataService;
// DataService par défaut
public EcrituresController()
{
dataService = new DbDataService();
}
// injection du DataService pour les tests
public EcrituresController(IDataService dataService)
{
this.dataService = dataService;
}
// db.Persons → dataService.Query<Person>()
// db.Persons.Find(id) → dataService.Query<Person>().SingleOrDefault(p => p.Id == id);
// db.Persons.Add(person) → dataService.Add(person)
// db.Entry(person).State = EntityState.Modified → dataService.Update(person);
// db.Persons.Remove(person) → dataService.Remove(person)
|
DataService
IDataService.cs
|
public interface IDataService : IDisposable
{
IQueryable<T> Query<T>() where T : class;
void Add<T>(T entity) where T : class;
void Update<T>(T entity) where T : class;
void Remove<T>(T entity) where T : class;
void SaveChanges();
}
|
DbDataService.cs
|
public class DbDataService : DbContext, IDataService
{
public DbSet<Person> Persons { get; set; }
public void Add<T>(T entity) where T : class
{
Set<T>().Add(entity);
}
public IQueryable<T> Query<T>() where T : class
{
return Set<T>();
}
public void Remove<T>(T entity) where T : class
{
Set<T>().Remove(entity);
}
public void Update<T>(T entity) where T : class
{
Entry(entity).State = EntityState.Modified;
}
void IDataService.SaveChanges()
{
SaveChanges();
}
}
|
InMemoryDataService.cs
|
class InMemoryDataService : IDataService
{
public Dictionary<Type, object> Sets = new Dictionary<Type, object>();
public List<object> Added = new List<object>();
public List<object> Updated = new List<object>();
public List<object> Removed = new List<object>();
public bool Saved;
public IQueryable<T> Query<T>() where T : class
{
return Sets[typeof(T)] as IQueryable<T>;
}
public void Add<T>(IQueryable<T> objects)
{
Sets.Add(typeof(T), objects);
}
public void Dispose()
{
}
public void Add<T>(T entity) where T : class
{
Added.Add(entity);
}
public void Update<T>(T entity) where T : class
{
Updated.Add(entity);
}
public void Remove<T>(T entity) where T : class
{
Removed.Add(entity);
}
public void SaveChanges()
{
Saved = true;
}
}
|
TestData.cs
|
class TestData
{
public static IQueryable<Person> Persons
{
get
{
var persons = new List<Person>();
// ...
return persons.AsQueryable();
|
FakeControllerContext
FakeControllerContext.cs
|
class FakeControllerContext : ControllerContext
{
HttpContextBase _context = new FakeHttpContext();
public override HttpContextBase HttpContext
{
get => _context;
set => _context = value;
}
}
class FakeHttpContext : HttpContextBase
{
HttpRequestBase _request = new FakeHttpRequest();
public override HttpRequestBase Request => _request;
}
class FakeHttpRequest : HttpRequestBase
{
public override string this[string key] => null;
public override NameValueCollection Headers => new NameValueCollection();
}
|
Exemples
Filtre de recherche
Controllers/PersonController.cs
|
public ActionResult Index(string searchTerm = null)
{
var persons = db.Persons.Include(p => p.Company)
.Where(e => searchTerm == null || p.Name.Contains(searchTerm))
.OrderBy(p => p.BirthDate)
.Take(20);
return View(persons.ToList());
}
|
Views/Persons/Index.cshtml
|
@model IEnumerable<Namespace.Models.Person>
<!-- get /?searchTerm=paul -->
<form method="get">
<input type="search" name="searchTerm" />
<input type="submit" value="Search" />
</form>
<!-- Grille de personnes -->
|
Filtre de recherche avec AJAX
Views/Persons/Index.cshtml
|
@* champs qui permet de filtrer les personnes en rechargeant seulement la table de personnes et non la page en entier *@
@using (Ajax.BeginForm(new AjaxOptions
{
HttpMethod = "get",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "personsList"
}))
{
<input type="search" name="searchTerm" />
<input type="submit" value="Search" />
}
@Html.Partial("_Persons", Model)
|
Views/Persons/_Persons.cshtml
|
<div id="personsList">
<!-- Grille de personnes -->
|
Controllers/PersonsController.cs
|
// Si c'est une requête AJAX, le controlleur retourne seulement la PartialView, sinon la vue complète
if (Request.IsAjaxRequest())
{
return PartialView("_Persons", persons);
}
return View(persons);
|
PagedList
Installation: NuGet → PagedList.Mvc
Controllers/PersonsController.cs
|
// numéro de la page en paramètre, 1 par défaut
public ActionResult Index(int page = 1)
{
var persons = db.Persons.ToPagedList(page, 10); // 10 éléments par page
return View(persons);
}
|
Views/Persons/Index.cshtml
|
@Html.Partial("_Persons", Model)
|
Views/Persons/_Persons.cshtml
|
@using PagedList
@using PagedList.Mvc
@model IEnumerable<Namespace.Models.Person>
<div id="personsList">
<div class="pagedList" data-target="#personsList">
@Html.PagedListPager((IPagedList)Model, page => Url.Action("Index", new { page }), PagedListRenderOptions.MinimalWithItemCountText)
</div>
<!-- Grille de personnes -->
<table class="table">
|
PagedList avec AJAX
|
$(function () {
var getPage = function () {
var $a = $(this);
var options = {
url: $a.attr("href"),
data: $("form").serialize(),
type: "get"
};
$.ajax(options).done(function (data) {
var target = $a.parents("div.pagedList").attr("data-target");
$(target).replaceWith(data);
});
return false;
};
// comme les balises "a" next et previous sont recréées à chaque changement de page on ne peut pas les sélectionner directement
// on sélectionne donc body-content, et on lie le clique sur ".pagedList a" à la méthode getPage
$(".body-content").on("click", ".pagedList a", getPage);
});
|
Installer avec NuGet: jQuery.UI.Combined, jQuery.UI.i18n
Scripts/jquery-ui-custom.js
|
if (!Modernizr.inputtypes.date) {
$(function () {
$("input[type = date]").datepicker($.extend(
{},
$.datepicker.regional['fr'],
{
showAnim: 'drop'
}
));
});
}
|
App_Start/BundleConfig.cs
|
bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
"~/Scripts/jquery.validate.js",
"~/Scripts/jquery-ui-1.12.1.js",
"~/Scripts/jquery-ui-i18n.js",
"~/Scripts/jquery-ui-custom.js")); // à mettre en dernier
bundles.Add(new StyleBundle("~/Content/css").Include(
"~/Content/site.css",
"~/Content/themes/base/all.css"));
|
JQuery UI Datepicker