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

哦,忘記了最重要的一件事,我們還沒收款的功能,這我們可虧大了!不過今天實在太累了,下篇我們再繼續開發收款的模塊吧!請繼續關注我們續篇!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM