第八话 Asp.Net MVC 3.0【MVC项目实战の四】
接着第七话搞完的导航菜单我们继续来完善我们的项目,接下来不啰嗦直接搞购物车。首先我们需要一个购物车的实体。
定义购物车实体
我们需要一个购物车实体来的模型域(Domain),因为购物车是构成我们应用程序的 业务领域。接下我们要创建购物车实体领域(Domain),在们的域模型(Domian)项目"SportsStore.Domain"的Entities文件下创建我们的购物车实体域模型,如下图1.
图1.我们的Cart.cs(购物车的实体域模型)的代码具体如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace SportsStore.Domain.Entities { public class Cart { private List<CartLine> lineConllection = new List<CartLine>(); //添加购物车 public void AddItem(Product product, int quantity) { CartLine line = this.lineConllection .Where(h => h.Product.ProductID == product.ProductID) .FirstOrDefault(); if (line == null) { this.lineConllection.Add(new CartLine { Product = product, Quantity = quantity }); } else { line.Quantity += quantity; } } //删除 public void RemoveLine(Product product) { this.lineConllection.RemoveAll(h => h.Product.ProductID == product.ProductID); } //计算 public decimal ComputeTotalValue() { return this.lineConllection.Sum(h => h.Product.Price * h.Quantity); } //清空购物车 public void Clear() { this.lineConllection.Clear(); } public IEnumerable<CartLine> Lines { get { return this.lineConllection; } } public class CartLine { public Product Product { get; set; } public int Quantity { get; set; } } } }
我们的前台页面需要一个"添加到购物车"的按钮,为了方便期间,还记得我们之前在展示商品信息的时候把展示商品信息的那部分有了一个局部视图搞的,所以我们在展示商品信息的页面就来添加这个按钮。
我们的那个局部试图页面就在我们的web项目/Views/Shared/ProductSummary.cshtml页面,修改ProductSummary.cshtml页面如下:
@model SportsStore.Domain.Entities.Product @{ ViewBag.Title = "ProductSummary"; } <div class="item"> <h3>@Model.Name</h3> @Model.Description @using (Html.BeginForm("AddToCart","Cart")) { @Html.HiddenFor(h=>h.ProductID) @Html.Hidden("returnUrl",this.Request.Url.PathAndQuery) <input type="submit" value="+ Add to cart" /> } <h4>@Model.Price.ToString("C")</h4> </div>
注:上面代码红色加粗的代码就是我们添加进来的按钮,当我们的表单提交时,就会触发Cart控制器里的AddToCart Action(方法)来执行。下来我们就要实现购物车的控制器(Controller)包括,整,删,改,查;下来在我们Web项目的的Controllers文件夹下创建我们的CartController(控制器),具体代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
using System; using System.Collections.Generic; using System.Linq; using System.Web; 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 IProductRepository repository; public CartController(IProductRepository repo) { this.repository = repo; } //添加购物车 public RedirectToRouteResult AddToCart(int productId, string returnUrl) { Product product = this.repository.Products.FirstOrDefault(h => h.ProductID == productId); if (product != null) { this.GetCart().AddItem(product, 1); } return this.RedirectToAction("Index", new { returnUrl }); } //移除商品 public RedirectToRouteResult RemoverFromCart(int productId, string returnUrl) { Product product = this.repository.Products.FirstOrDefault(h => h.ProductID == productId); if (productId != null) { this.GetCart().RemoveLine(product); } return this.RedirectToAction("Index", new { returnUrl }); } private Cart GetCart() { Cart cart = (Cart)Session["Cart"]; if (cart == null) { cart = new Cart(); this.Session["Cart"] = cart; } return cart; } } }
有一个特性是会话(Session)状态,他允许我们的数据关联会话(session)。这一理想适合我们的Cart类,我们希望每个用户都有自己的购物车,而且请求购物车的时间要长久,数据与会话(session)到期后要删除会话状态(session),这意味着我们不需要管理我们或存储我们的购物车对象。添加一个会话(Session)状态,我们只关注会话(Session)对象。
接下来我们需要显示购物车的内容,我们在CartController控制器里的AddToCart Action(方法)和RemoverFromCart Action(方法)都调用了RedirectToAction()方法,这个就重新定向了我们的URL连接到Index页面,我们需要传递两条跳消息到视图显示我们购物车的内容:购物车对象和URL连接,这也就是我们创建一个简单模型的目的。在我们的Web项目的Models文件夹下创建CartIndexViewModel类,具体代码如下:
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; } } }
既然我们的模型已经O了,接下来我就就可以实现CartControllr的Index,修改CartController具体代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; 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 IProductRepository repository; public CartController(IProductRepository repo) { this.repository = repo; } //添加购物车 public RedirectToRouteResult AddToCart(int productId, string returnUrl) { Product product = this.repository.Products.FirstOrDefault(h => h.ProductID == productId); if (product != null) { this.GetCart().AddItem(product, 1); } return this.RedirectToAction("Index", new { returnUrl }); } //移除商品 public RedirectToRouteResult RemoverFromCart(int productId, string returnUrl) { Product product = this.repository.Products.FirstOrDefault(h => h.ProductID == productId); if (productId != null) { this.GetCart().RemoveLine(product); } return this.RedirectToAction("Index", new { returnUrl }); } private Cart GetCart() { Cart cart = (Cart)Session["Cart"]; if (cart == null) { cart = new Cart(); this.Session["Cart"] = cart; } return cart; } public ViewResult Index(string returnUrl) { return this.View(new CartIndexViewModel { Cart = this.GetCart(), ReturnUrl = returnUrl }); } } }
接下来就是我们创建显示购物车内容的视图,右键我们的Index Action(方法)选择添加视图,选择创建强类型视图,并且选择CartIndexViewModel模型类,具体如下图2.
图2.我们希望购物车希望购物车中的显示物品的风格和其他页面一致,所以们在这里套用了_Layout.cshtml模版,创建好Index.cshtml修改如下:
@model SportsStore.WebUI.Models.CartIndexViewModel @{ ViewBag.Title = "Index"; } <h2>Your Cart</h2> <table width="90%" align="center"> <thead> <tr> <th align="center">Qunantity</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> </tr> } </tbody> <tfoot> <tr> <td colspan="3">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>
这个视图看起来和之前相比,可能就稍微复杂一点。列举了购物的行和新添加的行的HTML表单,随着每一行的总成本构成购物车的总成本。我们现在一定有一个基本的购物车了,当我们在商品页面添加商品到购物车,我们的购物车基本就会算出商品的总价格,如下图3-图4.
图3.
图4.接下来我们需要做更多的东西,比如用户可以删掉购物车里不想要的商品,还有购物车的结算等等。
使用模型绑定:Asp.Net MVC框架使用一种称为模型绑定的机制将来自HTTP请求创建为C#对象,这样就能够将它们作为参数传递给Controller(控制器)的Action方法。这也是MVC处理Form表单的原理。譬如MVC框架寻找Action(方法)的参数作为目标,使用模型绑定从Input元素获取值,并会将这些值转化为Action方法里面参数对应的类型以及相同的参数名称。我们喜欢使用会话状态(Session)特征在购物车中的控制器(controller)来存储和管理我们的购物车对象,我们将创建一个自定义的绑定来获取包含在Session里面的Cart对象,MVC框架会创建Cart对象并作为参数传递给CartController的Action方法。接下来我们将创建一个自定义的模型绑定器,在我们web项目的里新建一个文件夹(Binders)在该文件里创建一个类文件,叫CartModelBinder,它的代码具体如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; 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)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提供我们要创建模型对象的信息和工具来简化我们的操作。
其实我们只需要关注ControllerContext类,因为我们需要用他里面的Session这个东东。
我们需要告诉MVC框架,它可以使用我们的CartModelBinder类来创建购物车类的实例,所以我们需要修改Global.asax,具体操作如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; using SportsStore.WebUI.Infrastructure; using SportsStore.WebUI.Binders; using SportsStore.Domain.Entities; namespace SportsStore.WebUI { // 注意: 有关启用 IIS6 或 IIS7 经典模式的说明, // 请访问 http://go.microsoft.com/?LinkId=9394801 public class MvcApplication : System.Web.HttpApplication { public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new HandleErrorAttribute()); } public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( null, "", //只匹配空的URL new { controller = "Product", action = "List", Category = (string)null, page = 1 } ); routes.MapRoute(null, "Page{page}", //匹配像 /Page2,/page123,除了/PageXYZ new { controller = "Product", action = "List", category = (string)null }, new { page = @"\d+" }); //约束:页面数值必须是数字 routes.MapRoute(null, "{category}", //匹配类别 new { controller = "Product", action = "List", page = 1 }); routes.MapRoute(null, "{category}/Page{page}", //配置 类别和分页 new { controller = "Product", action = "List" }, new { page = @"\d+" }); //约束:页面数值必须是数字 routes.MapRoute(null, "{controller}/{action}"); //添加一条路由 routes.MapRoute( null, "Page{page}", new { controller = "Product", action = "List" } ); routes.MapRoute( "Default", // 路由名称 "{controller}/{action}/{id}", // 带有参数的 URL new { controller = "Product", action = "List", id = UrlParameter.Optional } // 参数默认值 ); } protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); //注册路由 ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory()); //注册CartModelBinder类 ModelBinders.Binders.Add(typeof(Cart), new CartModelBinder()); } } }
注:上面红色加粗的那句代码就是注册CartModelBinder类。我们现在可以修改我们的CartController类使用我们的模型绑定来代替之前的GetCart()方法,所以我们需要修改CartController如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; 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 IProductRepository repository; public CartController(IProductRepository repo) { this.repository = repo; } //添加购物车 public RedirectToRouteResult AddToCart(Cart cart, int productId, string returnUrl) { Product product = this.repository.Products.FirstOrDefault(h => h.ProductID == productId); if (product != null) { cart.AddItem(product, 1); } return this.RedirectToAction("Index", new { returnUrl }); } //移除商品 public RedirectToRouteResult RemoveFromCart(Cart cart, int productId, string returnUrl) { Product product = this.repository.Products.FirstOrDefault(h => h.ProductID == productId); if (product != null) { cart.RemoveLine(product); } return this.RedirectToAction("Index", new { cart = cart, returnUrl = returnUrl }); } private Cart GetCart() { Cart cart = (Cart)Session["Cart"]; if (cart == null) { cart = new Cart(); this.Session["Cart"] = cart; } return cart; } public ViewResult Index(Cart cart, string returnUrl) { return this.View(new CartIndexViewModel { Cart = cart, ReturnUrl = returnUrl }); } } }
AddToCart方法被调用时,它开始寻找Action方法的参数,它将会在可用的绑定列表里面寻找,试图找到一个能够创建每一个参数类型的实例。我们自定义的绑定会被用来创建Cart对象,并且MVC是通过Session功能来实现的。在我们自定义的绑定和默认绑定之间,mvc能够创建必备参数的集合来调用action方法。正是如此才允许我们重构Controller以至于我们不知道在请求被接收时Cart对象是怎样被创立的。
使用自定义模型绑定有一下几个好处:
- 我们将创建购物车的逻辑从Controller里面分离出来,这样就允许我们可以改变存储Cart对象的方式而不必去更改Controller。
- 任何Controller想使用Cart对象,只需要在其Action方法里面声明一个Cart参数即可.
- 良好的但愿测试
接下来,完善我们的购物车。给我们的购物车完善2个功能:1.允许用户删除购物商品项 2.在页面顶端增加一个显示购物车详情(迷你购物车)
首先我们允许用户删除购物车内自己不想要或者不喜欢的商品,我们要做的就是在购物车的每行商品后暴露给用户一个删除按钮,这样就方便用户删除掉自己不喜欢的东西。修改我们web项目的Views/Cart/Index.cshtml页面内容如下:
@model SportsStore.WebUI.Models.CartIndexViewModel @{ ViewBag.Title = "Cart Index"; } <h2>Your Cart</h2> <table width="90%" align="center"> <thead> <tr> <th align="center">Qunantity</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(h=>h.ReturnUrl) <input class="actionButtons" type="submit" value="Remove" /> } </td> </tr> } </tbody> <tfoot> <tr> <td colspan="3">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>
搞玩删除按钮,运行一下我们的web应用程序,结果如下图5所示。
图5.可以看出我们的删除购物车的商品项也已经是OK了,接下来完善我们的第二项功能,在页面顶端增加一个显示购物车详情(迷你购物车)在我们CartController类里添加这个功能的Action(执行方法),具体代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; 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 IProductRepository repository; public CartController(IProductRepository repo) { this.repository = repo; } //添加购物车 public RedirectToRouteResult AddToCart(Cart cart, int productId, string returnUrl) { Product product = this.repository.Products.FirstOrDefault(h => h.ProductID == productId); if (product != null) { cart.AddItem(product, 1); } return this.RedirectToAction("Index", new { returnUrl }); } //移除商品 public RedirectToRouteResult RemoveFromCart(Cart cart, int productId, string returnUrl) { Product product = this.repository.Products.FirstOrDefault(h => h.ProductID == productId); if (product != null) { cart.RemoveLine(product); } return this.RedirectToAction("Index", new { cart = cart, returnUrl = returnUrl }); } private Cart GetCart() { Cart cart = (Cart)Session["Cart"]; if (cart == null) { cart = new Cart(); this.Session["Cart"] = cart; } return cart; } public ViewResult Index(Cart cart, string returnUrl) { return this.View(new CartIndexViewModel { Cart = cart, ReturnUrl = returnUrl }); } //简易的购物车 public ViewResult Summary(Cart cart) { return this.View(cart); } } }
上面代码红色加粗的部分就是简易购物车的执行方法(Action),这个方法相当简单就是获取当前购物车的对象呈现出来渲染页面,我们需要简易的局部视图来搞定他。在Symmary这个Action上右键,选择添加视图,如下图5所示.
图5.这里还是选择的强类型视图。创建好Summary局部视图后,修改他的代码如下所示:
@model SportsStore.Domain.Entities.Cart @{ this.Layout = null; } <div id="cart"> <span class="caption"> <b>Your cart:</b> @Model.Lines.Sum(h=>h.Quantity) item(s), @Model.ComputeTotalValue().ToString("c") </span> @Html.ActionLink("Checkout", "Index", "Cart", new { returnUrl = this.Request.Url.PathAndQuery }, null) </div>
OK,搞完这个顶端的简易购物车,我们一定定义了返回这个局部视图的方法,而且我们需要在每个页面都呈现它,所以就要在我们的模版(_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">SPOPTS STORE</div> </div> <div id="categories">@{Html.RenderAction("Menu", "Nav");}</div> <div id="content"> @RenderBody() </div> </body> </html>
注:上面红色加粗部分就是给我们的简易购物车争取到的一块呈现的地方,注意这里Html.RenderAction();后面的分号不能去掉,如忘记写分号会编译出错。那现在运行一下我们的程序看看我们建议的购物车有没有达到预期的目的,运行如下图6所示。
图6.说明我们的也已经实现了顶部的简易购物车。购物里的东西需要提交支付才会生成订单,所以接下来也就是我们这个简易项目的最后一个阶段订单部分了,今天关于购物车就大致分享到这里,后续继续完善我们的项目。文章写的仓促,要是那里有描述不清楚或者写错的地方,还请路过的前辈们朋友多多批评指导,大家共同学习。