ASP.NET MVC

De Banane Atomic
Révision datée du 28 février 2018 à 14:42 par Nicolas (discussion | contributions) (→‎HTML Helpers)
(diff) ← Version précédente | Voir la version actuelle (diff) | Version suivante → (diff)
Aller à la navigationAller à la recherche

Liens

MVC

Model
  • Modèle de données
  • Accès aux données (bdd, webservice)
View
  • affichage
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

Cshtml.svg
@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.

OutputCache

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.
Csharp.svg
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

Cs.svg
[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.
Cs.svg
public class MyClass : IValidatableObject
{
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Property1 == "Test" && Property1 == 10)
        {
            yield return new ValidationResult("Custom error message");
        }
    }
}
Cshtml.svg
@* code permettant l'affichage des erreurs IValidatableObject *@
@Html.ValidationSummary(true, "", new { @class = "text-danger" })

Arborescence

  • App_Start
    • BundleConfig.cs
    • FilterConfig.cs
    • RouteConfig.cs
  • Content
    • images
    • themes
    • Site.css
  • Controllers
    • HomeController.cs
  • fonts
  • Models
  • Scripts
    • fichiers javascript
  • Views
    • Home (pour le controller Home)
      • Index.cshtml (pour l'action Index)
    • Shared (partagés par tous les controllers)
      • Error.cshtml
    • Web.config
  • Global.asax
  • Web.config

Configuration

AppSettings

Web.config
<appSettings>
  <add key="Key1" value="Value1" />
Cs.svg
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"));
));
Cshtml.svg
@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.

Powershell.svg
# 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
));
Cshtml.svg
@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

Cs.svg
// 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"));
Cshtml.svg
@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

Js.svg
$(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);

});

Datepicker JQuery UI

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