《Pro ASP.NET MVC 3 Framework》学习笔记之十五【示例项目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;}

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

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

时间: 2024-10-24 19:44:14

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

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

接下来是关于SportsStore的后台管理功能,也就是通常的CRUD操作.首先添加一个AdminController,代码如下: View Code using System.Web.Mvc; using SportsStore.Domain.Abstract; namespace SportsStore.WebUI.Controllers { public class AdminController : Controller { private IProductRepository repo

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

接着我们添加一个分页功能.修改ProductController,如下所示: public class ProductController : Controller {public int PageSize = 4;//后面会更改 private IProductsRepository repository;public ProductController(IProductsRepository productRepository) { repository = productRepositor

ASP.NET MVC 3 Framework学习笔记之Model Templates

.使用模板化的视图Helpers(Using Templated View Helpers) 模版化视图helpers的创意就是它们更加灵活.我们不用自己去指定应该用什么HTML元素来呈现一个模型的属性,MVC自己会搞定,在我们更新了视图模型时,也不用手动的更新视图.下面是一个例子:  代码如下 复制代码 //在Models里面添加Persons.cs using System; using System.Collections.Generic; using System.Linq; using

《Pro ASP.NET MVC 3 Framework》学习笔记目录

<Pro ASP.NET MVC 3 Framework>简介: 作者: Adam Freeman 和 Steven Sanderson 出版社: Apress; New 平装: 820页 语种: 英语 ISBN: 1430234040 声明:笔记里面按我自己的理解翻译了大部分内容,写这个笔记的目的:为了方便自己查阅,也为园友提供学习的方便. 我无意侵犯作者的任何权利,仅仅为了自己学习.也希望路过的朋友不要用于任何商业目的. 第一部分 ASP.NET MVC3介绍   <Pro ASP.

《Pro ASP.NET MVC 3 Framework》学习笔记之一【MVC的历程,优点,HelloWorld】

序论:asp.net mvc出现已经有两三年的时间了(2009开始1.0版本),但是这么方面的中文学习资料仍然非常少,特别是asp.net mvc3,几乎就没有中文的学习书籍.在英文的书籍中有两本是非常经典的mvc3教程:<Professional ASP.NET MVC 3>--作者:Jon Galloway , Phil Haack, Brad Wilson , K. Scott Allen和<Pro ASP.NET MVC 3 Framework>--作者:Steven Sa

ASP.NET MVC Web API 学习笔记----HttpClient简介

  1. HttpClient简单介绍  依稀还记得那个时候用WebClient,HttpWebRequest来发送一个请求,现在ASP.NET MVC4中自带了一个类HttpClient,用于接收HttpResponseMessage和发送HttpRequestMesssage. 问题在于既然WebClient,HttpWebRequest可以完成相应的功能,为什么还要使用HttpClient类,.NET Framework中既然提出了这样一个类肯定是有其特别之处的,这里罗列几个不同之处: (

ASP.NET MVC Web API 学习笔记---联系人增删改查

本章节简单介绍一下使用ASP.NET MVC Web API 做增删改查.目前很多Http服务还是通过REST或者类似RESP的模型来进行数据操作的.下面我们通过创建一个简单的Web API来管理联系人          说明:为了方便数据不使用真正的数据库,而是通过内存数据模拟    1.       Web API中包含的方法 Action HTTP method Relative URI GetAllContact GET /api/contact GetContact GET /api/

《Pro ASP.NET MVC 3 Framework》学习笔记之九【Ninject的使用-下】

接着上次的Ninject的笔记,如果你是初次路过,可以先看看我前面的笔记. 一,创建依赖链(Chains of Dependency) 当我们向Ninject请求创建一个类型时,Ninject会去检查该类型和其他类型之间的耦合关系.如果有额外的依赖,Ninject也会解析它们并创建我们需要的所有类的实例.为了进一步说明,我们创建一个新的接口和一个实现该接口的类.请注意我们的例子是跟前面的笔记衔接的,所以如果你打算跟着一起操作的话,最好能够去看看前面的笔记. 创建一个IDiscountHelper

《Pro ASP.NET MVC 3 Framework》学习笔记之四【领域模型介绍】

主题:应用领域驱动开发(Applying Domain-Driven Development) Domain Model是MVC程序的"心脏",其他的一切,包括Controllers和Views仅仅是用来跟Domain Model交互的一种方式,ASP.NET MVC并没有限制使用在Domain Model上面的技术,我们可以自由的选择跟.net framework交互的技术,并且这样的选择是非常多的.不仅如此,ASP.NET MVC为我们提供了基础的架构和约定来帮助Domain Mo

《Pro ASP.NET MVC 3 Framework》学习笔记之五【依赖注入及ninject工具使用】

一,创建松耦合的组件 1."分解关注点"是MVC模式里面一个非常重要的特性.我们想要在应用程序里面创建的组件尽可能的独立,这样我们就能管理比较少的依赖关系.理想情况下,每个组件都是孤立的,不知道其他组件的存在,处理应用程序的其他领域仅仅通过抽象接口,这就是所谓的松耦合,它让我们的应用程序更加容易测试和修改.通过一个简单的例子可以帮助我们理解,假如我们想写一个发邮件的组件,暂且就把这个组件命名为MyEmailSender,接着我们实现一个接口,这个接口定义了所有需要发送邮件的功能,也暂且