ASP.NET MVC et WEB API
Transcription
ASP.NET MVC et WEB API
1 Asp.Net MVC et WEB API I. MVC ......................................................................................................................................................... 4 1. « FROM SCRATCH » - DEMARRER AVEC UN PROJET ASP.NET VIDE................................................................................. 4 2. ROUTES ............................................................................................................................................................. 8 a. RouteConfig ................................................................................................................................................ 8 b. Attribut Route ............................................................................................................................................. 9 c. Area........................................................................................................................................................... 10 d. Récupérer les informations de route ........................................................................................................ 11 3. CONTROLEURS................................................................................................................................................... 12 a. « Action Result » ....................................................................................................................................... 12 b. Index ......................................................................................................................................................... 12 c. Détails ....................................................................................................................................................... 13 d. Create ....................................................................................................................................................... 13 e. Edit ............................................................................................................................................................ 14 f. Delete ........................................................................................................................................................ 14 g. Actions asynchrones ................................................................................................................................. 15 h. Actions Selectors ....................................................................................................................................... 15 i. Action filters ......................................................................................................................................... 16 j. Contrôleur Mvc comme Web Service ........................................................................................................ 17 4. VIEWS ............................................................................................................................................................. 20 a. _ViewStart ........................................................................................................................................... 22 b. _Layout (Master page/ Page de disposition) ....................................................................................... 22 @RenderSection() ...................................................................................................................................................... 23 c. d. e. Définir le modèle de la vue .................................................................................................................. 23 Html Helpers ........................................................................................................................................ 23 Code expressions pour insérer du C# dans le HTML .......................................................................... 25 Code blocks ................................................................................................................................................................ 25 f. g. h. i. Scripts .................................................................................................................................................. 25 SelectList .............................................................................................................................................. 26 Moteur de vue Aspx ............................................................................................................................. 26 Ajax Helpers ......................................................................................................................................... 26 Ajax Form ................................................................................................................................................................... 27 Ajax ActionLink........................................................................................................................................................... 28 5. VALIDATION ...................................................................................................................................................... 29 a. Data Annotations................................................................................................................................. 29 b. IValidatableObject ............................................................................................................................... 31 c. Modifier le style des erreurs ................................................................................................................ 32 d. Validation côté « Client » avec jQuery Validation Plugin ................................................................... 32 6. SECURITE..................................................................................................................................................... 33 a. Windows (Intranet) .............................................................................................................................. 33 b. « From scratch » - Sécurité « Forms » avec un projet Asp.Net vide .................................................... 34 Packages NuGet ......................................................................................................................................................... 34 Préparation ................................................................................................................................................................ 34 « IdentityConfig » (dossier « App_Start ») ............................................................................................................ 34 ApplicationUser..................................................................................................................................................... 36 Classe partielle « Startup » ................................................................................................................................... 37 Base de données ................................................................................................................................................... 38 2 Personnalisation des pages ................................................................................................................................... 38 Contrôleurs et ViewModels .................................................................................................................................. 39 Inscription (register) .................................................................................................................................................. 40 Connexion (Login) ...................................................................................................................................................... 43 Connexion «externe » avec Facebook, Google, Twitter ............................................................................................ 48 Facebook ............................................................................................................................................................... 48 Google ................................................................................................................................................................... 50 Twitter................................................................................................................................................................... 52 Code supplémentaire ............................................................................................................................................ 52 c. Autorisations (attribut « Authorize »).................................................................................................. 55 ValidateAntiForgeryToken ......................................................................................................................................... 56 AntiXSS ....................................................................................................................................................................... 57 d. e. SSL ........................................................................................................................................................ 57 Projet Visual Studio 2012 ..................................................................................................................... 58 InitializeSimpleMembership et WebSecurity ............................................................................................................. 58 SimpleRoleProvider et SimpleMembershipProvider ................................................................................................. 59 Register ...................................................................................................................................................................... 60 Login .......................................................................................................................................................................... 60 7. LOCALISATION .............................................................................................................................................. 61 Dates et Monnaies........................................................................................................................................ 61 Resources...................................................................................................................................................... 62 8. LESS ........................................................................................................................................................... 63 9. DEPENDENCY INJECTION (DI) .......................................................................................................................... 63 Avec Ninject ............................................................................................................................................................... 63 10. II. UPGRADE ................................................................................................................................................ 64 WEB API ................................................................................................................................................. 65 1. 2. Web Api config..................................................................................................................................... 65 Contrôleur Web Api ............................................................................................................................. 65 a. b. c. d. 3. 4. HTTP Status codes ............................................................................................................................................ 65 Ajout d’un contrôleur Web Api ........................................................................................................................ 65 Contrôleur Web API avec IhttpActionResult .................................................................................................... 66 Contrôleur Web API avec HttpResponseMessage ............................................................................................ 68 Attribut “Route” ................................................................................................................................... 71 Injection de dépendances .................................................................................................................... 71 Avec Ninject ............................................................................................................................................................... 71 5. Projet Client ......................................................................................................................................... 72 a. b. 6. 7. 8. 9. 10. a. b. III. dataService JavaScript ...................................................................................................................................... 72 HttpClient ......................................................................................................................................................... 73 Cors ...................................................................................................................................................... 75 Formatters pour le « Mapping » .......................................................................................................... 76 OData................................................................................................................................................... 76 Authentification Web Api .................................................................................................................... 77 MongoDB ........................................................................................................................................ 82 Installation de MongoDB C# driver .................................................................................................................. 82 Service WEB ..................................................................................................................................................... 83 BOOTSTRAP ........................................................................................................................................... 86 1. 2. INSTALLATION ET THEMES ............................................................................................................................... 86 GRID SYSTEM ............................................................................................................................................... 87 a. Cacher une colonne selon une résolution ............................................................................................ 87 b. Décalage de colonnes avec « offset » .................................................................................................. 88 2 3 c. 3. a. b. c. d. Image flottante .................................................................................................................................... 88 BASES ......................................................................................................................................................... 88 Typographie ......................................................................................................................................... 88 Boutons, groupes de boutons et dropdowns ....................................................................................... 89 Icones ................................................................................................................................................... 90 Formulaires, listes, et tables ................................................................................................................ 90 « input-group » .......................................................................................................................................................... 90 e. f. g. h. i. Navbar desktop et mobile.................................................................................................................... 90 Header et breadcrumb ......................................................................................................................... 91 Pagination............................................................................................................................................ 91 Well ...................................................................................................................................................... 92 Panels .................................................................................................................................................. 92 PLUGINS .............................................................................................................................................................. 92 a. Collapse et accordéon .......................................................................................................................... 92 b. Boite de dialogue ................................................................................................................................. 93 c. Alert ..................................................................................................................................................... 93 d. Tab ....................................................................................................................................................... 94 e. Tooltip .................................................................................................................................................. 94 f. Caroussel.............................................................................................................................................. 95 3 4 I. MVC ROUTES Contrôleur « http://... / » « contrôleur »/« action »/« id » Va chercher les données Modèles/ Données Action Result Vue (Affiche les données) Vue partielle « content » (texte, xml) JSON Redirection etc. 1. « From scratch » - Démarrer avec un projet Asp.Net vide 1. Créer un projet Asp.Net vide 2. Installer « Asp.Net Mvc 5 » avec les packages NuGet 3. Ajouter « RouteConfig » dans un dossier « App_Start » using using using using using using System; System.Collections.Generic; System.Linq; System.Web; System.Web.Mvc; System.Web.Routing; namespace MvcFromScratch { public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); Définition des routes de l’application. Ici la route par défaut dirigeant vers l’action « Index » du contrôleur « Home » routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); } } } 4 5 4. Ajouter fichier de configuration de l’application « Global.asax» using using using using using using System; System.Collections.Generic; System.Linq; System.Web; System.Web.Mvc; System.Web.Routing; namespace MvcFromScratch { public class Global : System.Web.HttpApplication { protected void Application_Start() { RouteConfig.RegisterRoutes(RouteTable.Routes); } } } Enregistrement des routes définies dans le fichier « RouteConfig » 5. Ajouter un contrôleur « Home » dans un dossier « Controllers » using using using using using System; System.Collections.Generic; System.Linq; System.Web; System.Web.Mvc; namespace MvcFromScratch.Controllers { public class HomeController : Controller { // GET: Home public ActionResult Index() { return Content("Bonjour!"); } } } L’ « action result » ici est une chaine de caractère. Cela affichera juste ce message dans une page Projet minimum 5 6 Avec Vue en « action result » public ActionResult Index() { return View(); } Vues : « _ViewStart.cshtml » hérite de « StartPage » (System.Web.Mvc) sert à définir la master page « par défaut » utilisée par toutes les pages de contenu. Toutes les autres pages héritent de « WebViewPage » (_Lauout, Index, etc.) 2 possibilités : Soit définir en haut de chaque vue la page dont elle hérite Exemple « _ViewStart.cshtml » @inherits System.Web.WebPages.StartPage @{ Layout = "~/Views/Shared/_Layout.cshtml"; } « _Layout.cshtml » la master page (ou page de disposition) @inherits System.Web.Mvc.WebViewPage <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> </head> <body style="background:red;"> <div> @RenderBody() </div> </body> </html> 6 7 Soit on crée un fichier de configuration « Web.config » pour le dossier des vues « Views » pour ne pas avoir à définir sur chaque page <?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"> <section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" /> <section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" /> </sectionGroup> </configSections> <system.web.webPages.razor> <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.2.2.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" /> <pages pageBaseType="System.Web.Mvc.WebViewPage"> <namespaces> <add namespace="System.Web.Mvc" /> <add namespace="System.Web.Mvc.Ajax" /> <add namespace="System.Web.Mvc.Html" /> <add namespace="System.Web.Routing" /> <add namespace="MvcFromScratch" /> </namespaces> </pages> </system.web.webPages.razor> </configuration> Ainsi on peut simplifier les pages… par exemple « _ViewStart.cshtml » @{ On supprime l’héritage en haut des pages Layout = "~/Views/Shared/_Layout.cshtml"; } Projet minium avec vues et master page Les vues sont rangées dans un dossier portant le nom de leur contrôleur _Layout.cshtml la master page _ViewStart.cshtml permet de définir la master page à appliquer par défaut aux pages de contenu 7 8 2. Routes a. RouteConfig Définir les routes de l’application dans « RouteConfig » (dossier « App_Start ») public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { action = "Index", controller="People", id = UrlParameter.Optional } ); } } Créer une seconde route public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); On place la route au-dessus de la route « par défaut » La route routes.MapRoute( name: "Ma route", « http://.../personal/ » url: "personal/{myparameter}", defaults: new { controller = "Home", action = "Personal", myparameter = "Bonjour" } ); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); } } Dans le contrôleur « Home », on ajoute l’action pour cette route public class HomeController : Controller { // code retiré public ActionResult Personal(string myparameter) { return Content(myparameter); } } 8 9 Renvoie par défaut la valeur définie dans « RouteConfig » Paramètre passé Dans le cas où on aurait pas défini de paramètre de route routes.MapRoute( name: "Ma route", url: "personal", defaults: new { controller = "Home", action = "Personal"} ); … On pourrait quand même passer un paramètre à l’action (code inchangé) en le nommant b. Attribut Route public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); Permettre l’utilisation de l’attribut « route » .Attention de bien mettre avant la route par défaut routes.MapMvcAttributeRoutes(); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); } } Utilisation : Dans le contrôleur « Home » [Route("personal")] public ActionResult Personal() { return Content("Route ok!"); } Si on veut ajouter des paramètres, les mettre entre accolades [Route("personal/{message}")] public ActionResult Personal(string message) { return Content(message); } 9 10 c. Area Il peut être intéressant de découper les gros projets en « Areas » Exemple public class MusicAreaRegistration : AreaRegistration { public override string AreaName { get { return "Music"; } } public override void RegisterArea(AreaRegistrationContext context) { context.MapRoute( "Music_default", "Music/{controller}/{action}/{id}", new { action = "Index", id = UrlParameter.Optional }, namespaces: new[] { "AreaDemo.Areas.Music.Controllers" } ); } } « Global.asax » AreaRegistration.RegisterAllAreas(); 10 11 d. Récupérer les informations de route public class HomeController : Controller { // code retiré public ActionResult Personal() { var controller = RouteData.Values["controller"]; // var action = RouteData.Values["action"]; // var id = RouteData.Values["id"]; // var result = string.Format("{0}, {1}, {2}", controller, action, id); return Content(result); } } Exemple depuis une vue on crée un lien action contrôleu r paramètre @Html.ActionLink("A envoyer!", "Personal", new { controller = "Home", id = 10 }) L’adresse du lien « http://localhost:7211/Home/Personal/10 » Depuis une action du contrôleur public ActionResult Index() { return RedirectToAction("Personal", "Home", new { id = 100 }); } 11 12 3. Contrôleurs a. « Action Result » ViewResult : une page complète public ActionResult Index() { IEnumerable<Person> people = return View(people); } _peopleRepository.GetAll(); PartialViewResult : une section de page return PartialView("_people", people); ContentResult : texte, xml return Content("Bonjour!"); JsonResult : objet JSON return Json(people,JsonRequestBehavior.AllowGet); RedirectToRouteResult : redirection vers une autre action return RedirectToRoute(new { controller = "Home", action = "index" }); return RedirectToAction("Index"); JavaScriptResult return JavaScript("alert('Bonjour!')"); + EmptyResult, FileResult, HttpUnauthorizedResult, etc. b. Index public ActionResult Index() { IEnumerable<Person> people = return View(people); } _peopleRepository.GetAll(); 12 13 c. Détails public ActionResult Details(int id) { Person person = _peopleRepository.GetOne(id); if (person == null) return HttpNotFound("Personne non trouvée"); return View(person); } d. Create public ActionResult Create() { Person newPerson = new Person(); return View(newPerson); } [HttpPost] public ActionResult Create(Person person) { if (ModelState.IsValid) { _peopleRepository.Add(person); return RedirectToAction("Index"); } return View(person); On crée dans un premier temps un nouvel élément que l’on affiche dans la vue « Create » on ajoute le nouvel élément (post). S’il y a des erreurs de validation on redirige pour que l’utilisateur corrige } 13 14 e. Edit public ActionResult Edit(int id = 0) { Person person = _peopleRepository.GetOne(id); if (person == null) return HttpNotFound("Personne non trouvée"); return View(person); } [HttpPost] public ActionResult Edit(Person person) { if (ModelState.IsValid) { _peopleRepository.Update(person); return RedirectToAction("Index"); } return View(person); } On récupère l’élément à modifier que l’on affiche dans la vue « Edit » on sauve les modifications apportées s’il n’y a pas d’erreurs f. Delete public ActionResult Delete(int id = 0) { Person person = _peopleRepository.GetOne(id); if (person == null) return HttpNotFound("Personne non trouvée"); return View(person); } [HttpPost, ActionName("Delete")] public ActionResult DeleteConfirmed(int id) { _peopleRepository.Delete(id); return RedirectToAction("Index"); } On récupère l’élément à supprimer que l’on affiche Si l’utilisateur confirme la suppression, on supprime réellement l’élément. 14 15 g. Actions asynchrones public async Task<ActionResult> Index() { IEnumerable<Person> people = await _peopleRepository.GetAllAsync(); return View(people); } h. Actions Selectors Attribut « HttpGet » par défaut, inutile de l’indiquer Attribut « HttpPost » ViewBag permet d’afficher des messages passés depuis le contrôleur. Exemple Dans le contrôleur public ActionResult Index() { ViewBag.Message1 = "Bonjour!"; ViewBag.Message2 = "Aurevoir :x"; return View(); } Dans la vue <h3>@ViewBag.Message1</h3> <h3>@ViewBag.Message2</h3> 15 16 i. Action filters Ce sont des attributs que l’on place au-dessus d’une action ou du contrôleur. Authorization filter (Attribut « Authorize ») Autorize et ValidateAntiForgeryToken Voir Authentification ValidateInput Accepte toutes les entrées (même s’il y a des balises, ce qui peut représenter un code malveillant) [HttpPost] [ValidateInput(true)] public ActionResult Edit(Person person) { // } Il est possible également d’utiliser l’attribut « AllowHtml » pour une propriété du modèle [AllowHtml] public string LastName { get; set; } Action filter (faire hériter de « ActionFilterAttribute » et override « OnActionExecuting », « OnActionExecuted ») Result filter : exemple « OutputCache » (met en cache 60 secondes) //[ChildActionOnly] [OutputCache(Duration=60)] public ActionResult Index() { IEnumerable<Person> people = _peopleRepository.GetAll(); return View(people); } Exception fliter Pour gérer une « unhandled exception ». Exemple redirige vers une vue [HandleError(View="Errors")] public ActionResult Edit(Person person) { Pour voir la page erreur comme l’utilisateur (et non page d’erreur « coté serveur ») 16 17 j. Contrôleur Mvc comme Web Service Même si utiliser un contrôleur Web Api est tout indiqué aujourd’hui, cela reste possible d’utiliser un contrôleur Mvc comme web service. public class PeopleWebServiceController : Controller { private IPeopleRepository _peopleRepository; public PeopleWebServiceController() : this(new FakePeopleRepository()) { } public PeopleWebServiceController(IPeopleRepository peopleRepository) { this._peopleRepository = peopleRepository; } public ActionResult Index() { IEnumerable<Person> people = _peopleRepository.GetAll(); return Json(people, JsonRequestBehavior.AllowGet); } public ActionResult Details(int id) { Person person = _peopleRepository.GetOne(id); if (person == null) return HttpNotFound(); return Json(person, JsonRequestBehavior.AllowGet); } [HttpPost] public void Create(Person person) { _peopleRepository.Add(person); } [HttpPost] public void Edit(Person person) { _peopleRepository.Update(person); } public void Delete(int id) { _peopleRepository.Delete(id); } } Json.net (documentation) Sérialization d’un objet string json = JsonConvert.SerializeObject(person); Désérialization de JSon JsonConvert.DeserializeObject<Person>(resultContent) 17 18 Tests avec Fiddler Index et détails Ajout Edition Suppression 18 19 Projet Client Utilisation de HttpClient(System.Net.Http) et de Json.Net (Projet WinRT, Wpf) public class PeopleService { string urlBase = "http://localhost:15089/peoplewebservice/"; public async Task<Person[]> GetAll() { using (HttpClient client = new HttpClient()) { HttpResponseMessage response = await client.GetAsync(urlBase); string resultContent = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject<Person[]>(resultContent); } } public async Task<Person> GetOne(int personID) { using (HttpClient client = new HttpClient()) { HttpResponseMessage result = await client.GetAsync(string.Format("{0}/details/{1}", urlBase, personID)); string resultContent = await result.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject<Person>(resultContent); } } public async void Add(Person person) { using (HttpClient client = new HttpClient()) { string json = JsonConvert.SerializeObject(person); HttpResponseMessage response = await client.PostAsync(string.Format("{0}/create", urlBase), new StringContent(json, Encoding.UTF8, "application/json")); string resultContent = await response.Content.ReadAsStringAsync(); person = JsonConvert.DeserializeObject<Person>(resultContent); } } public async void Update(Person person) { using (HttpClient client = new HttpClient()) { string json = JsonConvert.SerializeObject(person); HttpResponseMessage response = await client.PutAsync(string.Format("{0}/edit", urlBase), new StringContent(json, Encoding.UTF8, "application/json")); string resultContent = await response.Content.ReadAsStringAsync(); person = JsonConvert.DeserializeObject<Person>(resultContent); } } public async void Delete(int personID) { using (HttpClient client = new HttpClient()) { HttpResponseMessage response = await client.DeleteAsync(string.Format("{0}/delete/{1}", urlBase, personID)); response.EnsureSuccessStatusCode(); } } } 19 20 4. Views Il existe 2 moteurs de vues : Razor (*.cshtml) et Aspx (*.aspx) (Le code des contrôleurs ne diffère pas selon le moteur de vue utilisé) On peut ajouter une vue : - Depuis le menu contextuel sur une action d’un contrôleur - Sur le répertoire « Views » Un dossier de vues par contrôleur Contrôleurs Un dossier de vues par contrôleur Vue Index Details Create Edit Delete … Action Index Details Create Edit Delete 20 21 Création d’une vue Choix du modèle de la vue Vue partielle Vue partielle. On a l’habitude de nommer les vues partielles avec « _ » pour les différencier rapidement Utilisation : Dans la vue @Html.Partial("_people", Model) Contrôleur return PartialView("_people", people); 21 22 a. _ViewStart Définir la master page par défaut pour les pages de contenu @{ Layout = "~/Views/Shared/_Layout.cshtml"; } Pour affecter une autre master page à une vue particulière. Définir la master page en haut de la vue @{ Layout = "~/Views/Shared/_MyLayout.cshtml"; } b. _Layout (Master page/ Page de disposition) (Dossier « Shared ») Contient tout le HTML commun pour les pages ainsi que RenderBody() pour afficher le contenu des vues et RenderSection(). On peut créer plusieurs master pages. Exemple de master page <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@ViewBag.Title</title> @Styles.Render("~/Content/css") @Scripts.Render("~/bundles/modernizr") </head> <body> <header> <h1>Titre</h1> <nav> <ul> <li>@Html.ActionLink("Accueil", "Index")</li> <li>@Html.ActionLink("A propos", "Aboutus")</li> <li>@Html.ActionLink("Contact", "Contact")</li> </ul> </nav> </header> <div> @RenderBody() </div> <footer></footer> @Scripts.Render("~/bundles/jquery") @Scripts.Render("~/bundles/bootstrap") @RenderSection("scripts", required: false) </body> </html> 22 23 @RenderSection() Permet de définir ses propres sections Exemple Dans la master page (_layout.cshtml) <div> @if (IsSectionDefined("header")) { @RenderSection("header", required: false); } else { <h3>Header par défaut</h3> } </div> <div class="container"> @RenderBody() </div> Dans une vue @section header { <h3>Ma section header</h3> } c. Définir le modèle de la vue Exemple @model MvcOverview.Models.Person Ou pour une vue Liste @model IEnumerable<MvcOverview.Models.Person> Avec @using @using MvcOverview.Models @model IEnumerable<Person> d. Html Helpers Documentation Lien (Avec paramètre ici) @Html.ActionLink("Editer", "Edit", new { id = item.Id }) Action Insère le résultat de l’action (ici la vue « Create ») <p>@Html.Action("Create")</p> @url <a href="@Url.Action("Index","Home")"><img src="~/Images/logo.png" /></a> 23 24 Formulaire Html BeginForm EndForm TextArea TextBox Label CheckBox RadioButton ListBox Fortement typé CheckBoxFor EditorFor ListBoxFor RadioButtonFor TextAreaFor ValidationMessage ValidationSummary Vue en consultation (List, Details) Label (DisplayNameFor) @Html.DisplayNameFor(model => model.FirstName) Affiche la valeur (DisplayFor) @Html.DisplayFor(model => model.FirstName) Vue avec édition (Create, Edit) Label @Html.LabelFor(model => model.FirstName, htmlAttributes: new { @class = "control-label col-md-2" }) … puis champ en edition avec valeur et Validation @Html.EditorFor(model => model.FirstName, new { htmlAttributes = new { @class = "formcontrol" } }) @Html.ValidationMessageFor(model => model.FirstName, "", new { @class = "text-danger" }) Dans un formulaire @using (Html.BeginForm()) { @Html.ValidationSummary(true) @Html.AntiForgeryToken() @* code ici *@ } Exemple de formulaire @using (Html.BeginForm()) { @Html.ValidationSummary(true) <fieldset> <legend>People</legend> <div class="editor-label"> @Html.LabelFor(model => model.FirstName) </div> <div class="editor-field"> @Html.EditorFor(model => model.FirstName) @Html.ValidationMessageFor(model => model.FirstName) </div> @* etc. *@ <p> <input type="submit" value="Valider" /> </p> </fieldset> } <div> @Html.ActionLink("Retour", "Index") </div> 24 25 e. Code expressions pour insérer du C# dans le HTML Valeur <p>there are @Model.Count() people.</p> @@ Permet de ne pas confondre avec du C# Plusieurs instructions @(item.FirstName + ' ' + item.LastName) Code blocks Permet par exemple de créer ses variables et les réutiliser @{ ViewBag.Title = "Index"; var count = Model.Count(); } <p>there are @count people.</p> Dans les blocs @if ,@foreach … @foreach (var item in Model) { @: var fullName = string.Format("{0}, {1}", item.LastName, item.FirstName); <td> code block : </td> </tr> @fullName } Insérer du texte dans un code block <text>Mon texte</text> @:Mon texte Fonction @DoSomething() Exécute la fonction @helper DoSomething() { <span>Bonjour depuis la fonction!</span> } f. Scripts @section Scripts { <script> $(function () { }); </script> } 25 26 g. SelectList Pour afficher une liste avec sélection de l’élément. …source, « champ de sélection », « champ affiché »,selectedValue @Html.DropDownListFor(modelItem => item.CategoryID,new SelectList(Model.Categories,"CategoryID","CategoryName",item.CategoryID)) public class ClientViewModel { public IList<ClientModel> Clients { get; set; } public IList<CategoryModel> Categories { get; set; } } On peut aussi définir « SelectList » dans le modèle h. Moteur de vue Aspx Un mot sur le moteur de vue Aspx.Les pages ont pour extension *.aspx. On utilise : <%= %> ou <%: %> pour encadrer une instruction Ex :<%= Html.ActionLink("Home","Index") %> <% %> pour encadrer les blocs de code (exemple avec une variable ou if/foreach) i. Ajax Helpers Ajax options Url Confirm OnBegin,OnComplete,OnSuccess,OnFailure LoadingElementId LoadingElementDuration UpdateTargetId InsertionMode url requested Message de confirmation affiché dans une boite de dialogue L’élément affiché pendant le « chargement » (exemple un spinner) Durée d’affichage de l’élément de chargement Element modifé Replace,InsertAfter,InsertBefore Unobtrusive Ajax 26 27 Ajax Form Le vue @model IEnumerable<string> @{ ViewBag.Title = "Home Page"; } @* Ajax form *@ @using (Ajax.BeginForm("Index", new AjaxOptions { HttpMethod = "get", InsertionMode = InsertionMode.Replace, UpdateTargetId = "cityList", LoadingElementDuration = 500, LoadingElementId = "progress" })) { <input type="search" name="searchTerm" /> <input type="submit" value="Submit" /> } @* spinner *@ <div id="progress" style="display:none"> <img src="~/Images/spinner.gif" /> </div> @* partial view *@ @Html.Partial("_cities", Model) @* unobstrusive Ajax *@ @section scripts { <script src="~/Scripts/jquery.unobtrusive-ajax.js"></script> } La vue partielle @model IEnumerable<string> <div id="cityList"> <table> @foreach (var item in Model) { <tr> <td> @Html.DisplayFor(modelItem => item) </td> </tr> } </table> </div> 27 28 Le contrôleur public class HomeController : Controller { private static readonly string[] cities = { "Shanghai", "Moscou", "Seoul", "Beijing", "Tokyo", "Mexico", "New York", "Londres","Bangkok","Caire", "Rio de Janeiro", "Saint Petersburg", "Los Angeles", "Yokohama","Berlin", "Madrid", "Chicago", "Houston", "Philadelphie", "Phoenix","San Diego", "Dallas", "Indianapolis", "San Francisco", "Austin", "Columbus", "Fort Worth", "Charlotte", "Detroit","Boston", "Washington", "Denver","Portland", "Las Vegas","Atlanta", "Colorado Springs", "Omaha", "Miami", "Cleveland", "Minneapolis","Honolulu", "Buffalo", "Lincoln", "Orlando", "Chandler", "Laredo", "Madison", "Reno","Irving", "Toronto", "Montreal", "Vancouver", "Calgary", "Edmonton", "Quebec", }; public ActionResult Index(string searchTerm = null) { if (Request.IsAjaxRequest()) if (searchTerm != null) { var result = cities.Where(c => c.ToLower().StartsWith(searchTerm.ToLower())); return PartialView("_cities", result); } return View(cities); } } Ajax ActionLink @Ajax.ActionLink("City List", "Index", new AjaxOptions { UpdateTargetId = "cityList", HttpMethod = "get" }) 28 29 5. Validation Pour bien faire il faut une validation côté client et une validation à la mise à jour côté serveur. a. Data Annotations Documentation On distingue : Les attributs servant à mettre en forme Display DisplayFormat DisplayColumn pour la clé étrangère DataType permet de mettre en forme (exemple password) Les attributs de validation: Required StringLength MaxLength MinLength Compare Range RegularExpression Attribut Key Namespace « Schema » Table Column ComplexType DatabaseGeneratded InverseProperty ForeignKey NotMapped 29 30 Custom Attribute Exemple création d’un attribut permettant de valider Twitter public class TwitterAttribute : ValidationAttribute { private string pattern = "^@([A-Za-z0-9_]+)"; protected override ValidationResult IsValid(object value, ValidationContext validationContext) { if (value != null) { var valueAsString = value.ToString(); if (!Regex.IsMatch(valueAsString, "^@([A-Za-z0-9_]+)")) { return new ValidationResult("Enter a valide twitter"); } } return ValidationResult.Success; } private readonly int _maxWords; } 30 31 Utilisation [Display(Name = "Twitter : ")] [Twitter] public string Twitter { get; set; } Contrôleur [HttpPost] public ActionResult Create(Person person) { ValidatePerson(person); if (ModelState.IsValid) { _peopleRepository.Add(person); return RedirectToAction("Index"); } return View(person); } private void ValidatePerson(Person person) { string twitter = person.Twitter; if (string.IsNullOrWhiteSpace(twitter)) return; Vérifie si le formulaire ne contient pas d’erreurs on ajoute une erreur au modèle if (!Regex.IsMatch(twitter, "^@([A-Za-z0-9_]+)")) ModelState.AddModelError("Twitter", "Twitter invalide."); } b. IValidatableObject Le modèle peut implémenter IValidatableObject, la méthode « Validate » sera automatiquement appelée … public class Person : IValidatableObject { // … properties public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { if (!Regex.IsMatch(Twitter, "^@([A-Za-z0-9_]+)")) { yield return new ValidationResult("Enter a valide twitter", new[] { "Twitter" }); } } } 31 32 c. Modifier le style des erreurs .input-validation-error { border: 1px solid #e64343; box-shadow: 0 0 2px 0 rgba(230, 67, 67, 0.4); } .input-validation-error:focus { background: #fcecec; border: 1px solid #e64343; box-shadow: 0 0 2px 0 rgba(230, 67, 67, 0.4); } .validationMessage { color: Red; } d. Validation côté « Client » avec jQuery Validation Plugin http://jqueryvalidation.org/ Référence <script src="~/Scripts/jquery.validate.min.js"></script> <script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script> Exemple pour afficher un message d’erreur <div> @Html.LabelFor(model => model.FirstName) @Html.TextBoxFor(model => model.FirstName) @Html.ValidationMessageFor(model => model.FirstName) </div> 32 33 6. Sécurité Articles sur la sécurité Asp.Net a. Windows (Intranet) <p>Bonjour, @User.Identity.Name!</p> <configuration> <system.web> <compilation debug="true" targetFramework="4.5.1" /> <httpRuntime targetFramework="4.5.1" /> <authentication mode="Windows" /> </system.web> </configuration> 33 34 b. « From scratch » - Sécurité « Forms » avec un projet Asp.Net vide Packages NuGet Besoin de : EntityFramework AspNet.Identity.Core Identity.EntityFramework Identity.Owin Microsoft.Owin.Host.SystemWeb (méthodes d’extension) Depuis la console du gestionnaire de package PM> PM> PM> PM> PM> + Install-Package Install-Package Install-Package Install-Package Install-Package EntityFramework Microsoft.AspNet.Identity.Core Microsoft.AspNet.Identity.EntityFramework Microsoft.AspNet.Identity.Owin Microsoft.Owin.Host.SystemWeb Si on utilise Facebook, Google, Twitter, etc. pour une connexion externe. PM> install-package Microsoft.Owin.Security.Facebook PM> install-package Microsoft.Owin.Security.Google PM> install-package Microsoft.Owin.Security.Twitter Préparation « IdentityConfig » (dossier « App_Start ») using using using using using using using using using using using using using using System; System.Collections.Generic; System.Data.Entity; System.Linq; System.Security.Claims; System.Threading.Tasks; System.Web; Microsoft.AspNet.Identity; Microsoft.AspNet.Identity.EntityFramework; Microsoft.AspNet.Identity.Owin; Microsoft.Owin; Microsoft.Owin.Security; MvcFromScratch.Models; System.Net.Mail; namespace MvcFromScratch { public class EmailService : IIdentityMessageService { public Task SendAsync(IdentityMessage message) { var client = new SmtpClient("smtp.xyz.fr"); client.Send("[email protected]", message.Destination, message.Subject, message.Body); return Task.FromResult(0); } } public class SmsService : IIdentityMessageService { public Task SendAsync(IdentityMessage message) { // Connectez votre service SMS ici pour envoyer un message texte. 34 35 return Task.FromResult(0); } } public class ApplicationUserManager : UserManager<ApplicationUser> { public ApplicationUserManager(IUserStore<ApplicationUser> store) : base(store) { Configuration de ApplicationUserManager } public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context) { var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>())); manager.UserValidator = new UserValidator<ApplicationUser>(manager) { Configuration du nom d’utilisateur AllowOnlyAlphanumericUserNames = false, (autoriser les chiffres dans les RequireUniqueEmail = true }; noms ? email uniques ?) manager.PasswordValidator = new PasswordValidator { RequiredLength = 6, RequireNonLetterOrDigit = true, RequireDigit = true, RequireLowercase = true, RequireUppercase = true, }; Configuration du mot de passe (longueur min. ? lettre ou digit requis ? upper case requis ? ) Verrouillage après 5 erreurs d’authentification manager.UserLockoutEnabledByDefault = true; manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5); manager.MaxFailedAccessAttemptsBeforeLockout = 5; manager.RegisterTwoFactorProvider("Code téléphonique ", new PhoneNumberTokenProvider<ApplicationUser> { MessageFormat = "Votre code de sécurité est {0}" }); manager.RegisterTwoFactorProvider("Code d'e-mail", new EmailTokenProvider<ApplicationUser> { Subject = "Code de sécurité", BodyFormat = "Votre code de sécurité est {0}" }); manager.EmailService = new EmailService(); manager.SmsService = new SmsService(); var dataProtectionProvider = options.DataProtectionProvider; if (dataProtectionProvider != null) { manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity")); } return manager; } } // Configurer le gestionnaire de connexion d'application 35 36 public class ApplicationSignInManager : SignInManager<ApplicationUser, string> { public ApplicationSignInManager(ApplicationUserManager userManager, IAuthenticationManager authenticationManager) : base(userManager, authenticationManager) { } public override Task<ClaimsIdentity> CreateUserIdentityAsync(ApplicationUser user) { return user.GenerateUserIdentityAsync((ApplicationUserManager)UserManager); } public static ApplicationSignInManager Create(IdentityFactoryOptions<ApplicationSignInManager> options, IOwinContext context) { return new ApplicationSignInManager(context.GetUserManager<ApplicationUserManager>(), context.Authentication); } } } ApplicationUser Dans le dossier « Models » using using using using using System.Data.Entity; System.Security.Claims; System.Threading.Tasks; Microsoft.AspNet.Identity; Microsoft.AspNet.Identity.EntityFramework; namespace MvcFromScratch.Models { // consultez http://go.microsoft.com/fwlink/?LinkID=317594 public class ApplicationUser : IdentityUser { public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager) { var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie); return userIdentity; } } public class ApplicationDbContext : IdentityDbContext<ApplicationUser> { public ApplicationDbContext() : base("DefaultConnection", throwIfV1Schema: false) { } public static ApplicationDbContext Create() { return new ApplicationDbContext(); } } } 36 37 Classe partielle « Startup » Dans le dossier « App_Start » using using using using using using using using System; Microsoft.AspNet.Identity; Microsoft.AspNet.Identity.Owin; Microsoft.Owin; Microsoft.Owin.Security.Cookies; Microsoft.Owin.Security.Google; Owin; MvcFromScratch.Models; namespace MvcFromScratch { public partial class Startup { // rendez-vous sur http://go.microsoft.com/fwlink/?LinkId=301864 public void ConfigureAuth(IAppBuilder app) { app.CreatePerOwinContext(ApplicationDbContext.Create); app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create); app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create); // Autoriser l’application à utiliser un cookie app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Account/Login"), Provider = new CookieAuthenticationProvider { // Permet à l'application de valider le timbre de sécurité OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>( validateInterval: TimeSpan.FromMinutes(30), regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)) } }); app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5)); app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrow serCookie); app.UseFacebookAuthentication( Connexions appId: "475813752569575", « externes » appSecret: "123456789abcdefg "); app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions() { ClientId = "903536450598ps3vsah291ef6iekkqtqflc2d0g746hj.apps.googleusercontent.com", ClientSecret = "123456789abcdefg" }); // Supprimer les commentaires //app.UseTwitterAuthentication( // consumerKey: "", // consumerSecret: ""); } } } 37 38 A la racine du projet using Microsoft.Owin; using Owin; [assembly: OwinStartupAttribute(typeof(MvcFromScratch.Startup))] namespace MvcFromScratch { public partial class Startup { public void Configuration(IAppBuilder app) { ConfigureAuth(app); } } } Base de données Créer le dossier spécial « App_Data » dans lequel la base de données sera créée Chaine de connexion dans « Web.config » On nomme la base qui sera créée « Identity » <connectionStrings> par exemple <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\Identity.mdf;Initial Catalog=Identity;Integrated Security=True" providerName="System.Data.SqlClient" /> </connectionStrings> « AspNetUsers » Contient tous les utilisateurs (email, userName, passwordhash,etc.). Pas de mot de passe pour les utilisateurs connectés en « externe » (facebook, etc.) , mais ajout de l’Id dans « AspNetUserLogins » Personnalisation des pages Dans une vue partielle(_LoginPartial) ou la master page (_Layout) @using Microsoft.AspNet.Identity @if (Request.IsAuthenticated) { <h2>Bonjour @User.Identity.GetUserName() !</h2> @Html.ActionLink("Se déconnecter", "Logoff", "Account") On pourrait également vérifier si l’utilisateur appartient à un rôle, pour donner accès à la gestion du site par exemple } else { <li>@Html.ActionLink("S’inscrire", "Register", "Account")</li> <li>@Html.ActionLink("Se connecter", "Login", "Account")</li> } 38 39 Contrôleurs et ViewModels Créer « AccountController » dans le dossier « Controllers » using using using using using using using using using using using MvcFromScratch.Models; System; System.Globalization; System.Linq; System.Security.Claims; System.Threading.Tasks; System.Web; System.Web.Mvc; Microsoft.AspNet.Identity; Microsoft.AspNet.Identity.Owin; Microsoft.Owin.Security; namespace MvcFromScratch.Controllers { [Authorize] public class AccountController : Controller { private ApplicationSignInManager _signInManager; private ApplicationUserManager _userManager; public ApplicationSignInManager SignInManager { get { return _signInManager ?? HttpContext.GetOwinContext().Get<ApplicationSignInManager>(); } private set { _signInManager = value; } } public ApplicationUserManager UserManager { get { return _userManager ?? HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>(); } private set { _userManager = value; } } private IAuthenticationManager AuthenticationManager { get { return HttpContext.GetOwinContext().Authentication; } } private IAuthenticationManager AuthenticationManager { Get { return HttpContext.GetOwinContext().Authentication; } } private void AddErrors(IdentityResult result) { foreach (var error in result.Errors) ModelState.AddModelError("", error); } } } Créer « AccountViewModels » qui contiendra tous les ViewModels dans le dossier « Models » 39 40 Inscription (register) « AccountController » // GET: /Account/Register [AllowAnonymous] public ActionResult Register() { return View(); } // POST: /Account/Register [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> Register(RegisterViewModel model) { if (ModelState.IsValid) { var user = new ApplicationUser { UserName = model.Email, Email = model.Email }; var result = await UserManager.CreateAsync(user, model.Password); if (result.Succeeded) { on envoie un email de demande de confirmation // Avec confirmation string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id); var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme); await UserManager.SendEmailAsync(user.Id, "Confirmez votre compte", "Confirmez votre compte en cliquant <a href=\"" + callbackUrl + "\">ici</a>"); ViewBag.Message = "Un email vous a été envoyé afin de confirmer votre compte."; Sans demande de confirmation, on connecte return View("Info"); directement l’utilisateur après l’inscription et le redirige vers la page d’accueil // Sans confirmation // await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false); //return RedirectToAction("Index", "Home"); } AddErrors(result); Réaffiche le formulaire en cas d’erreurs de validation } return View(model); } // GET: /Account/ConfirmEmail [AllowAnonymous] public async Task<ActionResult> ConfirmEmail(string userId, string code) { if (userId == null || code == null) { return View("Error"); } var result = await UserManager.ConfirmEmailAsync(userId, code); return View(result.Succeeded ? "ConfirmEmail" : "Error"); } 40 41 ViewModels public class RegisterViewModel { [Required] [EmailAddress] [Display(Name = "Courrier électronique")] public string Email { get; set; } [Required] [StringLength(100, ErrorMessage = "La chaîne {0} doit comporter au moins {2} caractères.", MinimumLength = 6)] [DataType(DataType.Password)] [Display(Name = "Mot de passe")] public string Password { get; set; } [DataType(DataType.Password)] [Display(Name = "Confirmer le mot de passe ")] [Compare("Password", ErrorMessage = "Le mot de passe et le mot de passe de confirmation ne correspondent pas.")] public string ConfirmPassword { get; set; } } Vues: - Register (pour s’inscrire), ConfirmEmail (affichée après que l’utilisateur ait cliqué sur le lien de confirmation de l’email envoyé) …Vues dans le dossier « Account » des vues « Views » Info, Error (dossier « Shared ») Register @model MvcFromScratch.Models.RegisterViewModel @{ ViewBag.Title = "S’inscrire"; } <h2>@ViewBag.Title.</h2> @using (Html.BeginForm("Register", "Account", FormMethod.Post)) { @Html.AntiForgeryToken() <h4>Créer un nouveau compte.</h4> <hr /> @Html.ValidationSummary() <div> @Html.LabelFor(m => m.Email) @Html.TextBoxFor(m => m.Email) </div> <div> @Html.LabelFor(m => m.Password) @Html.PasswordFor(m => m.Password) </div> <div> @Html.LabelFor(m => m.ConfirmPassword) @Html.PasswordFor(m => m.ConfirmPassword) </div> <div> <input type="submit" class="btn btn-default" value="Inscription" /> </div> } 41 42 ConfirmEmail @{ ViewBag.Title = "Confirmation de l'e-mail"; } <h2>@ViewBag.Title.</h2> <div> <p> Merci d'avoir confirmé votre e-mail. Veuillez @Html.ActionLink("Cliquer ici pour vous connecter", "Login", "Account") </p> </div> Info @{ ViewBag.Title = "Info"; } <h2>@ViewBag.Title.</h2> <h3>@ViewBag.Message</h3> Error @{ ViewBag.Title = "Erreur"; } <h1>Erreur</h1> <p>@ViewBag.ErrorMessage</p> 42 43 Connexion (Login) « AccountController » // GET: /Account/Login [AllowAnonymous] public ActionResult Login(string returnUrl) { ViewBag.ReturnUrl = returnUrl; return View(); } // POST: /Account/Login [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> Login(LoginViewModel model, string returnUrl) { Vérifie que le formulaire ne if (!ModelState.IsValid) Vérifie que l’utilisateur a { comporte pas d’erreurs bien validé son compte avec return View(model); l’email } var user = await UserManager.FindByNameAsync(model.Email); if (user != null) { if (!await UserManager.IsEmailConfirmedAsync(user.Id)) { ViewBag.ErrorMessage = "Vous devez confirmer votre email."; return View("Error"); } } var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: true); switch (result) { case SignInStatus.Success: return RedirectToLocal(returnUrl); case SignInStatus.LockedOut: return View("Lockout"); case SignInStatus.RequiresVerification: return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe }); case SignInStatus.Failure: default: ModelState.AddModelError("", "Tentative de connexion non valide."); return View(model); } } private ActionResult RedirectToLocal(string returnUrl) { if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } return RedirectToAction("Index", "Home"); } // POST: /Account/LogOff [HttpPost] [ValidateAntiForgeryToken] Déconnexion public ActionResult LogOff() { AuthenticationManager.SignOut(); return RedirectToAction("Index", "Home"); } 43 44 Mot de passe oublié // GET: /Account/ForgotPassword [AllowAnonymous] public ActionResult ForgotPassword() { return View(); } // POST: /Account/ForgotPassword [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model) { if (ModelState.IsValid) { var user = await UserManager.FindByNameAsync(model.Email); if (user == null || !(await UserManager.IsEmailConfirmedAsync(user.Id))) { return View("ForgotPasswordConfirmation"); } var code = await UserManager.GeneratePasswordResetTokenAsync(user.Id); var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme); await UserManager.SendEmailAsync(user.Id, "Reset Password", "Cliquez sur ce lien pour réinitaliser votre mot de passe : " + callbackUrl); ViewBag.Link = callbackUrl; return View("ForgotPasswordConfirmation"); } return View(model); } // GET: /Account/ResetPassword [AllowAnonymous] public ActionResult ResetPassword(string code) { var id = Request.QueryString["userid"]; var model = new ResetPasswordViewModel() { Id = id, Code = code }; return code == null ? View("Error") : View(model); } // POST: /Account/ResetPassword [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> ResetPassword(ResetPasswordViewModel model) { if (!ModelState.IsValid) { return View(model); } var user = await UserManager.FindByIdAsync(model.Id); if (user == null) { return RedirectToAction("ResetPasswordConfirmation", "Account"); } var result = await UserManager.ResetPasswordAsync(user.Id, model.Code, model.Password); if (result.Succeeded) { return RedirectToAction("ResetPasswordConfirmation", "Account"); } AddErrors(result); 44 45 return View(); } // GET: /Account/ResetPasswordConfirmation [AllowAnonymous] public ActionResult ResetPasswordConfirmation() { return View(); } ViewModels public class LoginViewModel { [Required] [Display(Name = "Courrier électronique")] [EmailAddress] public string Email { get; set; } [Required] [DataType(DataType.Password)] [Display(Name = "Mot de passe")] public string Password { get; set; } [Display(Name = "Mémoriser le mot de passe ?")] public bool RememberMe { get; set; } } public class ForgotPasswordViewModel { [Required] [EmailAddress] [Display(Name = "E-mail")] public string Email { get; set; } } public class ResetPasswordViewModel { [Required] public string Id { get; set; } [Required] [StringLength(100, ErrorMessage = "La chaîne {0} doit comporter au moins {2} caractères.", MinimumLength = 6)] [DataType(DataType.Password)] [Display(Name = "Mot de passe")] public string Password { get; set; } [DataType(DataType.Password)] [Display(Name = "Confirmer le mot de passe")] [Compare("Password", ErrorMessage = "Le nouveau mot de passe et le mot de passe de confirmation ne correspondent pas.")] public string ConfirmPassword { get; set; } public string Code { get; set; } } 45 46 Vues: - Login (pour se connecter) …Vue dans le dossier « Account » des vues « Views » Pour la réinitalisalisation de mot de passe : ForgotPassword, ForgotPasswordConfirmation, ResetPassword, ResetPasswordConfirmation Lockout (affichée si l’utilsateur se trompe trop de fois dans ses identifiants), Error (dossier « Shared ») Login @using MvcFromScratch.Models @model LoginViewModel @{ ViewBag.Title = "Connexion"; } <h2>@ViewBag.Title.</h2> @using (Html.BeginForm("Login", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post)) { @Html.AntiForgeryToken() <h4>Utilisez un compte local pour vous connecter.</h4> <hr /> @Html.ValidationSummary(true) <div> @Html.LabelFor(m => m.Email) @Html.TextBoxFor(m => m.Email) @Html.ValidationMessageFor(m => m.Email, "", new { @class = "text-danger" }) </div> <div> @Html.LabelFor(m => m.Password) @Html.PasswordFor(m => m.Password) @Html.ValidationMessageFor(m => m.Password, "", new { @class = "text-danger" }) </div> <div> @Html.CheckBoxFor(m => m.RememberMe) @Html.LabelFor(m => m.RememberMe) </div> <div> <input type="submit" value="Connexion" /> </div> <p> @Html.ActionLink("Mot de passe oublié?", "ForgotPassword") </p> <p> @Html.ActionLink("S'inscrire comme nouvel utilisateur", "Register") </p> } <div> @Html.Partial("_ExternalLoginsListPartial", new ExternalLoginListViewModel { ReturnUrl = ViewBag.ReturnUrl }) </div> 46 47 ForgotPasword @model MvcFromScratch.Models.ForgotPasswordViewModel @{ ViewBag.Title = "Vous avez oublié votre mot de passe ?"; } <h2>@ViewBag.Title.</h2> @using (Html.BeginForm("ForgotPassword", "Account", FormMethod.Post)) { @Html.AntiForgeryToken() <h4>Entrez une adresse de messagerie.</h4> <hr /> @Html.ValidationSummary() <div> @Html.LabelFor(m => m.Email) @Html.TextBoxFor(m => m.Email) </div> <div> <input type="submit" value="Lien de courrier électronique" /> </div> } ForgotPasswordConfirmation @{ ViewBag.Title = "Confirmation du mot de passe oublié"; } <hgroup class="title"> <h1>@ViewBag.Title.</h1> </hgroup> <div> <p> Vérifiez votre adresse de messagerie pour réinitialiser votre mot de passe. </p> </div> ResetPasword @model MvcFromScratch.Models.ResetPasswordViewModel @{ ViewBag.Title = "Réinitialiser le mot de passe"; } <h2>@ViewBag.Title.</h2> @using (Html.BeginForm("ResetPassword", "Account", FormMethod.Post)) { @Html.AntiForgeryToken() <h4>Réinitialisez votre mot de passe.</h4> <hr /> @Html.ValidationSummary() @Html.HiddenFor(model => model.Id) @Html.HiddenFor(model => model.Code) <div> @Html.LabelFor(m => m.Password) @Html.PasswordFor(m => m.Password) </div> <div> @Html.LabelFor(m => m.ConfirmPassword) @Html.PasswordFor(m => m.ConfirmPassword) </div> <div> <input type="submit" value="Réinitialiser" /> </div> } 47 48 ResetPasswordConfirmation @{ ViewBag.Title = "Mot de passe réinitalisé"; } <hgroup class="title"> <h1>@ViewBag.Title.</h1> </hgroup> <div> <p> Votre mot de passe a été réinitialisé. Veuillez @Html.ActionLink("cliquer ici pour vous connecter", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" }) </p> </div> Connexion «externe » avec Facebook, Google, Twitter Facebook Développeurs Facebook 1. Créer une nouvelle application : Menu « My Apps » « add a new app » … web 2. Donner un nom à l’application puis « Create App Id » 3. Donner l’url du site « http://localhost:7211/ » par exemple et un email Cliquer sur show (demande le mot de passe facebook) pour afficher « app secret » 48 49 4. Passer le statut de l’application en « public » (on peut créer aussi une application de test) 5. Si ce n’est fait installer le package … PM> install-package Microsoft.Owin.Security.Facebook 6. Rendez-vous dans « Startup.Auth.cs » dossier « App_Start », dé-commenter et entrer l’app id et l’app secret app.UseFacebookAuthentication( appId: "475813752569575", appSecret: "123456789abcdefg"); L’utilisateur est ajouté dans la table « Asp.NetUsers » sans mot de passe ainsi que dans la table « AspNetUserLogins » avec son « UserId » 49 50 Google Développeurs Google 1. Créer un projet (donner un nom) 2. Créer un identifiant client Toujours indiquer « signin-google » en url de redirection 50 51 3. Activer « Google+ Api » 4. Si ce n’est fait installer le package … PM> install-package Microsoft.Owin.Security.Google 5. Rendez-vous dans « Startup.Auth.cs » dossier « App_Start » et dé-commenter et entrer le « Client id » et le « Client secret » app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions() { ClientId = "903536450598ps3vsah291ef6iekkqtqflc2d0g746hj.apps.googleusercontent.com", ClientSecret = "123456789abcdefg" }); 51 52 Twitter Développeurs Twitter Code supplémentaire // POST: /Account/ExternalLogin [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public ActionResult ExternalLogin(string provider, string returnUrl) { // Demandez une redirection vers le fournisseur de connexions externe return new ChallengeResult(provider, Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl })); } // GET: /Account/ExternalLoginCallback [AllowAnonymous] public async Task<ActionResult> ExternalLoginCallback(string returnUrl) { var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync(); if (loginInfo == null) { return RedirectToAction("Login"); } var result = await SignInManager.ExternalSignInAsync(loginInfo, isPersistent: false); switch (result) { case SignInStatus.Success: return RedirectToLocal(returnUrl); case SignInStatus.LockedOut: return View("Lockout"); case SignInStatus.RequiresVerification: return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = false }); case SignInStatus.Failure: default: // Si l'utilisateur n'a pas de compte ViewBag.ReturnUrl = returnUrl; ViewBag.LoginProvider = loginInfo.Login.LoginProvider; return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = loginInfo.Email }); } } // POST: /Account/ExternalLoginConfirmation [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl) { if (User.Identity.IsAuthenticated) { return RedirectToAction("Index", "Manage"); } if (ModelState.IsValid) { // Obtenez des informations sur l’utilisateur auprès du fournisseur var info = await AuthenticationManager.GetExternalLoginInfoAsync(); if (info == null) 52 53 { return View("ExternalLoginFailure"); } var user = new ApplicationUser { UserName = model.Email, Email = model.Email }; var result = await UserManager.CreateAsync(user); if (result.Succeeded) { result = await UserManager.AddLoginAsync(user.Id, info.Login); if (result.Succeeded) { await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false); return RedirectToLocal(returnUrl); } } AddErrors(result); } ViewBag.ReturnUrl = returnUrl; return View(model); } internal class ChallengeResult : HttpUnauthorizedResult { private const string XsrfKey = "XsrfId"; public ChallengeResult(string provider, string redirectUri) : this(provider, redirectUri, null) { } public ChallengeResult(string provider, string redirectUri, string userId) { LoginProvider = provider; RedirectUri = redirectUri; UserId = userId; } public string LoginProvider { get; set; } public string RedirectUri { get; set; } public string UserId { get; set; } public override void ExecuteResult(ControllerContext context) { var properties = new AuthenticationProperties { RedirectUri = RedirectUri }; if (UserId != null) { properties.Dictionary[XsrfKey] = UserId; } context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider); } } 53 54 ViewModels public class ExternalLoginListViewModel { public string ReturnUrl { get; set; } } public class ExternalLoginConfirmationViewModel { [Required] [Display(Name = "Courrier électronique")] public string Email { get; set; } } Vues _ExternalLoginsListPartial @model MvcFromScratch.Models.ExternalLoginListViewModel @using Microsoft.Owin.Security <h4>Utilisez un autre service pour vous connecter.</h4> <hr /> @{ var loginProviders = Context.GetOwinContext().Authentication.GetExternalAuthenticationTypes(); if (loginProviders.Count() == 0) { <div> <p> Il n'existe aucun service d'authentification externe configuré. Consultez <a href="http://go.microsoft.com/fwlink/?LinkId=403804">cet article</a> pour des détails sur la configuration de cette application ASP.NET en vue de la prise en charge de la connexion via des services externes. </p> </div> } else { using (Html.BeginForm("ExternalLogin", "Account", new { ReturnUrl = Model.ReturnUrl })) { @Html.AntiForgeryToken() <div id="socialLoginList"> <p> @foreach (AuthenticationDescription p in loginProviders) { <button type="submit" class="btn btn-default" id="@p.AuthenticationType" name="provider" value="@p.AuthenticationType" title="Connexion avec votre compte @p.Caption">@p.AuthenticationType</button> } </p> </div> } } } 54 55 ExternalLoginConfirmation @model MvcFromScratch.Models.ExternalLoginConfirmationViewModel @{ ViewBag.Title = "S’inscrire"; } <h2>@ViewBag.Title.</h2> <h3>Associer votre compte @ViewBag.LoginProvider.</h3> @using (Html.BeginForm("ExternalLoginConfirmation", "Account", FormMethod.Post)) { @Html.AntiForgeryToken() <h4>Formulaire d'association</h4> <hr /> @Html.ValidationSummary(true) <p> Vous avez été authentifié avec succès avec <strong>@ViewBag.LoginProvider</strong>. Veuillez entrer ci-dessous un nom d'utilisateur pour ce site et cliquer sur le bouton S'inscrire pour valider la connexion. </p> <div> @Html.LabelFor(m => m.Email) @Html.TextBoxFor(m => m.Email) @Html.ValidationMessageFor(m => m.Email, "", new { @class = "text-danger" }) </div> <div> <input type="submit" value="Inscription" /> </div> } ExternalLoginFailure @{ ViewBag.Title = "Échec de la connexion"; } <hgroup> <h2>@ViewBag.Title.</h2> <h3 class="text-danger">Échec de la connexion auprès du service.</h3> </hgroup> Exemples : Tutorial, et ici PM> Install-Package Microsoft.AspNet.Identity.Samples –Pre c. Autorisations (attribut « Authorize ») On peut l’appliquer à une action d’un contrôleur public class PeopleController : Controller { [Authorize] public ActionResult Index() { return View(); } } 55 56 … ou à tout le contrôleur. [Authorize] public class PeopleController : Controller { [AllowAnonymous] public ActionResult Index() { return View(); } } Permet un accès anonyme à une action … Autoriser l’accès à un rôle, utilisateur, etc. [Authorize(Roles="admin, member")] ReturnUrl Si on essaie d’accéder à une vue nécessitant d’être authentifié on est redirigé vers la page de login. Une fois authentifié on est redirigé … ValidateAntiForgeryToken Pour contrer les attaques malveillantes [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create(Person person) { if (ModelState.IsValid) { _peopleRepository.Add(person); return RedirectToAction("Index"); } return View(person); } …Dans la vue @using (Html.BeginForm()) { @Html.ValidationSummary(true) @Html.AntiForgeryToken() 56 57 AntiXSS [HttpPost] public ActionResult Create(Person person) { if (ModelState.IsValid) { person.LastName = Sanitizer.GetSafeHtmlFragment(person.LastName); _peopleRepository.Add(person); return RedirectToAction("Index"); } return View(person); } d. SSL // GET: /Account/Login [RequireHttps] [AllowAnonymous] public ActionResult Login(string returnUrl) { ViewBag.ReturnUrl = returnUrl; return View(); } 57 58 e. Projet Visual Studio 2012 InitializeSimpleMembership et WebSecurity Spécification de la chaine de connexion utilisée(DefaultConnection) WebSecurity.InitializeDatabaseConnection("DefaultConnection", "UserProfile", "UserId", "UserName", autoCreateTables: true); La base de données correspondant à la chaine de connexion est créée lors de l’enregistrement du premier membre si elle n’existe pas. Il est possible d’ajouter ses champs personnalisés et modifier la table « UserProfile » de la base avec la commande (console du gestionnaire de packages) PM> Update-Database -Verbose + enable-migrations On peut également supprimer « InitializeSimpleMembership » et utiliser sa propre connexion .Ajouter « WebSecurity.InitializeDatabaseConnection » dans global.asax et remplacer « UsersContext » par son propre DbContext. 58 59 [Table("UserProfile")] public class UserProfile { [Key] [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)] public int UserId { get; set; } public string UserName { get; set; } } // persist cookie = true bool isLogged = WebSecurity.Login("username", "password",true); WebSecurity.CreateUserAndAccount("username", "password"); Le password est haché automatiquement Si l’utilisateur coche « mémoriser le mot de passe » lorsqu’il se entre ses informations de connexion dans la page de login, un cookie est créé. OAuth et OpenID avec projet Visual Studio 2012 SimpleRoleProvider et SimpleMembershipProvider Web.config <system.web> <!-- etc. --> <roleManager enabled="true" defaultProvider="simple"> <providers> <clear/> <add name="simple" type="WebMatrix.WebData.SimpleRoleProvider, WebMatrix.WebData"/> </providers> </roleManager> <membership defaultProvider="simple"> <providers> <clear/> <add name="simple" type="WebMatrix.WebData.SimpleMembershipProvider, WebMatrix.WebData"/> </providers> </membership> </system.web> var roles = (SimpleRoleProvider)Roles.Provider; var membership = (SimpleMembershipProvider)Membership.Provider; if (!roles.RoleExists("Admin")) roles.CreateRole("Admin"); if (membership.GetUser("Marie", false) == null) membership.CreateUserAndAccount("Marie", "passwword"); if (!roles.GetRolesForUser("Marie").Contains("Admin")) roles.AddUsersToRoles(new[] { "Marie" }, new[] { "Admin" }); 59 60 Register [AllowAnonymous] public ActionResult Register() { return View(); } [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> Register(RegisterViewModel model) { if (ModelState.IsValid) { var user = new ApplicationUser() { UserName = model.Email, Email = model.Email }; IdentityResult result = await UserManager.CreateAsync(user, model.Password); if (result.Succeeded) { await SignInAsync(user, isPersistent: false); return RedirectToAction("Index", "Home"); } else { AddErrors(result); } } return View(model); } Login [AllowAnonymous] public ActionResult Login(string returnUrl) { ViewBag.ReturnUrl = returnUrl; return View(); } [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> Login(LoginViewModel model, string returnUrl) { if (ModelState.IsValid) { var user = await UserManager.FindAsync(model.Email, model.Password); if (user != null) { await SignInAsync(user, model.RememberMe); return RedirectToLocal(returnUrl); } else { ModelState.AddModelError("", "Nom d'utilisateur ou mot de passe non valide."); } } return View(model); } 60 61 7. Localisation Dates et Monnaies Dépendent de la culture courante Exemple : dans la vue on affiche une monnaie et la date, leur formatage change selon la culture. @{ var money = 9.99m; var today = DateTime.Now.ToShortDateString(); } <div>@money.ToString("c")</div> <div>@today</div> Web.config <system.web> <!----> <globalization culture="auto" uiCulture="auto"/> </system.web> Changer les préférences linguistiques d’IE … En montant English …En montant Français 61 62 Resources Ajouter un fichier de ressources Ajouter un fichier de ressources pour la langue par défaut (exemple : Resources.resx) et un fichier de ressources pour chaque langue supportée par le site (exemple : Resources.en.resx) Indiquer l’outil personnalisé pour uniquement Resources par défaut. Exemple en changeant la langue dans les préférences linguistiques d’IE (LocalizationDemo est le nom du projet) <div>@LocalizationDemo.Views.Home.Resources.Greeting</div> On peut également récupérer les ressources dans les contrôleurs var greeting = Resources.Greeting; Et utiliser les ressources avec les data annotations [Display(ResourceType=typeof(Resources),Name="Greeting")] public string FullName { get; set; } 62 63 8. Less Installer l’ extension pour Visual Studio « Web Essentials » (aperçu CSS, génération de la feuille de Styles, minimisation, etc.) 9. Dependency Injection (DI) Avec Ninject public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); RegisterServices(); } private void RegisterServices() { var kernel = new StandardKernel(); DependencyResolver.SetResolver(new NinjectDependencyResolver(kernel)); kernel.Bind<IPeopleRepository>().To<FakePeopleRepository>(); } } public class NinjectDependencyResolver :IDependencyResolver { private readonly IKernel _kernel; public NinjectDependencyResolver(IKernel kernel) { this._kernel = kernel; } public object GetService(Type serviceType) { return _kernel.TryGet(serviceType, new IParameter[0]); } public IEnumerable<object> GetServices(Type serviceType) { return _kernel.GetAll(serviceType, new IParameter[0]); } } 63 64 10. Upgrade Visual Studio 2013 ne prend en charge MVC qu’à partir de la version 4. Upgrade de MVC2 vers MVC3 avec ASP.NET MVC 3 Application Upgrader Upgrade de MVC3 vers MVC4 Ouvrir la solution (dont le projet Mvc ne peut être chargé) dans Visual Studio 2013 … chercher « Auto upgrade mvc 3 to mvc4 » avec le gestionnaire de package NuGet Il se peut qu’il faille ajouter une référence à « Microsoft.AspNet.Web.Optimization » (package NuGet) ensuite. Cibler le Framework 4.0 peut aussi éviter des problèmes de versions de fichiers. 64 65 II. Web Api 1. Web Api config La route par défaut pour un contrôleur Web Api 2. Contrôleur Web Api a. HTTP Status codes GET : 200 (ok), 404 (not found), 400, 500 POST : 201 (created), 400 (bad request), 500 (internal server error) PUT : 200, 404, 400, 500 PATCH : 200,404,400,500 DELETE : 204 (no content), 404, 400, 500 Général : 401 (unauthorized), 403 (forbidden), 405 (method not allowed) HttpStatusCode b. Ajout d’un contrôleur Web Api Menu contextuel sur le dossier « Controllers » du projet 65 66 c. Contrôleur Web API avec IhttpActionResult Permet de simplifier l’écriture (et réduire le code) de réponses HTTP en utilisant des méthodes : Classe implémentant de IhttpResult OkResult NotFoundResult ExceptionResult UnauthorizedResult BadRequestResult ConflictResult RedirectResult InvalidModelStateResult Méthode Ok NotFound Unauthorized BadRequest Conflict Redirect public class PeopleController : ApiController { private IPeopleRepository peopleRepository; public PeopleController() : this(new FakePeopleRepository()) { } public PeopleController(IPeopleRepository peopleRepository) { this.peopleRepository = peopleRepository; } Injection de dépendances [HttpGet] public IHttpActionResult Get() { try { var people = peopleRepository.GetAll(); return Ok(people); } catch (Exception ex) { return InternalServerError(ex); } } [HttpGet] public IHttpActionResult Get(int id) { try { var person = peopleRepository.GetOne(id); if (person != null) { return Ok(person); } else { return NotFound(); } } catch (Exception ex) { return InternalServerError(ex); } } [HttpPost] public IHttpActionResult Post([FromBody]Person person) { 66 67 try { var result = peopleRepository.Add(person); return Created(Request.RequestUri + "/" + result.Id.ToString(), result); } catch (Exception ex) { return InternalServerError(ex); } } [HttpPut] public IHttpActionResult Put(int id, [FromBody]Person person) { try { if (person == null) return BadRequest(); var personToUpdate = peopleRepository.GetOne(id); if (personToUpdate == null) { return NotFound(); } var result = peopleRepository.Update(person); return Ok(result); } catch (Exception ex) { return InternalServerError(ex); } } [HttpDelete] public IHttpActionResult Delete(int id) { try { var personToDelete = peopleRepository.GetOne(id); if (personToDelete == null) { return NotFound(); } peopleRepository.Delete(id); return StatusCode(HttpStatusCode.NoContent); } catch (Exception ex) { return InternalServerError(ex); } } } 67 68 d. Contrôleur Web API avec HttpResponseMessage public class PeopleController : ApiController { private IPeopleRepository peopleRepository; public PeopleController() : this(new FakePeopleRepository()) { } public PeopleController(IPeopleRepository peopleRepository) { this.peopleRepository = peopleRepository; } On pourrait également retourner [HttpGet] une liste générique en résultat par public HttpResponseMessage Get() { exemple try { var people = peopleRepository.GetAll(); return Request.CreateResponse(HttpStatusCode.OK,people); } catch (Exception ex) { return Request.CreateErrorResponse(HttpStatusCode.InternalServerError,ex); } } On pourrait également retourner [HttpGet] une instance de classe (exemple public HttpResponseMessage GetOne(int id) « Person ») { try { var person = peopleRepository.GetOne(id); if (person != null) { return Request.CreateResponse(HttpStatusCode.OK,person); } else { return Request.CreateResponse(HttpStatusCode.NotFound); } } catch (Exception ex) { return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex); } } [HttpPost] public HttpResponseMessage Post([FromBody]Person person) { try { var result = peopleRepository.Add(person); return Request.CreateResponse(HttpStatusCode.Created, result); } catch (Exception ex) { return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex); } } [HttpPut] public HttpResponseMessage Put(int id,[FromBody]Person person) { try 68 69 { if (person == null) return Request.CreateResponse(HttpStatusCode.BadRequest); var personToUpdate = peopleRepository.GetOne(id); if (personToUpdate == null) { return Request.CreateResponse(HttpStatusCode.NotFound); } var result = peopleRepository.Update(person); return Request.CreateResponse(HttpStatusCode.OK, result); } catch (Exception ex) { return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex); } } [HttpDelete] public HttpResponseMessage Delete(int id) { try { var personToDelete = peopleRepository.GetOne(id); if (personToDelete == null) { return Request.CreateResponse(HttpStatusCode.NotFound); } peopleRepository.Delete(id); return Request.CreateResponse(HttpStatusCode.NoContent); } catch (Exception ex) { return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex); } } } Retour au format Json par défaut. Si on désire un retour au format Xml…DataMember(membres à intégrer dans la réponse) et DataContract (System.Runtime.Serialization) [Serializable] [DataContract] public class Person { [DataMember] public int Id { get; set; } [DataMember] public string Name { get; set; } [DataMember] public string Twitter { get; set; } } …Et dans le header accept :text/xml 69 70 Test du Service avec Fiddler GET POST « get one » Ajout du type de contenu Retour au format JSON de l’élément créé PUT Ajout du paramètre « id » à l’url et passage de l’élément modifié DELETE 70 71 3. Attribut “Route” Permet de personnaliser ses routes et d’éviter les conflits entre routes. [HttpGet] [Route("api/people/{id}/twitter")] public HttpResponseMessage GetTwitter(int id) { // etc. } Route [Route("api/people/{id}/twitter")] [Route("api/people/{id:int:min(1)}/twitter")] [Route("api/people/{id:int}")] [Route("api/people/{name:alpha}")] [Route("api/people/{id?}")] [RoutePrefix("api/people")] [RouteArea("admin")] Description Route personnalisée Contraintes Multiples routes Appliquer à tout le contrôleur Prefix pour toutes les routes du contrôleur Mvc Areas 4. Injection de dépendances Avec Ninject Ajout du package NuGet « NInject » … ainsi que (pour Web Api) Global.asax public class WebApiApplication : System.Web.HttpApplication { protected void Application_Start() { GlobalConfiguration.Configure(WebApiConfig.Register); RegisterServices(); } private void RegisterServices() { var kernel = new StandardKernel(); GlobalConfiguration.Configuration.DependencyResolver = new NinjectResolver(kernel); kernel.Bind<IPeopleRepository>().To<PeopleRepository>(); } } 71 72 Le contrôleur n’a plus besoin de constructeur par défaut 5. Projet Client a. dataService JavaScript (Pour projet SPA, Angular, Durandal,…). Avec Knockout par exemple : var dataService = function () { 'use strict' var urlBase = 'http://localhost:12107/api/People', getPeople = function () { return $.getJSON(urlBase); }, searchPeople = function (search) { return $.getJSON(urlBase + '/find/' + search); }, getPerson = function (id) { return $.getJSON(urlBase + '/' + id); }, addPerson = function (person) { return $.ajax({ url: urlBase, type: 'POST', dataType: 'json', contentType: 'application/json; charset=utf-8', data: ko.toJSON(person) }); }, updatePerson = function (person) { return $.ajax({ url: urlBase, type: 'PUT', dataType: 'json', contentType: 'application/json; charset=utf-8', data: ko.toJSON(person) }); }, deletePerson = function (id) { return $.ajax({ url: urlBase + '/' + id, type: 'DELETE' }); }; return { getPeople: getPeople, searchPeople: searchPeople, getPerson: getPerson, updatePerson: updatePerson, addPerson: addPerson, deletePerson: deletePerson }; }(); 72 73 b. HttpClient (projets WinRT, Wpf,etc.) public class PeopleService { string urlBase ="http://localhost:12107/api/People"; public async Task<Person[]> GetAll() { using (HttpClient client = new HttpClient()) { HttpResponseMessage response = await client.GetAsync(urlBase); string resultContent = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject<Person[]>(resultContent); } } public async Task<Person> GetOne(int personID) { using (HttpClient client = new HttpClient()) { HttpResponseMessage result = await client.GetAsync(string.Format("{0}/{1}", urlBase, personID)); string resultContent = await result.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject<Person>(resultContent); } } public async void Add(Person person) { using (HttpClient client = new HttpClient()) { string json = JsonConvert.SerializeObject(person); HttpResponseMessage response = await client.PostAsync(urlBase, new StringContent(json, Encoding.UTF8, "application/json")); string resultContent = await response.Content.ReadAsStringAsync(); person = JsonConvert.DeserializeObject<Person>(resultContent); } } public async void Update(Person person) { using (HttpClient client = new HttpClient()) { string json = JsonConvert.SerializeObject(person); HttpResponseMessage response = await client.PutAsync(urlBase, new StringContent(json, Encoding.UTF8, "application/json")); string resultContent = await response.Content.ReadAsStringAsync(); person = JsonConvert.DeserializeObject<Person>(resultContent); } } public async void Delete(int personID) { using (HttpClient client = new HttpClient()) { HttpResponseMessage response = await client.DeleteAsync(string.Format("{0}/{1}", urlBase, personID)); response.EnsureSuccessStatusCode(); } } } 73 74 Il existe également des librairies facilitant encore l’utilisation de HtppClient comme EasyHttp public class EasyHttpPeopleService { string urlBase = "http://localhost:12107/api/People"; public Person[] GetAll() { HttpClient client = new HttpClient(); HttpResponse response = client.Get(urlBase); Person[] result = response.StaticBody<Person[]>(); return result; } public Person GetOne(int personID) { HttpClient client = new HttpClient(); HttpResponse response = client.Get(string.Format("{0}/{1}", urlBase, personID)); Person result = response.StaticBody<Person>(); return result; } public void Add(Person person) { HttpClient client = new HttpClient(); client.Post(urlBase, person, "application/json"); } public void Update(Person person) { HttpClient client = new HttpClient(); client.Put(urlBase, person, "application/json"); } public void Delete(int personID) { HttpClient client = new HttpClient(); client.Delete(string.Format("{0}/{1}", urlBase, personID)); } } 74 75 Les ports ne correspondant pas, on a une erreur 6. Cors Installer avec les packages NuGet PM> Install-Package Microsoft.AspNet.WebApi.Cors … Dans WebApiConfig … Sur le contrôleur Web API Ajouter l’attribut « EnableCorsAttribute ». Le premier argument correspondant à l’URL du client (on pourrait également mettre « * »), le second aux headers puis les méthodes. [EnableCorsAttribute("http://localhost:63679", "*","*")] public class PeopleController : ApiController { public IHttpActionResult Get() { var file = HostingEnvironment.MapPath(@"~/App_Data/people.json"); var json = File.ReadAllText(file); var people = JsonConvert.DeserializeObject<List<Person>>(json); return Ok(people); } } 75 76 7. Formatters pour le « Mapping » Dans le Fichier Json les noms des propriétés sont en camelCase, alors que les propriétés des modèles eux sont en PascalCase. public static class WebApiConfig { public static void Register(HttpConfiguration config) { // Configuration et services de l'API Web // Configurer l'API Web pour utiliser uniquement l'authentification de jeton du porteur. config.SuppressDefaultHostAuthentication(); config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType)); // Itinéraires de l'API Web config.MapHttpAttributeRoutes(); config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); config.EnableCors(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } } 8. OData Installer avec les packages NuGet PM> Install-Package Microsoft.AspNet.WebApi.OData Puis pour les méthodes des contrôleurs Ajouter l’attribut « EnableQuery » [EnableQuery()] [ResponseType(typeof(Person))] public IHttpActionResult Get() { try { var result = peopleRepository.GetAll(); return Ok(result.AsQueryable()); } catch (Exception ex) { return InternalServerError(ex); } } Si on n’utilise pas « IHttpActionResult » retourner « IQueryable<Person> » dans l’exemple Résultat retourné AsQueryable. Site, Documentation 76 77 9. Authentification Web Api Server « PeopleController » réclame d’être authentifié . [Authorize] public class PeopleController : ApiController { private static List<Person> people = new List<Person>() { new Person(1,"John","Papa","@john_papa"), new Person(2, "Ryan", "Niemeyer", "@rpniemeyer"), new Person(3, "Steve", "Sanderson", "@stevensanderson"), new Person(4, "Ward", "Bell", "@wardbell"), new Person(5, "Scott", "Allen","@OdeToCode"), new Person(6,"Yacine","Khammal","@YacineKhammal") }; [HttpGet] public IEnumerable<Person> Get() { return people; } } Startup.Auth et AccountController différent de la version « Mvc ».L’ApplicationUserManager(IdentityConfig.cs) change peu. AccountController Startup.Auth.cs 77 78 Client : On n’est autorisé à afficher la liste des personnes qu’une fois authentifié. <body> <header> <!--connexion --> <div id="login"> <form id="loginForm"> <input type="text" name="userName" placeholder="Nom d'utilisateur" value="[email protected]" /> <input type="password" name="password" placeholder="Mot de passe" value="Abc123_" /> <input type="submit" id="loginInput" value="Valider" /> <input type="button" id="showRegisterForm" value="Créer un compte?" /> </form> </div> <!-- enregistrement --> <div id="register"> <form id="registerForm"> <input type="text" id="userName" name="userName" value="[email protected]" /> <input type="text" name="email" value="[email protected]" /> <input type="password" id="password" name="password" value="Abc123_" /> <input type="password" name="confirmPassword" value="Abc123_" /> <input type="submit" id="registerInput" value="Valider" /> <input type="button" id="showLoginForm" value="Connexion" /> </form> </div> <!--connecté --> <div id="loggedIn"></div> </header> <section> <button id="getPeopleListButton">Lister les personnes</button> <div id="container"></div> </section> <script src="Scripts/jquery-2.1.3.js"></script> <script> $(function () { // Show / hide $("#register").hide(); $("#getPeopleListButton").hide(); $("#loggedIn").hide(); $("#showLoginForm").on("click", function () { $("#register").hide(); }); $("#showRegisterForm").on("click", function () { $("#register").show(); }); var accessToken = ""; var loggedIn = false; var register = function () { var url = "api/account/register"; var data = $("#registerForm").serialize(); // récupère les infos saisies dans le formulaire $.post(url, data) .done(function () { alert("Bienvenue !"); login(); // connexion 78 79 }) .fail(function () { alert("Echec lors de la création du compte."); }); return false; } // login var login = function () { var url = "Token"; var data = $("#loginForm").serialize(); //userName=abc%40laposte.net&email=abc%40laposte.net&password=Abc123_&confirmPassword= Abc123_ data = data + "&grant_type=password"; $.post(url, data) .success(function (data) { accessToken = data.access_token; loggedIn = true; $("#login").hide(); $("#register").hide(); var welcomeMessage = "<h2>Bonjour " + data.userName + "!</h2>"; $("#loggedIn").html(welcomeMessage); $("#loggedIn").show(); $("#getPeopleListButton").show(); }) .fail(function () { alert("Impossible de se connecter. vérifiez vos identifiants."); }); return false; } var getAll = function () { var url = "api/people"; if (loggedIn) { $.ajax(url, { type: "GET", headers: { "Authorization": "Bearer " + accessToken, "ContentType": "application/x-www-form-urlencoded" } }).success(function (data) { var tableHTML = "<table><tr><th>Nom</th><th>Twitter</th></tr>"; data.forEach(function (person) { tableHTML += "<tr><td>" + person.Name + "</td><td>" + person.Twitter + "</td></tr>"; }); tableHTML += "</table>"; $("#container").html(tableHTML); }) .fail(function (error) { alert(JSON.stringify(error, null, 4)); }); } 79 80 else { alert("Veuillez vous authentifier svp."); } } $("#loginInput").on("click", login); $("#registerInput").on("click", register); $("#getPeopleListButton").on("click", getAll); }); </script> </body> Login…on récupère le token … Récupération de la liste de personnes … 80 81 Activer SSL Depuis les propriétés du projet … Puis dans l’onglet Web changer l’URL URL SSL 81 82 10. MongoDB Connexion à une base de données NoSQL MongoDB avec un service WEB MVC. Drivers : - MongoDB C# Driver Simple-mongodb FluentMongo a. Installation de MongoDB C# driver Documentation Deux références sont ajoutées : people peopledb … Serveur … MongoClient MongoServer MongoDatabase MongoCollection 82 83 b. Service WEB Création d’une classe permettant de se connecter à la base de données. using using using using MongoDB.Bson; MongoDB.Bson.Serialization.Attributes; MongoDB.Driver; MongoDBWithMVC.Properties; namespace MongoDBWithMVC.Models { public class MongoDBContext { public MongoDatabase database; public MongoDBContext() { //mongodb://localhost:27017/ var client = new MongoClient(Settings.Default.MongoDBConnectionString); var server = client.GetServer(); database = server.GetDatabase(Settings.Default.DatabaseName); //peopledb } public MongoCollection<Person> People { get { return database.GetCollection<Person>("people"); } } } } Accès à la collection « people » de la base Le modèle utilisé pour la définition des documents public class Person { [BsonRepresentation(BsonType.ObjectId)] public string Id { get; set; } [BsonElementAttribute("name")] public string Name { get; set; } [BsonElementAttribute("twitter")] public string Twitter { get; set; } Pour l’identifiant (_id) attribué à l’insertion d’un document Pour la sérialisation lorsque le nom des éléments est différent } 83 84 Service WEB API public class PeopleController : ApiController { MongoDBContext context; public PeopleController() { context = new MongoDBContext(); } On utilise la classe de contexte créée pour se connecter et accéder à la collection « people » public IHttpActionResult Get() { try Méthode « FindAll » pour obtenir { var result = context.People.FindAll(); tous les documents return Ok(result); } catch (Exception ex) { return InternalServerError(ex); } } public IHttpActionResult Get(string id) Obtention du document par l’id { try { var person = context.People.FindOneById(new ObjectId(id)); if (person != null) { return Ok(person); } else { return NotFound(); } } catch (Exception ex) { return InternalServerError(ex); } } [HttpPost] public IHttpActionResult Post([FromBody]Person person) { Ajout d’un document à la collection try { « people » var result = context.People.Insert(person); return Created(Request.RequestUri + "/" + person.Id.ToString(), person); } catch (Exception ex) { return InternalServerError(ex); } } [HttpPut] public IHttpActionResult Put(string id, [FromBody]Person person) { try { 84 85 var personToUpdate = context.People.FindOneById(new ObjectId(id)); if (personToUpdate == null) { Modification puis sauvegarde du return NotFound(); document. } personToUpdate.Name = person.Name; personToUpdate.Twitter = person.Twitter; On pourrait aussi utiliser la méthode update+ updatebuilder context.People.Save(personToUpdate); return Ok(personToUpdate); } catch (Exception ex) { return InternalServerError(ex); } } [HttpDelete] public IHttpActionResult Delete(string id) { try { var personToDelete = context.People.FindOneById(new ObjectId(id)); if (personToDelete == null) Suppression du document par { return NotFound(); l’id } context.People.Remove(Query.EQ("_id", new ObjectId(id))); return StatusCode(HttpStatusCode.NoContent); } catch (Exception ex) { return InternalServerError(ex); } } } 85 86 III. Bootstrap 1. Installation et thèmes On peut « customiser » en sélectionnant seulement les éléments à inclure Ajout au projet : Télécharger depuis le site ou avec packages NuGet (ou sur Github pour avoir les fichiers LESS) Thèmes pour bootstrap Thème à télécharger sur boostwatch. On renomme le thème par exemple « flatly-bootstrap.css » que l’on ajoute au projet et on remplace le thème par défaut par celui-ci <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="viewport" content="width=device-width, inital-scale=1" /> <title>Bootstrap demo</title> Thème « par défaut » et thème <link href="css/bootstrap.css" rel="stylesheet" /> téléchargé sur boostwatch <link href="css/bootstrap-theme.css" rel="stylesheet" /> <!--<link href="css/themes/flatly-bootstrap.css" rel="stylesheet" />--> <link href="css/default.css" rel="stylesheet" /> </head> <body> <script src="js/jquery-2.1.3.js"></script> <script src="js/bootstrap.js"></script> </body> </html> 86 87 2. Grid system Grid system sur 12 colonnes : exemple de col-md-1 à col-md-12 occupant toute la largeur. Taille >=1200px de 992 à 1200px De 768 à 991px <768px Classe col-lg-xx col-md-xx col-sm-xx col-xs-xx <div class="row"> <div id="main" class="col-md-8 col-sm-10 col-xs-6"><p>Lorem ipsum ... </p></div> <div id="sidebar" class="col-md-4 col-sm-2 col-xs-6">Lorem ... </div> </div> La taille des colonnes s’adapte selon les résolutions col-md-8 et col-md-4 pour la « sidebar » sur taille « middle » Sur mobile 2 colonnes égales (col-xs6) a. Cacher une colonne selon une résolution Cachera la colonne sur mobile <div id="sidebar" class="col-md-4 hidden-xs">Lorem ipsum ... </div> 87 88 b. Décalage de colonnes avec « offset » De col-xx-offset-1 à col-xx-offset-12 Ex <div class="row"> <div class="col-md-8 col-md-offset-4">Lorem ipsum …</div> </div> Colonne « col-md-8 » avec décalage de 4 colonnes « col-mdoffset-4 » c. Image flottante Thumbnails <div class="row"> <div class="col-md-12"> <p><img src="images/foodies.jpg" width="300" class="pull-left thumbnail"/>Lorem …</p> </div> </div> « pull-left » place l’image à gauche (« pull-right » dispo) « thumbnail » ajoute une bordure et effet à la photo 3. Bases a. Typographie Documentation Classes commençant par « text-xx » <p class="text-muted">Lorem ipsum dolor sit amet</p> 88 89 b. Boutons, groupes de boutons et dropdowns Buttons <button class="btn btn-danger">En savoir plus ...</button> Dropdowns On pourrait utiliser des boutons radio avec label <div class="form-group"> <div class="btn-group"> <button class="btn btn-default active">Un</button> <button class="btn btn-default">Deux</button> <div class="btn-group"> <button id="option" class="btn btn-default">Choisir ...</button> <button class="btn btn-default" data-toggle="dropdown"><span class="caret"></span></button> <ul id="options" class="dropdown-menu"> <li><a href="#">Option un</a></li> <li><a href="#">Option deux</a></li> <li class="divider"></li> <li class="disabled"><a href="#">Option trois</a></li> </ul> </div> </div> </div> Script pour changer le texte du bouton <script> $(document).ready(function () { $("#options li a").on("click", function () { var option = $(this).text(); $("#option").text(option); }) }); </script> 89 90 c. Icones Glyphicons <button class="btn btn-default navbar-toggle" data-toggle="collapse" datatarget=".navbar-collapse"><span class="glyphicon glyphicon-chevrondown"></span></button> d. Formulaires, listes, et tables Forms, nav, tables « input-group » Documentation <div class="form-group"> <div class="col-md-10"> <div class="input-group"> <span class="input-group-addon">@</span> <input type="email" class="form-control" placeholder="Email"/> </div> </div> </div> e. Navbar desktop et mobile Documentation <header class="container"> <div id="menu" class="nav navbar-default"> <div class="navbar-header"> <button class="btn btn-default navbar-toggle" datatoggle="collapse" data-target=".navbar-collapse"><span class="glyphicon glyphiconchevron-down"></span></button> <h4><a href="#">Boostrap demo</a></h4> </div> <nav class="navbar navbar-collapse collapse"> <ul class="nav navbar-nav navbar-right"> <li class="nav active"><a href="#">Accueil</a></li> <li class="nav"><a href="#">A propos</a></li> <li class="nav"><a href="#">Contact</a></li> </ul> </nav> </div> </header> Version desktop 90 91 Avec thème « flatly » Version mobile Avec thème « flatly » f. Header et breadcrumb Header, breadcrumb « breadcrumb » « header » mis en forme <div id="body" class="container"> <ol class="breadcrumb"> <li><a href="index.html">Accueil</a></li> <li class="active">A propos</li> </ol> <div class="page-header"> <h1>Bootstrap demo</h1> <p>Une démonstration du Framework</p> </div> g. Pagination Documentation 91 92 <ul class="pagination"> <li class="disabled"><a href="#">«</a></li> <li class="active"><a href="#">1</a></li> <li><a href="#">2</a></li> <li><a href="#">3</a></li> <li><a href="#">»</a></li> </ul> h. Well On pourrait ajouter la classe « lead » pour mettre plus en avant <p class="well"> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec lacinia metus quis vulputate semper. </p> i. Panels Documentation Plugins a. Collapse et accordéon Documentation 92 93 b. Boite de dialogue Documentation <a href="#mydialogbox" class="btn btn-danger" data-toggle="modal">Ouvrir la boite de dialogue ...</a> <div id="mydialogbox" class="modal fade"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <a href="#" class="close" data-dissmiss="modal">×</a> <h3>Une boite de dialogue</h3> </div> <div class="modal-body"> Le contenu de la boite! </div> <div class="modal-footer"> le pied </div> </div> </div> </div> Depuis jQuery $("#mydialogbox").modal("show"); c. Alert Documentation On place une alerte au-dessus d’un formulaire « collapse » par défaut que l’on affiche si le formulaire comporte des erreurs par exemple 93 94 <div class="alert alert-warning collapse" id="formmessage"> <a href="#" class="close" data-dissmiss="alert">×</a> <p>Vous devez remplir tous les champs!</p> </div> <form> <!-- ... --> $("form").on("submit", function (e) { if($("#email").text()=="") { e.preventDefault(); $("#formmessage").show(); } }) d. Tab Documentation <ul class="nav nav-tabs nav-justified"> <li class="active"><a href="#formtab" data-toggle="tab">Contact</a></li> <li><a href="#maptab" data-toggle="tab">Map</a></li> </ul> <div class="tab-content"> <div class="well tab-pane active" id="formtab"> <form> <!-- ... --> </form> </div> <div class="well tab-pane" id="maptab"> <!-- ... --> </div> </div> e. Tooltip Documentation 94 95 Exemple toolptip sur les images (affiche la propriété title) $("img").tooltip(); On pourrait définir du « html » dans l’attribut « title » <img id="foodies" src="images/foodies.jpg" width="300" class="pull-left thumbnail" data-html="true" title="<h3>Un petit café?</h3><img src='images/foodies.jpg' class='img-responsive'/>" /> « data-html » f. Caroussel Documentation 95 96 Contrôles Caption Indicateurs Permet de lancer auto. (data-interval : intervalle entre chaque slide en ms) <div class="container"> <div class="row visible-lg visible-md"> <div class="col-lg-8 col-lg-offset-2 col-md-8 col-md-offset-2"> <div id="carousel" class="carousel slide" data-ride="carousel" data-interval="2000"> <!-- indicateurs --> <ol class="carousel-indicators"> <li data-target="#carousel" data-slide-to="0" class="active"></li> <li data-target="#carousel" data-slide-to="1"></li> <li data-target="#carousel" data-slide-to="2"></li> </ol> <!-- slides --> <div class="carousel-inner"> <div class="item active"> <img src="images/image_1.jpg" class="img-responsive" alt="" /> <div class="carousel-caption"><h4>Un petit café?</h4></div> </div> <div class="item"> <img src="images/image_2.jpg" class="img-responsive" alt="" /> <div class="carousel-caption"><h4>Un poussin</h4></div> </div> <div class="item"> <img src="images/image_3.jpg" class="img-responsive" alt="" /> <div class="carousel-caption"><h4>La grande roue</h4></div> </div> </div> <!-- Controls --> <a class="left carousel-control" href="#carousel" data-slide="prev"> <span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span> </a> <a class="right carousel-control" href="#carousel" data-slide="next"> <span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span> </a> </div> </div> </div> </div> jQuery 96 97 $("#carousel").carousel(); 97