跟我學ASP.NET MVC之七:SportsStrore一個完整的購物車


摘要:

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>

運行程序,得到表單驗證錯誤時的運行頁面。

這里看到,出錯的表單元素顯示紅色背景色和紅色邊框。表單上部用剛才定義的樣式,顯示了定義在實體類屬性的特性上的錯誤提示字符串。

 

 

 
        

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM