《Pro ASP.NET MVC 3 Framework》学习笔记之十五【示例项目SportsStore】-阿里云开发者社区

开发者社区> mszhangxuefei> 正文

《Pro ASP.NET MVC 3 Framework》学习笔记之十五【示例项目SportsStore】

简介: 绑定Shopping Cart 定义购物车Cart的实体,购物车是我们程序业务领域的一个部分,所以在我们领域模型(Domain Model)里面添加一个cart的实体是合理的。在SportsStore.
+关注继续查看

绑定Shopping Cart

定义购物车Cart的实体,购物车是我们程序业务领域的一个部分,所以在我们领域模型(Domain Model)里面添加一个cart的实体是合理的。在SportsStore.Domain的Entities文件夹下添加一个Cart的实体类,如下所示:

View Code
    public class Cart
{
private List<CartLine> lineCollection = new List<CartLine>();
//添加
public void AddItem(Product product, int quantity)
{
CartLine line = lineCollection.Where(p => p.Product.ProductID == product.ProductID).FirstOrDefault();
if (line == null)
{
lineCollection.Add(new CartLine {Product=product,Quantity=quantity });
}
else
{
line.Quantity += quantity;
}
}
//移除
public void RemoveLine(Product product)
{
lineCollection.RemoveAll(l => l.Product.ProductID == product.ProductID);
}
//求和
public decimal ComputeTotalValue()
{
return lineCollection.Sum(e => e.Product.Price * e.Quantity);
}
//清空
public void Clear()
{
lineCollection.Clear();
}
//购物车集合
public IEnumerable<CartLine> Lines
{
get { return lineCollection; }
}
}

//购物车项
public class CartLine
{
public Product Product{get;set;}
public int Quantity{get;set;}
}

添加几个测试方法测试下,如下所示:

View Code
        [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();
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
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_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"}
}.ToList());
//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 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");
}

编辑Views/Shared/ProductSummary.cshtml,如下所示:

View Code
@model SportsStore.Domain.Entities.Product
<div id="item">
<h3>@Model.Name</h3>
@Model.Description
@using (Html.BeginForm("AddToCart", "Cart"))
{
@Html.HiddenFor(x => x.ProductID)
@Html.Hidden("returnUrl", Request.Url.PathAndQuery)
<h4>@Model.Price.ToString("c")</h4>
<input type="submit" value="+ Add to cart" />
}
</div>

当我们提交表单时,会调用CartController下的AddToCart action方法。

Note:我们通过Html.BeginForm来创建一个表单的,默认情况下form表单是post提交。当然我们可以改变它,使用Get方法提交也可以。但是我们必须谨慎对待,因为HTTP规范要求Get请求必须是幂等的(idempotent),这意味着不能引起任何改变。我们这里向购物车添加Product显然是一个变化的过程,所以用Post最合适。

补充下:Http定义了与服务器交互的不同方法,最基本的方法有4种,分别是:GET,POST,PUT,DELETE.即查,改,增,删4个操作。针对这个主题(POST提交还是GET提交)下一章会有更多的讲解,还包括了对如果我们忽略了幂等GET请求产生的后果的解释。

接着添加一个样式,如下所示:

View Code
FORM { margin: 0; padding: 0; } 
DIV.item FORM { float:right; }
DIV.item INPUT {
color:White; background-color: #333; border: 1px solid black; cursor:pointer;
}

前面我们用Html.BeginForm方法在每一个Product列表创建了Form。这也意味着,每天点击Add to cart按钮时,会提交对应的Form表单。这可能让我们做WebForm的人有点意外,因为WebForm里面限制了每个页面只能有一个Form。但是asp.net mvc里面没有这个限制,完全可以根据我们的需要来创建Form表单。

接着创建CartController用来处理Add to cart按钮点击。代码如下所示:

View Code
using System;
using System.Linq;
using System.Web.Mvc;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using SportsStore.WebUI.Models;

namespace SportsStore.WebUI.Controllers
{
public class CartController : Controller
{
private IProductsRepository repository;
public CartController(IProductsRepository repo)
{
repository = repo;
}

public RedirectToRouteResult AddToCart(Cart cart, int productId, string returnUrl)
{
Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId);
if (product != null)
{
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)
{
cart.RemoveLine(product);
}
return RedirectToAction("Index", new { returnUrl });
}

public ViewResult Index(Cart cart, string returnUrl)
{
return View(new CartIndexViewModel { Cart = cart, ReturnUrl = returnUrl });
}

public ViewResult Summary(Cart cart)
{
return View(cart);
}

public ViewResult Checkout()
{
return View(new ShippingDetails());
}

}
}

在上面的代码里面,AddToCart和RemoveFromCart方法都调用了RedirectToAction方法。这是用来发送重定向的指令到浏览器,让浏览器请求一个新的URL。这里是让浏览器请求一个调用Index action方法的URL。下面实现这个Index方法并用它来展示Cart的内容。

我们需要传递两种信息给展示购物车内容的View,分别是Cart对象和展示当用户点击Continue Shopping按钮时的URL。为此,我们创建一个视图模型的类,在Models文件夹创建CartIndexViewModel的类,代码如下:

View Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using SportsStore.Domain.Entities;

namespace SportsStore.WebUI.Models
{
public class CartIndexViewModel
{
public Cart Cart { get; set; }
public string ReturnUrl { get; set; }
}
}

接着实现Cart controller里面的Index action方法,代码如下:

View Code
        public ViewResult Index(Cart cart, string returnUrl)
{
return View(new CartIndexViewModel { Cart = cart, ReturnUrl = returnUrl });
}

右键添加视图,如下所示:

Index View的代码如下所示:

View Code
@model SportsStore.WebUI.Models.CartIndexViewModel
@{
ViewBag.Title = "Sports Store:Your Cart";
}
<h2>
Your Cart</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>
@Html.ActionLink("Checkout now", "Checkout")
</p>

继续添加样式:

View Code
H2 { margin-top: 0.3em } 
TFOOT TD { border-top: 1px dotted gray; font-weight: bold; }
.actionButtons A, INPUT.actionButtons {
font: .8em Arial; color: White; margin: .5em;
text-decoration: none; padding: .15em 1.5em .2em 1.5em;
background-color: #353535; border: 1px solid black;
}

现在基本告一段落,可以运行程序看看效果了。接下来是使用模型绑定(Model Binding)。

asp.net mvc框架使用一种称为model binding的机制将来自HTTP请求创建为C#对象,这样就能够将它们作为参数传递给Controller的Action方法。这也是MVC处理Form表单的原理。例如MVC框架寻找Action方法的参数作为目标,使用model binding从Input元素获取值,并会将这些值转化为Action方法里面参数对应的类型以及相同的参数名称。
Model binders能够将请求里面任何可用的信息创建为C#类型,这是MVC框架一个非常核心的功能之一。

下面我们将创建一个自定义的模型绑定来完善CartController类。这里使用Session对象来存储和管理购物车对象(当然,这可能不符合实际情况,我们只管将注意力放在MVC上吧,呵呵)。

我们将创建一个自定义的绑定来获取包含在Session里面的Cart对象,MVC框架会创建Cart对象并作为参数传递给CartController的Action方法。MVC的model binding功能是非常强大且可伸缩的。对于这个部分,书的第二部分会有详细的讲解。

通过实现IModelBinder接口来创建自定义的model binding。在 SportsStore.WebUI里面创建一个新的文件夹Binders,接着在里面创建CartModelBinder.cs.代码如下:

View Code
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)
{
//从Session里面获取Cart
Cart cart = (Cart)controllerContext.HttpContext.Session[sessionKey];
//如果Session里面没有数据,则创建一个购物车
if (cart == null)
{
cart = new Cart();
controllerContext.HttpContext.Session[sessionKey] = cart;
}
return cart;
}
}
}

IModeBinder接口里面定义一个方法:BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext).提供的两个参数使我们创建领域模型对象成为可能。ControllerContext providers能够访问Controller类的所有信息,包括了来自浏览器的详细请求信息;ModelBindingContext提供我们要创建model object的信息和工具来简化我们的操作。

这里主要关注下ControllerContext类,它有一个HttpContext属性,它包含了Session对象,这正是我们需要的。

我们需要告诉MVC框架使用我们的CartModelBinder类创建Cart的实例,需要修改Global.asax,如下所示:

        protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();

RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);

ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory());
ModelBinders.Binders.Add(typeof(Cart), new CartModelBinder());
}

当MVC收到请求时,比如,AddToCart方法被调用时,它开始寻找action 方法的参数,它将会在可用的绑定列表里面寻找,试图找到一个能够创建每一个参数类型的实例。我们自定义的绑定会被用来创建Cart对象,并且MVC是通过Session功能来实现的。在我们自定义的绑定和默认绑定之间,mvc能够创建必备参数的集合来调用action方法。正是如此才允许我们重构Controller以至于我们不知道在请求被接收时Cart对象是怎样被创立的。

像这样使用自定义的绑定有几个好处:

1.我们将创建购物车的逻辑从Controller里面分离出来,这样就允许我们可以改变存储Cart对象的方式而不必去更改Controller。

2.任何Controller想使用Cart对象,只需要在其Action方法里面声明一个Cart参数即可

3.较好的进行单元测试

下面接着完善购物车功能

这里给购物车增加两个功能:1.允许用户删除购物项 2.在页面顶端增加一个显示购物车详情的功能
首先修改Views/Cart/Index.cshtml,如下所示:

View Code
@model SportsStore.WebUI.Models.CartIndexViewModel
@{
ViewBag.Title = "Sports Store:Your Cart";
}
<h2>
Your Cart</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>
@Html.ActionLink("Checkout now", "Checkout")
</p>

添加查看购物车详情功能:
在CartController里面添加一个Summary action方法,如下所示:

View Code
        public ViewResult Summary(Cart cart)
{
return View(cart);
}

Summary方法非常简单,仅仅需要呈现一个View,提供当前购物车作为View Data。当然这里要使用自定义绑定,我需要创建一个partial view。如下所示:

Summary Partial View的代码如下所示:

View Code
@model SportsStore.Domain.Entities.Cart
@{
Layout = null;
}
<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>

因为我们在每个页面都要显示,所有需要在_Layout.cshtml里面进行定义,如下所示:

<!DOCTYPE html>
<html>
<head>
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
</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>

接着添加样式,如下所示:

View Code
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;}

这时运行下程序,应该看到如下效果:

好啦,今天的笔记就到这里。请路过的朋友多指导,帮助!
晚安!

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器ECS远程登录用户名密码查询方法
阿里云服务器ECS远程连接登录输入用户名和密码,阿里云没有默认密码,如果购买时没设置需要先重置实例密码,Windows用户名是administrator,Linux账号是root,阿小云来详细说下阿里云服务器远程登录连接用户名和密码查询方法
2917 0
阿里云服务器安全组设置内网互通的方法
虽然0.0.0.0/0使用非常方便,但是发现很多同学使用它来做内网互通,这是有安全风险的,实例有可能会在经典网络被内网IP访问到。下面介绍一下四种安全的内网互联设置方法。 购买前请先:领取阿里云幸运券,有很多优惠,可到下文中领取。
9445 0
windows server 2008阿里云ECS服务器安全设置
最近我们Sinesafe安全公司在为客户使用阿里云ecs服务器做安全的过程中,发现服务器基础安全性都没有做。为了为站长们提供更加有效的安全基础解决方案,我们Sinesafe将对阿里云服务器win2008 系统进行基础安全部署实战过程! 比较重要的几部分 1.
5478 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,阿里云优惠总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系.
5735 0
阿里云服务器远程登录用户名和密码的查询方法
阿里云服务器远程连接登录用户名和密码在哪查看?阿里云服务器默认密码是什么?云服务器系统不同默认用户名不同
483 0
阿里云ECS云服务器初始化设置教程方法
阿里云ECS云服务器初始化是指将云服务器系统恢复到最初状态的过程,阿里云的服务器初始化是通过更换系统盘来实现的,是免费的,阿里云百科网分享服务器初始化教程: 服务器初始化教程方法 本文的服务器初始化是指将ECS云服务器系统恢复到最初状态,服务器中的数据也会被清空,所以初始化之前一定要先备份好。
3278 0
+关注
mszhangxuefei
姓名:张雪飞|2014-至今:从事前端相关开发工作,曾做过3年的asp.net mvc开发,潜心在大前端领域深入学习(node,vue,webpack,es6)个人技术博客:http://zhangxuefei.site
75
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
文娱运维技术
立即下载
《SaaS模式云原生数据仓库应用场景实践》
立即下载
《看见新力量:二》电子书
立即下载