第 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,並使用與先前相同的方法使用清單檢視範本。
不過,我們不會使用 Html.ActionLink 從購物車移除專案,而是使用 jQuery 來「連接」此檢視中具有 HTML 類別 RemoveLink 的所有連結的按一下事件。 這個 click 事件處理常式只會對 RemoveFromCart 控制器動作發出 AJAX 回呼,而不是張貼表單。 RemoveFromCart 會傳回 JSON 序列化的結果,我們的 jQuery 回呼接著會使用 jQuery 剖析並執行頁面的四個快速更新:
-
- 從清單中移除已刪除的相簿
-
- 更新標頭中的購物車計數
-
- 向使用者顯示更新訊息
-
- 更新購物車總價格
由於移除案例是由索引檢視內的 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>
現在我們可以按一下商店,並測試在購物車中新增和移除相簿。 執行應用程式並流覽至市集索引。
接下來,按一下 [內容類型] 以檢視相簿清單。
按一下 [相簿] 標題現在會顯示已更新的 [相簿詳細資料] 檢視,包括 [新增至購物車] 按鈕。
按一下 [新增至購物車] 按鈕會顯示購物車索引檢視與購物車摘要清單。
載入購物車之後,您可以按一下 [從購物車移除] 連結,以查看購物車的 Ajax 更新。
我們已建置可讓未註冊的使用者將專案新增至購物車的工作購物車。 在下一節中,我們將允許他們註冊並完成簽出程式。