Traduction du MVC Music Store Partie 8 : Panier avec utilisation d'Ajax pour les mises à jour
Date de publication : 10/01/2012. Date de mise à jour : 10/01/2012.
Par
Jon Galloway (Blog)
Jean-Michel Ormes (Traduction) (Home) (Blog)
Le MVC Music Store est une application d'étude qui introduit et explique étape par étape comment utiliser ASP.NET MVC et Visual Web Developer pour faire du développement web.
Le MVC Music Store est un exemple simplifié d'application de magasin qui vend des albums de musique en ligne, implémente l'administration du site, l'authentification d'utilisateurs et la fonctionnalité d'achat sous forme de panier.
Ce tutoriel détaille toutes les étapes à suivre pour construire le MVC Music Store. La partie 8 du tutoriel s'intitule "Panier avec utilisation d'Ajax pour les mises à jour".
Retrouvez l'ensemble des articles de la série sur cette page :
MVC Music Store
N'hésitez pas à laisser votre avis sur le contenu de l'article directement via le forum :
5 commentaires
Traduction
1. Introduction
2. Ajout des classes modèles Cart, Order et OrderDetail
3. Gestion de la logique métier du Panier
4. ViewModels
5. Le ShoppingCartController
6. Utilisation d'Ajax et jQuery pour les mises à jour
Remerciements
Traduction
1. Introduction
Nous allons permettre aux utilisateurs de placer des albums dans leurs paniers sans s'identifier, mais ils auront besoin de s'identifier en tant qu'invités pour compléter la commande. Les processus d'achat et de paiement seront séparés en deux contrôleurs : un ShoppingCartController qui permet d'ajouter anonymement des objets au panier, et un CheckoutController qui gère le processus de paiement. Nous allons commencer avec le panier dans cette section, puis construire le processus de paiement dans la section suivante.
2. Ajout des classes modèles Cart, Order et OrderDetail
Nos processus de paiement et de panier feront usage de nouvelles classes. Faites un clic-droit sur le dossier Models et ajoutez une classe Cart (Cart.cs) avec le code suivant.
|
using System. ComponentModel. DataAnnotations;
namespace MvcMusicStore. Models
{
public class Cart
{
[ Key]
public int RecordId { get; set; }
public string CartId { get; set; }
public int AlbumId { get; set; }
public int Count { get; set; }
public System. DateTime DateCreated { get; set; }
public virtual Album Album { get; set; }
}
}
|
Cette classe est assez similaire à d'autres classes que nous avons déjà utilisées jusqu'à présent, à l'exception de l'attribut [Key] pour la propriété RecordId. Nos éléments du panier auront un identifiant string appelé CartID qui permet les achats anonymes, mais la table inclut un entier en clé primaire appelé RecordId. Par convention, Entity Framework Code-First s'attend à ce que la clé primaire pour une table appelée Cart soit CartId ou ID, mais nous pouvons facilement changer cela par des annotations ou par code si on veut. Ceci est un exemple de la façon dont on peut utiliser les conventions simples d'Entity Framework Code-First quand elles nous conviennent, mais nous ne sommes pas contraints par elles quand ce n'est pas le cas.
Ensuite, ajoutez une classe Order (Order.cs) avec le code suivant.
|
using System. Collections. Generic;
namespace MvcMusicStore. Models
{
public partial class Order
{
public int OrderId { get; set; }
public string Username { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string PostalCode { get; set; }
public string Country { get; set; }
public string Phone { get; set; }
public string Email { get; set; }
public decimal Total { get; set; }
public System. DateTime OrderDate { get; set; }
public List< OrderDetail> OrderDetails { get; set; }
}
}
|
Cette classe trace le résumé et les informations de livraison pour une commande. Elle ne compile pas encore, car elle contient une propriété de navigation OrderDetails qui dépend d'une classe que nous n'avons pas encore créée. Corrigeons ceci maintenant en ajoutant une classe appelée OrderDetail.cs, en ajoutant le code suivant.
|
namespace MvcMusicStore. Models
{
public class OrderDetail
{
public int OrderDetailId { get; set; }
public int OrderId { get; set; }
public int AlbumId { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public virtual Album Album { get; set; }
public virtual Order Order { get; set; }
}
}
|
Nous allons faire une dernière mise à jour de notre classe MusicStoreEntities afin d'inclure des DbSets qui exposent ces nouvelles classes modèle, et en incluant également un DbSet<Artist>. La classe MusicStoreEntities mise à jour apparaît ci-dessous.
|
using System. Data. Entity;
namespace MvcMusicStore. Models
{
public class MusicStoreEntities : DbContext
{
public DbSet< Album> Albums { get; set; }
public DbSet< Genre> Genres { get; set; }
public DbSet< Artist> Artists {
get; set; }
public DbSet< Cart>
Carts { get; set; }
public DbSet< Order> Orders
{ get; set; }
public DbSet< OrderDetail>
OrderDetails { get; set; }
}
}
|
3. Gestion de la logique métier du Panier
Ensuite, nous allons créer la classe ShoppingCart dans le dossier Models. Le modèle ShoppingCart gère l'accès aux données de la table Panier. De plus, cela va gérer la logique métier à l'ajout et la suppression des éléments du panier.
Puisque nous ne voulons pas obliger les utilisateurs à s'inscrire juste pour ajouter des éléments à leurs paniers, nous allons leur assigner un identifiant unique temporaire (en utilisant un GUID, ou un identifiant globalement unique) lorsqu'ils accèdent au panier. Nous allons stocker cet ID en utilisant la classe Session d'ASP.NET.
Note : La Session d'ASP.NET est un endroit pratique pour stocker des informations spécifiques à un utilisateur qui expireront après avoir quitté le site. Tout abus de l'état de session peut avoir des conséquences sur les performances de sites plus importants, l'utilisation que nous en ferons conviendra parfaitement à des fins de démonstrations.
La classe ShoppingCart expose les méthodes suivantes :
AddToCart prend un album en paramètre et l'ajoute au panier de l'utilisateur. Puisque la table Panier recense la quantité de chaque album, cela inclut une logique de création d'une nouvelle ligne si nécessaire ou juste l'incrémentation de la quantité si l'utilisateur a déjà commandé une copie de l'album.
RemoveFromCart prend un ID d'Album et l'enlève du panier de l'utilisateur. Si l'utilisateur n'a qu'une copie de l'album dans son panier, la ligne est supprimée.
EmptyCart enlève tous les éléments du panier de l'utilisateur.
GetCartItems récupère une liste des CartItems pour l'affichage ou le traitement.
GetCount récupère le nombre total d'albums qu'un utilisateur a dans son panier.
GetTotal calcule le coût total de tous les éléments présents dans le panier.
CreateOrder convertit le panier en une commande pendant la phase de paiement.
GetCart méthode statique qui permet à nos contrôleurs d'obtenir un objet panier. Elle utilise la méthode GetCartId pour gérer la lecture du CartId depuis la session utilisateur. La méthode GetCartId nécessite le HttpContextBase afin qu'elle puisse lire le CartId de l'utilisateur depuis sa session.
Voici la classe ShoppingCart complète :
|
using System;
using System. Collections. Generic;
using System. Linq;
using System. Web;
using System. Web. Mvc;
namespace MvcMusicStore. Models
{
public partial class ShoppingCart
{
MusicStoreEntities storeDB = new MusicStoreEntities ();
string ShoppingCartId { get; set; }
public const string CartSessionKey = " CartId " ;
public static ShoppingCart GetCart (HttpContextBase context)
{
var cart = new ShoppingCart ();
cart. ShoppingCartId = cart. GetCartId (context);
return cart;
}
public static ShoppingCart GetCart (Controller controller)
{
return GetCart (controller. HttpContext);
}
public void AddToCart (Album album)
{
var cartItem = storeDB. Carts. SingleOrDefault (
c = > c. CartId = = ShoppingCartId
& & c. AlbumId = = album. AlbumId);
if (cartItem = = null )
{
cartItem = new Cart
{
AlbumId = album. AlbumId,
CartId = ShoppingCartId,
Count = 1 ,
DateCreated = DateTime. Now
} ;
storeDB. Carts. Add (cartItem);
}
else
{
cartItem. Count+ + ;
}
storeDB. SaveChanges ();
}
public int RemoveFromCart (int id)
{
var cartItem = storeDB. Carts. Single (
cart = > cart. CartId = = ShoppingCartId
& & cart. RecordId = = id);
int itemCount = 0 ;
if (cartItem ! = null )
{
if (cartItem. Count > 1 )
{
cartItem. Count- - ;
itemCount = cartItem. Count;
}
else
{
storeDB. Carts. Remove (cartItem);
}
storeDB. SaveChanges ();
}
return itemCount;
}
public void EmptyCart ()
{
var cartItems = storeDB. Carts. Where (
cart = > cart. CartId = = ShoppingCartId);
foreach (var cartItem in cartItems)
{
storeDB. Carts. Remove (cartItem);
}
storeDB. SaveChanges ();
}
public List< Cart> GetCartItems ()
{
return storeDB. Carts. Where (
cart = > cart. CartId = = ShoppingCartId). ToList ();
}
public int GetCount ()
{
int ? count = (from cartItems in storeDB. Carts
where cartItems. CartId = = ShoppingCartId
select (int ? )cartItems. Count). Sum ();
return count ? ? 0 ;
}
public decimal GetTotal ()
{
decimal ? total = (from cartItems in storeDB. Carts
where cartItems. CartId = = ShoppingCartId
select (int ? )cartItems. Count *
cartItems. Album. Price). Sum ();
return total ? ? decimal . Zero;
}
public int CreateOrder (Order order)
{
decimal orderTotal = 0 ;
var cartItems = GetCartItems ();
foreach (var item in cartItems)
{
var orderDetail = new OrderDetail
{
AlbumId = item. AlbumId,
OrderId = order. OrderId,
UnitPrice = item. Album. Price,
Quantity = item. Count
} ;
orderTotal + = (item. Count * item. Album. Price);
storeDB. OrderDetails. Add (orderDetail);
}
order. Total = orderTotal;
storeDB. SaveChanges ();
EmptyCart ();
return order. OrderId;
}
public string GetCartId (HttpContextBase context)
{
if (context. Session[ CartSessionKey] = = null )
{
if (! string . IsNullOrWhiteSpace (context. User. Identity. Name))
{
context. Session[ CartSessionKey] =
context. User. Identity. Name;
}
else
{
Guid tempCartId = Guid. NewGuid ();
context. Session[ CartSessionKey] = tempCartId. ToString ();
}
}
return context. Session[ CartSessionKey] . ToString ();
}
public void MigrateCart (string userName)
{
var shoppingCart = storeDB. Carts. Where (
c = > c. CartId = = ShoppingCartId);
foreach (Cart item in shoppingCart)
{
item. CartId = userName;
}
storeDB. SaveChanges ();
}
}
}
|
4. ViewModels
Notre ShoppingCartController aura besoin de communiquer quelques informations complexes à ses vues qui ne correspondent pas proprement à nos objets modèles. Nous ne voulons pas modifier nos modèles pour que cela convienne à nos vues ; les classes modèles devraient représenter notre domaine, et non l'interface utilisateur. Une solution serait de passer l'information à nos vues en utilisant la classe ViewBag, comme nous l'avons fait avec les informations des dropdownlists du Store Manager, mais passer beaucoup d'informations via le ViewBag est difficile à gérer.
Une solution serait d'utiliser le pattern ViewModel. Lorsque l'on utilise ce pattern, nous créons des classes fortement typées qui sont optimisées pour les scénarios de notre vue spécifique, et qui exposent les propriétés pour les valeurs/contenus dynamiques dont la vue à besoin. Nos classes contrôleur peuvent remplir et transmettre ces classes optimisées à nos vues afin de les utiliser. Cela permet un type sécurisé, lors de la compilation, et la présence de l'IntelliSense dans les vues.
Nous allons créer deux Vues Modèles pour les utiliser dans notre contrôleur Shopping Cart : le ShoppingCartViewModel va s'occuper du contenu du panier de l'utilisateur, et le ShoppingCartRemoveViewModel sera utilisé pour afficher les informations de confirmation lorsqu'un utilisateur enlève quelque chose de son panier.
Nous allons créer un dossier ViewModels à la racine du projet pour garder les choses organisées. Faites un clic droit sur le projet, sélectionnez Add / New Folder.
Appelez le dossier ViewModels.
Ensuite, ajoutez la classe modèle ShoppingCartViewModel dans le dossier ViewModels. Elle a deux propriétés : une liste d'éléments Cart, et une valeur décimale qui contient le prix total de tous les éléments du panier.
|
using System. Collections. Generic;
using MvcMusicStore. Models;
namespace MvcMusicStore. ViewModels
{
public class ShoppingCartViewModel
{
public List< Cart> CartItems { get; set; }
public decimal CartTotal { get; set; }
}
}
|
Maintenant, ajoutez le ShoppingCartRemoveViewModel au dossier ViewModels, avec les quatre propriétés suivantes.
|
namespace MvcMusicStore. ViewModels
{
public class ShoppingCartRemoveViewModel
{
public string Message { get; set; }
public decimal CartTotal { get; set; }
public int CartCount { get; set; }
public int ItemCount { get; set; }
public int DeleteId { get; set; }
}
}
|
5. Le ShoppingCartController
Le ShoppingCartController a trois buts principaux : ajouter des éléments au panier, enlever des éléments du panier, et visualiser les éléments du panier. Il fera usage des trois classes que nous venons juste de créer : ShoppingCartViewModel, ShoppingCartRemoveViewModel, et ShoppingCart. Comme dans StoreController et StoreManagerController, nous allons ajouter un champ qui sera une instance de MusicStoreEntities.
Ajoutez le ShoppingCartController en utilisant un template de contrôleur vide.
Voici le contrôleur ShoppingCartController complet. Les actions Index et Add devraient être très familières. Les actions Remove et CartSummary gèrent deux cas spéciaux, dont nous discuterons dans la section suivante.
|
using System. Linq;
using System. Web. Mvc;
using MvcMusicStore. Models;
using MvcMusicStore. ViewModels;
namespace MvcMusicStore. Controllers
{
public class ShoppingCartController : Controller
{
MusicStoreEntities storeDB = new MusicStoreEntities ();
public ActionResult Index ()
{
var cart = ShoppingCart. GetCart (this . HttpContext);
var viewModel = new ShoppingCartViewModel
{
CartItems = cart. GetCartItems (),
CartTotal = cart. GetTotal ()
} ;
return View (viewModel);
}
public ActionResult AddToCart (int id)
{
var addedAlbum = storeDB. Albums
. Single (album = > album. AlbumId = = id);
var cart = ShoppingCart. GetCart (this . HttpContext);
cart. AddToCart (addedAlbum);
return RedirectToAction (" Index " );
}
[ HttpPost]
public ActionResult RemoveFromCart (int id)
{
var cart = ShoppingCart. GetCart (this . HttpContext);
string albumName = storeDB. Carts
. Single (item = > item. RecordId = = id). Album. Title;
int itemCount = cart. RemoveFromCart (id);
var results = new ShoppingCartRemoveViewModel
{
Message = Server. HtmlEncode (albumName) +
" has been removed from your shopping cart. " ,
CartTotal = cart. GetTotal (),
CartCount = cart. GetCount (),
ItemCount = itemCount,
DeleteId = id
} ;
return Json (results);
}
[ ChildActionOnly]
public ActionResult CartSummary ()
{
var cart = ShoppingCart. GetCart (this . HttpContext);
ViewData[ " CartCount " ] = cart. GetCount ();
return PartialView (" CartSummary " );
}
}
}
|
6. Utilisation d'Ajax et jQuery pour les mises à jour
Nous allons ensuite créer une page Index du Panier qui est fortement typée au ShoppingCartViewModel et qui utilise le template List utilisant la même méthode que précédemment.
Cependant, au lieu d'utiliser un Html.ActionLink pour enlever les éléments du panier, nous utiliserons jQuery pour "lier" l'événement click pour tous les liens dans cette vue qui ont la classe HTML RemoveLink. Plutôt que de poster le formulaire, ce gestionnaire d'événements va juste faire un appel AJAX à notre action RemoveFromCart. Le RemoveFromCart renvoie un résultat JSON sérialisé, qui est automatiquement transmis à la méthode JavaScript spécifiée dans notre paramètre OnSuccess- handleUpdate dans ce cas. La fonction Javascript handleUpdate analyse les résultats JSON et effectue quatre mises à jour rapides de la page en utilisant jQuery :
- Enlève les albums supprimés de la liste
- Met à jour le compteur du panier dans l'entête
- Affiche un message de mise à jour à l'utilisateur
- Met à jour le prix total du panier
Puisque le scénario de suppression est géré par un appel Ajax dans la vue Index, nous n'avons pas besoin d'une vue supplémentaire pour l'action RemoveFromCar. Voic le code complet de la vue /ShoppingCart/Index :
|
@model MvcMusicStore. ViewModels. ShoppingCartViewModel
@{
ViewBag. Title = " Shopping Cart " ;
}
< script src= " /Scripts/jquery-1.4.4.min.js "
type= " text/javascript " > < / script>
< script type= " text/javascript " >
$(function () {
$(" .RemoveLink " ). click (function () {
var recordToDelete = $(this ). attr (" data-id " );
if (recordToDelete ! = ' ' ) {
$. post (" /ShoppingCart/RemoveFromCart " , { " id " : recordToDelete } ,
function (data) {
if (data. ItemCount = = 0 ) {
$(' #row- ' + data. DeleteId). fadeOut (' slow ' );
} else {
$(' #item-count- ' + data. DeleteId). text (data. ItemCount);
}
$(' #cart-total ' ). text (data. CartTotal);
$(' #update-message ' ). text (data. Message);
$(' #cart-status ' ). text (' Cart ( ' + data. CartCount + ' ) ' );
} );
}
} );
} );
function handleUpdate () {
var json = context. get_data ();
var data = Sys. Serialization. JavaScriptSerializer. deserialize (json);
if (data. ItemCount = = 0 ) {
$(' #row- ' + data. DeleteId). fadeOut (' slow ' );
} else {
$(' #item-count- ' + data. DeleteId). text (data. ItemCount);
}
$(' #cart-total ' ). text (data. CartTotal);
$(' #update-message ' ). text (data. Message);
$(' #cart-status ' ). text (' Cart ( ' + data. CartCount + ' ) ' );
}
< / script>
< h3>
< em> Review< / em> your cart:
< / h3>
< p class = " button " >
@Html. ActionLink (" Checkout
> > " , " AddressAndPayment" , " Checkout" )
< / p>
< div id= " update-message " >
< / div>
< table>
< tr>
< th>
Album Name
< / th>
< th>
Price (each)
< / th>
< th>
Quantity
< / th>
< th> < / th>
< / tr>
@foreach (var item in
Model. CartItems)
{
< tr id= " row-@item.RecordId " >
< td>
@Html. ActionLink (item. Album. Title,
" Details " , " Store " , new { id = item. AlbumId } , null )
< / td>
< td>
@item. Album. Price
< / td>
< td id= " item-count-@item.RecordId " >
@item. Count
< / td>
< td>
< a href= " # " class = " RemoveLink "
data- id= " @item.RecordId " > Remove
from cart< / a>
< / td>
< / tr>
}
< tr>
< td>
Total
< / td>
< td>
< / td>
< td>
< / td>
< td id= " cart-total " >
@Model. CartTotal
< / td>
< / tr>
< / table>
|
Afin de tester ceci, nous devons être en mesure d'ajouter des éléments de notre panier. Nous allons mettre à jour notre vue Store Details afin d'inclure un bouton "Add to cart". Tant qu'on y est, nous pouvons inclure certaines informations additionnelles de l'album que nous avons ajoutées depuis que nous avons fait la dernière mise à jour de cette vue : Genre, Artist, Price, et Album Art. Le code mis à jour de la vue Details du Store devrait apparaître comme suit.
|
@model MvcMusicStore. Models. Album
@{
ViewBag. Title = " Album - " + Model. Title;
}
< h2> @Model. Title< / h2>
< p>
< img alt= " @Model.Title "
src= " @Model.AlbumArtUrl " / >
< / p>
< div id= " album-details " >
< p>
< em> Genre: < / em>
@Model. Genre. Name
< / p>
< p>
< em> Artist: < / em>
@Model. Artist. Name
< / p>
< p>
< em> Price: < / em>
@String. Format (" {0:F} " ,
Model. Price)
< / p>
< p class = " button " >
@Html. ActionLink (" Add to
cart" , " AddToCart" ,
" ShoppingCart " , new { id = Model. AlbumId } , " " )
< / p>
< / div>
|
Maintenant nous pouvons cliquer sur le store et essayer d'ajouter et enlever des albums de notre panier. Exécutez l'application et allez sur l'Index du Store.
Puis, cliquez sur un genre pour voir la liste des albums.
Maintenant, le fait de cliquer sur le titre d'un album nous montre la vue Details de l'album mise à jour, incluant le bouton "Add to cart".
Cliquer sur le bouton "Add to cart" montre la vue Index de notre panier avec la liste des éléments.
Après le chargement de votre panier, vous pouvez cliquer sur le lien Remove du panier pour voir Ajax faire ses mises à jour.
Nous avons construit un panier qui fonctionne et qui permet aux utilisateurs non enregistrés d'ajouter des éléments à leurs paniers. Dans la section suivante, nous allons leur permettre de s'inscrire et de compléter le processus de paiement.
Remerciements