摘要:

SportsStore应用程序进展很顺利,但是我不能销售产品直到设计了一个购物车。在这篇文章里,我就将创建一个购物车。

在目录下的每个产品旁边添加一个添加到购物车按钮。点击这个按钮将显示客户到目前为止选择的产品摘要,包含总价格。这时候,用户可以点击继续购物按钮返回产品目录,或者点击现在下单按钮完成订单结束购物过程。

定义Cart实体类

在SportsStore.Domain工程的Entities文件夹下,创建代码文件Cart.cs。

 using System.Collections.Generic;
using System.Linq; namespace SportsStore.Domain.Entities
{
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> CartLines
{
get { return lineCollection; }
}
} public class CartLine
{
public Product Product { get; set; }
public int Quantity { get; set; }
}
}

Cart类使用了CartLine类,他们定义在同一个代码文件内,保存一个客户选择的产品,以及客户想买的数量。我定义了添加条目到购物车的方法,从购物车删除之前已经添加的条目的方法,计算购物车内条目总价格,以及删除所有条目清空购物车的方法。我还提供了一个通过IEnumrable<CartLine>访问购物车内容的属性。这些都很直观,通过一点点LINQ很容易用C#实施。

定义视图模型类

在SportsStore.WebUI工程的Models文件夹内,创建代码文件CartIndexViewModel。

 using SportsStore.Domain.Entities;

 namespace SportsStore.WebUI.Models
{
public class CartIndexViewModel
{
public Cart Cart { get; set; }
public string ReturnUrl { get; set; }
}
}

该模型类有两个属性。Cart属性保存了购物车信息,ReturnUrl保存了产品目录的URL,需要这个信息是因为,客户可以随时点击继续购物按钮,返回之前的产品目录URL。

添加购物车控制器CartController

 using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using SportsStore.WebUI.Models;
using System.Linq;
using System.Web.Mvc; namespace SportsStore.WebUI.Controllers
{
public class CartController : Controller
{
private IProductRepository repository; public CartController(IProductRepository productRepository)
{
repository = productRepository;
} public ActionResult Index(string returnUrl)
{
return View(new CartIndexViewModel
{
Cart = GetCart(),
ReturnUrl = returnUrl
});
} public RedirectToRouteResult AddToCart(int productId, string returnUrl)
{
Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId);
if (product != null)
{
GetCart().AddItem(product, );
}
return RedirectToAction("Index", new { returnUrl = returnUrl });
} public RedirectToRouteResult RemoveFromCart(int productId, string returnUrl)
{
Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId);
if (product != null)
{
GetCart().RemoveLine(product);
}
return RedirectToAction("Index", new { returnUrl });
} private Cart GetCart()
{
Cart cart = (Cart)Session["Cart"];
if (cart == null)
{
cart = new Cart();
Session["Cart"] = cart;
} return cart;
}
}
}

该控制器的一些解释:

  • GetCart方法:从Session里获取购物车对象,如果该对象为空,则创建这个对象,添加到Session,并返回该对象。
  • Index方法:传入returnUrl参数,返回购物车摘要信息视图。该视图的模型类是CartIndexViewModel,模型类对象的Cart属性通过调用方法GetCart返回,ReturnUrl属性使用方法参数赋值。
  • AddToCart方法:传入productId参数和returnUrl参数,添加产品到购物车,并返回重定向的购物车摘要信息视图。方法的返回类型是RedirectToRouteResult,该类的基类是ActionResult。
  • RemoveFromCart方法:传入productId参数和returnUrl参数,从购物车中删除产品,并返回重定向的购物车摘要信息视图。
  • AddToCart方法和RemoveFromCart方法都是通过调用Controller基类的RedirectToAction方法,返回重定向视图类RedirectToRouteResult的对象。
  • RedirectToAction方法的第一个参数是Action名称,第二个无类型对象参数提供传入Action的参数值。这里将重定向到Cart控制器的Index方法。

添加到购物车按钮

修改ProductSummary.cshtml视图,添加Add to Cart按钮。

 @model SportsStore.Domain.Entities.Product

 <div class="well">
<h3>
<strong>@Model.Name</strong>
<span class="pull-right label label-primary">@Model.Price.ToString("c")</span>
</h3>
@using (Html.BeginForm("AddToCart", "Cart"))
{
<div class="pull-right">
@Html.HiddenFor(x => x.ProductID)
@Html.Hidden("returnUrl", Request.Url.PathAndQuery)
<input type="submit" class="btn btn-success" value="Add to cart" />
</div>
}
<span class="lead"> @Model.Description</span>
</div>
  • 使用Html.BeginForm帮助方法,生成AddToCart表单。方法的第一个参数是Action名称AddToCart,第二个参数是控制器名称Cart。
  • 使用Html.HiddenFor帮助方法,生成表单的hidden html元素,该元素的name属性是字符串ProductID,值是该产品的ProductID值。
  • 使用Html.Hidden帮助方法,生成表单的hidden html元素,该元素的name属性是字符串returnUrl,值是当前页面的Url。
  • 控制器的AddToCart方法将通过表单元素的名称,获取要传入该方法的参数productID和returnUrl的值(大小写不敏感)。

添加购物车详细信息视图

在Views文件夹的Cart文件夹内,添加Index.cshtml。

 @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.CartLines)
{
<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>
  • 这个视图以表格的形式,展示了购物车摘要产品信息,包含了产品名称、购买数量、单价、价格信息。
  • 每个产品条目后面,添加删除表单和删除按钮,这里的表单和按钮,同之前添加到购物车按钮一样。
  • 表格底部,调用ComputeTotalValue方法,返回总价格。
  • 页面底部中间,显示一个Continue Shopping按钮,ReturnUrl属性指向之前的产品目录Url,点击后返回产品目录页面。

运行程序,得到运行结果。

这里我选择了Chess目录,浏览器地址栏上的URL变成了:http://localhost:17596/Chess

如果我点击Human Chess Board产品的Add To Cart按钮,得到页面:

注意这时候的浏览器地址栏的地址变成了:http://localhost:17596/Cart/Index?returnUrl=%2FChess,包含的购物车的Cart/Index,以及以问号?开始的参数?returnUrl=%2FChess。returnUrl的值就是刚才的页面地址。

如果再点击Continue Shoppinga按钮,将返回到returnUrl指向的页面,既是刚才的页面:http://localhost:17596/Chess

添加购物车摘要视图

我还需要添加一个显示购物车摘要信息的小部件,可以在所有应用程序页面上都能看到,点击后返回购物车详细信息。这个小部件和导航条目类似,需要使用返回PartialViewResult的Action方法,在_Layout.cshtml视图中,使用Html.Action方法嵌入这个视图。

首先修改CartController控制器,添加Summary方法。

         public PartialViewResult Summary()
{
return PartialView(GetCart());
}

然后,添加Summary视图。

 @model SportsStore.Domain.Entities.Cart

 <div class="navbar-right">
@Html.ActionLink("My Cart", "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.CartLines.Sum(x => x.Quantity) item(s),
@Model.ComputeTotalValue().ToString("c")
</div>

最后,修改_Layout.cshtml文件,调用Html帮助类方法Action,嵌入这个视图到头部导航栏内。

 <!DOCTYPE html>

 <html>
<head>
<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>
<style>
.navbar-right {
float: right !important;
margin-right: 15px;
margin-left: 15px;
}
</style>
</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>

这里添加页面样式navbar-right,使得购物车摘要信息部件在头部导航栏内靠右显示。

运行程序,得到运行结果。

 使用模板绑定

MVC使用一个名叫模板绑定的系统,为了传参数给行为方法,它从HTTP请求创建C#对象并作为参数传给行为方法。MVC框架就是这样来处理表单的。它看到目标行为方法的参数,然后使用模板绑定得到浏览器发送过来的表单里元素的值,然后根据名称转化成对应类型的相同名称的参数,传给行为方法。

模板绑定可以从请求里的任何信息中创建C#类型。这是MVC框架的核心特征之一。我将创建一个客户的模板绑定来改进CartController控制器。

在SportsStore.WebUI工程的Infrastructure文件夹内创建子文件夹Binders,并在子文件夹下创建代码文件CartModelBinder.cs。

 using SportsStore.Domain.Entities;
using System.Web.Mvc; namespace SportsStore.WebUI.Infrastructure.Binders
{
public class CartModelBinder : IModelBinder
{
private const string sessionKey = "Cart"; public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
// get the Cart from the session
Cart cart = null;
if (controllerContext.HttpContext.Session != null)
{
cart = (Cart)controllerContext.HttpContext.Session[sessionKey];
}
// create the Cart if there wasn't one in the session data
if (cart == null)
{
cart = new Cart();
if (controllerContext.HttpContext.Session != null)
{
controllerContext.HttpContext.Session[sessionKey] = cart;
}
}
// return the cart
return cart;
}
}
}
  • CartModelBinder类继承接口IModelBinder,并实现接口的方法BindModel。
  • 接口方法BindModel提供两个参数,参数类型ControllerContext:controllerContext获取控制器上下文环境信息,参数类型ModelBindingContext:bindingContext获取绑定的上下文信息。
  • 接口方法BindModel返回类型是object,他返回的对象的值就是Action方法参数的值。
  • 参数controllerContext对象的HttpContext属性保存了HTTP请求中的信息,通过controllerContext.HttpContext.Session获取HTTP请求中的Session信息。
  • 我的BindMode方法的方法体代码和CartController控制器的GetCart方法相同。都是从Session里获取购物车对象,如果该对象为空,则创建这个对象,添加到Session,并返回该对象。

有了模板绑定方法后,还需要将模板方法在Global.asax.cs代码的事件Application_Start内,通过调用ModelBinders.Binders.Add方法,注册到MVC应用程序里。

修改Global.asax代码。

 using SportsStore.Domain.Entities;
using SportsStore.WebUI.Infrastructure.Binders;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing; namespace SportsStore
{
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes); ModelBinders.Binders.Add(typeof(Cart), new CartModelBinder());
}
}
}
ModelBinders.Binders是ModelBinders的静态属性,它是一个继承自IDictionary类型的对象,它的Add方法提供两个参数完成模板绑定类型的绑定。第一个参数是返回类型参数,第二个参数实例化一个继承自IModelBinder类型的对象。
这样,我现在可以修改CartController控制器的各个Action方法,添加Cart类型参数,并使用模板绑定方式获得Cart对象参数的值。
 using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using SportsStore.WebUI.Models;
using System.Linq;
using System.Web.Mvc; namespace SportsStore.WebUI.Controllers
{
public class CartController : Controller
{
private IProductRepository repository; public CartController(IProductRepository productRepository)
{
repository = productRepository;
} public ActionResult Index(Cart cart, string returnUrl)
{
return View(new CartIndexViewModel
{
Cart = cart,
ReturnUrl = returnUrl
});
} 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 = 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 PartialViewResult Summary(Cart cart)
{
return PartialView(cart);
}
}
}

控制器方法的参数Cart:cart将从模板绑定类的方法BindModel中,返回cart对象。

 提交订单

在SportsStore.Domain工程里的Entities文件夹内,创建代码文件ShippingDetails,表示物流表单信息。

 namespace SportsStore.Domain.Entities
{
public class ShippingDetails
{
public string Name { get; set; }
public string Line1 { get; set; }
public string Line2 { get; set; }
public string Line3 { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zip { get; set; }
public string Country { get; set; }
public bool GiftWrap { get; set; }
}
}

创建提交订单的接口IOrderProcessor。

 using SportsStore.Domain.Entities;

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

该接口只有一个方法ProcessOrder,接受Cart类型参数和ShippingDefails参数,处理订单。

创建订单处理类EmailOrderProcessor,实现接口IOrderProcessor。

 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 = "your email address to receive Email";
public string MailFromAddress = "your email address to send Email";
public bool UseSsl = true;
public string Username = "user name of email account to send Email";
public string Password = "password of email account to send Email";
public string ServerName = "smtp server address";
public int ServerPort = smtp port;
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.UseSsl;
smtpClient.Host = emailSettings.ServerName;
//smtpClient.Port = emailSettings.ServerPort;
smtpClient.UseDefaultCredentials = true;
smtpClient.Credentials = new NetworkCredential(emailSettings.Username, emailSettings.Password);
if (emailSettings.WriteAsFile)
{
//smtpClient.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory;
//smtpClient.PickupDirectoryLocation = emailSettings.FileLocation;
//smtpClient.EnableSsl = false;
}
else
{
smtpClient.DeliveryMethod = smtpClient.DeliveryMethod;
}
StringBuilder body = new StringBuilder()
.AppendLine("A new order has been submitted")
.AppendLine("---")
.AppendLine("Items:");
foreach (var line in cart.CartLines)
{
var subtotal = line.Product.Price * line.Quantity;
body.AppendFormat("{0} x {1} (subtotal: {2:c}) ",
line.Quantity,
line.Product.Name,
subtotal);
body.AppendLine();
}
body.AppendFormat("Total order value: {0:c}", cart.ComputeTotalValue())
.AppendLine()
.AppendLine("---")
.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, emailSettings.MailToAddress, "New order submitted!", body.ToString());
if (emailSettings.WriteAsFile)
{
mailMessage.BodyEncoding = Encoding.ASCII;
}
try
{
smtpClient.Send(mailMessage);
}
catch (System.Exception e)
{ }
}
}
}
}

这里使用SmtpClient对象发送邮件。

  • EmailOrderProcessor类对象里包含了EmailSettings对象,EmailSettings对象保存了用于接收和发送邮件的配置信息。
  • EmailOrderProcessor类对象使用构造函数方式,实例化EmailSettings对象。

在Infrastructure文件夹内找到代码文件NinjectDependencyResolver.cs,在AddBindings方法内,添加Ninject注册对接口IOrderProcessor和类EmailOrderProcessor的绑定。

         private void AddBindings()
{
kernel.Bind<IProductRepository>().To<EFProductRepository>(); EmailSettings emailSettings = new EmailSettings
{
WriteAsFile = bool.Parse(System.Configuration.ConfigurationManager.AppSettings["Email.WriteAsFile"] ?? "false")
};
kernel.Bind<IOrderProcessor>().To<EmailOrderProcessor>().WithConstructorArgument("settings", emailSettings);
}

这里,使用方法WithConstructorArgument("settings", emailSettings),将对象emailSettings,通过构造函数方式注入到EmailOrderProcessor对象内的emailSettings属性。

现在我们可以修改CartController控制器,添加提交订单的业务逻辑代码了。

 using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using SportsStore.WebUI.Models;
using System.Linq;
using System.Web.Mvc; namespace SportsStore.WebUI.Controllers
{
public class CartController : Controller
{
private IProductRepository repository;
private IOrderProcessor orderProcessor;

public CartController(IProductRepository productRepository, IOrderProcessor productOrderProcessor)
{
repository = productRepository;
orderProcessor = productOrderProcessor;
} public ActionResult Index(Cart cart, string returnUrl)
{
return View(new CartIndexViewModel
{
Cart = cart,
ReturnUrl = returnUrl
});
} 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 = 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 PartialViewResult Summary(Cart cart)
{
return PartialView(cart);
} public ViewResult Checkout()
{
return View(new ShippingDetails());
} [HttpPost]
public ViewResult Checkout(Cart cart, ShippingDetails shippingDetails)
{
if (cart.CartLines.Count() == )
{
ModelState.AddModelError("", "Sorry, your cart is empty!");
}
if (ModelState.IsValid)
{
orderProcessor.ProcessOrder(cart, shippingDetails);
cart.Clear();
return View("Completed");
}
else
{
return View(shippingDetails);
}
}
}
}
  • 跟接口属性epository属性一样,通过构造函数注入接口属性orderProcessor属性值。
  • 两个Action方法名称都是Checkout,MVC通过方法特性识别它们。第一个Checkout没有添加任何方法特性,在HTTP GET请求的时候调用它。第二个Checkout有[HttpPost]特性修饰,在HTTP POST请求的时候调用它。
  • 第一个Checkout方法,返回ShippingDetails类型对象作为视图模型的视图,用于填写物流表单。
  • 第二个Checkout方法,第一个参数类型是Cart,通过模板方法返回参数值。第二个参数类型是ShippingDetails,通过获取视图的表单元素值获取该对象。
  • 如果购物车内为空,则通过调用ModelState.AddModelError方法,向视图添加错误消息。添加错误消息后,ModelState的IsValid属性将为false。
  • 如果ModelState.IsValid属性为true,则处理该订单,这里是发送邮件。清空购物车,并返回Complete视图给客户。
  • 如果ModelState.IsValid属性为false,还是发送HTTP GET请求的Checkout视图,将刚才填写的shippingDetails对象返回至Checkout视图。这样,客户可以看到和修改刚才填写的信息。

最后是修改视图。

订单详情视图Index.cshtml上添加Checkout按钮。

 @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.CartLines)
{
<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>
@Html.ActionLink("Checkout now", "Checkout", null, new { @class = "btn btn-primary" })
</div>

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

注意这里,我都是使用的Html.TextBoxFor方法和Html.EditorFor方法,通过传入一个lamda表达式的形式,创建表单元素。这样创建的表单元素能够对应Action的参数ShippingDetails类型的属性名称,自动创建对象并传给方法参数。

Completed视图:

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

运行程序,得到运行结果。

Checkout视图:

点击Complete order按钮,返回Complete视图。

添加表单验证

刚才的表单,如果不填写任何内容,也能提交成功。这种操作是非法的,现在我们需要给表单添加客户端验证。

一般的客户端开发,都是将验证的JavaScript代码直接写在页面上。但是MVC框架提供了从实体类到客户端的验证方式,那就是给实体类属性添加ValidationAttribute。

修改ShippingDetails内,给属性添加ValidationAttribute特性。

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

这里,为几个必填字段添加了Required特性,提供的参数是个字符串,如果实体绑定失败的时候(ModelState.IsValid为false),用于在客户端显示该字符串。

修改Checkout视图,添加@Html.ValidationSummary(),用于在一个页面区域显示完整的表单错误消息。

 @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())
{
@Html.ValidationSummary()
<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>
}

为了更美观,我再Content文件夹内添加样式表文件ErrorStyles.css,并在_Layout.cshtml引用它。

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

实体对象验证失败后,新生成的视图表单元素,将包含这些css的class属性,在统一的ErrorStyles.css样式表内定义这些样式。

新的_Layout.cshtml文件

 <!DOCTYPE html>

 <html>
<head>
<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>
<style>
.navbar-right {
float: right !important;
margin-right: 15px;
margin-left: 15px;
}
</style>
</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>

运行程序,得到表单验证错误时的运行页面。

这里看到,出错的表单元素显示红色背景色和红色边框。表单上部用刚才定义的样式,显示了定义在实体类属性的特性上的错误提示字符串。

												

最新文章

  1. jQuery的$.ajax
  2. Word 2010 发布博文测试
  3. sqlserver开窗函数
  4. 手把手教你把Vim改装成一个IDE编程环境(图文)
  5. uploadify 使用 详细 说明
  6. uva 10651 - Pebble Solitaire(记忆化搜索)
  7. Java中static、final用法
  8. Android学习路径(七)建立Action Bar
  9. 初学 Java Script (算数运算及逻辑术语)
  10. wireshark 随笔
  11. 在ubuntu上部署一个samba服务器
  12. (NO.00003)iOS游戏简单的机器人投射游戏成形记(十六)
  13. java中53个关键字的意义及使用方法
  14. 2018Action Recognition from Skeleton Data via Analogical Generalization over Qualitative Representations
  15. DeepLearning.ai-Week1-Convolution+model+-+Application
  16. weblogic系列漏洞整理 -- 1. weblogic安装
  17. Centos6.5 防火墙开放端口
  18. C#4.0特性
  19. Jmeter笔记:响应断言详解
  20. 2017-2018-1 20155208 课堂测试(ch06)(补做)

热门文章

  1. Android的Binder的起源-android学习之旅(100)
  2. 属性动画基础之ValueAnimator
  3. vue2.0-基于elementui换肤[自定义主题]
  4. CRM客户关系管理系统(十一)
  5. 大数据批量导入,解决办法,实践从定时从 sqlserver 批量同步数据到 mySql
  6. 进程间通信——IPC之共享内存
  7. MariaDB/MySQL用户和权限管理
  8. Java语言概论
  9. 学好js的步骤
  10. 远离压力,提高效率——Getting things done读书笔记