你的位置:首页 > ASP.net教程

[ASP.net教程]【MVC 4】7.SportsSore:完成购物车

作者:[美]Adam Freeman      来源:《精通ASP.NET MVC 4》

本文将继续构建 SportsStore 示例应用程序。在上一章中,添加了对购物车的基本支持,现在打算改善并完成其功能。

1.使用模型绑定

 MVC 框架使用了一个叫作“模型绑定”的系统,以便通过 HTTP 请求来创建一些 C# 对象,目的是把它们作为参数值传递给动作方法。例如,MVC 处理表单的方式就是这样。框架会考察目标动作方法的参数,并用一个模型绑定器来获取表单中 input 元素的值,并把它们转换成同名的参数类型。

模型绑定器能够通过请求中可用的信息来创建 C# 类型,这是 MVC 框架的核心特性之一。本节将创建一个自定义模型绑定器来盖上 CartController 类。

人们喜欢使用 Cart 控制器中的会话状态特性来存储和管理 Cart 对象,但却不喜欢它要采取的工作方式。它不符合本应用程序模型的其余部分,而那是基于动作方法参数的(因为动作方法参数的操作以模型为基础,而会话状态的操作需要设置键值对,两者的工作方式不一致)。另外。除非模仿基类的 Session 参数,否则不能适当的对 CartController 类进行单元测试,而这意味着需要模仿 Controller 类(控制器的基类),以及其他一些不希望处理的东西。

为了解决这一问题,我们打算创建一个自定义模型绑定器,以获得包含在会话数据中的 Cart 对象(注意,常规的模型绑定器能够直接处理请求中的数据来创建模型对象,这里创建自定义绑定器的目的是为了处理会话中的数据,手工用会话数据创建 Cart 对象)。然后,MVC 框架能够创建 Cart 对象,并把它们作为参数传递给 Controller 类的动作方法。

创建自定义模型绑定器

通过实现 IModelBinder 接口,可以创建一个自定义模型绑定器。在 SportsStore.WebUI 项目中新建文件夹“Binders”,并新建类文件 CartModelBinder.cs ,代码如下:

 

using SportsStore.Domain.Entities;using System.Web.Mvc;namespace SportsStore.WebUI.Binders{  public class CartModelBinder : IModelBinder  {    private const string sessionKey = "Cart";    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)    {      //通过会话获取 Cart      Cart cart = (Cart)controllerContext.HttpContext.Session[sessionKey];      //若会话中没有 Cart ,则创建一个      if (cart == null)      {        cart = new Cart();        controllerContext.HttpContext.Session[sessionKey] = cart;      }      //返回 cart      return cart;    }  }}

这些 IModelBinder 接口定义了一个方法: BindModel 。所提供的两个参数使得创建域模型对象成为可能。 ControllerContext 对控制器类所具有的全部信息提供了访问,这些信息包含了客户端请求的细节。 ModelBindingContext 提供了要求建立的模型对象的信息,以及使绑定更易于处理的工具。

对于本例的目的而言,所关心的是 ControllerContext 类,它具有 HttpContext 属性,它又相应地有一个 Session 属性,该属性能够获取和设置会话数据。通过读取会话数据的键值可以获取 Cart,而在会话中还没有 Cart 时,又可以创建一个 Cart 。

需要告诉 MVC 框架,它可以使用 CartModelBinder 类来创建 Cart 的实例。这需要在 Global.asax 的 Application_Start 方法中进行注册:

using SportsStore.Domain.Entities;using SportsStore.WebUI.Binders;using SportsStore.WebUI.Infrastructure;using System.Web.Http;using System.Web.Mvc;using System.Web.Optimization;using System.Web.Routing;namespace SportsStore.WebUI{  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);      ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory());       ModelBinders.Binders.Add(typeof(Cart), new CartModelBinder());    }  }}

现在可以更新 CartController 类,删除 GetCart 方法而依靠现在的模型绑定器,MVC 框架会自动地应用它,代码如下:

using SportsStore.Domain.Abstract;using SportsStore.Domain.Entities;using SportsStore.WebUI.Models;using System;using System.Collections.Generic;using System.Linq;using System.Web;using System.Web.Mvc;namespace SportsStore.WebUI.Controllers{  public class CartController : Controller  {    private IProductRepository repository;    public CartController(IProductRepository 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      });    }  }}

上面代码删除了 GetCart 方法,并对每个动作方法添加了 Cart 参数。当 MVC 框架接收到一个请求,比如,要求调用 AddToCart 方法时,会首先考察动作方法的参数,然后考察可用的绑定器列表,并试图找到一个能够创建每个参数类型实例的绑定器。这会要求自定义绑定器创建一个 Cart 对象,而这时通过利用会话状态特性来完成的。通过自定义绑定器的默认绑定器,MVC 框架能够创建一组调用动作方法所需要的参数,这让开发者能够重构控制器,以便在接收到请求时知道如何创建 Cart 对象。

像这样使用自定义模型绑定器有几个好处。第一个好处是把用来创建 Cart 与创建控制器的逻辑分离开来了,这让开发者能够修改存储 Cart 对象,而不需要修改控制器。第二个好处是任何使用 Cart 对象的控制器类,都能够简单地把这些对象声明为动作方法参数,并能够利用自定义模型绑定器。第三个好处是它能够对 Cart 控制器进行单元测试,而不需要模仿大量的ASP.NET 通道。

 

2.完成购物车

前面已经介绍了自定义模型绑定器,现在到了添加两个新特性来完成购物车功能的时候了。第一个特性将允许客户删除购物车物品,第二个特性将在页面的顶部显示购物车的摘要。

 

2.1 删除购物车物品

前面已经定义了控制器中的 RemoveFromCart 动作方法,因此,让客户删除物品只不过是在视图中将这个方法暴露出来的事情。本文打算在购物车摘要的每一行中添加一个“Remove”按钮来做这件事。对 Views/Cart/Index.cshtml 所做的修改如下:

  <tbody>    @foreach (var line in Model.Cart.Lines)    {      <tr>        <td class="aling_center">@line.Quantity</td>        <td class="aling_left">@line.Product.Name</td>        <td class="aling_right">@line.Product.Price.ToString("c")</td>        <td class="aling_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>

注:可以使用强类型的 Html.HiddenFor 辅助器方法,为 ReturnUrl 模型属性创建一个隐藏字段,但这需要使用基于字符串的 Html.Hidden 辅助器方法,对 ProductID 字段做同样的事情。 如果写成“Html.HiddenFor(x=>line.Product.ProductID)”,该辅助器方法便会渲染一个以“line.Product.ProductID”为名称的隐藏字段。该字段名与 CartController.RemoveFromCart 动作方法的参数名不匹配,这会使默认的模型绑定器无法工作,因此 MVC 框架便不能调用此方法了。

运行应用程序,可以看到效果:

 

2.2 添加购物车摘要

现在已经实现了一个功能化的购物车,但将该购物车集成到解密的方式还存在一个问题:只有通过查看购物车摘要屏幕,客户才能知道他们的购物车里有些什么。而且,他们只能通过把一个新的物品加入购物车,才能看到购物车的摘要屏幕。

为了解决这一问题,本节打算添加一个小部件,它汇总购物车的内容,并能够通过点击来显示购物车内容。下面将采用与添加导航不见十分相似的方式来完成这一工作——作为一个动作,把它的输出注入到 Razor 布局。

首先,需要对 CartController 类添加一个简单的方法,代码如下:

    public PartialViewResult Summary(Cart cart)    {      return PartialView(cart);    }

可以看出,这是一个简单的方法。它只需要渲染一个视图,以当前 Cart (它是自定义模型绑定器获得的)作为视图数据。还需要一个分部视图,它在对这个 Summary 方法调用做出响应时被渲染。添加对应的 Summary 视图文件,代码如下:

@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>

这是一个简单的视图,它显示了购物车的物品数、这些物品的总费用,以及把购物车内容显示给用户的一个链接。现在,已经定义了有 Summary 动作方法所返回的这个视图,可以在 _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>

最后一步是添加一些 CSS规则,对该分部视图中的元素进行格式化。对 SportsStore.WebUI 项目中的 Site.css 文件添加样式文件如下:

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

运行程序即可以看到效果,对购物车添加物品时,物品数以及总费用都会增加,如下图所示:

利用这个附件,现在可以让客户知道自己的购物车中有什么。这个附件也显示的提供了一个结算离店的办法。从中再一次看到用 RenderAction 把一个动作方法所渲染的输出组合到一个 Web 页面是多么容易。这是将应用程序功能分解成清晰可重用模块的一种很好的技术。

 

3.递交订单

现在到了显示 SportsStore 最后一个客户特性的时候了:结算并完成订单的能力。下面将扩充域模型,以提供收集用户送货细节的支持,并添加一个处理这些细节的特性。

 

3.1 扩充域模型

在 SportsStore.Domain 项目的 Entities 文件夹中新建类 ShippingDetails 。这是用来表示客户送货细节的类,具体代码如下:

 

using System.ComponentModel.DataAnnotations;namespace SportsStore.Domain.Entities{  public class ShippingDetails  {    [Required(ErrorMessage = "Please enter a name")]    public string Name { get; set; }    [Required(ErrorMessage = "Please enter the first address line")]    public string Line1 { get; set; }    public string Line2 { get; set; }    public string Line3 { get; set; }    [Required(ErrorMessage = "Please enter a city name")]    public string City { get; set; }    [Required(ErrorMessage = "Please enter a state name")]    public string State { get; set; }    public string Zip { get; set; }    [Required(ErrorMessage = "Please enter a country name")]    public string Country { get; set; }    public bool GiftWrap { get; set; }  }}

上述代码利用了 System.ComponentModel.DataAnnotations 命名空间的验证注解属性,正如之前文章 【MVC 4】1.第一个 MVC 应用程序 所做的那样。

 

3.2 添加结算过程

本例的目的是到达一个程序节点,以此作为用户输入其送货细节并递交订单的入口。为此,需要在购物车摘要视图上添加一个“Checkout now”按钮。修改 Views/Cart/Index.cshtml 文件,修改代码如下:

...<p class="aling_center actionButtons">  <a href="@Model.ReturnUrl">Continue shopping</a>  @Html.ActionLink("Checkout now", "Checkout")</p>...

黄色为修改部分,这一修改生成了一个链接,点击这个链接时,调用 Cart 控制器的 Checkout 动作方法。显示效果如下:

 

现在需要在 CartController 类中定义这个 Checkout 方法:

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

此 Checkout 方法返回默认视图,并传递一个新的 ShippingDetails 对象作为视图模型。为了创建相应的视图文件 Views/Cart/Checkout.cshtml ,视图内容如下:

@model SportsStore.Domain.Entities.ShippingDetails@{  ViewBag.Title = "SportsStore: Checkout";}<h2>Check out now</h2>Please enter your details, and we'll ship your goods right away!@using (Html.BeginForm()){   <h3>Ship to</h3>  <div>Name:@Html.EditorFor(x => x.Name)</div>    <h3>Address</h3>  <div>Line 1: @Html.EditorFor(x => x.Line1)</div>  <div>Line 2: @Html.EditorFor(x => x.Line2)</div>  <div>Line 3: @Html.EditorFor(x => x.Line3)</div>  <div>City: @Html.EditorFor(x => x.City)</div>  <div>State: @Html.EditorFor(x => x.State)</div>  <div>Zip: @Html.EditorFor(x => x.Zip)</div>  <div>Country: @Html.EditorFor(x => x.Country)</div>    <h3>Options</h3>  <label>    @Html.EditorFor(x => x.GiftWrap)    Gift wrap these items  </label>    <p class="align_center">    <input class="actionButtons" type="submit" value="Complete order" />  </p>}

运行程序,可以看到该视图是如何渲染的,效果如下图所示,该视图为收集客户的送货细节渲染了一个表单。

本例用 Html.EditorFor 辅助器方法为每个表单字段渲染了一个 input 元素。该方法是模板化辅助器方法的一个例子(注意,对模型对象的每个属性使用的都是 Html.EditorFor 辅助器方法,并未针对各个属性的类型,去选定特定的辅助器方法)。它让 MVC 框架去决定一个视图模型属性需要采用哪种 input 元素,而不是进行明确的指定(例如,使用 Html.TextBoxFor)。

从显示效果可以看出, MVC 框架为布尔属性渲染了一个复选框,如“Gift wrap these items”选择,而对那些字符串属性渲染了文本框。

 

3.3 实现订单处理器

 在这个应用程序中还需要一个组件,以便能够对订单的细节进行处理。为了与 MVC 模型原理保持一致,本例打算为此功能定义一个借口、编写该接口的一个实现,然后用 DI 容器 Ninject 把两者关联起来。

 

定义接口

在 SportsStore.Domain 项目的 Abstract 文件夹中新建接口 IOrderProcessor ,内容如下:

using SportsStore.Domain.Entities;namespace SportsStore.Domain.Abstract{  public interface IOrderProcessor  {    void ProcessOrder(Cart cart, ShippingDetails shippingDetails);  }}

 

实现接口

IOrderProcessor 的实现打算采用的订单处理方式是向网站管理员发送订单右击。当然,这简化了销售过程。大多数电子商务网站不会简单的发送订单邮件,而且也没有提供信用卡处理或其他支付形式的支持,只是希望把事情维持在关注 MVC 方面,因此采用了这种发送邮件作为订单处理的方式。

在 SportsStore.Domain 项目的 Concrete 文件夹中新建类 EmailOrderProcessor,这个类使用了包含在 .NET 框架中内建的 SMTP(简单邮件传输协议)支持,以发送一份电子邮件。具体代码如下:

 

using SportsStore.Domain.Abstract;using SportsStore.Domain.Entities;using System.Net;using System.Net.Mail;using System.Text;namespace SportsStore.Domain.Concrete{  public class EmailSettings  {    public string MailToAddress = "orders@example.com";    public string MailFromAddress = "sportsstore@example.com";    public bool UserSsl = true;    public string Username = "MySmtpUsername";    public string Password = "MySmtpPassword";    public string ServerName = "smtp.example.com";    public int ServerPort = 587;    public bool WriteAsFile = false;    public string FileLocation = @"c:\sports_store_emails";  }  public class EmailOrderProcessor : IOrderProcessor  {    private EmailSettings emailSettings;    public EmailOrderProcessor(EmailSettings settings)    {      emailSettings = settings;    }    public void ProcessOrder(Cart cart, ShippingDetails shippingInfo)    {      using (var smtpClient = new SmtpClient())      {        smtpClient.EnableSsl = emailSettings.UserSsl;        smtpClient.Host = emailSettings.ServerName;        smtpClient.Port = emailSettings.ServerPort;        smtpClient.UseDefaultCredentials = false;        smtpClient.Credentials = new NetworkCredential(emailSettings.Username, emailSettings.Password);        if (emailSettings.WriteAsFile)        {          smtpClient.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory;          smtpClient.PickupDirectoryLocation = emailSettings.FileLocation;          smtpClient.EnableSsl = false;        }        StringBuilder body = new StringBuilder()          .AppendLine("A new order has been submitted")          .AppendLine("---")          .AppendLine("Items:");        foreach (var line in cart.Lines)        {          var subTotal = line.Product.Price * line.Quantity;          body.AppendFormat("{0} x {1} (subtotal: {2:c})", line.Quantity, line.Product.Name, subTotal);        }        body.AppendFormat("Total order value: {0:c}", cart.ComputeTotalValue())          .AppendLine("---")          .AppendLine("Ship to:")          .AppendLine(shippingInfo.Name)          .AppendLine(shippingInfo.Line1)          .AppendLine(shippingInfo.Line2 ?? "")          .AppendLine(shippingInfo.Line3 ?? "")          .AppendLine(shippingInfo.City)          .AppendLine(shippingInfo.State ?? "")          .AppendLine(shippingInfo.Country)          .AppendLine(shippingInfo.Zip)          .AppendLine("---")          .AppendFormat("Gift wrap: {0}", shippingInfo.GiftWrap ? "Yes" : "No");        MailMessage mailmessage = new MailMessage(          emailSettings.MailFromAddress,//Form          emailSettings.MailToAddress,//To          "New order submitted!",//Subject          body.ToString());//Body        if (emailSettings.WriteAsFile)        {          mailmessage.BodyEncoding = Encoding.ASCII;        }        smtpClient.Send(mailmessage);      }    }  }}

为了使事情更简单些,也定义了 EmailSettings 类。 EmailOrderProcessor 的构造器需要这个类(EmailSettings 类)的一个实例,该实例包含了配置 .NET 邮件类所需要的全部设置信息。

提示:如果没有可用的 SMTP 服务器也没关系,可以将 EmailSettings.WriteAsFile属性设置为true,这样会把邮件消息作为文件写到由 FileLocation 属性指定的目录。该目录必须依据存在且是可写入的。邮件文件的扩展名将为 .eml ,但它们可以被任何文本编辑器所读取。

 

3.4 注册(接口)实现

现在,有了 IOrderProcessor 接口的一个实现以及配置它的手段,便可以用 Ninject 来创建它的实例。编辑 SportsStore.WebUI 项目中的 NinjectController 类(在 Infrastructure 文件夹中),对 AddBindings 方法进行修改:

using Moq;using Ninject;using SportsStore.Domain.Abstract;using SportsStore.Domain.Entities;using System;using System.Linq;using System.Collections.Generic;using System.Web.Mvc;using System.Web.Routing;using SportsStore.Domain.Concrete;using System.Configuration;namespace SportsStore.WebUI.Infrastructure{  public class NinjectControllerFactory : DefaultControllerFactory  {    private IKernel ninjectKernel;    public NinjectControllerFactory()    {      ninjectKernel = new StandardKernel();      AddBindings();    }    protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)    {      return controllerType == null        ? null        : (IController)ninjectKernel.Get(controllerType);    }    private void AddBindings()    {      //put bindings here      ninjectKernel.Bind<IProductRepository>().To<EFProductRepository>();      EmailSettings emailSettings = new EmailSettings      {        WriteAsFile = bool.Parse(ConfigurationManager.AppSettings["Email.WriteAsFile"] ?? "false")      };      ninjectKernel.Bind<IOrderProcessor>().To<EmailOrderProcessor>().WithConstructorArgument("settings", emailSettings);    }  }}

上述代码创建了一个 EmailSettings 对象,将其用于 Ninject 的 WithConstructorArgument 方法,以便在需要创建一个新实例对 IOrderProcessor 接口的请求进行服务时,把它注入到 EmailOrderProcessor 构造器中。上述代码只为 EmailSettings 中一个属性 WriteAsFiles 指定了值。使用 ConfigurationManager.AppSettings 属性来读取该属性的值,这让用户能够访问已经放在 Web.config 文件中的应用程序,在应用程序配置文件 Web.config 中读取这个 WriteAsFile 属性的设置值,这也是从应用程序配置文件 Web.config 中读取某个属性设置值的方法。配置文件修改如下:

 <appSettings>  <add key="webpages:Version" value="2.0.0.0" />  <add key="webpages:Enabled" value="false" />  <add key="PreserveLoginUrl" value="true" />  <add key="ClientValidationEnabled" value="true" />  <add key="UnobtrusiveJavaScriptEnabled" value="true" />   <add key="Email.WriteAsFile" value="true"/> </appSettings>

 

3.5 完成购物车控制器

为了完成 CartController 类,需要修改构造器,以使它要求 IOrderProcessor 接口的一个实现,并添加一个新的动作方法,它将在客户点击“Complete order”按钮时,处理 HTTP 表单的 POST 请求。

using SportsStore.Domain.Abstract;using SportsStore.Domain.Entities;using SportsStore.WebUI.Models;using System;using System.Collections.Generic;using System.Linq;using System.Web;using System.Web.Mvc;namespace SportsStore.WebUI.Controllers{  public class CartController : Controller  {    private IProductRepository repository;    private IOrderProcessor orderProcessor;    public CartController(IProductRepository repo, IOrderProcessor proc)    {      repository = repo;       orderProcessor = proc;    }    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 PartialViewResult Summary(Cart cart)    {      return PartialView(cart);    }    [HttpPost]    public ViewResult Checkout(Cart cart, ShippingDetails shippingDetails)    {      if (cart.Lines.Count() == 0)      {        ModelState.AddModelError("", "Sorry,your cart is empty!");      }      if (ModelState.IsValid)      {        orderProcessor.ProcessOrder(cart, shippingDetails);        cart.Clear();        return View("Completed");      }      else      {        return View(shippingDetails);      }    }    public ViewResult Checkout()    {      return View(new ShippingDetails());    }  }}

 

可以看出,这里所添加的 Checkout 动作方法是用 HttpPost 注解属性来修饰的,这表明该方法将用于对 POST 请求的处理 —— 在此例中,这是用户递交表单的时候。再次重申,ShippingDetails 参数(这是使用 HTTP 表单数据自动创建的)和 Cart 参数(这是用自定义绑定器创建的)都要依赖于模型绑定器系统。

注:构造器中的修改迫使用户需要对 CartController 类创建的单元测试进行更新。为新的构造器参数传递 null ,便会使单元测试能够通过编译。

在上述代码中, MVC 框架会检查验证约束,这些约束是用数据注解属性而运用于 ShippingDetails 的,并通过 ModelState 属性把非法情况传递给该动作方法。因此,可以通过检查 Model.IsValid 属性来查看是否存在问题。注意,如果购物车中无物品,还调用 ModelState.AddModelError 方法注册了一条错误消息。

 

3.6 显示验证错误

如果客户输入了非法的错误信息,有问题的那些非法表单字段将被高亮,但没有消息被显示出来。更糟的是,如果客户试图对一个空购物车进行结算,这不会完成这个订单,但客户却根本看不到任何错误信息。为了解决这个问题,需要对视图添加一个验证摘要。下面代码显示了添加到 Checkout.cshtml 视图的内容。

...
<h2>Check out now</h2>Please enter your details, and we'll ship your goods right away!@using (Html.BeginForm()){ @Html.ValidationSummary();
<h3>Ship to</h3> <div>Name:@Html.EditorFor(x => x.Name)</div>
...

现在,当客户提供非法送货数据或试图对空购物车进行结算时,系统会向他们显示一些有用的错误消息,如下图所示:

 

3.7 显示致谢界面

为了完成结算过程,需要向客户显示一个已经完成订单处理的确认页面,并感谢他们的购物。新建视图文件 Views/Cart/Completed.cshtml ,代码如下:

@{  ViewBag.Title = "SportsStore: OrderSubmitted";}<h2>Thanks!</h2>Thanks for placing your order. We'll ship your goods as soon as possible.

现在,客户可以进行从选择产品到结算离开的整个购物过程。如果客户提供有效的的送货细节(且购物车中有物品),当他们点击“Complete order”按钮时,便会看到一个致谢页面,如下图所示: