构建一个真实的应用电子商务SportsStore(八)
使用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测试数据/?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测试数据/s/EOJ5b/1370615290
哦,忘记了最重要的一件事,我们还没收款的功能,这我们可亏大了!不过今天实在太累了,下篇我们再继续开发收款的模块吧!请继续关注我们续篇!
作者: Leo_wl
出处: http://HdhCmsTestcnblogs测试数据/Leo_wl/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
版权信息查看更多关于构建一个真实的应用电子商务SportsStore(八)的详细内容...