王志岳

塔塔信息技术(中国)有限公司 | CoderBus

导航

使用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吧:

image

@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文件中,运行一下吧!

image

 

为了方便大家调试跟踪,我把截至到本篇的项目源代码发布到了网盘上,这是全量包,去下载吧:

http://vdisk.weibo.com/s/EOJ5b/1370615290

哦,忘记了最重要的一件事,我们还没收款的功能,这我们可亏大了!不过今天实在太累了,下篇我们再继续开发收款的模块吧!请继续关注我们续篇!

posted on 2013-06-07 22:31  王志岳  阅读(2510)  评论(6编辑  收藏  举报

开发者导航