我們的項目進展相當的不錯,但是現在還不能真正的出售商品,因為我們沒有為顧客提供購物車。今天,我們就加入購物車的功能,畢竟賺錢才是贏道理啊!購物車的邏輯看起來應該像這樣:
我們需要在每件商品的旁邊都加一個"Add to cart”的按鈕,客戶可以隨時的添加自己選擇的商品。現在就讓我們到Domain工程的Entities文件夾去添加一個Cart類吧:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace SportsStore.Domain.Entities { public class Cart { private List<CartLine> lineCollection = new List<CartLine>(); public void AddItem(Product product, int quantity) { CartLine line = lineCollection .Where(p => p.Product.ProductID == product.ProductID) .FirstOrDefault(); if (line == null) { lineCollection.Add(new CartLine { Product = product, Quantity = quantity }); } else { line.Quantity += quantity; } } public void RemoveLine(Product product) { lineCollection.RemoveAll(l => l.Product.ProductID == product.ProductID); } public decimal ComputeTotalValue() { return lineCollection.Sum(e => e.Product.Price * e.Quantity); } public void Clear() { lineCollection.Clear(); } public IEnumerable<CartLine> Lines { get { return lineCollection; } } } public class CartLine { public Product Product { get; set; } public int Quantity { get; set; } } }
Cart類使用CartLine去展示客戶選擇的產品和想購買的數量,我們已經定義了添加商品到購物車的方法, 從購物車刪除商品的方法,計算購物車中的商品總額的方法和重置購物車,清空說有商品的方法。我們也提供了一個屬性,使用IEnumerble<CartLine>.去訪問購物車的內容,所有這些通過LinQ很容易實現。
測試購物車
購物車是我們網站中非常重要的功能,我們必須確保它能正常的運行。為此我們必須要對它進行測試,我們現在就在測試工程中添加一個購物車的測試文件,叫做CartTests.cs。我們要測試的第一項內容是,當一個商品第一次被添加到購物車時,我們希望購物車能添加一個新的CartLine。
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using SportsStore.Domain.Entities; using System.Linq; namespace SportsStore.UnitTests { [TestClass] public class CartTests { [TestMethod] public void Can_Add_New_Lines() { // Arrange - create some test products Product p1 = new Product { ProductID = 1, Name = "P1" }; Product p2 = new Product { ProductID = 2, Name = "P2" }; // Arrange - create a new cart Cart target = new Cart(); // Act target.AddItem(p1, 1); target.AddItem(p2, 1); CartLine[] results = target.Lines.ToArray(); // Assert Assert.AreEqual(results.Length, 2); Assert.AreEqual(results[0].Product, p1); Assert.AreEqual(results[1].Product, p2); } } }
然而,這必然存在一個隱患,如果顧客已經添加過這個商品,再次添加時,我們希望增加的是購物車中的數量,而不是創建一個新的CartLine.
[TestMethod] public void Can_Add_Quantity_For_Existing_Lines() { // Arrange - create some test products Product p1 = new Product { ProductID = 1, Name = "P1" }; Product p2 = new Product { ProductID = 2, Name = "P2" }; // Arrange - create a new cart Cart target = new Cart(); // Act target.AddItem(p1, 1); target.AddItem(p2, 1); target.AddItem(p1, 10); CartLine[] results = target.Lines.OrderBy(c => c.Product.ProductID).ToArray(); // Assert Assert.AreEqual(results.Length, 2); Assert.AreEqual(results[0].Quantity, 11); Assert.AreEqual(results[1].Quantity, 1); }
用戶也會隨時改變想法,從購物車中刪除商品,這個方法我們應實現了,但我們還是需要去Check,這里也需要測試。
[TestMethod] public void Can_Remove_Line() { // Arrange - create some test products Product p1 = new Product { ProductID = 1, Name = "P1" }; Product p2 = new Product { ProductID = 2, Name = "P2" }; Product p3 = new Product { ProductID = 3, Name = "P3" }; // Arrange - create a new cart Cart target = new Cart(); // Arrange - add some products to the cart target.AddItem(p1, 1); target.AddItem(p2, 3); target.AddItem(p3, 5); target.AddItem(p2, 1); // Act target.RemoveLine(p2); // Assert Assert.AreEqual(target.Lines.Where(c => c.Product == p2).Count(), 0); Assert.AreEqual(target.Lines.Count(), 2); }
我們還要測試一下對商品總額的計算是否正確,說干就干,添加如下測試方法:
[TestMethod] public void Calculate_Cart_Total() { // Arrange - create some test products Product p1 = new Product { ProductID = 1, Name = "P1", Price = 100M}; Product p2 = new Product { ProductID = 2, Name = "P2" , Price = 50M}; // Arrange - create a new cart Cart target = new Cart(); // Act target.AddItem(p1, 1); target.AddItem(p2, 1); target.AddItem(p1, 3); decimal result = target.ComputeTotalValue(); // Assert Assert.AreEqual(result, 450M); }
我們還要測試一下,當我們重置購物車的時候,購物車里的內容是否正確:
[TestMethod] public void Can_Clear_Contents() { // Arrange - create some test products Product p1 = new Product { ProductID = 1, Name = "P1", Price = 100M }; Product p2 = new Product { ProductID = 2, Name = "P2", Price = 50M }; // Arrange - create a new cart Cart target = new Cart(); // Arrange - add some items target.AddItem(p1, 1); target.AddItem(p2, 1); // Act - reset the cart target.Clear(); // Assert Assert.AreEqual(target.Lines.Count(), 0); }
好了,現在我們得為添加購物車設置一個觸發點了,那就是為商品加上“Add to cart” 按鈕,編輯Views/Shared/ProductSummary.cshtml 文件,為它添加一個按鈕。
@model SportsStore.Domain.Entities.Product <div class="item"> <h3>@Model.Name</h3> @Model.Description @using(Html.BeginForm("AddToCart", "Cart")) { @Html.HiddenFor(x => x.ProductID) @Html.Hidden("returnUrl", Request.Url.PathAndQuery) <input type="submit" value="+ Add to cart" /> } <h4>@Model.Price.ToString("c")</h4> </div>
我們添加了一個Razor語句塊,為每個產品鏈接創建一個小HTML,當我們提交時,它將調用Cart控制器的AddToCart action 方法,現在我們就去看看Cart控制器吧!哦,差點忘了,我們還得為我們這個BeginForm生成的表單添加一個css,不然它實在太丑了,打開你的Site.css文件,添加如下代碼:
FORM { margin: 0; padding: 0; } DIV.item FORM { float:right; } DIV.item INPUT { color:White; background-color: #333; border: 1px solid black; cursor:pointer; }
在每個產品相上使用Html.BeginForm helper意味着每個“Add to cart”按鈕都將被渲染在自己分離的Html表單元素中,這可能讓你驚訝,ASP.NET MVC每一頁的forms沒有數量限制嗎?的確是這樣,你可以有盡可能多的表單,只要你需要絕對沒問題。這不是什么技術需求,然而,我們的表單都提交給同一個控制器,並且帶有不同的參數,這使得按鈕的處理非常簡單。
實現Cart控制器
右擊控制器文件夾,添加一個名為iCartController的控制器:
using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace SportsStore.WebUI.Controllers { public class CartController : Controller { private IProductsRepository repository; public CartController(IProductsRepository repo) { repository = repo; } public RedirectToRouteResult AddToCart(int productId, string returnUrl) { Product product = repository.Products .FirstOrDefault(p => p.ProductID == productId); if (product != null) { GetCart().AddItem(product, 1); } return RedirectToAction("Index", new { returnUrl }); } public RedirectToRouteResult RemoveFromCart(int productId, string returnUrl) { Product product = repository.Products .FirstOrDefault(p => p.ProductID == productId); if (product != null) { GetCart().RemoveLine(product); } return RedirectToAction("Index", new { returnUrl }); } private Cart GetCart() { Cart cart = (Cart)Session["Cart"]; if (cart == null) { cart = new Cart(); Session["Cart"] = cart; } return cart; } } }
在這個控制器中有幾點需要注意:首先,我們使用了ASP.NET session state特性去存取Cart對象,這也是GetCart方法的用意所在。 ASP.NET有很好的session特性,它能使用cookies 或 URL rewriting與來自用戶的請求結合在一起,去形成一個單一的瀏覽session。一個相關的特性是session state, 它允許我們使用session去整合數據。
我們還需要傳遞信息到View,當用戶點擊了繼續購物按鈕時,購物車對象和鏈接地址需要顯示出來,我們想在Model文件夾創建一個簡單的Viewmodel,命名為CartIndexViewModel:
using SportsStore.Domain.Entities; namespace SportsStore.WebUI.Models { public class CartIndexViewModel { public Cart Cart { get; set; } public string ReturnUrl { get; set; } } }
現在我們需要在Cart控制器中實現Index方法:
using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using SportsStore.WebUI.Models; namespace SportsStore.WebUI.Controllers { public class CartController : Controller { private IProductsRepository repository; public CartController(IProductsRepository repo) { repository = repo; } public ViewResult Index(string returnUrl) { return View(new CartIndexViewModel { Cart = GetCart(), ReturnUrl = returnUrl }); } public RedirectToRouteResult AddToCart(int productId, string returnUrl) { Product product = repository.Products .FirstOrDefault(p => p.ProductID == productId); if (product != null) { GetCart().AddItem(product, 1); } return RedirectToAction("Index", new { returnUrl }); } public RedirectToRouteResult RemoveFromCart(int productId, string returnUrl) { Product product = repository.Products .FirstOrDefault(p => p.ProductID == productId); if (product != null) { GetCart().RemoveLine(product); } return RedirectToAction("Index", new { returnUrl }); } private Cart GetCart() { Cart cart = (Cart)Session["Cart"]; if (cart == null) { cart = new Cart(); Session["Cart"] = cart; } return cart; } } }
最后一步,我們要創建一個強類型的Index View,右擊Index方法,選擇添加視圖:
修改代碼如下:
@model SportsStore.WebUI.Models.CartIndexViewModel @{ ViewBag.Title = "Sports Store: 你的購物車"; } <h2>你的購物車</h2> <table width="90%" align="center"> <thead><tr> <th align="center">Quantity</th> <th align="left">Item</th> <th align="right">Price</th> <th align="right">Subtotal</th> </tr></thead> <tbody> @foreach(var line in Model.Cart.Lines) { <tr> <td align="center">@line.Quantity</td> <td align="left">@line.Product.Name</td> <td align="right">@line.Product.Price.ToString("c")</td> <td align="right">@((line.Quantity * line.Product.Price).ToString("c"))</td> </tr> } </tbody> <tfoot> <tr> <td colspan="3" align="right">Total:</td> <td align="right"> @Model.Cart.ComputeTotalValue().ToString("c") </td> </tr> </tfoot> </table> <p align="center" class="actionButtons"> <a href="@Model.ReturnUrl">Continue shopping</a> </p>
添加樣式單代碼:
H2 { margin-top: 0.3em } TFOOT TD { border-top: 1px dotted gray; font-weight: bold; } .actionButtons A, INPUT.actionButtons { font: .8em Arial; color: White; margin: .5em; text-decoration: none; padding: .15em 1.5em .2em 1.5em; background-color: #353535; border: 1px solid black; }
運行程序並選擇產品添加到購物車,你將看到如下畫面:
現在我們已經有了一個基本的購物功能,但它還有很多問題,在下一篇中,我們將進一步改進並完善我們的購物車,如果你已經想到了更好的完善方案,在下一篇中,我們可以對比一下,看看我們想的是否一致!請繼續關注我的續篇!