【Pro ASP.NET MVC 3 Framework】.學習筆記.6.SportsStore:導航

在以前的章節,偶們設置了核心的基礎設施,如今咱們將使用基礎設計添加關鍵特性,你將會看到投資是如何回報的。咱們可以很簡單很容易地添加劇要的面向客戶的特性。沿途,你也會看到一些MVC框架提供的附加的特性。html

1 添加導航控件框架

若是使用分類導航,須要作如下三個方面:ide

  • 加強List action模型,讓它能過濾repository中的Product對象
  • 重訪並加強URL方案,修改咱們的重路由策略
  • 建立sidebar風格的分類列表,高亮當前分類,並連接其它分類

1.1 過濾Product列表單元測試

偶們要加強視圖模型類ProductViewModel。爲了渲染sidebar,咱們要傳送當前分類給view。測試

1 public class ProductsListViewModel 2 { 3 public IEnumerable < Product > Products { get ; set ; } 4 public PagingInfo PagingInfo { get ; set ; } 5 public string CurrentCategory { get ; set ; } 6 }

咱們給視圖模型新增了CurrentCategory屬性,下一步是更新ProductController類,讓List action方法會以分類過濾Product對象,並是我用咱們新增的屬性指示那個分類被選中。spa

1 public ViewResult List( string category, int ? id) 2 { 3 int page = id.HasValue ? id.Value : 1 ; 4 ProductsListViewModel viewModel = new ProductsListViewModel 5 { 6 Products = repository.Products 7 .Where(p => category == null || p.Category == category) 8 .OrderBy(p => p.ProductID) 9 .Skip((page - 1 ) * pageSize) 10 .Take(pageSize), 11 PagingInfo = new PagingInfo 12 { 13 CurrentPage = page, 14 ItemPerpage = pageSize, 15 TotalItems = repository.Products.Count() 16 }, 17 CurrentCategory = category 18 }; 19 return View(viewModel); 20 }

咱們修改了三個部分。第一,咱們添加一個叫作category的參數。第二,改進Linq查詢,若是category不是Null,僅匹配Category屬性的Product對象被選擇。最後一個改變是設置CurrentCategory的屬性。這些變化會致使不能正確計算TotalItems的值。設計

1.2 更新已存在的單元測試3d

咱們修改了List action方法的簽名,它會放置一些已經存在的單元測試方法被編譯。爲了解決此事,傳遞null做爲List方法的第一個參數。例如Can_Send_Pagination_View_Model,會變成這樣orm

1 ProductsListViewModel result = (ProductsListViewModel)controller.List( null , 2 ).Model;

經過使用null,咱們像之前同樣,獲得了所有的repository。htm

1.3 分類過濾單元測試

1 [TestMethod] 2 public void Can_Filter_Products() 3 { 4 Mock < IProductRepository > mock = new Mock < IProductRepository > (); 5 mock.Setup(m => m.Products).Returns( new Product[]{ 6 new Product {ProductID = 1 ,Name = " P1 " ,Category = " Cat1 " }, 7 new Product {ProductID = 2 ,Name = " P2 " ,Category = " Cat2 " }, 8 new Product {ProductID = 3 ,Name = " P3 " ,Category = " Cat1 " }, 9 new Product {ProductID = 4 ,Name = " P4 " ,Category = " Cat2 " }, 10 new Product {ProductID = 5 ,Name = " P5 " ,Category = " Cat3 " } 11 }.AsQueryable()); 12 13 // Arrange 14 ProductController controller = new ProductController(mock.Object); 15 controller.pageSize = 3 ; 16 17 // Action 18 Product[] result = ((ProductsListViewModel)controller.List( " Cat2 " , 1 ).Model).Products.ToArray(); 19 20 // Assert 21 Assert.AreEqual(result.Length, 2 ); 22 Assert.IsTrue(result[ 0 ].Name == " P2 " && result[ 0 ].Category == " Cat2 " ); 23 Assert.IsTrue(result[ 1 ].Name == " P4 " && result[ 1 ].Category == " Cat2 " ); 24 }

1.4 改善URL方案

沒有人像看到或使用醜陋的URLs,如/?category=Soccer。

1 public static void RegisterRoutes(RouteCollection routes) 2 { 3 routes.IgnoreRoute( " {resource}.axd/{*pathInfo} " ); 4 5 routes.MapRoute( null , 6 "" , // 匹配空URL,如 / 7 new 8 { 9 controller = " Product " , 10 action = " List " , 11 category = ( string ) null , 12 id = 1 13 } 14 ); 15 16 routes.MapRoute( 17 null , 18 " Page{id} " , // 匹配 /Page2 ,可是不能匹配 /PageX 19 new { controller = " Product " , action = " List " , category = ( string ) null }, 20 new { id = @" \d+ " } // 約束:id必須是數字 21 ); 22 23 routes.MapRoute( null , 24 " {category} " , // 匹配 /Football 或 /沒有斜線的任何字符 25 new 26 { 27 controller = " Product " , 28 action = " List " , 29 id = 1 30 }); 31 32 routes.MapRoute( 33 null , // 路由名稱 34 " {category}/Page{id} " , // 匹配 /Football/Page567 35 new { controller = " Product " , action = " List " }, 36 new { id = @" \d+ " } 37 ); 38 39 }

路由添加的順序是很重要的。若是改變順序,會有意想不到的效果。

URL Leads To
/ 顯示全部分類的products列表的第一頁
/Page2 顯示全部類別的items列表的第二頁
/Soccer 顯示指定分類的items列表的第一頁
/Soccer/Page2 顯示指定分類的items列表的指定頁
/Anything/Else 調用Anything controller的Else action

路由系統既能處理來自客戶端的請求,也能處理咱們發出的URLs請求。

Url.Action方法是生成外向連接的最方便的方式。以前,咱們用它來顯示Page links,如今,爲了分類過濾,須要傳遞這個信息給helper方法。

1 @Html.PageLinks(Model.PagingInfo, x => Url.Action( " List " , 2 new { id = x,category = Model.CurrentCategory}))

經過傳遞CurrentCategory咱們生成的URL不會丟失分類過濾信息。

2 構建分類導航目錄

咱們會在多個controllers中用到這個分類列表,因此它應該獨立,並能夠重用。MVC框架有child action的概念,特別適合用來建立可重用的導航控件。Child Action依賴RenderAction這個HTML helper方法,它能讓你在當前view中包含數量的action方法的輸出。

這個方法給咱們一個真實的controller,包含任何咱們須要的程序邏輯,並能像其餘controller同樣單元測試。這確實是一個不錯的方法,建立程序的小片斷,保持整個MVC框架的方法。

2.1 建立導航控件

須要建立一個新的NavController controller,Menu action,用來渲染導航目錄,並將方法的輸出注入到layout。

1 public string Menu() 2 { 3 return " Hello from NavController " ; 4 }

要想在layout中渲染child action,編輯_Layout.cshtml文件,調用RenderAction help方法。

1 < div id = " categories " > 2 @{ Html.RenderAction( " Menu " , " Nav " ); } 3 </ div >

RenderAction方法直接將content寫入response流,像RenderPartial方法同樣。這意味着方法返回void,它不能使用常規的Razor@tag。咱們必須在Razor代碼塊中閉合調用方法,並使用分號終止聲明。也能夠使用Action方法,若是不喜歡代碼塊語法。

2.2 生成分類列表

咱們不想在controller中生成URLs,咱們用helper方法來作這些。全部咱們要在Menu action方法中作的,就是建立一個分類列表:

1 public class NavController : Controller 2 { 3 // 4 // GET: /Nav/ 5 private IProductRepository repository; 6 7 public NavController(IProductRepository repo) 8 { 9 repository = repo; 10 } 11 12 public PartialViewResult Menu() 13 { 14 IEnumerable < string > categories = repository.Products 15 .Select(x => x.Category) 16 .Distinct() 17 .OrderBy(x => x); 18 19 return PartialView(categories); 20 }

Menu action方法很簡單,它只用Linq查詢,得到分類的名字的列表,並傳輸他們到視圖。

2.3 生成分類列表的單元測試

咱們的目標是要生成一個按字母表排列的沒有重複項的列表。最簡單的方式,是提供含有重複分類的,沒有排列順序的測試數據,傳遞給NavController,斷言數據已經處理了乾淨了。

1 [TestMethod] 2 public void Can_Create_Categories() 3 { 4 Mock < IProductRepository > mock = new Mock < IProductRepository > (); 5 mock.Setup(m => m.Products).Returns( new 6 Product[]{ 7 new Product{ProductID = 1 ,Name = " P1 " ,Category = " Apples " }, 8 new Product{ProductID = 2 ,Name = " P2 " ,Category = " Apples " }, 9 new Product{ProductID = 3 ,Name = " P3 " ,Category = " Plums " }, 10 new Product{ProductID = 4 ,Name = " P4 " ,Category = " Oranges " } 11 }.AsQueryable()); 12 13 NavController target = new NavController(mock.Object); 14 15 string [] results = ((IEnumerable < string > )target.Menu().Model).ToArray(); 16 17 Assert.AreEqual(results.Length, 3 ); 18 Assert.AreEqual(results[ 0 ], " Apples " ); 19 Assert.AreEqual(results[ 1 ], " Oranges " ); 20 Assert.AreEqual(results[ 2 ], " Plums " ); 21 }

2.4 建立部分視圖

視圖名Menu,選中建立部分視圖,模型類填IEnumerable<string>

1 @model IEnumerable < string > 2 3 @{ 4 Layout = null ; 5 } 6 7 @Html.ActionLink( " Home " , " List " , " Product " ) 8 9 @foreach(var link in Model){ 10 @Html.RouteLink(link, new 11 { 12 controller = " Product " , 13 action = " List " , 14 category = link, 15 id = 1 16 }) 17 }

咱們添加叫作Home的連接,會顯示在分類列表的頂部,讓和用戶返回到沒有分類過濾的,全部products列表的首頁。爲了作到這點,使用了ActionLink helper方法,使用偶們早前配置的路由信息生成HTML anchor元素。

而後枚舉分類名字,使用RouteLink方法爲他們建立鏈接。有點像ActionLink,但它讓咱們提供一組name/value pairs,當從路由配置生成URL時。

2.4 高亮當前分類

通常咱們會建立一個包含分類列表和被選中的分類的視圖模型。可是此次,咱們展現View Bag特性。這個特性容許咱們不使用視圖模型,從controller傳遞數據到view。

1 public ViewResult Menu( string category = null ) 2 { 3 ViewBag.SelectedCategory = category; 4 5 IEnumerable < string > categories = repository.Products 6 .Select(x => x.Category) 7 .Distinct() 8 .OrderBy(x => x); 9 10 return View(categories); 11 }

咱們添加給Menu action方法添加了category參數,它由路由配置自動提供。咱們給View的ViewBag動態建立了SelectedCategory屬性,並設置它的值。ViewBag是一個動態對象。

2.5 報告被選中分類的單元測試

經過讀取ViewBag中屬性的值,咱們能夠測試Menu action方法是否正確地添加了被選中分類的細節。

1 [TestMethod] 2 public void Indicates_Selected_Category() 3 { 4 Mock < IProductRepository > mock = new Mock < IProductRepository > (); 5 mock.Setup(m => m.Products).Returns( 6 new Product[]{ 7 new Product{ProductID = 1 ,Name = " P1 " ,Category = " Apples " }, 8 new Product{ProductID = 4 ,Name = " P4 " ,Category = " Oranges " } 9 }.AsQueryable()); 10 11 // Arrange - create to controller 12 NavController target = new NavController(mock.Object); 13 14 // Arrage - define the category to selected 15 string categoryToSelect = " Apples " ; 16 17 // Action 18 string result = target.Menu(categoryToSelect).ViewBag.SelectedCategory; 19 20 // Assert 21 Assert.AreEqual(categoryToSelect, result); 22 }

咱們不須要轉換ViewBag屬性的值,這是相對於ViewData先進的地方。

1 new { 2 @class = link == ViewBag.SelectedCategory ? " selected " : null 3 }

在Menu.cshtml局部視圖中的@html.RouteLink增長第三個參數。第一個參數是string linkText,第二個參數是object routeValues,第三個參數是object htmlAttributes。當前選中的分類會被指派 selected CSS類。

注意在匿名對象中的@class,做爲新參數傳遞給RouteLink helper方法。它不是Razor tag。HTML使用class給元素指派CSS樣式,C#使用class建立class。咱們使用了C#特性,避免與HTML關鍵字class衝突。@符號容許咱們使用保留的關鍵字。若是咱們僅調用class參數,不加@,編譯器會假設咱們定義了一個新的C#類型。當咱們使用@符號,編譯器會知道咱們想要建立在匿名類型中建立一個叫作class的參數。

2.6 修正頁面總數

當前,頁數指向全部的產品。當使用分類後,頁數應不一樣。咱們能夠經過更新List action方法的ProductController,修復它。分頁信息攜帶分類到總數。

1 TotalItems = category == null ? 2 repository.Products.Count(): 3 repository.Products.Where(e => e.Category == category).Count()

若是分類被選中,咱們返回這個分類的items數。若是沒有選中,返回總數。

1 [TestMethod] 2 public void Generate_Category_Specific_Product_Count() 3 { 4 Mock < IProductRepository > mock = new Mock < IProductRepository > (); 5 mock.Setup(m => m.Products).Returns( 6 new Product[]{ 7 new Product {ProductID = 1 ,Name = " P1 " ,Category = " Cat1 " }, 8 new Product {ProductID = 2 ,Name = " P2 " ,Category = " Cat2 " }, 9 new Product {ProductID = 3 ,Name = " P3 " ,Category = " Cat1 " }, 10 new Product {ProductID = 4 ,Name = " P4 " ,Category = " Cat2 " }, 11 new Product {ProductID = 5 ,Name = " P5 " ,Category = " Cat3 " } 12 }.AsQueryable()); 13 // Arrange - create a controller and make the page size 3 items 14 ProductController target = new ProductController(mock.Object); 15 target.pageSize = 3 ; 16 17 // Action - test the product counts for different categories 18 int res1 = ((ProductsListViewModel)target.List( " Cat1 " ).Model).PagingInfo.TotalItems; 19 int res2 = ((ProductsListViewModel)target.List( " Cat2 " ).Model).PagingInfo.TotalItems; 20 int res3 = ((ProductsListViewModel)target.List( " Cat3 " ).Model).PagingInfo.TotalItems; 21 int res4 = ((ProductsListViewModel)target.List( null ).Model).PagingInfo.TotalItems; 22 23 // Assert 24 Assert.AreEqual(res1, 2 ); 25 Assert.AreEqual(res2, 2 ); 26 Assert.AreEqual(res3, 1 ); 27 Assert.AreEqual(res4, 5 ); 28 }
相關文章
相關標籤/搜索