Parte 8: Carrello con aggiornamenti Ajax
di Jon Galloway
MVC Music Store è un'applicazione di esercitazione che introduce e spiega dettagliatamente come usare ASP.NET MVC e Visual Studio per lo sviluppo Web.
MVC Music Store è un'implementazione leggera del negozio di esempio che vende album musicali online e implementa l'amministrazione del sito di base, l'accesso degli utenti e la funzionalità del carrello acquisti.
Questa serie di esercitazioni illustra in dettaglio tutti i passaggi eseguiti per compilare l'applicazione di esempio MVC Music Store ASP.NET. La parte 8 riguarda il carrello acquisti con Aggiornamenti Ajax.
Consentiremo agli utenti di inserire album nel carrello senza registrare, ma dovranno registrarsi come utenti guest per completare il checkout. Il processo di acquisto e checkout verrà separato in due controller: un Controller ShoppingCart che consente l'aggiunta anonima di elementi a un carrello e un checkout controller che gestisce il processo di checkout. Si inizierà con il carrello acquisti in questa sezione, quindi si creerà il processo di estrazione nella sezione seguente.
Aggiunta delle classi del modello Cart, Order e OrderDetail
I nostri processi carrello e checkout useranno alcune nuove classi. Fare clic con il pulsante destro del mouse sulla cartella Models e aggiungere una classe Cart (Cart.cs) con il codice seguente.
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; }
}
}
Questa classe è piuttosto simile ad altre usate finora, ad eccezione dell'attributo [Key] per la proprietà RecordId. Gli elementi del carrello avranno un identificatore di stringa denominato CartID per consentire acquisti anonimi, ma la tabella include una chiave primaria integer denominata RecordId. Per convenzione, Entity Framework Code-First prevede che la chiave primaria per una tabella denominata Cart sia CartId o ID, ma è possibile eseguirne facilmente l'override tramite annotazioni o codice, se necessario. Questo è un esempio di come è possibile usare le convenzioni semplici in Entity Framework Code-First quando si adattano, ma non sono vincolate da esse quando non lo fanno.
Aggiungere quindi una classe Order (Order.cs) con il codice seguente.
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; }
}
}
Questa classe tiene traccia delle informazioni di riepilogo e consegna per un ordine. Non verrà ancora compilata perché include una proprietà di navigazione OrderDetails che dipende da una classe che non è ancora stata creata. È ora possibile risolvere il problema aggiungendo una classe denominata OrderDetail.cs, aggiungendo il codice seguente.
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; }
}
}
Verrà eseguito un ultimo aggiornamento alla classe MusicStoreEntities per includere DbSet che espongono le nuove classi Model, incluso anche un oggetto DbSet<Artist>. La classe MusicStoreEntities aggiornata viene visualizzata come indicato di seguito.
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; }
}
}
Gestione della logica di business del carrello acquisti
Verrà quindi creata la classe ShoppingCart nella cartella Models. Il modello ShoppingCart gestisce l'accesso ai dati alla tabella Cart. Inoltre, gestirà la logica di business per l'aggiunta e la rimozione di elementi dal carrello acquisti.
Poiché non si vuole richiedere agli utenti di iscriversi per un account solo per aggiungere elementi al carrello acquisti, gli utenti verranno assegnati un identificatore univoco temporaneo (usando un GUID o un identificatore univoco globale) quando accedono al carrello acquisti. Questo ID verrà archiviato usando la classe ASP.NET Session.
Nota: la sessione di ASP.NET è un luogo pratico in cui archiviare informazioni specifiche dell'utente che scadranno dopo aver lasciato il sito. Anche se l'uso improprio dello stato sessione può avere implicazioni sulle prestazioni nei siti più grandi, l'uso della luce funzionerà bene a scopo dimostrativo.
La classe ShoppingCart espone i metodi seguenti:
AddToCart accetta un album come parametro e lo aggiunge al carrello dell'utente. Poiché la tabella Cart tiene traccia della quantità per ogni album, include la logica per creare una nuova riga, se necessario o incrementare semplicemente la quantità se l'utente ha già ordinato una copia dell'album.
RemoveFromCart accetta un ID album e lo rimuove dal carrello dell'utente. Se l'utente ha una sola copia dell'album nel carrello, la riga viene rimossa.
EmptyCart rimuove tutti gli elementi dal carrello acquisti di un utente.
GetCartItems recupera un elenco di CartItems per la visualizzazione o l'elaborazione.
GetCount recupera un numero totale di album che un utente ha nel carrello acquisti.
GetTotal calcola il costo totale di tutti gli articoli nel carrello.
CreateOrder converte il carrello acquisti in un ordine durante la fase di checkout.
GetCart è un metodo statico che consente ai controller di ottenere un oggetto carrello. Usa il metodo GetCartId per gestire la lettura di CartId dalla sessione dell'utente. Il metodo GetCartId richiede HttpContextBase in modo che possa leggere il CartId dell'utente dalla sessione dell'utente.
Ecco la classe ShoppingCart completa:
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();
}
}
}
ViewModel
Il controller carrello acquisti dovrà comunicare alcune informazioni complesse alle relative visualizzazioni che non eseguono il mapping pulito agli oggetti Model. Non vogliamo modificare i modelli in base alle nostre visualizzazioni; Le classi del modello devono rappresentare il dominio, non l'interfaccia utente. Una soluzione consiste nel passare le informazioni alle visualizzazioni usando la classe ViewBag, come abbiamo fatto con le informazioni dell'elenco a discesa Store Manager, ma passando molte informazioni tramite ViewBag diventa difficile da gestire.
Una soluzione a questo scopo consiste nell'usare il modello ViewModel . Quando si usa questo modello vengono create classi fortemente tipizzate ottimizzate per gli scenari di visualizzazione specifici e che espongono proprietà per i valori/contenuti dinamici necessari per i modelli di visualizzazione. Le classi controller possono quindi popolare e passare queste classi ottimizzate per la visualizzazione al modello di visualizzazione da usare. In questo modo, il controllo dei tipi, il controllo in fase di compilazione e l'editor IntelliSense all'interno dei modelli di visualizzazione.
Verranno creati due modelli di visualizzazione da utilizzare nel controller del carrello acquisti: ShoppingCartViewModel conterrà il contenuto del carrello dell'utente e shoppingCartRemoveViewModel verrà usato per visualizzare le informazioni di conferma quando un utente rimuove un elemento dal carrello.
Verrà ora creata una nuova cartella ViewModels nella radice del progetto per mantenere organizzate le cose. Fare clic con il pulsante destro del mouse sul progetto, scegliere Aggiungi/Nuova cartella.
Assegnare alla cartella il nome ViewModels.
Aggiungere quindi la classe ShoppingCartViewModel nella cartella ViewModels. Ha due proprietà: un elenco di elementi carrello e un valore decimale per contenere il prezzo totale per tutti gli articoli nel carrello.
using System.Collections.Generic;
using MvcMusicStore.Models;
namespace MvcMusicStore.ViewModels
{
public class ShoppingCartViewModel
{
public List<Cart> CartItems { get; set; }
public decimal CartTotal { get; set; }
}
}
Aggiungere ora ShoppingCartRemoveViewModel alla cartella ViewModels, con le quattro proprietà seguenti.
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; }
}
}
Controller carrello acquisti
Il controller Carrello acquisti ha tre scopi principali: l'aggiunta di elementi a un carrello, la rimozione di elementi dal carrello e la visualizzazione degli elementi nel carrello. Userà le tre classi appena create: ShoppingCartViewModel, ShoppingCartRemoveViewModel e ShoppingCart. Come in StoreController e StoreManagerController, aggiungeremo un campo per contenere un'istanza di MusicStoreEntities.
Aggiungere un nuovo controller Carrello acquisti al progetto usando il modello Controller vuoto.
Ecco il controller ShoppingCart completo. Le azioni Index e Add Controller dovrebbero avere un aspetto molto familiare. Le azioni del controller Remove e CartSummary gestiscono due casi speciali, che verranno illustrati nella sezione seguente.
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 Aggiornamenti con jQuery
Verrà quindi creata una pagina di indice del carrello acquisti fortemente tipizzata in ShoppingCartViewModel e verrà usato il modello Visualizzazione elenco usando lo stesso metodo di prima.
Tuttavia, invece di usare html.ActionLink per rimuovere elementi dal carrello, si userà jQuery per "collegare" l'evento click per tutti i collegamenti in questa visualizzazione con la classe HTML RemoveLink. Invece di pubblicare il modulo, questo gestore eventi click eseguirà solo un callback AJAX all'azione del controller RemoveFromCart. RemoveFromCart restituisce un risultato serializzato JSON, che il callback jQuery analizza ed esegue quattro aggiornamenti rapidi alla pagina usando jQuery:
-
- Rimuove l'album eliminato dall'elenco
-
- Aggiornamenti il conteggio del carrello nell'intestazione
-
- Visualizza un messaggio di aggiornamento all'utente
-
- Aggiornamenti il prezzo totale del carrello
Poiché lo scenario di rimozione viene gestito da un callback Ajax all'interno della visualizzazione Indice, non è necessaria una visualizzazione aggiuntiva per l'azione RemoveFromCart. Ecco il codice completo per la visualizzazione /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>
Per testare questo risultato, dobbiamo essere in grado di aggiungere articoli al carrello acquisti. Aggiorneremo la visualizzazione Dettagli negozio per includere un pulsante "Aggiungi al carrello". Anche se ci troviamo, possiamo includere alcune informazioni aggiuntive sull'album che abbiamo aggiunto dopo l'ultimo aggiornamento di questa visualizzazione: Genre, Artist, Price e Album Art. Il codice di visualizzazione Dettagli archivio aggiornato viene visualizzato come illustrato di seguito.
@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>
Ora è possibile fare clic sul negozio e testare l'aggiunta e la rimozione di album da e verso il carrello acquisti. Eseguire l'applicazione e passare all'indice dello Store.
Fare quindi clic su un genere per visualizzare un elenco di album.
Facendo clic su un titolo album viene ora visualizzata la visualizzazione Dettagli album aggiornata, incluso il pulsante "Aggiungi al carrello".
Facendo clic sul pulsante "Aggiungi al carrello" viene visualizzata la visualizzazione Indice carrello acquisti con l'elenco di riepilogo del carrello acquisti.
Dopo aver caricato il carrello acquisti, è possibile fare clic sul collegamento Rimuovi dal carrello per visualizzare l'aggiornamento Ajax al carrello acquisti.
Abbiamo costruito un carrello acquisti funzionante che consente agli utenti non registrati di aggiungere elementi al carrello. Nella sezione seguente verranno consentiti di registrarli e completare il processo di pagamento.