Condividi tramite


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.

Screenshot della finestra del progetto che mostra il menu di scelta rapida con le opzioni Aggiungi e Nuova cartella evidenziate in giallo.

Assegnare alla cartella il nome ViewModels.

Screenshot del Esplora soluzioni che mostra la cartella appena creata e denominata, Visualizza modelli, evidenziata con una casella nera.

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.

Screenshot della finestra Aggiungi controller con Il controller del carrello acquisti nel campo Nome controller e evidenziato in blu.

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.

Screenshot della finestra Aggiungi visualizzazione che mostra il campo Nome visualizzazione, il motore di visualizzazione, la classe modello e gli elenchi a discesa Scaffold e la casella di selezione Usa un file di layout.

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:

    1. Rimuove l'album eliminato dall'elenco
    1. Aggiornamenti il conteggio del carrello nell'intestazione
    1. Visualizza un messaggio di aggiornamento all'utente
    1. 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.

Screenshot della finestra Music Store che mostra i dettagli del genere definiti da tutti i dati degli album immessi nel database.

Fare quindi clic su un genere per visualizzare un elenco di album.

Screenshot della finestra Music Store che mostra l'elenco di album associati al genere Disco nel database degli album.

Facendo clic su un titolo album viene ora visualizzata la visualizzazione Dettagli album aggiornata, incluso il pulsante "Aggiungi al carrello".

Screenshot della finestra Music Store che mostra la visualizzazione Dettagli album aggiornata e 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.

Screenshot della finestra di Music Store che mostra la visualizzazione Carrello acquisti con un elenco di riepilogo di tutti gli elementi nel carrello.

Dopo aver caricato il carrello acquisti, è possibile fare clic sul collegamento Rimuovi dal carrello per visualizzare l'aggiornamento Ajax al carrello acquisti.

Screenshot della finestra di Music Store che mostra la visualizzazione Carrello acquisti con l'album rimosso dall'elenco di riepilogo.

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.