使用MVC4,Ninject,EF,Moq,构建一个真实的应用电子商务SportsStore(八)
我们喜欢使用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
哦,忘记了最重要的一件事,我们还没收款的功能,这我们可亏大了!不过今天实在太累了,下篇我们再继续开发收款的模块吧!请继续关注我们续篇!