添加Navigation控件
上篇我們已經對UI部分做了整理,但是我們網站看起來仍然很奇怪,因為用戶無法選擇他們想看的商品類別,必須要一頁一頁的瀏覽,直到找到自己想要買的東西。我經常在網上瀏覽一些技術站點,並添加他們到我的收藏夾,但收藏夾里的條目太多了,還是不能方便的找到自己想看的網址,偶然發現了一個網站,叫做開發者導航(http://www.devseek.net),它收錄了我所需要的所有網址,這正是我想要的,於是我今天也用這個導航的字眼,來為我們的網站添加一個分類過濾的功能。我們今天的內容主要有三個部分:
1.增強ProductController類的List action功能,使它能夠分類商品。
2.修改並加強URL scheme 和我們的rerouting策略。
3.在邊條上創建分類列表,並高亮當前的分類和連接。
過濾產品列表
為了渲染我們的邊條,我們需要和我們ProductsListViewModel類溝通,過濾出產品分類的列表,現在就打開這個文件,讓我們為它做個Enhancement。
using System.Collections.Generic; using SportsStore.Domain.Entities; namespace SportsStore.WebUI.Models { public class ProductsListViewModel { public IEnumerable<Product> Products { get; set; } public PagingInfo PagingInfo { get; set; } public string CurrentCategory { get; set; } } }
我們為這個類添加了一個當前分類的屬性,用來顯示當前用戶選擇的分類。我們要更新我們ProductController,使它能夠使用這個屬性:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using SportsStore.WebUI.Models; namespace SportsStore.WebUI.Controllers { public class ProductController : Controller { private IProductsRepository repository; public int PageSize = 4; public ProductController(IProductsRepository productRepository) { this.repository = productRepository; } public ViewResult List(string category, int page = 1) { ProductsListViewModel model = new ProductsListViewModel { Products = repository.Products .Where(p => category == null || p.Category == category) .OrderBy(p => p.ProductID) .Skip((page - 1) * PageSize) .Take(PageSize), PagingInfo = new PagingInfo { CurrentPage = page, ItemsPerPage = PageSize, TotalItems = repository.Products.Count() }, CurrentCategory = category }; return View(model); } } }
上面的代碼我們做了3個改變。第一,我們添加了一個新的參數叫做category. 這個參數通過我們的第二個變化被使用,這第二個變化就是我改進了Linq查詢,如果category參數不是null,只有匹配這個分類的產品才能被選擇。這最后一個改變就是設置CurrentCategory 屬性的值,然而,我們這3點改變,就意味着PagingInfo.TotalItems的值是不正確的,我們必須解決這個問題。
更新現有的測試方法
我們改變了List的參數列表,這使得我們必須更新我們現有的測試方法,為了保證我們的測試方法都可用,我們要為他們添加一個null值,作為第一個參數傳遞,找到Can_Paginate方法,將List(2).Model改成List(null, 2).Model。運行你的應用,能看到如下畫面:
這和我們上篇最后的結果是一樣的,現在你在地址欄中添加如下參數:?category=Soccer ,是你的地址欄看上去像這樣http://localhost:47072/?category=Soccer 你會看到這樣的畫面:
我們的測試文件現在需要添加一個功能,使它能夠正確的過濾一個分類並接收一個指定分類的產品:
[TestMethod] public void Can_Filter_Products() { // 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 = "Cat1"}, new Product {ProductID = 2, Name = "P2", Category = "Cat2"}, new Product {ProductID = 3, Name = "P3", Category = "Cat1"}, new Product {ProductID = 4, Name = "P4", Category = "Cat2"}, new Product {ProductID = 5, Name = "P5", Category = "Cat3"} }.AsQueryable()); // Arrange - create a controller and make the page size 3 items ProductController controller = new ProductController(mock.Object); controller.PageSize = 3; // Action Product[] result = ((ProductsListViewModel)controller.List("Cat2", 1).Model) .Products.ToArray(); // Assert Assert.AreEqual(result.Length, 2); Assert.IsTrue(result[0].Name == "P2" && result[0].Category == "Cat2"); Assert.IsTrue(result[1].Name == "P4" && result[1].Category == "Cat2"); }
這個測試創建了一個mock repository,它包含了類別中的一個Product對象,一個被指定使用在Action方法中的分類,並且結果被check,確保在右側的產品對象都是正確的。
改善URL Scheme
我們的URL地址看上去太丑了,也不專業,現在我們必須花點時間去改善一下App_Start/RouteConfig.cs文件:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; namespace SportsStore.WebUI { public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute(null, "", new { controller = "Product", action = "List", category = (string)null, page = 1 } ); routes.MapRoute(null, "Page{page}", new { controller = "Product", action = "List", category = (string)null }, new { page = @"\d+" } ); routes.MapRoute(null, "{category}", new { controller = "Product", action = "List", page = 1 } ); routes.MapRoute(null, "{category}/Page{page}", new { controller = "Product", action = "List" }, new { page = @"\d+" } ); routes.MapRoute(null, "{controller}/{action}"); } } }
URL |
導航到 |
/ |
列出說有產品的第一頁列表 |
/Page2 |
列出所有產品的指定頁 |
/Soccer |
列出指定類別的產品的第一頁 |
/Soccer/Page2 |
列出指定類別的產品的指定頁 |
/Anything/Else |
調用Anything 控制器的Else方法 |
這是我們URL Scheme的具體含義。MVC使用ASP.NET routing 系統去處理從用戶端發來的請求,同時,它也向外發出URL scheme,這就是我們能夠嵌入到網頁中的地址,我們要做的就是這些應用中的地址都是被組裝起來的。
現在我們就去添加一些對分類過濾的支持:
@model SportsStore.WebUI.Models.ProductsListViewModel @{ ViewBag.Title = "Products"; } @foreach (var p in Model.Products) { Html.RenderPartial("ProductSummary", p); } <div class="pager"> @Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new {page = x, category = Model.CurrentCategory})) </div>
構建一個分類導航菜單
我們需要提供給用戶一種途徑,使用戶能夠選擇某種分類,這就需要我們必須提供分類信息給用戶,讓他們去選擇,而這個分類的信息必須要在多個控制器中運用,這就要求它必須是自包含的並且可重用的。在ASP.NET MVC框架中有個child actions的概念, 大家都喜歡用它來創建諸如可重用的導航控件之類的東西。一個child action 依賴與HTML helper方法,這個方法被稱為RenderAction,它讓我們從當前view的任意action方法中包含輸出,現在我們創建一個新的Controller(我們稱它為NavController) 和一個action方法 (菜單) ,並且渲染一個導航菜單,然后從這個方法中注入output到layout中。這個方法給了我們一個真正的控制器,無論我們的應用邏輯是什么都可以使用它,並且我們都能像其他控制器那用去測試它。
創建Navigation控制器
右擊WebUI中的Controllers文件夾,創建一個名為NavController的控制器,選擇空的MVC模板,刪除自動生成的index方法,添加代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace SportsStore.WebUI.Controllers { public class NavController : Controller { // // GET: /Nav/ public string Menu() { return "Hello from NavController"; } } }
這個方法返回一個消息字符串,但這對於我們整合一個child action到這個應用的其他部分已經足夠用了。我們希望這個分類列表展現在所有頁面上,所以我們將在layout中渲染這個child action,而不是在一個指定的View中。 現在我們編輯Views/Shared/_Layout.cshtml 文件,讓它調用RenderAction helper方法。
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> <link href="~/Content/Site.css" type="text/css" rel="stylesheet" /> </head> <body> <div id="header"> <div class="title">SPORTS STORE</div> </div> <div id="categories"> @{ Html.RenderAction("Menu", "Nav"); } </div> <div id="content"> @RenderBody() </div> </body> </html>
運行應用,你將看到我們的邊條上已經出現了這條消息字符串:
產生分類列表
現在,我們就在Menu action方法中創建分類列表:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using SportsStore.Domain.Abstract; namespace SportsStore.WebUI.Controllers { public class NavController : Controller { // // GET: /Nav/ private IProductsRepository repository; public NavController(IProductsRepository repo) { repository = repo; } public PartialViewResult Menu() { IEnumerable<string> categories = repository.Products .Select(x => x.Category) .Distinct() .OrderBy(x => x); return PartialView(categories); } } }
我們的控制器現在接受一個IProductsRepository的實現,這個實現是通過Ninject提供的,還有一個變化,就是我們使用Linq從repository中獲得分類信息,請注意,我們調用了一個PartialView的方法,返回了一個PartialViewResult對象。現在讓我們去更新一下我們的測試文件吧!添加如下代碼到你的測試文件:
[TestMethod] public void Can_Create_Categories() { // 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"}, new Product {ProductID = 2, Name = "P2", Category = "Apples"}, new Product {ProductID = 3, Name = "P3", Category = "Plums"}, new Product {ProductID = 4, Name = "P4", Category = "Oranges"}, }.AsQueryable()); // Arrange - create the controller NavController target = new NavController(mock.Object); // Act = get the set of categories string[] results = ((IEnumerable<string>)target.Menu().Model).ToArray(); // Assert Assert.AreEqual(results.Length, 3); Assert.AreEqual(results[0], "Apples"); Assert.AreEqual(results[1], "Oranges"); Assert.AreEqual(results[2], "Plums"); }
現在讓我們去創建這個PartialView吧!
創建PartialView
在NavController中,右擊Menu方法,選擇添加View,並輸入IEnumerable<string>在模型類輸入框中。
修改Menu.cshtml文件如下:
@model IEnumerable<string> @Html.ActionLink("Home", "List", "Product") @foreach (var link in Model) { @Html.RouteLink(link, new { controller = "Product", action = "List", category = link, page = 1 }) }
在Site.css文件中添加如下代碼:
DIV#categories A { font: bold 1.1em "Arial Narrow","Franklin Gothic Medium",Arial; display: block; text-decoration: none; padding: .6em; color: Black; border-bottom: 1px solid silver; } DIV#categories A.selected { background-color: #666; color: White; } DIV#categories A:hover { background-color: #CCC; } DIV#categories A.selected:hover { background-color: #666; }
運行你的應用,能應該能看到如下畫面:
我們還要需要進一步完善,因為我們現在還不能讓用戶清楚的看出當前選擇了那種分類,我們要高亮當前選中的分類,這樣看起來才更加友好、實用。
修改我們的NavController中的Menu方法如下:
public PartialViewResult Menu(string category = null) { ViewBag.SelectedCategory = category; IEnumerable<string> categories = repository.Products .Select(x => x.Category) .Distinct() .OrderBy(x => x); return PartialView(categories); }
為我們的測試文件添加一個選中的測試方法:
[TestMethod] public void Indicates_Selected_Category() { // 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"}, new Product {ProductID = 4, Name = "P2", Category = "Oranges"}, }.AsQueryable()); // Arrange - create the controller NavController target = new NavController(mock.Object); // Arrange - define the category to selected string categoryToSelect = "Apples"; // Action string result = target.Menu(categoryToSelect).ViewBag.SelectedCategory; // Assert Assert.AreEqual(categoryToSelect, result); }
更新Menu.cshtml如下:
@model IEnumerable<string> @Html.ActionLink("Home", "List", "Product") @foreach (var link in Model) { @Html.RouteLink(link, new { controller = "Product", action = "List", category = link, page = 1 }, new { @class = link == ViewBag.SelectedCategory ? "selected" : null }) }
運行一下看看結果吧!
糾正頁碼
從上圖中我們很輕易就能看出,我們的頁碼是錯的,我們只有兩個產品,卻顯示了有3頁,我們必須糾正這個錯誤!打開ProductController,找到List方法,修改如下:
public ViewResult List(string category, int page = 1) { ProductsListViewModel model = new ProductsListViewModel { Products = repository.Products .Where(p => category == null || p.Category == category) .OrderBy(p => p.ProductID) .Skip((page - 1) * PageSize) .Take(PageSize), PagingInfo = new PagingInfo { CurrentPage = page, ItemsPerPage = PageSize, TotalItems = category == null ? repository.Products.Count() : repository.Products.Where(e => e.Category == category).Count() }, CurrentCategory = category }; return View(model); }
添加測試方法到測試文件:
[TestMethod] public void Generate_Category_Specific_Product_Count() { // 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 = "Cat1"}, new Product {ProductID = 2, Name = "P2", Category = "Cat2"}, new Product {ProductID = 3, Name = "P3", Category = "Cat1"}, new Product {ProductID = 4, Name = "P4", Category = "Cat2"}, new Product {ProductID = 5, Name = "P5", Category = "Cat3"} }.AsQueryable()); // Arrange - create a controller and make the page size 3 items ProductController target = new ProductController(mock.Object); target.PageSize = 3; // Action - test the product counts for different categories int res1 = ((ProductsListViewModel)target .List("Cat1").Model).PagingInfo.TotalItems; int res2 = ((ProductsListViewModel)target .List("Cat2").Model).PagingInfo.TotalItems; int res3 = ((ProductsListViewModel)target .List("Cat3").Model).PagingInfo.TotalItems; int resAll = ((ProductsListViewModel)target .List(null).Model).PagingInfo.TotalItems; // Assert Assert.AreEqual(res1, 2); Assert.AreEqual(res2, 2); Assert.AreEqual(res3, 1); Assert.AreEqual(resAll, 5); }
運行一下,現在看下我們成果吧!
好了,今天就到這里里吧!內容實在是有點多,但都是必須的,而且實用的技術,下一篇中,我們將為我們的應用添加一個購物車,這是電子商務網站上必須的功能,不然怎么賣商品呢?如果您覺得我的文章實用,對你有所幫助,請推薦它給你的朋友,請繼續關注我的續篇!