Partie 8 : Panier d’achat avec des mises à jour Ajax
par Jon Galloway
Le Magasin de musique MVC est une application de tutoriel qui présente et explique pas à pas comment utiliser ASP.NET MVC et Visual Studio pour le développement web.
Le Magasin de musique MVC est un exemple d’implémentation de magasin léger qui vend des albums de musique en ligne et implémente l’administration de site de base, la connexion utilisateur et les fonctionnalités de panier d’achat.
Cette série de tutoriels détaille toutes les étapes effectuées pour générer l’exemple d’application ASP.NET magasin de musique MVC. La partie 8 traite du panier d’achat avec Ajax Mises à jour.
Nous autoriserons les utilisateurs à placer des albums dans leur panier sans s’inscrire, mais ils devront s’inscrire en tant qu’invités pour terminer le paiement. Le processus d’achat et de paiement sera séparé en deux contrôleurs : un contrôleur ShoppingCart qui permet d’ajouter anonymement des articles à un panier et un contrôleur de caisse qui gère le processus de paiement. Nous allons commencer par le panier d’achat dans cette section, puis générer le processus de paiement dans la section suivante.
Ajout des classes de modèles Cart, Order et OrderDetail
Nos processus de panier d’achat et de paiement utilisent de nouvelles classes. Cliquez avec le bouton droit sur le dossier Modèles 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 que nous avons utilisées jusqu’à présent, à l’exception de l’attribut [Key] pour la propriété RecordId. Nos éléments De panier auront un identificateur de chaîne nommé CartID pour permettre l’achat anonyme, mais la table inclut une clé primaire entière nommée RecordId. Par convention, Entity Framework Code-First s’attend à ce que la clé primaire d’une table nommée CartId ou ID soit, mais nous pouvons facilement la remplacer par le biais d’annotations ou de code si nous le voulons. Il s’agit d’un exemple de la façon dont nous pouvons utiliser les conventions simples dans Entity Framework Code-First quand elles nous conviennent, mais nous n’en sommes pas contraints.
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 effectue le suivi des informations de résumé et de livraison d’une commande. Il ne sera pas encore compilé, car il a une propriété de navigation OrderDetails qui dépend d’une classe que nous n’avons pas encore créée. Nous allons maintenant résoudre ce problème en ajoutant une classe nommé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 effectuer une dernière mise à jour de notre classe MusicStoreEntities pour inclure des DbSets qui exposent ces nouvelles classes Model, y compris un Artiste> DbSet<. La classe MusicStoreEntities mise à jour s’affiche comme suit.
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; }
}
}
Gestion de la logique métier du panier d’achat
Ensuite, nous allons créer la classe ShoppingCart dans le dossier Modèles. Le modèle ShoppingCart gère l’accès aux données à la table Cart. En outre, il gère la logique métier pour l’ajout et la suppression d’éléments du panier d’achat.
Étant donné que nous ne voulons pas exiger que les utilisateurs s’inscrivent simplement pour ajouter des éléments à leur panier d’achat, nous allons attribuer aux utilisateurs un identificateur unique temporaire (à l’aide d’un GUID ou d’un identificateur global unique) lorsqu’ils accèdent au panier. Nous allons stocker cet ID à l’aide de la classe session ASP.NET.
Remarque : la session ASP.NET est un endroit pratique pour stocker des informations spécifiques à l’utilisateur qui expireront après qu’ils quittent le site. Bien que l’utilisation incorrecte de l’état de session puisse avoir des conséquences sur les performances sur des sites plus grands, notre utilisation de la lumière fonctionnera bien à des fins de démonstration.
La classe ShoppingCart expose les méthodes suivantes :
AddToCart prend un album en tant que paramètre et l’ajoute au panier de l’utilisateur. Étant donné que la table Cart suit la quantité pour chaque album, elle inclut une logique pour créer une ligne si nécessaire ou simplement incrémenter la quantité si l’utilisateur a déjà commandé une copie de l’album.
RemoveFromCart prend un ID d’album et le supprime du panier de l’utilisateur. Si l’utilisateur n’avait qu’une seule copie de l’album dans son panier, la ligne est supprimée.
EmptyCart supprime tous les éléments du panier d’achat d’un utilisateur.
GetCartItems récupère une liste d’éléments CartItems pour l’affichage ou le traitement.
GetCount récupère le nombre total d’albums qu’un utilisateur a dans son panier d’achat.
GetTotal calcule le coût total de tous les éléments du panier.
CreateOrder convertit le panier d’achat en commande pendant la phase de paiement.
GetCart est une méthode statique qui permet à nos contrôleurs d’obtenir un objet de panier. Il utilise la méthode GetCartId pour gérer la lecture du CartId à partir de la session de l’utilisateur. La méthode GetCartId nécessite HttpContextBase pour pouvoir lire le CartId de l’utilisateur à partir de la session de l’utilisateur.
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;
}
// Helper method to simplify shopping cart calls
public static ShoppingCart GetCart(Controller controller)
{
return GetCart(controller.HttpContext);
}
public void AddToCart(Album album)
{
// Get the matching cart and album instances
var cartItem = storeDB.Carts.SingleOrDefault(
c => c.CartId == ShoppingCartId
&& c.AlbumId == album.AlbumId);
if (cartItem == null)
{
// Create a new cart item if no cart item exists
cartItem = new Cart
{
AlbumId = album.AlbumId,
CartId = ShoppingCartId,
Count = 1,
DateCreated = DateTime.Now
};
storeDB.Carts.Add(cartItem);
}
else
{
// If the item does exist in the cart,
// then add one to the quantity
cartItem.Count++;
}
// Save changes
storeDB.SaveChanges();
}
public int RemoveFromCart(int id)
{
// Get the cart
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);
}
// Save changes
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);
}
// Save changes
storeDB.SaveChanges();
}
public List<Cart> GetCartItems()
{
return storeDB.Carts.Where(
cart => cart.CartId == ShoppingCartId).ToList();
}
public int GetCount()
{
// Get the count of each item in the cart and sum them up
int? count = (from cartItems in storeDB.Carts
where cartItems.CartId == ShoppingCartId
select (int?)cartItems.Count).Sum();
// Return 0 if all entries are null
return count ?? 0;
}
public decimal GetTotal()
{
// Multiply album price by count of that album to get
// the current price for each of those albums in the cart
// sum all album price totals to get the cart total
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();
// Iterate over the items in the cart,
// adding the order details for each
foreach (var item in cartItems)
{
var orderDetail = new OrderDetail
{
AlbumId = item.AlbumId,
OrderId = order.OrderId,
UnitPrice = item.Album.Price,
Quantity = item.Count
};
// Set the order total of the shopping cart
orderTotal += (item.Count * item.Album.Price);
storeDB.OrderDetails.Add(orderDetail);
}
// Set the order's total to the orderTotal count
order.Total = orderTotal;
// Save the order
storeDB.SaveChanges();
// Empty the shopping cart
EmptyCart();
// Return the OrderId as the confirmation number
return order.OrderId;
}
// We're using HttpContextBase to allow access to cookies.
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
{
// Generate a new random GUID using System.Guid class
Guid tempCartId = Guid.NewGuid();
// Send tempCartId back to client as a cookie
context.Session[CartSessionKey] = tempCartId.ToString();
}
}
return context.Session[CartSessionKey].ToString();
}
// When a user has logged in, migrate their shopping cart to
// be associated with their username
public void MigrateCart(string userName)
{
var shoppingCart = storeDB.Carts.Where(
c => c.CartId == ShoppingCartId);
foreach (Cart item in shoppingCart)
{
item.CartId = userName;
}
storeDB.SaveChanges();
}
}
}
ViewModels
Notre contrôleur de panier d’achat devra communiquer des informations complexes à ses vues qui ne sont pas mappées proprement à nos objets Model. Nous ne voulons pas modifier nos modèles en fonction de nos vues ; Les classes de modèle doivent représenter notre domaine, pas l’interface utilisateur. Une solution consiste à transmettre les informations à nos vues à l’aide de la classe ViewBag, comme nous l’avons fait avec les informations de liste déroulante store Manager, mais la transmission d’un grand nombre d’informations via ViewBag devient difficile à gérer.
Une solution consiste à utiliser le modèle ViewModel . Lors de l’utilisation de ce modèle, nous créons des classes fortement typées qui sont optimisées pour nos scénarios d’affichage spécifiques et qui exposent des propriétés pour les valeurs dynamiques/le contenu requis par nos modèles d’affichage. Nos classes de contrôleur peuvent ensuite remplir et passer ces classes optimisées pour l’affichage à notre modèle d’affichage à utiliser. Cela permet la sécurité de type, la vérification au moment de la compilation et l’éditeur IntelliSense dans les modèles d’affichage.
Nous allons créer deux modèles d’affichage à utiliser dans notre contrôleur de panier d’achat : le ShoppingCartViewModel contiendra le contenu du panier d’achat de l’utilisateur, et le ShoppingCartRemoveViewModel sera utilisé pour afficher les informations de confirmation lorsqu’un utilisateur supprime un élément de son panier.
Nous allons créer un dossier ViewModels à la racine de notre projet pour que les choses restent organisées. Cliquez avec le bouton droit sur le projet, puis sélectionnez Ajouter/Nouveau dossier.
Nommez le dossier ViewModels.
Ensuite, ajoutez la classe ShoppingCartViewModel dans le dossier ViewModels. Il a deux propriétés : une liste d’éléments de panier et une valeur décimale pour contenir 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; }
}
}
Ajoutez maintenant 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; }
}
}
Contrôleur de panier d’achat
Le contrôleur Panier d’achat a trois objectifs main : l’ajout d’éléments à un panier, la suppression d’éléments du panier et l’affichage des éléments dans le panier. Il utilise les trois classes que nous venons de créer : ShoppingCartViewModel, ShoppingCartRemoveViewModel et ShoppingCart. Comme dans StoreController et StoreManagerController, nous allons ajouter un champ pour contenir un instance de MusicStoreEntities.
Ajoutez un nouveau contrôleur Panier d’achat au projet à l’aide du modèle Contrôleur vide.
Voici le contrôleur ShoppingCart complet. Les actions Index et Ajouter un contrôleur doivent sembler très familières. Les actions de contrôleur Supprimer et CartSummary gèrent deux cas spéciaux, que nous aborderons 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();
//
// GET: /ShoppingCart/
public ActionResult Index()
{
var cart = ShoppingCart.GetCart(this.HttpContext);
// Set up our ViewModel
var viewModel = new ShoppingCartViewModel
{
CartItems = cart.GetCartItems(),
CartTotal = cart.GetTotal()
};
// Return the view
return View(viewModel);
}
//
// GET: /Store/AddToCart/5
public ActionResult AddToCart(int id)
{
// Retrieve the album from the database
var addedAlbum = storeDB.Albums
.Single(album => album.AlbumId == id);
// Add it to the shopping cart
var cart = ShoppingCart.GetCart(this.HttpContext);
cart.AddToCart(addedAlbum);
// Go back to the main store page for more shopping
return RedirectToAction("Index");
}
//
// AJAX: /ShoppingCart/RemoveFromCart/5
[HttpPost]
public ActionResult RemoveFromCart(int id)
{
// Remove the item from the cart
var cart = ShoppingCart.GetCart(this.HttpContext);
// Get the name of the album to display confirmation
string albumName = storeDB.Carts
.Single(item => item.RecordId == id).Album.Title;
// Remove from cart
int itemCount = cart.RemoveFromCart(id);
// Display the confirmation message
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);
}
//
// GET: /ShoppingCart/CartSummary
[ChildActionOnly]
public ActionResult CartSummary()
{
var cart = ShoppingCart.GetCart(this.HttpContext);
ViewData["CartCount"] = cart.GetCount();
return PartialView("CartSummary");
}
}
}
Ajax Mises à jour avec jQuery
Nous allons ensuite créer une page d’index de panier d’achat fortement typée dans ShoppingCartViewModel et qui utilise le modèle Affichage de liste à l’aide de la même méthode qu’auparavant.
Toutefois, au lieu d’utiliser html.ActionLink pour supprimer des éléments du panier, nous allons utiliser jQuery pour « connecter » l’événement de clic pour tous les liens de cette vue qui ont la classe HTML RemoveLink. Au lieu de publier le formulaire, ce gestionnaire d’événements de clic effectue simplement un rappel AJAX à notre action de contrôleur RemoveFromCart. RemoveFromCart retourne un résultat sérialisé JSON, que notre rappel jQuery analyse ensuite et effectue quatre mises à jour rapides de la page à l’aide de jQuery :
-
- Supprime l’album supprimé de la liste
-
- Mises à jour le nombre de paniers dans l’en-tête
-
- Affiche un message de mise à jour à l’utilisateur
-
- Mises à jour le prix total du panier
Étant donné que le scénario de suppression est géré par un rappel Ajax dans la vue Index, nous n’avons pas besoin d’une vue supplémentaire pour l’action SupprimerFromCart. Voici 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 () {
// Document.ready -> link up remove event handler
$(".RemoveLink").click(function () {
// Get the id from the link
var recordToDelete = $(this).attr("data-id");
if (recordToDelete != '') {
// Perform the ajax post
$.post("/ShoppingCart/RemoveFromCart", {"id": recordToDelete },
function (data) {
// Successful requests get here
// Update the page elements
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>
Pour tester cela, nous devons être en mesure d’ajouter des articles à notre panier d’achat. Nous allons mettre à jour notre vue Détails du Store pour inclure un bouton « Ajouter au panier ». Pendant que nous y sommes, nous pouvons inclure certaines des informations supplémentaires sur l’album que nous avons ajoutées depuis la dernière mise à jour de cette vue : Genre, Artiste, Prix et Album Art. Le code de vue Store Details mis à jour s’affiche comme indiqué ci-dessous.
@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 dans le magasin et tester l’ajout et la suppression d’albums dans et de notre panier d’achat. Exécutez l’application et accédez à l’index du Magasin.
Ensuite, cliquez sur un genre pour afficher une liste d’albums.
Le fait de cliquer sur un titre d’album affiche maintenant notre affichage des détails de l’album mis à jour, y compris le bouton « Ajouter au panier ».
Le fait de cliquer sur le bouton « Ajouter au panier » affiche notre affichage Index du panier avec la liste récapitulative du panier.
Après avoir chargé votre panier, vous pouvez cliquer sur le lien Supprimer du panier pour voir la mise à jour Ajax de votre panier.
Nous avons créé un panier d’achat opérationnel qui permet aux utilisateurs non inscrits d’ajouter des éléments à leur panier. Dans la section suivante, nous allons les autoriser à s’inscrire et à terminer le processus de validation.