ASP.NET MVC5(七)SportsStore购物车

作者 Zhendong Ho 日期 2021-04-24
ASP.NET MVC5(七)SportsStore购物车

使用模型绑定

MVC框架使用了模型绑定系统,通过HTTP请求来创建C#对象,将他们作为参数值传递给动作方法

例如,MVC会考察目标动作方法的参数,用一个模型绑定器来获取由浏览器发送过来的表单值,并在传递给动作方法之前将它们转换成同名参数的类型。

模型绑定器能够通过请求中可用的各种信息来创建C#类,这是MVC框架的核心特性之一。

创建自定义模型绑定器

通过实现System.Web.Mvc.IModelBinder接口,可以创建一个自定义模型绑定器。

在SportsStore.WebUI项目的Infrastructure文件夹下,新建Binders文件夹,在其中创建CartModelBinder.cs文件。

public class CartModelBinder : IModelBinder
{
private const string sessionKey = "Cart";

public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
// 通过会话获取Cart
Cart cart = null;
if (controllerContext.HttpContext.Session != null)
{
cart = (Cart)controllerContext.HttpContext.Session[sessionKey];
}

// 若会话中没有Cart,创建一个
if (cart == null)
{
cart = new Cart();
if (controllerContext.HttpContext.Session != null)
{
controllerContext.HttpContext.Session[sessionKey] = cart;
}
}

// 返回cart
return cart;
}
}

注意

  • IModelBinder接口定义了一个方法:BindModel,为其提供的两个参数能够用来创建域模型对象
  • ControllerContext能够访问控制器类的全部信息,包括客户端请求的细节。
  • ModelBindingContext能够提供的信息包括:要求你建立的模型对象以及使绑定过程更易于处理的工具。

修改Global.asaxApplication_Start方法,告诉MVC框架,使用CartModelBinder类创建Cart对象。

protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);

ModelBinders.Binders.Add(typeof(Cart), new CartModelBinder()); // 使用CartModelBinder类创建Cart对象
}

更新CartController类,删除GetCart方法,并依靠模型绑定器为控制器提供Cart对象。

public class CartController : Controller
{
private IProductRepository repository;

public CartController(IProductRepository repo)
{
repository = repo;
}

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

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.Remove(product);
}
return RedirectToAction("Index", new { returnUrl });
}
}

注意:CartController删除了GetCart方法,并对每个动作方法添加了一个Cart参数。当MVC框架接收到一个请求,比如调用AddToCart方法,会先考察动作方法的参数,然后考察可用的绑定器列表,尝试找到一个能够为各个参数类型创建实例的绑定器。

使用自定义模型绑定器的好处

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

单元测试:购物车控制器

CartTests单元测试,添加一些方法,测试该控制器的3个不同方面

  • AddToCart方法应该将所选的产品添加到客户的购物车
  • 将一个产品添加到购物车之后,应该将用户重定向到Index视图。
  • 用户随后可以返回到产品分类页面,URL应该被正确地传递给Index动作方法。
[TestClass]
public class CartTests
{
// ...其他测试方法

/// <summary>
/// 测试添加到购物车
/// </summary>
[TestMethod]
public void Can_Add_To_Cart()
{
// 准备——创建模仿存储库
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[] {
new Product { ProductID = 1, Name = "P1", Category = "Apples" },
}.AsQueryable());

// 准备——创建Cart
Cart cart = new Cart();

// 准备——创建控制器
CartController target = new CartController(mock.Object);

// 动作——对cart添加一个产品
target.AddToCart(cart, 1, null);

// 断言
Assert.AreEqual(cart.Lines.Count(), 1);
Assert.AreEqual(cart.Lines.ToArray()[0].Product.ProductID, 1);
}

/// <summary>
/// 测试添加购物车后重定向到Index视图
/// </summary>
[TestMethod]
public void Adding_Product_To_Cart_Goes_To_Cart_Screen()
{
// 准备——创建模仿存储库
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[] {
new Product { ProductID = 1, Name = "P1", Category = "Apples" },
}.AsQueryable());

// 准备——创建Cart
Cart cart = new Cart();

// 准备——创建控制器
CartController target = new CartController(mock.Object);

// 动作——向cart添加一个产品
RedirectToRouteResult result = target.AddToCart(cart, 1, "myUrl");

// 断言
Assert.AreEqual(result.RouteValues["action"], "Index");
Assert.AreEqual(result.RouteValues["returnUrl"], "myUrl");
}

/// <summary>
/// 测试returnUrl是否正确传递到Index动作方法
/// </summary>
[TestMethod]
public void Can_View_Cart_Contents()
{
// 准备——创建Cart
Cart cart = new Cart();

// 准备——创建控制器
CartController target = new CartController(null);

// 动作——调用Index动作方法
CartIndexViewModel result = (CartIndexViewModel)target.Index(cart, "myUrl").ViewData.Model;

// 断言
Assert.AreSame(result.Cart, cart);
Assert.AreEqual(result.ReturnUrl, "myUrl");
}
}

完成购物车功能

接下来添加两个新特性来完成购物车的功能。

  1. 允许客户删除购物车的商品
  2. 在页面的顶部显示购物车的摘要

删除购物车物品

修改Views/Cart/Index.cshtml文件,在购物车摘要的每一行中添加Remove(删除)按钮

@model SportsStore.WebUI.Models.CartIndexViewModel

@{
ViewBag.Title = "Sports Store: Your Cart";
}

<style>
#cartTable td {
vertical-align: middle;
}
</style>

<h2>Your cart</h2>
<table id="cartTable" class="table">
<thead>
<tr>
<th>Quantity</th>
<th>Item</th>
<th class="text-right">Price</th>
<th class="text-right">Subtotal</th>
</tr>
</thead>
<tbody>
@foreach (var line in Model.Cart.Lines)
{
<tr>
<td class="text-center">@line.Quantity</td>
<td class="text-left">@line.Product.Name</td>
<td class="text-right">@line.Product.Price.ToString("c")</td>
<td class="text-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="btn btn-sm btn-warning" type="submit" value="Remove" />
}
</td>
</tr>
}
</tbody>
<tfoot>
<tr>
<td colspan="3" class="text-right">Total:</td>
<td class="text-right">@Model.Cart.ComputeTotalValue().ToString("c")</td>
</tr>
</tfoot>
</table>

<div class="text-center">
<a class="btn btn-primary" href="@Model.ReturnUrl">Continue shopping</a>
</div>

注意

  • 使用强类型的Html.HiddenFor辅助器方法,为ReutrnUrl模型属性创建一个隐藏字段。
  • 必须对ProductID字段使用Html.Hidden辅助器方法,因为该字段名与RemoveFromCart动作方法的参数名不匹配,这会使默认的模型绑定器无法工作。

添加购物车摘要

添加一个小部件,它汇总了购物车的内容,并在整个应用程序中都能通过单击来显示购物车的内容。

CartController中添加Summary动作方法

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

添加Summary视图,以Cart对象作为视图数据。

@model SportsStore.Domain.Entities.Cart

<div class="navbar-right">
@Html.ActionLink("checkout", "Index", "Cart",
new { returnUrl = Request.Url.PathAndQuery },
new { @class = "btn btn-default navbar-btn" })
</div>

<div class="navbar-text navbar-right">
<b>Your cart:</b>
@Model.Lines.Sum(x => x.Quantity) item(s),
@Model.ComputeTotalValue().ToString("c")
</div>

该视图显示了购物车中的物品数量物品的总价,以及显示购物车详情的链接

修改_Layout.cshtml文件,添加使用Summary分部视图

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="~/Content/bootstrap.css" rel="stylesheet" />
<link href="~/Content/bootstrap-theme.css" rel="stylesheet" />
<title>@ViewBag.Title</title>
</head>
<body>
<div class="navbar navbar-inverse" role="navigation">
<a class="navbar-brand" href="#">SPORTS STORE</a>
@Html.Action("Summary", "Cart")
</div>
<div class="row panel">
<div id="categories" class="col-xs-3">
@Html.Action("Menu", "Nav")
</div>
<div class="col-xs-8">
@RenderBody()
</div>
</div>
</body>
</html>

注意:使用Html.Action辅助器方法,可以将一个动作方法输出组合到其他视图。这是将应用程序功能分解成清晰可重用模块的一种很好的技术。

递交订单

扩充域模型

SportsStore.Domain项目Entities文件夹中,添加ShippingDetails.cs文件,该类用于表示客户送货细节

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命名空间验证注解属性
  • ShippingDetails类没有任何功能,因此不需要单元测试。

添加结算过程

修改Views\Cart\Index.cshtml文件,在购物车摘要视图添加“Checkout now(立即结算)”按钮

...
<div class="text-center">
<a class="btn btn-primary" href="@Model.ReturnUrl">Continue shopping</a>
@Html.ActionLink("Checkout now", "Checkout", null, new { @class = "btn btn-primary" })
</div>
...

接下来在CartController中定义Checkout动作方法

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

Checkout动作方法,返回默认视图,并传递一个新的ShippingDetails对象作为视图模型。

创建这个Checkout视图

@model SportsStore.Domain.Entities.ShippingDetails

@{
ViewBag.Title = "SportStore: Checkout";
}

<h2>Check out now</h2>
<p>Please enter your details, and we'll ship your goods right away!</p>
@using (Html.BeginForm())
{
<h3>Ship to</h3>
<div class="form-group">
<label>Name:</label>
@Html.TextBoxFor(x => x.Name, new { @class = "form-control" })
</div>

<h3>Address</h3>
<div class="form-group">
<label>Line 1:</label>
@Html.TextBoxFor(x => x.Line1, new { @class = "form-control" })
</div>
<div class="form-group">
<label>Line 2:</label>
@Html.TextBoxFor(x => x.Line2, new { @class = "form-control" })
</div>
<div class="form-group">
<label>Line 3:</label>
@Html.TextBoxFor(x => x.Line3, new { @class = "form-control" })
</div>

<div class="form-group">
<label>City:</label>
@Html.TextBoxFor(x => x.City, new { @class = "form-control" })
</div>
<div class="form-group">
<label>State:</label>
@Html.TextBoxFor(x => x.State, new { @class = "form-control" })
</div>
<div class="form-group">
<label>Zip:</label>
@Html.TextBoxFor(x => x.Zip, new { @class = "form-control" })
</div>
<div class="form-group">
<label>Country:</label>
@Html.TextBoxFor(x => x.Country, new { @class = "form-control" })
</div>

<h3>Options</h3>
<div class="checkbox">
<label>
@Html.EditorFor(x => x.GiftWrap)
Gift wrap these items
</label>
</div>

<div class="text-center">
<input class="btn btn-primary" type="submit" value="Complete order" />
</div>
}

运行应用程序,点击页面顶部的Checkout按钮,再点击Checkout now按钮,即可看到该视图。

为了减少Checkout视图中的重复标记。可以获取视图模型对象的元数据,将它与C#和Razor的混合表达式相结合。

修改Checkout.cshtml文件如下。

@model SportsStore.Domain.Entities.ShippingDetails

@{
ViewBag.Title = "SportStore: Checkout";
}

<h2>Check out now</h2>
<p>Please enter your details, and we'll ship your goods right away!</p>
@using (Html.BeginForm())
{
<h3>Ship to</h3>
<div class="form-group">
<label>Name:</label>
@Html.TextBoxFor(x => x.Name, new { @class = "form-control" })
</div>

<h3>Address</h3>
foreach (var property in ViewData.ModelMetadata.Properties)
{
if (property.PropertyName != "Name" && property.PropertyName != "GiftWrap")
{
<div class="form-group">
<label>@(property.DisplayName ?? property.PropertyName)</label>
@Html.TextBox(property.PropertyName, null, new { @class = "form-control" })
</div>
}
}

<h3>Options</h3>
<div class="checkbox">
<label>
@Html.EditorFor(x => x.GiftWrap)
Gift wrap these items
</label>
</div>

<div class="text-center">
<input class="btn btn-primary" type="submit" value="Complete order" />
</div>
}

注意

  • 静态的ViewData.ModelMetadata属性返回的是一个System.Web.Mvc.ModelMetaData对象,该对象提供了视图的模型类型信息
  • 代码中的foreachif关键字在Razor表达式的范围之内,因此不需要加@前缀
  • ??空合并运算符。如果DisplayName为空,则返回PropertyName,否则返回DisplayName。

修改ShippingDetails.cs文件,在模型类上运用Display注解属性

//...
[Display(Name = "Line 1")]
public string Line1 { get; set; }
[Display(Name = "Line 2")]
public string Line2 { get; set; }
[Display(Name = "Line 3")]
public string Line3 { get; set; }
//...

Display注解属性设置Name值后,可以在视图中通过DisplayName属性读取所设置的值。

实现订单处理器

定义接口

SportsStore.Domain项目Abstract文件夹中,添加IOrderProcessor接口

public interface IOrderProcessor
{
void ProcessOrder(Cart cart, ShippingDetails shippingDetails);
}

实现接口

IOrderProcessor的实现,打算采用的订单处理方式是向网站管理员发送订单邮件

SportsStore.Domain项目Concrete文件夹中,新建EmailOrderProcessor.cs文件,并使用.NET框架下的SMTP发送电子邮件。

/// <summary>
/// 配置.NET邮件类
/// </summary>
public class EmailSettings
{
public string MailToAddress = "orders@example.com";
public string MailFromAddress = "sportsstore@example.com";
public bool UseSsl = true;
public string Username = "MySmtpUsername";
public string Password = "MySmtpPassword";
public string ServerName = "smtp.example.com";
public int ServerPort = 587;
public bool WriteAsFile = false; // 如果没有SMTP服务器,此项可设置为true,将邮件消息作为文件写到指定目录
public string FileLocation = @"e:\sports_store_emails";
}

public class EmailOrderProcessor : IOrderProcessor
{
private EmailSettings emailSettings;

public EmailOrderProcessor(EmailSettings settings)
{
emailSettings = settings;
}

public void ProcessOrder(Cart cart, ShippingDetails shipppingInfo)
{
using (var smtpClient = new SmtpClient())
{
smtpClient.EnableSsl = emailSettings.UseSsl;
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(shipppingInfo.Name)
.AppendLine(shipppingInfo.Line1)
.AppendLine(shipppingInfo.Line2 ?? "")
.AppendLine(shipppingInfo.Line3 ?? "")
.AppendLine(shipppingInfo.City)
.AppendLine(shipppingInfo.State ?? "")
.AppendLine(shipppingInfo.Country)
.AppendLine(shipppingInfo.Zip)
.AppendLine("---")
.AppendFormat("Gift wrap: {0}", shipppingInfo.GiftWrap ? "yes" : "No");

MailMessage mailMessage = new MailMessage(
emailSettings.MailFromAddress, // From
emailSettings.MailToAddress, // To
"New order submitted!", // Subject
body.ToString()); // Body

if (emailSettings.WriteAsFile)
{
mailMessage.BodyEncoding = Encoding.ASCII;
}

smtpClient.Send(mailMessage);
}
}
}

注册接口实现

编辑NinjectDependencyResolver.cs文件,修改AddBindings方法,配置IOrderProcessor接口的实现。

private void AddBindings()
{
kernel.Bind<IProductRepository>().To<EFProductRepository>();

EmailSettings emailSettings = new EmailSettings
{
WriteAsFile = bool.Parse(ConfigurationManager.AppSettings["Email.WriteAsFile"] ?? "false")
};

kernel.Bind<IOrderProcessor>().To<EmailOrderProcessor>().WithConstructorArgument("settings", emailSettings);
}

在应用程序配置文件web.config添加配置

<appSettings>
<add key="Email.WriteAsFile" value="true" />
</appSettings>

完成购物车控制器

修改CartController,构造器接受一个IOrderProcessor接口的实现,并添加新的动作方法,在客户单击完成订单时,处理POST请求

public class CartController : Controller
{
private IProductRepository repository;
private IOrderProcessor orderProcessor;

public CartController(IProductRepository repo, IOrderProcessor proc)
{
repository = repo;
orderProcessor = proc;
}

// ...

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

注意

  • Checkout方法是用HttpPost注解属性修饰,表明该方法用于处理POST请求
  • 由于修改了构造器,因此需要修改CartController的单元测试,为构造器参数传递null,以通过编译。
  • MVC框架检查验证约束,通过ModelState属性把非法情况传递给动作方法,并通过ModelState.IsValid属性检查是否存在问题。可以调用ModelState.AddModelError方法注册一条错误信息。

单元测试:订单处理

需要对Checkout重载方法进行测试。

[TestClass]
public class CartTests
{
// 其他测试方法...

/// <summary>
/// 测试确保不能对空购物车进行结算。返回视图是默认视图,且传递给视图的模型状态被标记为非法
/// </summary>
[TestMethod]
public void Cannot_Checkout_Empty_Cart()
{
// 准备——创建一个模仿的订单处理器
Mock<IOrderProcessor> mock = new Mock<IOrderProcessor>();

// 准备——创建一个空的购物车
Cart cart = new Cart();

// 准备——创建一个运输信息实例
ShippingDetails shippingDetails = new ShippingDetails();

// 准备——创建一个控制器实例
CartController target = new CartController(null, mock.Object);

// 动作
ViewResult result = target.Checkout(cart, shippingDetails);

// 断言——检查,订单尚未传递给处理器
mock.Verify(m => m.ProcessOrder(It.IsAny<Cart>(), It.IsAny<ShippingDetails>()), Times.Never());

// 断言——检查,该方法返回的是默认视图
Assert.AreEqual("", result.ViewName);

// 断言——检查,给视图传递的是一个非法模型
Assert.AreEqual(false, result.ViewData.ModelState.IsValid);
}

/// <summary>
/// 把错误注入到视图模型,模仿客户输入非法运货数据
/// </summary>
[TestMethod]
public void Cannot_Checkout_Invalid_ShippingDetails()
{
// 准备——创建一个模仿的订单处理器
Mock<IOrderProcessor> mock = new Mock<IOrderProcessor>();

// 准备——创建含有一个物品的购物车
Cart cart = new Cart();
cart.AddItem(new Product(), 1);

// 准备——创建一个控制器实例
CartController target = new CartController(null, mock.Object);

// 准备——把一个错误添加到模型
target.ModelState.AddModelError("error", "error");

// 动作——试图结算
ViewResult result = target.Checkout(cart, new ShippingDetails());

// 断言——检查,订单尚未传递给处理器
mock.Verify(m => m.ProcessOrder(It.IsAny<Cart>(), It.IsAny<ShippingDetails>()), Times.Never());

// 断言——检查,方法返回的是默认视图
Assert.AreEqual("", result.ViewName);

// 断言——检查,我们正在把一个非法模型传递给视图
Assert.AreEqual(false, result.ViewData.ModelState.IsValid);
}

/// <summary>
/// 确保在适当的时候能够处理订单
/// </summary>
[TestMethod]
public void Can_Checkout_And_Submit_Order()
{
// 准备——创建一个模仿订单处理器
Mock<IOrderProcessor> mock = new Mock<IOrderProcessor>();

// 准备——创建含有一个物品的购物车
Cart cart = new Cart();
cart.AddItem(new Product(), 1);

// 准备——创建一个控制器实例
CartController target = new CartController(null, mock.Object);

// 动作——试图结算
ViewResult result = target.Checkout(cart, new ShippingDetails());

// 断言——检查,订单已经被传递给处理器
mock.Verify(m => m.ProcessOrder(It.IsAny<Cart>(), It.IsAny<ShippingDetails>()), Times.Once());

// 断言——检查,方法返回的是“Completed”视图
Assert.AreEqual("Completed", result.ViewName);

// 断言——检查,我们正在把一个有效的模型传递给视图
Assert.AreEqual(true, result.ViewData.ModelState.IsValid);
}
}

显式验证错误

MVC框架使用ShippingDetails类中的验证注解属性来验证用户的输入,必须将问题显示给用户,通过高亮问题字段以及显示问题摘要来报告验证错误。

用户提交的数据在验证之前发送给服务器,经过处理并生成结果页面之后,用户才会得到错误报告(服务器端验证)

Checkout.cshtml文件中,添加验证摘要“@Html.ValidationSummary()”

...
@using (Html.BeginForm())
{
@Html.ValidationSummary()
<h3>Ship to</h3>
<div class="form-group">
<label>Name:</label>
@Html.TextBoxFor(x => x.Name, new { @class = "form-control" })
</div>
...
}

创建一些CSS样式,用于用户输入非法数据时,所使用的非法元素。

SportsStore.WebUI项目Content文件夹中,添加样式表文件ErrorStyles.css

.field-validation-error {
color: #f00;
}

.field-validation-valid {
display: none;
}

.input-validation-error {
border: 1px solid #f00;
background-color: #fee;
}

.validation-summary-errors {
font-weight: bold;
color: #f00;
}

.validation-summary-valid {
display: none;
}

_Layout.cshtml文件中引用ErrorStyles.css样式文件。

<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="~/Content/bootstrap.css" rel="stylesheet" />
<link href="~/Content/bootstrap-theme.css" rel="stylesheet" />
<link href="~/Content/ErrorStyles.css" rel="stylesheet" />
<title>@ViewBag.Title</title>
</head>

显示致谢页面

结算成功之后,需要向客户显示一个已经完成订单处理的确认页面,并感谢他们购物。

Views\Cart文件夹中,新建Completed.cshtml文件。

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

运行项目,当客户填写完运输信息,点击“Complete order”按钮时,便会看到致谢页面。