摘要:
SportsStore應用程序進展很順利,但是我不能銷售產品直到設計了一個購物車。在這篇文章里,我就將創建一個購物車。
在目錄下的每個產品旁邊添加一個添加到購物車按鈕。點擊這個按鈕將顯示客戶到目前為止選擇的產品摘要,包含總價格。這時候,用戶可以點擊繼續購物按鈕返回產品目錄,或者點擊現在下單按鈕完成訂單結束購物過程。
定義Cart實體類
在SportsStore.Domain工程的Entities文件夾下,創建代碼文件Cart.cs。
1 using System.Collections.Generic; 2 using System.Linq; 3 4 namespace SportsStore.Domain.Entities 5 { 6 public class Cart 7 { 8 private List<CartLine> lineCollection = new List<CartLine>(); 9 public void AddItem(Product product, int quantity) 10 { 11 CartLine line = lineCollection.Where(p => p.Product.ProductID == product.ProductID).FirstOrDefault(); 12 if (line == null) 13 { 14 lineCollection.Add(new CartLine 15 { 16 Product = product, 17 Quantity = quantity 18 }); 19 } 20 else { 21 line.Quantity += quantity; 22 } 23 } 24 25 public void RemoveLine(Product product) 26 { 27 lineCollection.RemoveAll(l => l.Product.ProductID == product.ProductID); 28 } 29 30 public decimal ComputeTotalValue() 31 { 32 return lineCollection.Sum(e => e.Product.Price * e.Quantity); 33 } 34 35 public void Clear() 36 { 37 lineCollection.Clear(); 38 } 39 40 public IEnumerable<CartLine> CartLines 41 { 42 get { return lineCollection; } 43 } 44 } 45 46 public class CartLine 47 { 48 public Product Product { get; set; } 49 public int Quantity { get; set; } 50 } 51 }
Cart類使用了CartLine類,他們定義在同一個代碼文件內,保存一個客戶選擇的產品,以及客戶想買的數量。我定義了添加條目到購物車的方法,從購物車刪除之前已經添加的條目的方法,計算購物車內條目總價格,以及刪除所有條目清空購物車的方法。我還提供了一個通過IEnumrable<CartLine>訪問購物車內容的屬性。這些都很直觀,通過一點點LINQ很容易用C#實施。
定義視圖模型類
在SportsStore.WebUI工程的Models文件夾內,創建代碼文件CartIndexViewModel。
1 using SportsStore.Domain.Entities; 2 3 namespace SportsStore.WebUI.Models 4 { 5 public class CartIndexViewModel 6 { 7 public Cart Cart { get; set; } 8 public string ReturnUrl { get; set; } 9 } 10 }
該模型類有兩個屬性。Cart屬性保存了購物車信息,ReturnUrl保存了產品目錄的URL,需要這個信息是因為,客戶可以隨時點擊繼續購物按鈕,返回之前的產品目錄URL。
添加購物車控制器CartController
1 using SportsStore.Domain.Abstract; 2 using SportsStore.Domain.Entities; 3 using SportsStore.WebUI.Models; 4 using System.Linq; 5 using System.Web.Mvc; 6 7 namespace SportsStore.WebUI.Controllers 8 { 9 public class CartController : Controller 10 { 11 private IProductRepository repository; 12 13 public CartController(IProductRepository productRepository) 14 { 15 repository = productRepository; 16 } 17 18 public ActionResult Index(string returnUrl) 19 { 20 return View(new CartIndexViewModel 21 { 22 Cart = GetCart(), 23 ReturnUrl = returnUrl 24 }); 25 } 26 27 public RedirectToRouteResult AddToCart(int productId, string returnUrl) 28 { 29 Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId); 30 if (product != null) 31 { 32 GetCart().AddItem(product, 1); 33 } 34 return RedirectToAction("Index", new { returnUrl = returnUrl }); 35 } 36 37 public RedirectToRouteResult RemoveFromCart(int productId, string returnUrl) 38 { 39 Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId); 40 if (product != null) 41 { 42 GetCart().RemoveLine(product); 43 } 44 return RedirectToAction("Index", new { returnUrl }); 45 } 46 47 private Cart GetCart() 48 { 49 Cart cart = (Cart)Session["Cart"]; 50 if (cart == null) 51 { 52 cart = new Cart(); 53 Session["Cart"] = cart; 54 } 55 56 return cart; 57 } 58 } 59 }
該控制器的一些解釋:
- 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按鈕。
1 @model SportsStore.Domain.Entities.Product 2 3 <div class="well"> 4 <h3> 5 <strong>@Model.Name</strong> 6 <span class="pull-right label label-primary">@Model.Price.ToString("c")</span> 7 </h3> 8 @using (Html.BeginForm("AddToCart", "Cart")) 9 { 10 <div class="pull-right"> 11 @Html.HiddenFor(x => x.ProductID) 12 @Html.Hidden("returnUrl", Request.Url.PathAndQuery) 13 <input type="submit" class="btn btn-success" value="Add to cart" /> 14 </div> 15 } 16 <span class="lead"> @Model.Description</span> 17 </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。
1 @model SportsStore.WebUI.Models.CartIndexViewModel 2 3 @{ 4 ViewBag.Title = "Sports Store: Your Cart"; 5 } 6 <style> 7 #cartTable td { 8 vertical-align: middle; 9 } 10 </style> 11 <h2>Your cart</h2> 12 <table id="cartTable" class="table"> 13 <thead> 14 <tr> 15 <th>Quantity</th> 16 <th>Item</th> 17 <th class="text-right">Price</th> 18 <th class="text-right">Subtotal</th> 19 </tr> 20 </thead> 21 <tbody> 22 @foreach (var line in Model.Cart.CartLines) 23 { 24 <tr> 25 <td class="text-center">@line.Quantity</td> 26 <td class="text-left">@line.Product.Name</td> 27 <td class="text-right"> 28 @line.Product.Price.ToString("c") 29 </td> 30 <td class="text-right"> 31 @((line.Quantity * line.Product.Price).ToString("c")) 32 </td> 33 <td> 34 @using (Html.BeginForm("RemoveFromCart", "Cart")) 35 { 36 @Html.Hidden("ProductId", line.Product.ProductID) 37 @Html.HiddenFor(x => x.ReturnUrl) 38 <input class="btn btn-sm btn-warning" type="submit" value="Remove" /> 39 } 40 </td> 41 </tr> 42 } 43 </tbody> 44 <tfoot> 45 <tr> 46 <td colspan="3" class="text-right">Total:</td> 47 <td class="text-right"> 48 @Model.Cart.ComputeTotalValue().ToString("c") 49 </td> 50 </tr> 51 </tfoot> 52 </table> 53 <div class="text-center"> 54 <a class="btn btn-primary" href="@Model.ReturnUrl">Continue shopping</a> 55 </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方法。
1 public PartialViewResult Summary() 2 { 3 return PartialView(GetCart()); 4 }
然后,添加Summary視圖。
1 @model SportsStore.Domain.Entities.Cart 2 3 <div class="navbar-right"> 4 @Html.ActionLink("My Cart", "Index", "Cart", new { returnUrl = Request.Url.PathAndQuery }, new { @class = "btn btn-default navbar-btn" }) 5 </div> 6 <div class="navbar-text navbar-right"> 7 <b>Your cart:</b> 8 @Model.CartLines.Sum(x => x.Quantity) item(s), 9 @Model.ComputeTotalValue().ToString("c") 10 </div>
最后,修改_Layout.cshtml文件,調用Html幫助類方法Action,嵌入這個視圖到頭部導航欄內。
1 <!DOCTYPE html> 2 3 <html> 4 <head> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <link href="~/Content/bootstrap.css" rel="stylesheet" /> 7 <link href="~/Content/bootstrap-theme.css" rel="stylesheet" /> 8 <title>@ViewBag.Title</title> 9 <style> 10 .navbar-right { 11 float: right !important; 12 margin-right: 15px; 13 margin-left: 15px; 14 } 15 </style> 16 </head> 17 <body> 18 <div class="navbar navbar-inverse" role="navigation"> 19 <a class="navbar-brand" href="#">SPORTS STORE</a> 20 @Html.Action("Summary", "Cart") 21 </div> 22 <div class="row panel"> 23 <div id="categories" class="col-xs-3"> 24 @Html.Action("Menu", "Nav") 25 </div> 26 <div class="col-xs-8"> 27 @RenderBody() 28 </div> 29 </div> 30 </body> 31 </html>
這里添加頁面樣式navbar-right,使得購物車摘要信息部件在頭部導航欄內靠右顯示。
運行程序,得到運行結果。
使用模板綁定
MVC使用一個名叫模板綁定的系統,為了傳參數給行為方法,它從HTTP請求創建C#對象並作為參數傳給行為方法。MVC框架就是這樣來處理表單的。它看到目標行為方法的參數,然后使用模板綁定得到瀏覽器發送過來的表單里元素的值,然后根據名稱轉化成對應類型的相同名稱的參數,傳給行為方法。
模板綁定可以從請求里的任何信息中創建C#類型。這是MVC框架的核心特征之一。我將創建一個客戶的模板綁定來改進CartController控制器。
在SportsStore.WebUI工程的Infrastructure文件夾內創建子文件夾Binders,並在子文件夾下創建代碼文件CartModelBinder.cs。
1 using SportsStore.Domain.Entities; 2 using System.Web.Mvc; 3 4 namespace SportsStore.WebUI.Infrastructure.Binders 5 { 6 public class CartModelBinder : IModelBinder 7 { 8 private const string sessionKey = "Cart"; 9 10 public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) 11 { 12 // get the Cart from the session 13 Cart cart = null; 14 if (controllerContext.HttpContext.Session != null) 15 { 16 cart = (Cart)controllerContext.HttpContext.Session[sessionKey]; 17 } 18 // create the Cart if there wasn't one in the session data 19 if (cart == null) 20 { 21 cart = new Cart(); 22 if (controllerContext.HttpContext.Session != null) 23 { 24 controllerContext.HttpContext.Session[sessionKey] = cart; 25 } 26 } 27 // return the cart 28 return cart; 29 } 30 } 31 }
- 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代碼。
1 using SportsStore.Domain.Entities; 2 using SportsStore.WebUI.Infrastructure.Binders; 3 using System; 4 using System.Collections.Generic; 5 using System.Linq; 6 using System.Web; 7 using System.Web.Mvc; 8 using System.Web.Routing; 9 10 namespace SportsStore 11 { 12 public class MvcApplication : System.Web.HttpApplication 13 { 14 protected void Application_Start() 15 { 16 AreaRegistration.RegisterAllAreas(); 17 RouteConfig.RegisterRoutes(RouteTable.Routes); 18 19 ModelBinders.Binders.Add(typeof(Cart), new CartModelBinder()); 20 } 21 } 22 }
ModelBinders.Binders是ModelBinders的靜態屬性,它是一個繼承自IDictionary類型的對象,它的Add方法提供兩個參數完成模板綁定類型的綁定。第一個參數是返回類型參數,第二個參數實例化一個繼承自IModelBinder類型的對象。
這樣,我現在可以修改CartController控制器的各個Action方法,添加Cart類型參數,並使用模板綁定方式獲得Cart對象參數的值。
1 using SportsStore.Domain.Abstract; 2 using SportsStore.Domain.Entities; 3 using SportsStore.WebUI.Models; 4 using System.Linq; 5 using System.Web.Mvc; 6 7 namespace SportsStore.WebUI.Controllers 8 { 9 public class CartController : Controller 10 { 11 private IProductRepository repository; 12 13 public CartController(IProductRepository productRepository) 14 { 15 repository = productRepository; 16 } 17 18 public ActionResult Index(Cart cart, string returnUrl) 19 { 20 return View(new CartIndexViewModel 21 { 22 Cart = cart, 23 ReturnUrl = returnUrl 24 }); 25 } 26 27 public RedirectToRouteResult AddToCart(Cart cart, int productId, string returnUrl) 28 { 29 Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId); 30 if (product != null) 31 { 32 cart.AddItem(product, 1); 33 } 34 return RedirectToAction("Index", new { returnUrl = returnUrl }); 35 } 36 37 public RedirectToRouteResult RemoveFromCart(Cart cart, int productId, string returnUrl) 38 { 39 Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId); 40 if (product != null) 41 { 42 cart.RemoveLine(product); 43 } 44 return RedirectToAction("Index", new { returnUrl }); 45 } 46 47 public PartialViewResult Summary(Cart cart) 48 { 49 return PartialView(cart); 50 } 51 } 52 }
控制器方法的參數Cart:cart將從模板綁定類的方法BindModel中,返回cart對象。
提交訂單
在SportsStore.Domain工程里的Entities文件夾內,創建代碼文件ShippingDetails,表示物流表單信息。
1 namespace SportsStore.Domain.Entities 2 { 3 public class ShippingDetails 4 { 5 public string Name { get; set; } 6 public string Line1 { get; set; } 7 public string Line2 { get; set; } 8 public string Line3 { get; set; } 9 public string City { get; set; } 10 public string State { get; set; } 11 public string Zip { get; set; } 12 public string Country { get; set; } 13 public bool GiftWrap { get; set; } 14 } 15 }
創建提交訂單的接口IOrderProcessor。
1 using SportsStore.Domain.Entities; 2 3 namespace SportsStore.Domain.Abstract 4 { 5 public interface IOrderProcessor 6 { 7 void ProcessOrder(Cart cart, ShippingDetails shippingDetails); 8 } 9 }
該接口只有一個方法ProcessOrder,接受Cart類型參數和ShippingDefails參數,處理訂單。
創建訂單處理類EmailOrderProcessor,實現接口IOrderProcessor。
1 using SportsStore.Domain.Abstract; 2 using SportsStore.Domain.Entities; 3 using System.Net; 4 using System.Net.Mail; 5 using System.Text; 6 7 namespace SportsStore.Domain.Concrete 8 { 9 public class EmailSettings 10 { 11 public string MailToAddress = "your email address to receive Email"; 12 public string MailFromAddress = "your email address to send Email"; 13 public bool UseSsl = true; 14 public string Username = "user name of email account to send Email"; 15 public string Password = "password of email account to send Email"; 16 public string ServerName = "smtp server address"; 17 public int ServerPort = smtp port; 18 public bool WriteAsFile = false; 19 public string FileLocation = @"c:\sports_store_emails"; 20 } 21 22 public class EmailOrderProcessor : IOrderProcessor 23 { 24 private EmailSettings emailSettings; 25 26 public EmailOrderProcessor(EmailSettings settings) 27 { 28 emailSettings = settings; 29 } 30 31 public void ProcessOrder(Cart cart, ShippingDetails shippingInfo) 32 { 33 using (var smtpClient = new SmtpClient()) 34 { 35 smtpClient.EnableSsl = emailSettings.UseSsl; 36 smtpClient.Host = emailSettings.ServerName; 37 //smtpClient.Port = emailSettings.ServerPort; 38 smtpClient.UseDefaultCredentials = true; 39 smtpClient.Credentials = new NetworkCredential(emailSettings.Username, emailSettings.Password); 40 if (emailSettings.WriteAsFile) 41 { 42 //smtpClient.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory; 43 //smtpClient.PickupDirectoryLocation = emailSettings.FileLocation; 44 //smtpClient.EnableSsl = false; 45 } 46 else 47 { 48 smtpClient.DeliveryMethod = smtpClient.DeliveryMethod; 49 } 50 StringBuilder body = new StringBuilder() 51 .AppendLine("A new order has been submitted") 52 .AppendLine("---") 53 .AppendLine("Items:"); 54 foreach (var line in cart.CartLines) 55 { 56 var subtotal = line.Product.Price * line.Quantity; 57 body.AppendFormat("{0} x {1} (subtotal: {2:c}) ", 58 line.Quantity, 59 line.Product.Name, 60 subtotal); 61 body.AppendLine(); 62 } 63 body.AppendFormat("Total order value: {0:c}", cart.ComputeTotalValue()) 64 .AppendLine() 65 .AppendLine("---") 66 .AppendLine() 67 .AppendLine("Ship to:") 68 .AppendLine(shippingInfo.Name) 69 .AppendLine(shippingInfo.Line1) 70 .AppendLine(shippingInfo.Line2 ?? "") 71 .AppendLine(shippingInfo.Line3 ?? "") 72 .AppendLine(shippingInfo.City) 73 .AppendLine(shippingInfo.State ?? "") 74 .AppendLine(shippingInfo.Country) 75 .AppendLine(shippingInfo.Zip) 76 .AppendLine("---") 77 .AppendFormat("Gift wrap: {0}", 78 shippingInfo.GiftWrap ? "Yes" : "No"); 79 MailMessage mailMessage = new MailMessage(emailSettings.MailFromAddress, emailSettings.MailToAddress, "New order submitted!", body.ToString()); 80 if (emailSettings.WriteAsFile) 81 { 82 mailMessage.BodyEncoding = Encoding.ASCII; 83 } 84 try 85 { 86 smtpClient.Send(mailMessage); 87 } 88 catch (System.Exception e) 89 { 90 91 } 92 } 93 } 94 } 95 }
這里使用SmtpClient對象發送郵件。
- EmailOrderProcessor類對象里包含了EmailSettings對象,EmailSettings對象保存了用於接收和發送郵件的配置信息。
- EmailOrderProcessor類對象使用構造函數方式,實例化EmailSettings對象。
在Infrastructure文件夾內找到代碼文件NinjectDependencyResolver.cs,在AddBindings方法內,添加Ninject注冊對接口IOrderProcessor和類EmailOrderProcessor的綁定。
1 private void AddBindings() 2 { 3 kernel.Bind<IProductRepository>().To<EFProductRepository>(); 4 5 EmailSettings emailSettings = new EmailSettings 6 { 7 WriteAsFile = bool.Parse(System.Configuration.ConfigurationManager.AppSettings["Email.WriteAsFile"] ?? "false") 8 }; 9 kernel.Bind<IOrderProcessor>().To<EmailOrderProcessor>().WithConstructorArgument("settings", emailSettings); 10 }
這里,使用方法WithConstructorArgument("settings", emailSettings),將對象emailSettings,通過構造函數方式注入到EmailOrderProcessor對象內的emailSettings屬性。
現在我們可以修改CartController控制器,添加提交訂單的業務邏輯代碼了。
1 using SportsStore.Domain.Abstract; 2 using SportsStore.Domain.Entities; 3 using SportsStore.WebUI.Models; 4 using System.Linq; 5 using System.Web.Mvc; 6 7 namespace SportsStore.WebUI.Controllers 8 { 9 public class CartController : Controller 10 { 11 private IProductRepository repository; 12 private IOrderProcessor orderProcessor; 13 14 public CartController(IProductRepository productRepository, IOrderProcessor productOrderProcessor) 15 { 16 repository = productRepository; 17 orderProcessor = productOrderProcessor; 18 } 19 20 public ActionResult Index(Cart cart, string returnUrl) 21 { 22 return View(new CartIndexViewModel 23 { 24 Cart = cart, 25 ReturnUrl = returnUrl 26 }); 27 } 28 29 public RedirectToRouteResult AddToCart(Cart cart, int productId, string returnUrl) 30 { 31 Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId); 32 if (product != null) 33 { 34 cart.AddItem(product, 1); 35 } 36 return RedirectToAction("Index", new { returnUrl = returnUrl }); 37 } 38 39 public RedirectToRouteResult RemoveFromCart(Cart cart, int productId, string returnUrl) 40 { 41 Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId); 42 if (product != null) 43 { 44 cart.RemoveLine(product); 45 } 46 return RedirectToAction("Index", new { returnUrl }); 47 } 48 49 public PartialViewResult Summary(Cart cart) 50 { 51 return PartialView(cart); 52 } 53 54 public ViewResult Checkout() 55 { 56 return View(new ShippingDetails()); 57 } 58 59 [HttpPost] 60 public ViewResult Checkout(Cart cart, ShippingDetails shippingDetails) 61 { 62 if (cart.CartLines.Count() == 0) 63 { 64 ModelState.AddModelError("", "Sorry, your cart is empty!"); 65 } 66 if (ModelState.IsValid) 67 { 68 orderProcessor.ProcessOrder(cart, shippingDetails); 69 cart.Clear(); 70 return View("Completed"); 71 } 72 else 73 { 74 return View(shippingDetails); 75 } 76 } 77 } 78 }
- 跟接口屬性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按鈕。
1 @model SportsStore.WebUI.Models.CartIndexViewModel 2 3 @{ 4 ViewBag.Title = "Sports Store: Your Cart"; 5 } 6 <style> 7 #cartTable td { 8 vertical-align: middle; 9 } 10 </style> 11 <h2>Your cart</h2> 12 <table id="cartTable" class="table"> 13 <thead> 14 <tr> 15 <th>Quantity</th> 16 <th>Item</th> 17 <th class="text-right">Price</th> 18 <th class="text-right">Subtotal</th> 19 </tr> 20 </thead> 21 <tbody> 22 @foreach (var line in Model.Cart.CartLines) 23 { 24 <tr> 25 <td class="text-center">@line.Quantity</td> 26 <td class="text-left">@line.Product.Name</td> 27 <td class="text-right"> 28 @line.Product.Price.ToString("c") 29 </td> 30 <td class="text-right"> 31 @((line.Quantity * line.Product.Price).ToString("c")) 32 </td> 33 <td> 34 @using (Html.BeginForm("RemoveFromCart", "Cart")) 35 { 36 @Html.Hidden("ProductId", line.Product.ProductID) 37 @Html.HiddenFor(x => x.ReturnUrl) 38 <input class="btn btn-sm btn-warning" type="submit" value="Remove" /> 39 } 40 </td> 41 </tr> 42 } 43 </tbody> 44 <tfoot> 45 <tr> 46 <td colspan="3" class="text-right">Total:</td> 47 <td class="text-right"> 48 @Model.Cart.ComputeTotalValue().ToString("c") 49 </td> 50 </tr> 51 </tfoot> 52 </table> 53 <div class="text-center"> 54 <a class="btn btn-primary" href="@Model.ReturnUrl">Continue shopping</a> 55 @Html.ActionLink("Checkout now", "Checkout", null, new { @class = "btn btn-primary" }) 56 </div>
Checkout視圖:
1 @model SportsStore.Domain.Entities.ShippingDetails 2 3 @{ 4 ViewBag.Title = "SportStore: Checkout"; 5 } 6 <h2>Check out now</h2> 7 <p>Please enter your details, and we'll ship your goods right away!</p> 8 @using (Html.BeginForm()) 9 { 10 <h3>Ship to</h3> 11 <div class="form-group"> 12 <label>Name:</label> 13 @Html.TextBoxFor(x => x.Name, new { @class = "form-control" }) 14 </div> 15 <h3>Address</h3> 16 <div class="form-group"> 17 <label>Line 1:</label> 18 @Html.TextBoxFor(x => x.Line1, new { @class = "form-control" }) 19 </div> 20 <div class="form-group"> 21 <label>Line 2:</label> 22 @Html.TextBoxFor(x => x.Line2, new { @class = "form-control" }) 23 </div> 24 <div class="form-group"> 25 <label>Line 3:</label> 26 @Html.TextBoxFor(x => x.Line3, new { @class = "form-control" }) 27 </div> 28 <div class="form-group"> 29 <label>City:</label> 30 @Html.TextBoxFor(x => x.City, new { @class = "form-control" }) 31 </div> 32 <div class="form-group"> 33 <label>State:</label> 34 @Html.TextBoxFor(x => x.State, new { @class = "form-control" }) 35 </div> 36 <div class="form-group"> 37 <label>Zip:</label> 38 @Html.TextBoxFor(x => x.Zip, new { @class = "form-control" }) 39 </div> 40 <div class="form-group"> 41 <label>Country:</label> 42 @Html.TextBoxFor(x => x.Country, new { @class = "form-control" }) 43 </div> 44 <h3>Options</h3> 45 <div class="checkbox"> 46 <label> 47 @Html.EditorFor(x => x.GiftWrap) 48 Gift wrap these items 49 </label> 50 </div> 51 <div class="text-center"> 52 <input class="btn btn-primary" type="submit" value="Complete order" /> 53 </div> 54 }
注意這里,我都是使用的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特性。
1 using System.ComponentModel.DataAnnotations; 2 3 namespace SportsStore.Domain.Entities 4 { 5 public class ShippingDetails 6 { 7 [Required(ErrorMessage = "Please enter a name")] 8 public string Name { get; set; } 9 [Required(ErrorMessage = "Please enter the first address line")] 10 public string Line1 { get; set; } 11 public string Line2 { get; set; } 12 public string Line3 { get; set; } 13 [Required(ErrorMessage = "Please enter a city name")] 14 public string City { get; set; } 15 [Required(ErrorMessage = "Please enter a state name")] 16 public string State { get; set; } 17 public string Zip { get; set; } 18 [Required(ErrorMessage = "Please enter a country name")] 19 public string Country { get; set; } 20 public bool GiftWrap { get; set; } 21 } 22 }
這里,為幾個必填字段添加了Required特性,提供的參數是個字符串,如果實體綁定失敗的時候(ModelState.IsValid為false),用於在客戶端顯示該字符串。
修改Checkout視圖,添加@Html.ValidationSummary(),用於在一個頁面區域顯示完整的表單錯誤消息。
1 @model SportsStore.Domain.Entities.ShippingDetails 2 3 @{ 4 ViewBag.Title = "SportStore: Checkout"; 5 } 6 <h2>Check out now</h2> 7 <p>Please enter your details, and we'll ship your goods right away!</p> 8 @using (Html.BeginForm()) 9 { 10 @Html.ValidationSummary() 11 <h3>Ship to</h3> 12 <div class="form-group"> 13 <label>Name:</label> 14 @Html.TextBoxFor(x => x.Name, new { @class = "form-control" }) 15 </div> 16 <h3>Address</h3> 17 <div class="form-group"> 18 <label>Line 1:</label> 19 @Html.TextBoxFor(x => x.Line1, new { @class = "form-control" }) 20 </div> 21 <div class="form-group"> 22 <label>Line 2:</label> 23 @Html.TextBoxFor(x => x.Line2, new { @class = "form-control" }) 24 </div> 25 <div class="form-group"> 26 <label>Line 3:</label> 27 @Html.TextBoxFor(x => x.Line3, new { @class = "form-control" }) 28 </div> 29 <div class="form-group"> 30 <label>City:</label> 31 @Html.TextBoxFor(x => x.City, new { @class = "form-control" }) 32 </div> 33 <div class="form-group"> 34 <label>State:</label> 35 @Html.TextBoxFor(x => x.State, new { @class = "form-control" }) 36 </div> 37 <div class="form-group"> 38 <label>Zip:</label> 39 @Html.TextBoxFor(x => x.Zip, new { @class = "form-control" }) 40 </div> 41 <div class="form-group"> 42 <label>Country:</label> 43 @Html.TextBoxFor(x => x.Country, new { @class = "form-control" }) 44 </div> 45 <h3>Options</h3> 46 <div class="checkbox"> 47 <label> 48 @Html.EditorFor(x => x.GiftWrap) 49 Gift wrap these items 50 </label> 51 </div> 52 <div class="text-center"> 53 <input class="btn btn-primary" type="submit" value="Complete order" /> 54 </div> 55 }
為了更美觀,我再Content文件夾內添加樣式表文件ErrorStyles.css,並在_Layout.cshtml引用它。
1 .field-validation-error {color: #f00;} 2 .field-validation-valid { display: none;} 3 .input-validation-error { border: 1px solid #f00; background-color:#fee; } 4 .validation-summary-errors { font-weight: bold; color: #f00;} 5 .validation-summary-valid { display: none;}
實體對象驗證失敗后,新生成的視圖表單元素,將包含這些css的class屬性,在統一的ErrorStyles.css樣式表內定義這些樣式。
新的_Layout.cshtml文件
1 <!DOCTYPE html> 2 3 <html> 4 <head> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <link href="~/Content/bootstrap.css" rel="stylesheet" /> 7 <link href="~/Content/bootstrap-theme.css" rel="stylesheet" /> 8 <link href="~/Content/ErrorStyles.css" rel="stylesheet" /> 9 <title>@ViewBag.Title</title> 10 <style> 11 .navbar-right { 12 float: right !important; 13 margin-right: 15px; 14 margin-left: 15px; 15 } 16 </style> 17 </head> 18 <body> 19 <div class="navbar navbar-inverse" role="navigation"> 20 <a class="navbar-brand" href="#">SPORTS STORE</a> 21 @Html.Action("Summary", "Cart") 22 </div> 23 <div class="row panel"> 24 <div id="categories" class="col-xs-3"> 25 @Html.Action("Menu", "Nav") 26 </div> 27 <div class="col-xs-8"> 28 @RenderBody() 29 </div> 30 </div> 31 </body> 32 </html>
運行程序,得到表單驗證錯誤時的運行頁面。
這里看到,出錯的表單元素顯示紅色背景色和紅色邊框。表單上部用剛才定義的樣式,顯示了定義在實體類屬性的特性上的錯誤提示字符串。