【MVC 4】7.SportsSore:完成购物车

时间:2023-03-09 00:15:06
【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, );
}
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 框架便不能调用此方法了。

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

【MVC 4】7.SportsSore:完成购物车

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

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

【MVC 4】7.SportsSore:完成购物车

利用这个附件,现在可以让客户知道自己的购物车中有什么。这个附件也显示的提供了一个结算离店的办法。从中再一次看到用 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 动作方法。显示效果如下:

【MVC 4】7.SportsSore:完成购物车

现在需要在 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 : @Html.EditorFor(x => x.Line1)</div>
<div>Line : @Html.EditorFor(x => x.Line2)</div>
<div>Line : @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>
}

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

【MVC 4】7.SportsSore:完成购物车

本例用 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 = ;
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, );
}
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() == )
{
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>
...

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

【MVC 4】7.SportsSore:完成购物车

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”按钮时,便会看到一个致谢页面,如下图所示:

【MVC 4】7.SportsSore:完成购物车

【MVC 4】7.SportsSore:完成购物车