Sdílet prostřednictvím


Část 8: Nákupní košík s aktualizacemi Ajax

Jon Galloway

MVC Music Store je výuková aplikace, která představuje a vysvětluje podrobný postup použití ASP.NET MVC a sady Visual Studio pro vývoj pro web.

MVC Music Store je odlehčená ukázková implementace, která prodává hudební alba online a implementuje základní správu webu, přihlašování uživatelů a funkce nákupního košíku.

Tato série kurzů podrobně popisuje všechny kroky k vytvoření ukázkové aplikace ASP.NET MVC Music Store. Část 8 se zabývá nákupním košíkem s ajax Aktualizace.

Umožníme uživatelům umístit alba do košíku bez registrace, ale budou se muset zaregistrovat jako hosté, aby mohli dokončit pokladnu. Proces nákupu a pokladny bude rozdělen do dvou kontrolerů: kontroleru ShoppingCart, který umožňuje anonymní přidávání položek do košíku, a kontroleru pokladny, který zpracovává proces placení. V této části začneme s nákupním košíkem a pak v následující části vytvoříme proces pokladny.

Přidání tříd modelu Cart, Order a OrderDetail

Naše procesy nákupního košíku a pokladny budou využívat některé nové třídy. Klikněte pravým tlačítkem na složku Models a přidejte třídu Cart (Cart.cs) s následujícím kódem.

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; }
    }
}

Tato třída je dost podobná ostatním, které jsme zatím používali, s výjimkou atributu [Key] pro vlastnost RecordId. Naše položky košíku budou mít identifikátor řetězce s názvem CartID, který umožňuje anonymní nakupování, ale tabulka obsahuje celočíselný primární klíč s názvem RecordId. Podle konvence Entity Framework Code-First očekává, že primárním klíčem tabulky s názvem Cart bude CartId nebo ID, ale pokud chceme, můžeme to snadno přepsat pomocí poznámek nebo kódu. Toto je příklad toho, jak můžeme použít jednoduché konvence v Entity Frameworku Code-First, když nám vyhovují, ale pokud ne, nejsou jimi omezeni.

Potom přidejte třídu Order (Order.cs) s následujícím kódem.

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; }
    }
}

Tato třída sleduje souhrnné informace a informace o doručení objednávky. Ještě se nezkompiluje, protože má navigační vlastnost OrderDetails, která závisí na třídě, kterou jsme ještě nevytvořili. Pojďme to teď vyřešit přidáním třídy s názvem OrderDetail.cs a přidáním následujícího kódu.

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; }
    }
}

Provedeme poslední aktualizaci naší třídy MusicStoreEntities tak, aby zahrnovala DbSets, které zpřístupňují tyto nové třídy modelu, včetně Třídy DbSet<Artist>. Aktualizovaná třída MusicStoreEntities se zobrazí níže.

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; }
    }
}

Správa obchodní logiky nákupního košíku

Dále vytvoříme třídu ShoppingCart ve složce Models. Model ShoppingCart zpracovává přístup k datům v tabulce Košík. Kromě toho bude zpracovávat obchodní logiku pro přidávání a odebírání položek z nákupního košíku.

Vzhledem k tomu, že nechceme vyžadovat, aby si uživatelé zaregistrovali účet jenom kvůli přidávání položek do nákupního košíku, přiřadíme uživatelům při přístupu k nákupnímu košíku dočasný jedinečný identifikátor (pomocí identifikátoru GUID nebo globálně jedinečného identifikátoru). Toto ID uložíme pomocí třídy ASP.NET Session.

Poznámka: Relace ASP.NET je vhodným místem pro ukládání informací specifických pro uživatele, jejichž platnost vyprší po opuštění webu. I když zneužití stavu relace může mít vliv na výkon na větších webech, naše použití světla bude fungovat dobře pro demonstrační účely.

Třída ShoppingCart zveřejňuje následující metody:

AddToCart přebírá album jako parametr a přidává ho do košíku uživatele. Vzhledem k tomu, že tabulka Cart sleduje množství pro každé album, zahrnuje logiku vytvoření nového řádku v případě potřeby nebo jen zvýšení množství, pokud uživatel již jednu kopii alba objednal.

RemoveFromCart převezme ID alba a odebere ho z košíku uživatele. Pokud měl uživatel v košíku jenom jednu kopii alba, řádek se odebere.

EmptyCart odebere všechny položky z nákupního košíku uživatele.

GetCartItems načte seznam CartItems pro zobrazení nebo zpracování.

GetCount načte celkový počet alb, která má uživatel v nákupním košíku.

GetTotal vypočítá celkové náklady na všechny položky v košíku.

CreateOrder převede nákupní košík na objednávku během fáze placení.

GetCart je statická metoda, která umožňuje našim kontrolerů získat objekt košíku. Používá metodu GetCartId ke čtení CartId z relace uživatele. GetCartId Metoda vyžaduje HttpContextBase, aby mohl číst CartId uživatele z relace uživatele.

Tady je kompletní třída ShoppingCart:

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();
        }
    }
}

Modely zobrazení

Náš kontroler nákupního košíku bude muset sdělit některé komplexní informace svým zobrazením, které se nemapují čistě na objekty modelu. Nechceme upravovat naše modely tak, aby vyhovovaly našim názorům; Třídy modelu by měly představovat naši doménu, ne uživatelské rozhraní. Jedním z řešení by bylo předat tyto informace našim zobrazením pomocí třídy ViewBag, jako jsme to udělali v rozevíracím seznamu Správce obchodu, ale předání velkého množství informací přes ViewBag je obtížné spravovat.

Řešením je použít model ViewModel . Při použití tohoto vzoru vytváříme třídy silného typu, které jsou optimalizované pro naše konkrétní scénáře zobrazení a které zveřejňují vlastnosti pro dynamické hodnoty a obsah, které naše šablony zobrazení potřebují. Naše třídy kontroleru pak mohou tyto třídy optimalizované pro zobrazení naplnit a předat do naší šablony zobrazení k použití. To umožňuje zabezpečení typů, kontrolu kompilace a editor IntelliSense v rámci šablon zobrazení.

Vytvoříme dva modely zobrazení pro použití v našem kontroleru nákupního košíku: nákupní model ShoppingCartViewModel bude obsahovat obsah nákupního košíku uživatele a model ShoppingCartRemoveViewModel se použije k zobrazení potvrzovacích informací, když uživatel něco odebere ze svého košíku.

Pojďme vytvořit novou složku ViewModels v kořenovém adresáři našeho projektu, abychom měli všechno uspořádané. Klikněte pravým tlačítkem na projekt a vyberte Přidat nebo nová složka.

Snímek obrazovky s oknem projektu zobrazující nabídku zobrazenou po kliknutí pravým tlačítkem a žlutě zvýrazněnými možnostmi Přidat a Nová složka

Složku pojmenujte ViewModels.

Snímek obrazovky s Průzkumník řešení nově vytvořená a nově pojmenovaná složka Zobrazit modely zvýrazněná černým rámečkem

Dále přidejte třídu ShoppingCartViewModel do složky ViewModels. Má dvě vlastnosti: seznam položek košíku a desetinnou hodnotu pro uložení celkové ceny všech položek v košíku.

using System.Collections.Generic;
using MvcMusicStore.Models;
 
namespace MvcMusicStore.ViewModels
{
    public class ShoppingCartViewModel
    {
        public List<Cart> CartItems { get; set; }
        public decimal CartTotal { get; set; }
    }
}

Teď přidejte ShoppingCartRemoveViewModel do složky ViewModels s následujícími čtyřmi vlastnostmi.

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; }
    }
}

Kontroler nákupního košíku

Kontroler nákupního košíku má tři hlavní účely: přidání položek do košíku, odebrání položek z košíku a zobrazení položek v košíku. Bude využívat tři třídy, které jsme právě vytvořili: ShoppingCartViewModel, ShoppingCartRemoveViewModel a ShoppingCart. Stejně jako u StoreController a StoreManagerController přidáme pole pro uložení instance MusicStoreEntities.

Přidejte do projektu nový kontroler Nákupního košíku pomocí šablony Prázdný kontroler.

Snímek obrazovky s oknem Přidat kontroler s kontrolerem nákupního košíku v poli Název kontroleru a modře zvýrazněným

Tady je kompletní kontroler ShoppingCart. Akce Indexovat a Přidat kontroler by měly vypadat velmi povědomě. Akce kontroleru Odebrat a CartSummary řeší dva speciální případy, které probereme v následující části.

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 Aktualizace s jQuery

Dále vytvoříme stránku indexu nákupního košíku, která je silně zadáváná do modelu ShoppingCartViewModel a používá šablonu Zobrazení seznamu pomocí stejné metody jako předtím.

Snímek obrazovky s oknem Přidat zobrazení zobrazující pole Název zobrazení, modul zobrazení, třídu modelu a vygenerované uživatelské rozhraní a výběr souboru použít rozložení

Místo toho, abychom k odebrání položek z košíku použili Html.ActionLink, použijeme jQuery k "propojení" události click pro všechny odkazy v tomto zobrazení, které mají třídu HTML RemoveLink. Místo publikování formuláře tato obslužná rutina události kliknutí provede zpětné volání AJAX do akce kontroleru RemoveFromCart. Metoda RemoveFromCart vrátí serializovaný výsledek JSON, který naše zpětné volání jQuery pak parsuje a provede čtyři rychlé aktualizace stránky pomocí jQuery:

    1. Odebere odstraněné album ze seznamu.
    1. Aktualizace počet košíků v záhlaví
    1. Zobrazí uživateli zprávu o aktualizaci.
    1. Aktualizace celkovou cenu košíku

Vzhledem k tomu, že scénář odebrání zpracovává zpětné volání Ajax v zobrazení Index, nepotřebujeme pro akci RemoveFromCart další zobrazení. Tady je úplný kód pro zobrazení /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>

Abychom to mohli otestovat, musíme být schopni přidávat položky do nákupního košíku. Aktualizujeme zobrazení Podrobnosti obchodu tak, aby obsahovalo tlačítko Přidat do košíku. Zatímco jsme u toho, můžeme zahrnout některé další informace o albu, které jsme přidali od poslední aktualizace tohoto zobrazení: Žánr, Interpret, Cena a Obrázek alba. Zobrazí se aktualizovaný kód zobrazení Store Details (Podrobnosti obchodu), jak je znázorněno níže.

@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>

Nyní můžeme kliknout na obchod a otestovat přidávání a odebírání alb do a z našeho nákupního košíku. Spusťte aplikaci a přejděte k indexu storu.

Snímek obrazovky s oknem Obchodu s hudbou zobrazující podrobnosti o žánru definovaném ze všech dat alb zadaných do databáze

Potom kliknutím na Žánr zobrazíte seznam alb.

Snímek obrazovky s oknem Obchodu s hudbou se seznamem alb přidružených k žánru Disco v databázi alb

Kliknutím na název alba se teď zobrazí aktualizované zobrazení Podrobnosti o albu, včetně tlačítka Přidat do košíku.

Snímek obrazovky s oknem Obchodu s hudbou s aktualizovaným zobrazením Podrobnosti alba a tlačítkem Přidat do košíku

Kliknutím na tlačítko Přidat do košíku se zobrazí naše zobrazení Index nákupního košíku se souhrnným seznamem nákupního košíku.

Snímek obrazovky s oknem Obchodu s hudbou se zobrazením Nákupní košík se souhrnným seznamem všech položek v košíku

Po načtení nákupního košíku můžete kliknout na odkaz Odebrat z košíku a zobrazit aktualizaci Ajax do nákupního košíku.

Snímek obrazovky s oknem Obchodu s hudbou se zobrazením Nákupní košík s albem odebraným ze souhrnného seznamu

Vytvořili jsme funkční nákupní košík, který umožňuje neregistrovaným uživatelům přidávat položky do košíku. V následující části jim umožníme registraci a dokončení procesu rezervace.