《Pro ASP.NET MVC 3 Framework》學習筆記之十五【示例項目SportsStore】


綁定Shopping Cart

定義購物車Cart的實體,購物車是我們程序業務領域的一個部分,所以在我們領域模型(Domain Model)里面添加一個cart的實體是合理的。在SportsStore.Domain的Entities文件夾下添加一個Cart的實體類,如下所示:

View Code
    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> Lines
{
get { return lineCollection; }
}
}

//購物車項
public class CartLine
{
public Product Product{get;set;}
public int Quantity{get;set;}
}

添加幾個測試方法測試下,如下所示:

View Code
        [TestMethod]
public void Can_Add_New_Lines()
{
//Arrange -create some test products
Product p1 = new Product { ProductID = 1, Name = "p1" };
Product p2 = new Product { ProductID = 2, Name = "p2" };
//Arrange -create a new cart
Cart target = new Cart();
target.AddItem(p1, 1);
target.AddItem(p2, 1);
CartLine[] results = target.Lines.ToArray();
//Assert
Assert.AreEqual(results.Length, 2);
Assert.AreEqual(results[0].Product, p1);
Assert.AreEqual(results[1].Product, p2);
}

[TestMethod]
public void Can_Add_Quantity_For_Existing_Lines()
{
//Arrange -create some test products
Product p1 = new Product { ProductID = 1, Name = "p1" };
Product p2 = new Product { ProductID = 2, Name = "p2" };
//Arrange
Cart target = new Cart();
//Act
target.AddItem(p1, 1);
target.AddItem(p2, 1);
target.AddItem(p1, 10);
CartLine[] results = target.Lines.OrderBy(c => c.Product.ProductID).ToArray();
//Assert
Assert.AreEqual(results.Length, 2);
Assert.AreEqual(results[0].Quantity, 11);
Assert.AreEqual(results[1].Quantity, 1);
}

[TestMethod]
public void Can_Remove_Line()
{
// Arrange - create some test products
Product p1 = new Product { ProductID = 1, Name = "P1" };
Product p2 = new Product { ProductID = 2, Name = "P2" };
Product p3 = new Product { ProductID = 3, Name = "P3" };

// Arrange - create a new cart
Cart target = new Cart();
// Arrange - add some products to the cart
target.AddItem(p1, 1);
target.AddItem(p2, 3);
target.AddItem(p3, 5);
target.AddItem(p2, 1);

// Act
target.RemoveLine(p2);

// Assert
Assert.AreEqual(target.Lines.Where(c => c.Product == p2).Count(), 0);
Assert.AreEqual(target.Lines.Count(), 2);
}

[TestMethod]
public void Calculate_Cart_Total()
{
// Arrange - create some test products
Product p1 = new Product { ProductID = 1, Name = "P1", Price = 100M };
Product p2 = new Product { ProductID = 2, Name = "P2", Price = 50M };

// Arrange - create a new cart
Cart target = new Cart();

// Act
target.AddItem(p1, 1);
target.AddItem(p2, 1);
target.AddItem(p1, 3);
decimal result = target.ComputeTotalValue();

// Assert
Assert.AreEqual(result, 450M);
}

[TestMethod]
public void Can_Add_To_Cart()
{
//Arrange -create the mock repository
Mock<IProductsRepository> mock = new Mock<IProductsRepository>();
mock.Setup(m => m.Products).Returns(new Product[] {
new Product{ProductID=1,Name="P1",Category="Apples"}
}.ToList());
//Arrange -create a Cart
Cart cart = new Cart();
//Arrange -create the controller
CartController target = new CartController(mock.Object);
//Act -add a product to the cart
target.AddToCart(cart, 1, null);

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

[TestMethod]
public void Can_View_Cart_Contents()
{
//Arrange -create a Cart
Cart cart = new Cart();
//Arrange -create the controller
CartController target = new CartController(null);
//Act -call the Index action method
CartIndexViewModel result = (CartIndexViewModel)target.Index(cart, "myUrl").ViewData.Model;

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

編輯Views/Shared/ProductSummary.cshtml,如下所示:

View Code
@model SportsStore.Domain.Entities.Product
<div id="item">
<h3>@Model.Name</h3>
@Model.Description
@using (Html.BeginForm("AddToCart", "Cart"))
{
@Html.HiddenFor(x => x.ProductID)
@Html.Hidden("returnUrl", Request.Url.PathAndQuery)
<h4>@Model.Price.ToString("c")</h4>
<input type="submit" value="+ Add to cart" />
}
</div>

當我們提交表單時,會調用CartController下的AddToCart action方法。

Note:我們通過Html.BeginForm來創建一個表單的,默認情況下form表單是post提交。當然我們可以改變它,使用Get方法提交也可以。但是我們必須謹慎對待,因為HTTP規范要求Get請求必須是冪等的(idempotent),這意味着不能引起任何改變。我們這里向購物車添加Product顯然是一個變化的過程,所以用Post最合適。

補充下:Http定義了與服務器交互的不同方法,最基本的方法有4種,分別是:GET,POST,PUT,DELETE.即查,改,增,刪4個操作。針對這個主題(POST提交還是GET提交)下一章會有更多的講解,還包括了對如果我們忽略了冪等GET請求產生的后果的解釋。

接着添加一個樣式,如下所示:

View Code
FORM { margin: 0; padding: 0; } 
DIV.item FORM { float:right; }
DIV.item INPUT {
color:White; background-color: #333; border: 1px solid black; cursor:pointer;
}

前面我們用Html.BeginForm方法在每一個Product列表創建了Form。這也意味着,每天點擊Add to cart按鈕時,會提交對應的Form表單。這可能讓我們做WebForm的人有點意外,因為WebForm里面限制了每個頁面只能有一個Form。但是asp.net mvc里面沒有這個限制,完全可以根據我們的需要來創建Form表單。

接着創建CartController用來處理Add to cart按鈕點擊。代碼如下所示:

View Code
using System;
using System.Linq;
using System.Web.Mvc;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using SportsStore.WebUI.Models;

namespace SportsStore.WebUI.Controllers
{
public class CartController : Controller
{
private IProductsRepository repository;
public CartController(IProductsRepository repo)
{
repository = repo;
}

public RedirectToRouteResult AddToCart(Cart cart, int productId, string returnUrl)
{
Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId);
if (product != null)
{
cart.AddItem(product, 1);
}
return RedirectToAction("Index", new { returnUrl });
}

public RedirectToRouteResult RemoveFromCart(Cart cart, int productId, string returnUrl)
{
Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId);
if (product != null)
{
cart.RemoveLine(product);
}
return RedirectToAction("Index", new { returnUrl });
}

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

public ViewResult Summary(Cart cart)
{
return View(cart);
}

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

}
}

在上面的代碼里面,AddToCart和RemoveFromCart方法都調用了RedirectToAction方法。這是用來發送重定向的指令到瀏覽器,讓瀏覽器請求一個新的URL。這里是讓瀏覽器請求一個調用Index action方法的URL。下面實現這個Index方法並用它來展示Cart的內容。

我們需要傳遞兩種信息給展示購物車內容的View,分別是Cart對象和展示當用戶點擊Continue Shopping按鈕時的URL。為此,我們創建一個視圖模型的類,在Models文件夾創建CartIndexViewModel的類,代碼如下:

View Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using SportsStore.Domain.Entities;

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

接着實現Cart controller里面的Index action方法,代碼如下:

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

右鍵添加視圖,如下所示:

Index View的代碼如下所示:

View Code
@model SportsStore.WebUI.Models.CartIndexViewModel
@{
ViewBag.Title = "Sports Store:Your Cart";
}
<h2>
Your Cart</h2>
<table width="90%" align="center">
<thead>
<tr>
<th align="center">
Quantity
</th>
<th align="left">
Item
</th>
<th align="right">
Price
</th>
<th align="right">
Subtotal
</th>
</tr>
</thead>
<tbody>
@foreach (var line in Model.Cart.Lines)
{
<tr>
<td align="center">@line.Quantity
</td>
<td align="left">@line.Product.Name
</td>
<td align="right">@line.Product.Price.ToString("c")
</td>
<td align="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="actionButtons" type="submit" value="Remove" />
}
</td>
</tr>
}
</tbody>
<tfoot>
<tr>
<td colspan="3" align="right">
Total:
</td>
<td align="right">
@Model.Cart.ComputeTotalValue().ToString("c")
</td>
</tr>
</tfoot>
</table>
<p align="center" class="actionButtons">
<a href="@Model.ReturnUrl">Continue shopping</a>
@Html.ActionLink("Checkout now", "Checkout")
</p>

繼續添加樣式:

View Code
H2 { margin-top: 0.3em } 
TFOOT TD { border-top: 1px dotted gray; font-weight: bold; }
.actionButtons A, INPUT.actionButtons {
font: .8em Arial; color: White; margin: .5em;
text-decoration: none; padding: .15em 1.5em .2em 1.5em;
background-color: #353535; border: 1px solid black;
}

現在基本告一段落,可以運行程序看看效果了。接下來是使用模型綁定(Model Binding)。

asp.net mvc框架使用一種稱為model binding的機制將來自HTTP請求創建為C#對象,這樣就能夠將它們作為參數傳遞給Controller的Action方法。這也是MVC處理Form表單的原理。例如MVC框架尋找Action方法的參數作為目標,使用model binding從Input元素獲取值,並會將這些值轉化為Action方法里面參數對應的類型以及相同的參數名稱。
Model binders能夠將請求里面任何可用的信息創建為C#類型,這是MVC框架一個非常核心的功能之一。

下面我們將創建一個自定義的模型綁定來完善CartController類。這里使用Session對象來存儲和管理購物車對象(當然,這可能不符合實際情況,我們只管將注意力放在MVC上吧,呵呵)。

我們將創建一個自定義的綁定來獲取包含在Session里面的Cart對象,MVC框架會創建Cart對象並作為參數傳遞給CartController的Action方法。MVC的model binding功能是非常強大且可伸縮的。對於這個部分,書的第二部分會有詳細的講解。

通過實現IModelBinder接口來創建自定義的model binding。在 SportsStore.WebUI里面創建一個新的文件夾Binders,接着在里面創建CartModelBinder.cs.代碼如下:

View Code
using System;
using System.Web.Mvc;
using SportsStore.Domain.Entities;

namespace SportsStore.WebUI.Binders
{
public class CartModelBinder : IModelBinder
{
private const string sessionKey = "Cart";

public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
//從Session里面獲取Cart
Cart cart = (Cart)controllerContext.HttpContext.Session[sessionKey];
//如果Session里面沒有數據,則創建一個購物車
if (cart == null)
{
cart = new Cart();
controllerContext.HttpContext.Session[sessionKey] = cart;
}
return cart;
}
}
}

IModeBinder接口里面定義一個方法:BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext).提供的兩個參數使我們創建領域模型對象成為可能。ControllerContext providers能夠訪問Controller類的所有信息,包括了來自瀏覽器的詳細請求信息;ModelBindingContext提供我們要創建model object的信息和工具來簡化我們的操作。

這里主要關注下ControllerContext類,它有一個HttpContext屬性,它包含了Session對象,這正是我們需要的。

我們需要告訴MVC框架使用我們的CartModelBinder類創建Cart的實例,需要修改Global.asax,如下所示:

        protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();

RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);

ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory());
ModelBinders.Binders.Add(typeof(Cart), new CartModelBinder());
}

當MVC收到請求時,比如,AddToCart方法被調用時,它開始尋找action 方法的參數,它將會在可用的綁定列表里面尋找,試圖找到一個能夠創建每一個參數類型的實例。我們自定義的綁定會被用來創建Cart對象,並且MVC是通過Session功能來實現的。在我們自定義的綁定和默認綁定之間,mvc能夠創建必備參數的集合來調用action方法。正是如此才允許我們重構Controller以至於我們不知道在請求被接收時Cart對象是怎樣被創立的。

像這樣使用自定義的綁定有幾個好處:

1.我們將創建購物車的邏輯從Controller里面分離出來,這樣就允許我們可以改變存儲Cart對象的方式而不必去更改Controller。

2.任何Controller想使用Cart對象,只需要在其Action方法里面聲明一個Cart參數即可

3.較好的進行單元測試

下面接着完善購物車功能

這里給購物車增加兩個功能:1.允許用戶刪除購物項 2.在頁面頂端增加一個顯示購物車詳情的功能
首先修改Views/Cart/Index.cshtml,如下所示:

View Code
@model SportsStore.WebUI.Models.CartIndexViewModel
@{
ViewBag.Title = "Sports Store:Your Cart";
}
<h2>
Your Cart</h2>
<table width="90%" align="center">
<thead>
<tr>
<th align="center">
Quantity
</th>
<th align="left">
Item
</th>
<th align="right">
Price
</th>
<th align="right">
Subtotal
</th>
</tr>
</thead>
<tbody>
@foreach (var line in Model.Cart.Lines)
{
<tr>
<td align="center">@line.Quantity
</td>
<td align="left">@line.Product.Name
</td>
<td align="right">@line.Product.Price.ToString("c")
</td>
<td align="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="actionButtons" type="submit" value="Remove" />
}
</td>
</tr>
}
</tbody>
<tfoot>
<tr>
<td colspan="3" align="right">
Total:
</td>
<td align="right">
@Model.Cart.ComputeTotalValue().ToString("c")
</td>
</tr>
</tfoot>
</table>
<p align="center" class="actionButtons">
<a href="@Model.ReturnUrl">Continue shopping</a>
@Html.ActionLink("Checkout now", "Checkout")
</p>

添加查看購物車詳情功能:
在CartController里面添加一個Summary action方法,如下所示:

View Code
        public ViewResult Summary(Cart cart)
{
return View(cart);
}

Summary方法非常簡單,僅僅需要呈現一個View,提供當前購物車作為View Data。當然這里要使用自定義綁定,我需要創建一個partial view。如下所示:

Summary Partial View的代碼如下所示:

View Code
@model SportsStore.Domain.Entities.Cart
@{
Layout = null;
}
<div id="cart">
<span class="caption"><b>Your cart:</b>
@Model.Lines.Sum(x => x.Quantity) item(s),
@Model.ComputeTotalValue().ToString("c")
</span>
@Html.ActionLink("Checkout", "Index", "Cart", new { returnUrl = Request.Url.PathAndQuery }, null)
</div>

因為我們在每個頁面都要顯示,所有需要在_Layout.cshtml里面進行定義,如下所示:

<!DOCTYPE html>
<html>
<head>
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
</head>
<body>
<div id="header">
@{Html.RenderAction("Summary", "Cart");}
<div class="title">
Sports Store</div>
</div>
<div id="categories">
@{Html.RenderAction("Menu", "Nav");}
</div>
<div id="content">
@RenderBody()
</div>
</body>
</html>

接着添加樣式,如下所示:

View Code
DIV#cart { float:right; margin: .8em; color: Silver;  
background-color: #555; padding: .5em .5em .5em 1em; }
DIV#cart A { text-decoration: none; padding: .4em 1em .4em 1em; line-height:2.1em;
margin-left: .5em; background-color: #333; color:White; border: 1px solid black;}

這時運行下程序,應該看到如下效果:

好啦,今天的筆記就到這里。請路過的朋友多指導,幫助!
晚安!


免責聲明!

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



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