我們喜歡使用session state在Cart控制器中存儲和管理我們Cart對象,但是我們不喜歡這種做事的方式,而且那些基於action方法參數的應用模塊也不適用這種方式,我們無法測試控制器類,除非我們Mock基類的Session參數,這就意味着要mock整個控制器類和我們所有需要的東西,這太不現實了。為了解決這個問題,我們就必須使用MVC的另一個重要特性Model binders,MVC框架使用Model binding從Http請求中創建C# 對像,傳遞給action方法作為參數,我們現在就創建一個自定義的model binder,去獲取session data中包含的Cart對像。
創建自定義的Model Binder
要創建自定義的model binder,就要實現IModelBinder 接口.在你的SportsStore.WebUI工程中建一個文件夾叫做Binders,並且創建一個叫做CartModelBinder的類:
using System; using System.Web.Mvc; using SportsStore.Domain.Entities; namespace SportsStore.WebUI.Binders { public class CartModelBinder : IModelBinder { private const string sessionKey = "Cart"; public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { // get the Cart from the session Cart cart = (Cart)controllerContext.HttpContext.Session[sessionKey]; // create the Cart if there wasn't one in the session data if (cart == null) { cart = new Cart(); controllerContext.HttpContext.Session[sessionKey] = cart; } // return the cart return cart; } } }
IModelBinder 接口定義了一個方法: BindModel. ControllerContext提供了訪問控制器所有信息的能力,包括來自客戶端請求的詳細信息, ModelBindingContext 給了你關於你將要綁定的模塊的信息。ControllerContext類有一個HttpContext屬性,它又包含了一個Session屬性,我們可以操作session data. 現在,我們要通知MVC使用我們的CartModelBinder類去創建Cart實例,我們需要修改一下Global.asax文件的 Application_Start方法。
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Http; using System.Web.Mvc; using System.Web.Optimization; using System.Web.Routing; using SportsStore.WebUI.Infrastructure; using SportsStore.Domain.Entities; using SportsStore.WebUI.Binders; namespace SportsStore.WebUI { // 注意: 有關啟用 IIS6 或 IIS7 經典模式的說明, // 請訪問 http://go.microsoft.com/?LinkId=9394801 public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); //Added by wangzhiyue //We need to tell MVC that we want to use the NinjectController //class to create controller objects ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory()); ModelBinders.Binders.Add(typeof(Cart), new CartModelBinder()); //Added end AuthConfig.RegisterAuth(); } } }
現在我們需要更新CartController 類,刪除GetCart方法:
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(Cart cart, string returnUrl) { return View(new CartIndexViewModel { // Cart = GetCart(), Cart = cart, ReturnUrl = returnUrl }); } public RedirectToRouteResult AddToCart(Cart cart, int productId, string returnUrl) { Product product = repository.Products .FirstOrDefault(p => p.ProductID == productId); if (product != null) { // GetCart().AddItem(product, 1); cart.AddItem(product, 1); } return RedirectToAction("Index", new { returnUrl }); } public RedirectToRouteResult RemoveFromCart(Cart cart, int productId, string returnUrl) { Product product = repository.Products .FirstOrDefault(p => p.ProductID == productId); if (product != null) { //GetCart().RemoveLine(product); cart.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; //} } }
現在去完善一下CartTests.cs文件吧:
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using SportsStore.Domain.Entities; using System.Linq; using Moq; using SportsStore.Domain.Abstract; using SportsStore.WebUI.Controllers; using System.Web.Mvc; using SportsStore.WebUI.Models; 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); } [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); } [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); } [TestMethod] public void Can_Add_To_Cart() { // Arrange - create the mock repository Mock<IProductsRepository> mock = new Mock<IProductsRepository>(); mock.Setup(m => m.Products).Returns(new Product[] { new Product {ProductID = 1, Name = "P1", Category = "Apples"}, }.AsQueryable()); // Arrange - create a Cart Cart cart = new Cart(); // Arrange - create the controller CartController target = new CartController(mock.Object); // Act - add a product to the cart target.AddToCart(cart, 1, null); // Assert Assert.AreEqual(cart.Lines.Count(), 1); Assert.AreEqual(cart.Lines.ToArray()[0].Product.ProductID, 1); } [TestMethod] public void Adding_Product_To_Cart_Goes_To_Cart_Screen() { // Arrange - create the mock repository Mock<IProductsRepository> mock = new Mock<IProductsRepository>(); mock.Setup(m => m.Products).Returns(new Product[] { new Product {ProductID = 1, Name = "P1", Category = "Apples"}, }.AsQueryable()); // Arrange - create a Cart Cart cart = new Cart(); // Arrange - create the controller CartController target = new CartController(mock.Object); // Act - add a product to the cart RedirectToRouteResult result = target.AddToCart(cart, 2, "myUrl"); // Assert Assert.AreEqual(result.RouteValues["action"], "Index"); Assert.AreEqual(result.RouteValues["returnUrl"], "myUrl"); } [TestMethod] public void Can_View_Cart_Contents() { // Arrange - create a Cart Cart cart = new Cart(); // Arrange - create the controller CartController target = new CartController(null); // Act - call the Index action method CartIndexViewModel result = (CartIndexViewModel)target.Index(cart, "myUrl").ViewData.Model; // Assert Assert.AreSame(result.Cart, cart); Assert.AreEqual(result.ReturnUrl, "myUrl"); } } }
我們已經定義了RemoveFromCart方法,所以從購物車中刪除商品,只是要暴露這個方法給用戶,我們修改一下Views/Cart/Index.cshtml文件,去實現這個功能:
@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> <td> @using (Html.BeginForm("RemoveFromCart", "Cart")) { @Html.Hidden("ProductId", line.Product.ProductID) @Html.HiddenFor(x => x.ReturnUrl) <input class="actionButtons" type="submit" value="Remove" /> } </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>
還有個問題,就是用戶只能在每次添加商品時才看到自己消費的summary,這實在不方便,我們應該為CartController添加一個Summary的action方法,讓用戶隨時可以查看:
public PartialViewResult Summary(Cart cart) { return PartialView(cart); }
現在就去創建一個強類型的partial view吧:
@model SportsStore.Domain.Entities.Cart <div id="cart"> <span class="caption"> <b>Your cart:</b> @Model.Lines.Sum(x => x.Quantity) item(s), @Model.ComputeTotalValue().ToString("c") </span> @Html.ActionLink("Checkout", "Index", "Cart", new { returnUrl = Request.Url.PathAndQuery }, null) </div>
我們還要把這個View渲染到_Layout.cshtml文件中:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> <link href="~/Content/Site.css" type="text/css" rel="stylesheet" /> </head> <body> <div id="header"> @{Html.RenderAction("Summary", "Cart");} <div class="title">SPORTS STORE</div> </div> <div id="categories"> @{ Html.RenderAction("Menu", "Nav"); } </div> <div id="content"> @RenderBody() </div> </body> </html>
DIV#cart { float:right; margin: .8em; color: Silver; background-color: #555; padding: .5em .5em .5em 1em; } DIV#cart A { text-decoration: none; padding: .4em 1em .4em 1em; line-height:2.1em; margin-left: .5em; background-color: #333; color:White; border: 1px solid black;}
把上面的樣式單添加到你的Site.css文件中,運行一下吧!
為了方便大家調試跟蹤,我把截至到本篇的項目源代碼發布到了網盤上,這是全量包,去下載吧:
http://vdisk.weibo.com/s/EOJ5b/1370615290
哦,忘記了最重要的一件事,我們還沒收款的功能,這我們可虧大了!不過今天實在太累了,下篇我們再繼續開發收款的模塊吧!請繼續關注我們續篇!