共用方式為


第 8 部分:購物車與 Ajax 更新

作者 :JonGaloway

MVC 音樂市集是一個教學課程應用程式,會介紹並說明如何使用 ASP.NET MVC 和 Visual Studio 進行 Web 開發。

MVC 音樂市集是輕量型的市集實作,可線上銷售音樂相簿,並實作基本網站管理、使用者登入和購物車功能。

本教學課程系列詳細說明建置 ASP.NET MVC 音樂市集範例應用程式所採取的所有步驟。 第 8 部分涵蓋購物車與 Ajax 更新。

我們將允許使用者在購物車中放置相簿而不註冊,但必須註冊為來賓才能完成結帳。 購物和結帳程式將會分成兩個控制器:可匿名將專案新增至購物車的 ShoppingCart 控制器,以及處理結帳程式的結帳控制器。 我們將從本節中的購物車開始,然後在下一節中建置結帳程式。

新增購物車、訂單和 OrderDetail 模型類別

我們的購物車和結帳程式將會使用一些新的類別。 以滑鼠右鍵按一下 Models 資料夾,並使用下列程式碼 (Cart.cs) 新增 Cart 類別。

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

這個類別與到目前為止我們所使用的其他類別非常類似,但 RecordId 屬性的 [Key] 屬性除外。 我們的購物車專案會有名為 CartID 的字串識別碼,以允許匿名購物,但資料表包含名為 RecordId 的整數主鍵。 根據慣例,Entity Framework Code-First預期,名為 Cart 的資料表主鍵會是 CartId 或識別碼,但如果我們想要的話,我們可以輕鬆地透過注釋或程式碼覆寫。 這是如何在 Entity Framework 中使用簡單慣例的範例,Code-First適合我們時,但不會受到它們的限制。

接下來,使用下列程式碼 (Order.cs) 新增 Order 類別。

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

此類別會追蹤訂單的摘要和傳遞資訊。 它尚未編譯,因為它具有 OrderDetails 導覽屬性,這取決於我們尚未建立的類別。 現在讓我們藉由新增名為 OrderDetail.cs 的類別來修正此問題,並新增下列程式碼。

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

我們會對 MusicStoreEntities 類別進行最後一次更新,以包含公開這些新模型類別的 DbSets,也包括 DbSet < Artist > 。 更新的 MusicStoreEntities 類別如下所示。

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

管理購物車商務邏輯

接下來,我們將在 Models 資料夾中建立 ShoppingCart 類別。 ShoppingCart 模型會處理購物車資料表的資料存取。 此外,它也會處理商務邏輯,以新增和移除購物車中的專案。

因為我們不想要要求使用者註冊帳戶,只要將專案新增至購物車,我們就會使用 GUID 指派暫時的唯一識別碼 (,或在存取購物車時) 全域唯一識別碼。 我們將使用 ASP.NET Session 類別來儲存此識別碼。

注意:ASP.NET 會話是儲存使用者特定資訊的位置,會在他們離開網站之後過期。 雖然誤用會話狀態可能會對較大的網站造成效能影響,但我們的光線用途適用于示範用途。

ShoppingCart 類別會公開下列方法:

AddToCart 會採用相簿做為參數,並將它新增至使用者的購物車。 由於購物車資料表會追蹤每個相簿的數量,因此它會包含視需要建立新資料列的邏輯,或者如果使用者已經訂購一份相簿,則只遞增數量。

RemoveFromCart 會取得相簿識別碼,並從使用者的購物車中移除它。 如果使用者在其購物車中只有一份相簿,則會移除該資料列。

EmptyCart 會從使用者的購物車中移除所有專案。

GetCartItems 會擷取用於顯示或處理的 CartItems 清單。

GetCount 會擷取使用者在其購物車中擁有的相簿總數。

GetTotal 會計算購物車中所有專案的總成本。

CreateOrder 會在結帳階段將購物車轉換成訂單。

GetCart 是一種靜態方法,可讓我們的控制器取得購物車物件。 它會使用 GetCartId 方法來處理從使用者的會話讀取 CartId。 GetCartId 方法需要 HttpCoNtextBase,以便從使用者的會話讀取使用者的 CartId。

以下是完整的 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();
        }
    }
}

ViewModels

購物車控制器必須將一些複雜的資訊傳達給其檢視,而檢視不會完全對應至模型物件。 我們不想修改模型以符合我們的檢視;模型類別應該代表我們的領域,而不是使用者介面。 其中一個解決方案是使用 ViewBag 類別將資訊傳遞至檢視,如同我們在市集管理員下拉式清單中所做的一樣,但透過 ViewBag 傳遞許多資訊會難以管理。

這是使用 ViewModel 模式的解決方案。 使用此模式時,我們會建立針對特定檢視案例優化的強型別類別,並公開檢視範本所需的動態值/內容屬性。 然後,我們的控制器類別可以填入這些檢視優化類別,並將其傳遞至要使用的檢視範本。 這可在檢視範本內啟用型別安全、編譯時間檢查和編輯器 IntelliSense。

我們將建立兩個用於購物車控制器的檢視模型:ShoppingCartViewModel 會保存使用者購物車的內容,而當使用者從購物車中移除某個專案時,會使用 ShoppingCartRemoveViewModel 來顯示確認資訊。

讓我們在專案的根目錄中建立新的 ViewModels 資料夾,讓專案保持組織。 以滑鼠右鍵按一下專案,選取 [新增/ 新增資料夾]。

顯示以滑鼠右鍵按一下功能表的專案視窗螢幕擷取畫面,其中醒目提示黃色的 [新增和新增資料夾] 選項。

將資料夾命名為 ViewModels。

方案總管螢幕擷取畫面,其中顯示新建立和新命名的資料夾、檢視模型,並以黑色方塊醒目提示。

接下來,在 ViewModels 資料夾中新增 ShoppingCartViewModel 類別。 它有兩個屬性:購物車專案清單,以及小數點值,可保存購物車中所有專案的總價格。

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

現在,將 ShoppingCartRemoveViewModel 新增至 ViewModels 資料夾,其中包含下列四個屬性。

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

購物車控制器

購物車控制器有三個主要用途:將專案新增至購物車、從購物車中移除專案,以及檢視購物車中的專案。 它會使用我們剛才建立的三個類別:ShoppingCartViewModel、ShoppingCartRemoveViewModel 和 ShoppingCart。 如同 StoreController 和 StoreManagerController,我們將新增欄位來保存 MusicStoreEntities 的實例。

使用空白控制器範本,將新的購物車控制器新增至專案。

[新增控制器] 視窗的螢幕擷取畫面,其中 [控制器名稱] 欄位中具有 [購物車控制器],並以藍色醒目提示。

以下是完整的 ShoppingCart 控制器。 索引和新增控制器動作看起來應該很熟悉。 Remove 和 CartSummary 控制器動作會處理兩個特殊案例,我們將在下一節討論。

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

使用 jQuery 的 Ajax 更新

接下來,我們將建立一個 「購物車索引」頁面,其強型別為 ShoppingCartViewModel,並使用與先前相同的方法使用清單檢視範本。

[新增檢視] 視窗的螢幕擷取畫面,其中顯示 [檢視名稱] 欄位、[檢視引擎]、[模型類別] 和 [Scaffold] 下拉式清單,以及 [使用版面配置檔案選擇器]。

不過,我們不會使用 Html.ActionLink 從購物車移除專案,而是使用 jQuery 來「連接」此檢視中具有 HTML 類別 RemoveLink 的所有連結的按一下事件。 這個 click 事件處理常式只會對 RemoveFromCart 控制器動作發出 AJAX 回呼,而不是張貼表單。 RemoveFromCart 會傳回 JSON 序列化的結果,我們的 jQuery 回呼接著會使用 jQuery 剖析並執行頁面的四個快速更新:

    1. 從清單中移除已刪除的相簿
    1. 更新標頭中的購物車計數
    1. 向使用者顯示更新訊息
    1. 更新購物車總價格

由於移除案例是由索引檢視內的 Ajax 回呼所處理,因此我們不需要 RemoveFromCart 巨集指令的額外檢視。 以下是 /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>

為了測試這一點,我們必須能夠將專案新增至購物車。 我們將更新 [ 商店詳細資料 ] 檢視,以包含 [新增至購物車] 按鈕。 當我們使用時,我們可以包含自上次更新此檢視以來新增的一些「相簿」其他資訊:內容類型、藝術師、價格和相簿藝術師。 更新的市集詳細資料檢視程式碼隨即出現,如下所示。

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

現在我們可以按一下商店,並測試在購物車中新增和移除相簿。 執行應用程式並流覽至市集索引。

[音樂市集] 視窗的螢幕擷取畫面,其中顯示從所有輸入資料庫之相簿資料所定義的內容類型詳細資料。

接下來,按一下 [內容類型] 以檢視相簿清單。

[音樂市集] 視窗的螢幕擷取畫面,其中顯示與相簿資料庫中 Disco 內容類型相關聯的相簿清單。

按一下 [相簿] 標題現在會顯示已更新的 [相簿詳細資料] 檢視,包括 [新增至購物車] 按鈕。

[音樂市集] 視窗的螢幕擷取畫面,其中顯示已更新的 [相簿詳細資料] 檢視和 [新增至購物車] 按鈕。

按一下 [新增至購物車] 按鈕會顯示購物車索引檢視與購物車摘要清單。

[音樂市集] 視窗的螢幕擷取畫面,其中顯示購物車檢視,其中包含購物車中所有專案的摘要清單。

載入購物車之後,您可以按一下 [從購物車移除] 連結,以查看購物車的 Ajax 更新。

[音樂市集] 視窗的螢幕擷取畫面,其中顯示從摘要清單中移除的購物車檢視。

我們已建置可讓未註冊的使用者將專案新增至購物車的工作購物車。 在下一節中,我們將允許他們註冊並完成簽出程式。