跟我学ASP.NET MVC之七:SportsStrore一个完整的购物车
摘要:
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>
运行程序,得到表单验证错误时的运行页面。
这里看到,出错的表单元素显示红色背景色和红色边框。表单上部用刚才定义的样式,显示了定义在实体类属性的特性上的错误提示字符串。
最新文章
- jQuery的$.ajax
- Word 2010 发布博文测试
- sqlserver开窗函数
- 手把手教你把Vim改装成一个IDE编程环境(图文)
- uploadify 使用 详细 说明
- uva 10651 - Pebble Solitaire(记忆化搜索)
- Java中static、final用法
- Android学习路径(七)建立Action Bar
- 初学 Java Script (算数运算及逻辑术语)
- wireshark 随笔
- 在ubuntu上部署一个samba服务器
- (NO.00003)iOS游戏简单的机器人投射游戏成形记(十六)
- java中53个关键字的意义及使用方法
- 2018Action Recognition from Skeleton Data via Analogical Generalization over Qualitative Representations
- DeepLearning.ai-Week1-Convolution+model+-+Application
- weblogic系列漏洞整理 -- 1. weblogic安装
- Centos6.5 防火墙开放端口
- C#4.0特性
- Jmeter笔记:响应断言详解
- 2017-2018-1 20155208 课堂测试(ch06)(补做)