本章的重點是對產品信息增加排序和分頁的功能,以及使用ASP.NET Routing特性添加更加友好的URL支持。
注意:如果你想按照本章的代碼編寫示例,你必須完成第四章或者直接從www.apress.com下載第四章的源代碼。
5.1 按照價格對產品進行排序
為了演示如何進行排序,我們將使用一個簡單的例子讓用戶可以對產品按照價格進行排序。
首先,我們向Controllers\ProductsController.cs文件中的Index方法添加一個switch語句,以便可以按照價格對產品信息進行排序,修改之處如下列高亮顯示的代碼:
1 public ActionResult Index(string category, string search, string sortBy) 2 { 3 // instantiate a new view model 4 ProductIndexViewModel viewModel = new ProductIndexViewModel(); 5 6 // select the products 7 var products = db.Products.Include(p => p.Category); 8 9 // perform the search and save the search string to the vieModel 10 if (!string.IsNullOrEmpty(search)) 11 { 12 products = products.Where(p => p.Name.Contains(search) || p.Description.Contains(search) || p.Category.Name.Contains(search)); 13 viewModel.Search = search; 14 } 15 16 // group search results into categories and count how many item in each category 17 viewModel.CatsWithCount = from matchinngProducts in products 18 where matchinngProducts.CategoryID != null 19 group matchinngProducts by matchinngProducts.Category.Name into catGroup 20 select new CategoryWithCount() 21 { 22 CategoryName = catGroup.Key, 23 ProductCount = catGroup.Count() 24 }; 25 26 if (!string.IsNullOrEmpty(category)) 27 { 28 products = products.Where(p => p.Category.Name == category); 29 } 30 31 // sort the results 32 switch (sortBy) 33 { 34 case "price_lowest": 35 products = products.OrderBy(p => p.Price); 36 break; 37 case "price_highest": 38 products = products.OrderByDescending(p => p.Price); 39 break; 40 default: 41 break; 42 } 43 44 viewModel.Products = products; 45 46 return View(viewModel); 47 }
這段代碼分別使用Entity Framework的OrderBy和OrderByDescending方法,按照價格對產品信息進行升序或降序操作。不調試啟動應用程序,然后手動修改URL以測試排序是否按照預期正確工作,URL的格式分別為Products?sortBy=price_lowest和Products?sortBy=price_highest。產品信息應該分別顯示為最低價格顯示在列表的頭部和最高價格顯示在列表的頭部。圖5-1顯示的是按照最高價格來顯示產品信息。
圖5-1:按照最高價格優先排序的產品列表
5.1.1 向產品的索引(Index)視圖添加排序
我們需要向站點中添加一些用戶接口控件以允許用戶可以按照他們的意圖來進行排序。為了演示這個功能,我們需要添加一個下拉列表以及一個填充該下拉列表值和文本的字典。
首先,向\ViewModels\ProductIndexViewModel.cs文件中的ProductIndexViewModel類添加SortBy和Sorts屬性,具體代碼如下列高亮顯示的代碼:
1 using System.Collections.Generic; 2 using System.Linq; 3 using System.Web.Mvc; 4 using BabyStore.Models; 5 6 namespace BabyStore.ViewModels 7 { 8 public class CategoryWithCount 9 { 10 public int ProductCount { get; set; } 11 public string CategoryName { get; set; } 12 public string CatNameWithCount 13 { 14 get 15 { 16 return CategoryName + " (" + ProductCount.ToString() + ")"; 17 } 18 } 19 } 20 21 public class ProductIndexViewModel 22 { 23 public IQueryable<Product> Products { get; set; } 24 public string Search { get; set; } 25 public IEnumerable<CategoryWithCount> CatsWithCount { get; set; } 26 public string Category { get; set; } 27 public string SortBy { get; set; } 28 public Dictionary<string, string> Sorts { get; set; } 29 30 public IEnumerable<SelectListItem> CatFilterItems 31 { 32 get 33 { 34 var allCats = CatsWithCount.Select(cc => new SelectListItem 35 { 36 Value = cc.CategoryName, 37 Text = cc.CatNameWithCount 38 }); 39 40 return allCats; 41 } 42 } 43 } 44 }
SortBy屬性用於保存在視圖中的Select元素的名稱,Sorts屬性用於保存顯示在Select元素中的數據。
現在,我們需要在ProductController類中為Sorts屬性賦值。修改\Controllers\ProductsController.cs文件,在Index方法的最后,返回視圖的代碼之前添加下列高亮顯示的代碼:
1 // GET: Products 2 public ActionResult Index(string category, string search, string sortBy) 3 { 4 // instantiate a new view model 5 ProductIndexViewModel viewModel = new ProductIndexViewModel(); 6 7 // select the products 8 var products = db.Products.Include(p => p.Category); 9 10 // perform the search and save the search string to the vieModel 11 if (!string.IsNullOrEmpty(search)) 12 { 13 products = products.Where(p => p.Name.Contains(search) || p.Description.Contains(search) || p.Category.Name.Contains(search)); 14 viewModel.Search = search; 15 } 16 17 // group search results into categories and count how many item in each category 18 viewModel.CatsWithCount = from matchinngProducts in products 19 where matchinngProducts.CategoryID != null 20 group matchinngProducts by matchinngProducts.Category.Name into catGroup 21 select new CategoryWithCount() 22 { 23 CategoryName = catGroup.Key, 24 ProductCount = catGroup.Count() 25 }; 26 27 if (!string.IsNullOrEmpty(category)) 28 { 29 products = products.Where(p => p.Category.Name == category); 30 } 31 32 // sort the results 33 switch (sortBy) 34 { 35 case "price_lowest": 36 products = products.OrderBy(p => p.Price); 37 break; 38 case "price_highest": 39 products = products.OrderByDescending(p => p.Price); 40 break; 41 default: 42 break; 43 } 44 45 viewModel.Products = products; 46 47 viewModel.Sorts = new Dictionary<string, string> 48 { 49 { "Price low to high", "price_lowest" }, 50 { "Price low to low", "price_highest" } 51 }; 52 53 return View(viewModel); 54 }
最后,我們需要向視圖添加一個控件,以便用戶可以進行選擇。為了完成這個功能,我們在Views\Products\Index.cshtml文件的按照分類來過濾產品信息的代碼后面,添加下列高亮顯示的代碼:
1 @model BabyStore.ViewModels.ProductIndexViewModel 2 3 @{ 4 ViewBag.Title = "Index"; 5 } 6 7 <h2>Index</h2> 8 9 <p> 10 @Html.ActionLink("Create New", "Create") 11 @using(Html.BeginForm("Index", "Products", FormMethod.Get)) 12 { 13 <label>Filter by category:</label>@Html.DropDownListFor(vm => vm.Category, Model.CatFilterItems, "All"); 14 <label>Sort by:</label>@Html.DropDownListFor(vm => vm.SortBy, new SelectList(Model.Sorts, "Value", "Key"), "Default") 15 <input type="submit" value="Filter"/> 16 <input type="hidden" name="Search" id="Search" value="@Model.Search"/> 17 } 18 </p> 19 <table class="table"> 20 <tr> 21 <th> 22 @Html.DisplayNameFor(model => model.Category) 23 </th> 24 <th> 25 @Html.DisplayNameFor(model => model.Products.First().Name) 26 </th> 27 <th> 28 @Html.DisplayNameFor(model => model.Products.First().Description) 29 </th> 30 <th> 31 @Html.DisplayNameFor(model => model.Products.First().Price) 32 </th> 33 <th></th> 34 </tr> 35 36 @foreach (var item in Model.Products) { 37 <tr> 38 <td> 39 @Html.DisplayFor(modelItem => item.Category.Name) 40 </td> 41 <td> 42 @Html.DisplayFor(modelItem => item.Name) 43 </td> 44 <td> 45 @Html.DisplayFor(modelItem => item.Description) 46 </td> 47 <td> 48 @Html.DisplayFor(modelItem => item.Price) 49 </td> 50 <td> 51 @Html.ActionLink("Edit", "Edit", new { id=item.ID }) | 52 @Html.ActionLink("Details", "Details", new { id=item.ID }) | 53 @Html.ActionLink("Delete", "Delete", new { id=item.ID }) 54 </td> 55 </tr> 56 } 57 58 </table>
新的Select控件使用視圖模型的SortBy屬性作為它的名字。使用視圖模型的Sorts屬性來生成現在在Select控件中的數據,其中Select控件的顯示文本使用Sorts屬性的Value值來指定,Select控件中數據的值使用Sorts屬性的Key值來指定,如圖5-2所示。
圖5-2:在產品索引(Index)頁面上的排序下拉列表
不調試啟動應用程序,然后點擊產品鏈接,在分類過濾下列列表后面,我們會看到一個用於按照價格排序的下列列表,如圖5-2所示。我們可以使用這個新控件按照價格對產品信息進行排序。
5.2 添加分頁
在這一小節,我們將學習一種添加分頁的方法,以允許用戶可以對產品搜索的結果進行分頁,而不是將整個結果顯示一個一個比較大的列表中。我們將使用流行的PagedList.Mvc包來實現該功能,該包由Troy Goode編寫和維護。我們之所以選擇它來進行分頁功能的實現,是因為該包比較容易設置和使用。在本書的后邊章節,我們將學習如何編寫我們自己的異步分頁代碼,並使用一個HTML輔助器來顯示分頁控件。
5.2.1 安裝PagedList.Mvc
首先,我們需要安裝包,點擊【項目】-【管理NuGet程序包】,打開NuGet包管理器窗體,在該窗體中,選擇瀏覽標簽,然后搜索pagedlist,如圖5-3所示。點擊安裝按鈕安裝PagedList.Mvc的最新版本(目前最新版本為4.5.0)。當安裝PagedList.Mvc后,PagedList包也被安裝上了。
圖5-3:NuGet包管理器中顯示的PagedList.Mvc
5.2.2 為實現分頁更新視圖模型和控制器
一旦安裝完PagedList.Mvc,第一件事就是要修改ProductIndexViewModel,以便將Products屬性的類型修改為IPagedList。修改ViewModels\ProductIndexViewModel.cs文件中的代碼如下列所示的高亮代碼:
1 using System.Collections.Generic; 2 using System.Linq; 3 using System.Web.Mvc; 4 using BabyStore.Models; 5 using PagedList; 6 7 namespace BabyStore.ViewModels 8 { 9 ... ... 10 11 public class ProductIndexViewModel 12 { 13 public IPagedList<Product> Products { get; set; } 14 public string Search { get; set; } 15 public IEnumerable<CategoryWithCount> CatsWithCount { get; set; } 16 public string Category { get; set; } 17 public string SortBy { get; set; } 18 public Dictionary<string, string> Sorts { get; set; } 19 20 ... ... 21 } 22 }
我們現在需要修改ProductsController類的Index方法,以便Products作為PagedList返回(使用ToPagedList()方法完成)。為了使用PagedLIst,我們還需要設置默認排序。為了使用PagedList包,我們首先需要在該文件的頂部添加using PagedList;代碼,然后修改Controllers\ProductsController.cs文件為下列高亮顯示的代碼。
1 public ActionResult Index(string category, string search, string sortBy, int? page) 2 { 3 // instantiate a new view model 4 ProductIndexViewModel viewModel = new ProductIndexViewModel(); 5 6 // select the products 7 var products = db.Products.Include(p => p.Category); 8 9 // perform the search and save the search string to the vieModel 10 if (!string.IsNullOrEmpty(search)) 11 { 12 products = products.Where(p => p.Name.Contains(search) || p.Description.Contains(search) || p.Category.Name.Contains(search)); 13 viewModel.Search = search; 14 } 15 16 // group search results into categories and count how many item in each category 17 viewModel.CatsWithCount = from matchinngProducts in products 18 where matchinngProducts.CategoryID != null 19 group matchinngProducts by matchinngProducts.Category.Name into catGroup 20 select new CategoryWithCount() 21 { 22 CategoryName = catGroup.Key, 23 ProductCount = catGroup.Count() 24 }; 25 26 if (!string.IsNullOrEmpty(category)) 27 { 28 products = products.Where(p => p.Category.Name == category); 29 viewModel.Category = category; 30 } 31 32 // sort the results 33 switch (sortBy) 34 { 35 case "price_lowest": 36 products = products.OrderBy(p => p.Price); 37 break; 38 case "price_highest": 39 products = products.OrderByDescending(p => p.Price); 40 break; 41 default: 42 products = products.OrderBy(p => p.Name); 43 break; 44 } 45 46 const int pageItems = 3; 47 int currentPage = (page ?? 1); 48 viewModel.Products = products.ToPagedList(currentPage, pageItems); 49 viewModel.SortBy = sortBy; 50 51 viewModel.Sorts = new Dictionary<string, string> 52 { 53 { "Price low to high", "price_lowest" }, 54 { "Price low to low", "price_highest" } 55 }; 56 57 return View(viewModel); 58 }
第一處改動是添加了一個int? page參數,它是一個可空整型,表示用戶在視圖中選擇的當前頁碼。當第一次加載產品的索引(Index)頁面時,用戶還沒有選擇任何頁碼,因此,這個參數可以為null。
我們必須確保當前的分類也要保存在視圖模型中,因此,我們添加了viewModel.Category = category;這行代碼。
代碼products = products.OrderBy(p => p.Name);用於對產品列表進行默認排序,這是因為PagedList要求列表必須是一個有序列表。
接着,我們使用代碼const int pageItems = 3;來指定每頁顯示的條目數。然后,我們聲明了一個整型變量int currentPage = (page ?? 1);來保存當前頁碼,該變量的值是page參數的值,或者是1(當page變量為null時)。
我們使用代碼viewModel.Products = products.ToPagedList(currentPage, PageItems);,對產品信息調用了ToPagedList方法,並將當前頁和每頁顯示的條目數傳遞給了ToPagedList方法,然后將該方法的返回值賦值給了視圖模型的Products屬性。
我們使用代碼viewModel.SortBy = sortBy;將sortBy參數的值保存到視圖模型的SortBy屬性中,以便我們從一頁移動到另一頁時,產品的排序保持不變。
5.2.3 為實現分頁更新產品的索引(Index)視圖
在視圖模型和控制器中完成了實現分頁的代碼之后,現在,我們需要更新\Views\Products\Index.cshtml文件來顯示一個分頁控件,以便用戶可以在各頁之間移動。我們同時也添加了有多少條目被發現的指示信息。為了完成這些功能,我們在該文件中添加了一個using語句,一個產品總數的指示信息以及在該頁底部顯示一個頁面之間的鏈接,具體代碼如下面的高亮顯示的代碼:
1 @model BabyStore.ViewModels.ProductIndexViewModel 2 @using PagedList.Mvc 3 4 @{ 5 ViewBag.Title = "Index"; 6 } 7 8 <h2>Index</h2> 9 10 <p> 11 @(string.IsNullOrWhiteSpace(Model.Search)?"Showing all":"You search for " + Model.Search + " found") @Model.Products.TotalItemCount products 12 </p> 13 14 <p> 15 @Html.ActionLink("Create New", "Create") 16 @using(Html.BeginForm("Index", "Products", FormMethod.Get)) 17 { 18 <label>Filter by category:</label>@Html.DropDownListFor(vm => vm.Category, Model.CatFilterItems, "All"); 19 <label>Sort by:</label>@Html.DropDownListFor(vm => vm.SortBy, new SelectList(Model.Sorts, "Value", "Key"), "Default") 20 <input type="submit" value="Filter"/> 21 <input type="hidden" name="Search" id="Search" value="@Model.Search"/> 22 } 23 </p> 24 <table class="table"> 25 <tr> 26 <th> 27 @Html.DisplayNameFor(model => model.Category) 28 </th> 29 <th> 30 @Html.DisplayNameFor(model => model.Products.First().Name) 31 </th> 32 <th> 33 @Html.DisplayNameFor(model => model.Products.First().Description) 34 </th> 35 <th> 36 @Html.DisplayNameFor(model => model.Products.First().Price) 37 </th> 38 <th></th> 39 </tr> 40 41 @foreach (var item in Model.Products) { 42 <tr> 43 <td> 44 @Html.DisplayFor(modelItem => item.Category.Name) 45 </td> 46 <td> 47 @Html.DisplayFor(modelItem => item.Name) 48 </td> 49 <td> 50 @Html.DisplayFor(modelItem => item.Description) 51 </td> 52 <td> 53 @Html.DisplayFor(modelItem => item.Price) 54 </td> 55 <td> 56 @Html.ActionLink("Edit", "Edit", new { id=item.ID }) | 57 @Html.ActionLink("Details", "Details", new { id=item.ID }) | 58 @Html.ActionLink("Delete", "Delete", new { id=item.ID }) 59 </td> 60 </tr> 61 } 62 63 </table> 64 65 <div> 66 Page @(Model.Products.PageCount < Model.Products.PageNumber ? 0 : Model.Products.PageNumber) of @Model.Products.PageCount 67 @Html.PagedListPager(Model.Products, page => Url.Action("Index", new { category = Model.Category, search = Model.Search, sortBy = Model.SortBy, page})) 68 </div>
指示有多少產品被發現的代碼如下所示:
1 <p> 2 @(string.IsNullOrWhiteSpace(Model.Search)?"Showing all":"You search for " + Model.Search + " found") @Model.Products.TotalItemCount products 3 </p>
這段代碼使用?:(也稱之為三元)操作符檢查搜索條件是否為null或空,如果結果為true,代碼的輸出結果為“Showing all xx products”,否則,如果用戶輸入了一個搜索條件,結果顯示為“Your search for search term found xx products”。實際上,這個操作符是if語句的快捷方式,關於?:操作符的更多信息可以在https://msdn.microsoft.com/en-gb/library/ty67wk28.aspx找到。
最后,分頁鏈接使用下列代碼生成:
1 <div> 2 Page @(Model.Products.PageCount < Model.Products.PageNumber ? 0 : Model.Products.PageNumber) of @Model.Products.PageCount 3 @Html.PagedListPager(Model.Products, page => Url.Action("Index", new { category = Model.Category, search = Model.Search, sortBy = Model.SortBy, page})) 4 </div>
為了便於顯示,這段代碼包裹在div標簽內。使用?:操作符的第一行代碼決定是否有任何頁碼顯示,它顯示“Page 0 of 0”或者“Page x of y”,x表示當前頁碼,y表示總頁數。
下一行代碼使用來自於PagedList.Mvc命名空間的PagedListPager輔助器。該輔助器接收一個產品列表參數,並為每個頁面生成一個超鏈接。Url.Action用於生成一個含有當前頁參數超鏈接目標。我們將一個匿名類型(含有當前分類、搜索條件、排序信息和分頁)傳遞給該輔助器方法,以便每個頁面的鏈接中都包含一個查詢字符串,這個查詢字符串包含有當前分類、搜索條件、排序信息和分頁信息。這意味着,當從一個頁面移動到另一個頁面時,搜索條件、選擇的分類和排序規則都被保存下來。如果沒有這樣做,產品列表將會被重置為顯示所有產品信息。
圖5-4顯示了使用上述代碼后,我們執行了一個對sleeping的搜索,然后對其結果按照sleeping分類進行過濾,並按照價格從高到低進行排序,並且移動到第2頁的效果。
圖5-4:伴有搜索條件、排序和按分類過濾的分頁效果
5.3 路由
到目前為止,我們一直使用作為URL一部分的查詢字符串中的參數,將分類和分頁信息由視圖傳遞給ProductController類的Index動作方法。這些URL的格式基本上為/Products?category=Sleeping&page=2,使用ASP.NET路由特性可以改進這些URL,以便讓這些URL對於用戶和搜索引擎更加友好和有意義。ASP.NET路由不僅能夠用於MVC,也可以用於Web Forms和Web API。但是在應用到Web Forms時,使用方法稍有不同。
為了讓事情更加可控,我們打算只為分類和分頁生成路由。不為搜索或排序生成路由的原因是:路由系統需要為每一種情況都制定唯一的一種模式,該模式可以用來唯一標識它們自己。例如,我們可以使用page前綴加數字的形式來生成一個路由。如果為每種情況都添加一個路由,會使路由系統過度復雜,難以維護。
關於路由的最重要的一個事情是:最具體的路由要添加在路由列表的前面,更加通用的路由要添加在路由列表的后面。路由系統根據最先被定義的路由模式來匹配一個URL,並且只有在不匹配時,才會對下一條路由進行處理,當匹配時,就停止搜索。如果存在一個一般路由和一個更加具體的路由都匹配一個URL,但是更加具體的路由在一般路由的后面定義,那么更加具體的路由永遠都不會被使用。
5.3.1 添加路由
我們將會采取和創建項目時,基架為我們在\App_Start\RouteConfig.cs文件中添加路由的方法一樣,來添加新的路由。基架為我們添加的路由格式為:
1 routes.MapRoute( 2 name: "Name", 3 url: "Rule", 4 defaults: DefaultValues 5 );
name參數表示路由的名字,它可以為空,但是,在本書中我們都會使用該參數以區分不同的路由。
url參數包含與URL格式相匹配的某個路由的一條規則,它可以包含多個格式和參數,如下所示:
- url參數被分為多個片段,每個片段匹配URL的某個部分。
- URL必須與片段具有相同數量的參數,以匹配每一個片段,除非使用了默認值或通配符(參見下面列表中的每一個解釋)。
- 每個片段可以是:
- 靜態URL片段:例如,“Products”,這將會匹配/Products形式的URL,並且調用與之有關的控制器和動作方法。
- 片段變量:用於匹配任何事物。例如,“Products/{category}”將會匹配URL中跟在Products/后面的任何事物,並將其賦值給變量category,然后變量category會被傳遞給該路由的目標動作方法的參數(該參數的名稱必須為category)。
- 靜態片段和片段變量的組合:將會匹配與任何指定格式相匹配的URL。例如,“Products/Page{page}”將會匹配諸如Products/Page2或Products/Page99形式的URL,並且會將2或99賦值給變量page。
- 全匹配的片段變量:例如,“Products/{*everything}”將會匹配URL中Products/后面的部分,而不管URL包含多少片段數,也不管這些片段數的值是什么,它們都會賦給everything變量。在這個項目中,我們沒有使用全匹配的片段變量。
- 每一個片段也可以指定為可選的,或者具有一個默認值(如果與之對應的URL部分為空)。一個比較好的使用默認值的例子是項目創建時,基架為我們創建的默認路由。這個路由使用下面的代碼指定控制器和動作方法的默認值,並且定義了一個可選的id變量。
1 routes.MapRoute( 2 name: "Default", 3 url: "{controller}/{action}/{id}", 4 defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } 5 );
我們添加一個與URL的格式/Products/Category(比如Products/Sleeping只顯示分類為Sleeping的產品信息)相匹配的路由,來開始我們的路由學習。在\App_Start\RouteConfig.cs文件的RegisterRoutes方法的默認路由上面,添加下列高亮顯示的代碼:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Web; 5 using System.Web.Mvc; 6 using System.Web.Routing; 7 8 namespace BabyStore 9 { 10 public class RouteConfig 11 { 12 public static void RegisterRoutes(RouteCollection routes) 13 { 14 routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 15 16 routes.MapRoute( 17 name: "ProductsbyCategory", 18 url: "Products/{category}", 19 defaults: new { controller = "Products", action = "Index" } 20 ); 21 22 routes.MapRoute( 23 name: "Default", 24 url: "{controller}/{action}/{id}", 25 defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } 26 ); 27 } 28 } 29 }
不調試啟動應用程序,然后點擊分類鏈接,在分類索引頁面點擊Sleeping鏈接,這個鏈接打開的URL為/Product/Sleeping,如圖5-5所示,這歸功於我們剛剛添加的新路由ProductsbyCategory。
圖5-5:格式為Products/Category的URL
到目前為止,一切看起來還不錯,我們現在可以使用Products/Category格式的ULR了。但是,這里有一個問題。嘗試着點擊Create New鏈接,產品創建頁面沒有顯示,取而代之的是一個顯示了空的產品列表的頁面。之所以出現這個問題,是因為新路由對出現在Products后面的任何事物看作是一個分類,而我們沒有一個叫做Create的分類,因此,沒有產品信息返回,如圖5-6所示。
圖5-6:出了問題的Create New鏈接
現在點擊回退按鈕返回到產品的索引(Index)頁面,試着點擊Edit、Details和Delete鏈接,它們依然正常工作!我們可能想知道為什么會這樣,答案是這些鏈接都包含有一個ID參數,例如,編輯鏈接的格式為/Products/Edit/6,這個格式匹配原來的默認路由("{controller}/{action}/{id})而不是我們剛剛創建的ProductsbyCategory路由("Products/{category})。
為了解決這個問題,我們需要為格式為Products/Create的URL添加一個更加具體的路由。在App_Start\RouteConfig.cs文件中的RegisterRoutes方法的ProductsbyCategory路由上面添加下面高亮顯示的新路由代碼:
1 public static void RegisterRoutes(RouteCollection routes) 2 { 3 routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 4 5 routes.MapRoute( 6 name: "ProductsCreate", 7 url: "Products/Create", 8 defaults: new { controller = "Products", action = "Create" } 9 ); 10 11 routes.MapRoute( 12 name: "ProductsbyCategory", 13 url: "Products/{category}", 14 defaults: new { controller = "Products", action = "Index" } 15 ); 16 17 routes.MapRoute( 18 name: "Default", 19 url: "{controller}/{action}/{id}", 20 defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } 21 ); 22 }
不調試啟動應用程序,然后點擊Create New鏈接,因為ProductsCreate路由,現在創建新產品的功能又能正常實現了。非常重要的一點是要把ProductsCreate路由放在ProductsByCategory路由上面,否則,它永遠不會被使用。如果把它放在ProductsByCategory路由的下面,路由系統將會首先匹配“Products/{category}”,並將停止繼續匹配路由。
下一步,我們將會為分頁添加一個路由,以便應用程序可以使用格式為/Products/Page2的URL。更新App_Start\RouteConfig.cs文件的RegisterRoutes方法,在該方法的ProductbyCategory路由上面添加一個新的路由,其代碼如下所示:
新的ProductsByPage路由將會匹配任何形如Products/PageX的URL,在這兒X表示頁碼。再一次申明,這個路由要放在ProductsbyCategory的前面,否則,它將永遠不會被使用。然后,點擊頁面下方的分頁控件中的頁碼,現在形如Products/PageX格式的URL將會出現在地址欄中。比如,5-7顯示的是點擊分頁控件中的數字4時,所生成的URL為Products/Page4的結果。
圖5-7:格式為Products/PageX的路由
到目前為止,我們已經為Products/Category和Product/PageX添加了路由,但是,我們還沒有為Product/Category/PageX添加路由,為了添加一條允許這種格式的新路由,我們在App_Start\RouteConfig.cs文件中的RegisterRoutes方法的ProductsByPage路由上面,添加下列代碼:
1 public static void RegisterRoutes(RouteCollection routes) 2 { 3 routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 4 5 routes.MapRoute( 6 name: "ProductsCreate", 7 url: "Products/Create", 8 defaults: new { controller = "Products", action = "Create" } 9 ); 10 11 routes.MapRoute( 12 name: "ProductsbyCategorybyPage", 13 url: "Products/{category}/Page{page}", 14 defaults: new { controller = "Products", action = "Index" } 15 ); 16 17 routes.MapRoute( 18 name: "ProductsbyPage", 19 url: "Products/Page{page}", 20 defaults: new { controller = "Products", action = "Index" } 21 ); 22 23 routes.MapRoute( 24 name: "ProductsbyCategory", 25 url: "Products/{category}", 26 defaults: new { controller = "Products", action = "Index" } 27 ); 28 29 routes.MapRoute( 30 name: "Default", 31 url: "{controller}/{action}/{id}", 32 defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } 33 ); 34 }
不調試啟動應用程序,點擊分類鏈接,然后點擊Sleeping鏈接,緊接着再點擊分頁控件中的頁碼2,現在生成的URL的格式應該為Products/Sleeping/Page2,因為它匹配新的路由ProductsbyCategorybyPage。如圖5-8所示。
圖5-8:ProductsbyCategorybyPage路由的格式
我們現在似乎添加完了所有的路由,但是,由於新添加路由的影響,現在應用程序存在一些小問題。先看第一個遺留問題,啟動應用程序,導航到圖5-8所示的頁面。然后,在分類下拉列表中選擇另一個分類,然后點擊Filter按鈕,我們將會看到結果沒有變化,依然是遺留的Sleeping分類的產品信息。這是因為HTML表單的目標現在不再是正確的ProductsController控制器的Index動作方法了。為了解決這個問題,我們向/App_Start/RouteConfig.cs文件添加最后一個路由,然后配置HTML表單來使用它。
首先,在App_Start\RoutesConfig.cs文件中的RegisterRoutes方法的默認路由的上面添加一個新路由,代碼如下:
1 public static void RegisterRoutes(RouteCollection routes) 2 { 3 routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 4 5 routes.MapRoute( 6 name: "ProductsCreate", 7 url: "Products/Create", 8 defaults: new { controller = "Products", action = "Create" } 9 ); 10 11 routes.MapRoute( 12 name: "ProductsbyCategorybyPage", 13 url: "Products/{category}/Page{page}", 14 defaults: new { controller = "Products", action = "Index" } 15 ); 16 17 routes.MapRoute( 18 name: "ProductsbyPage", 19 url: "Products/Page{page}", 20 defaults: new { controller = "Products", action = "Index" } 21 ); 22 23 routes.MapRoute( 24 name: "ProductsbyCategory", 25 url: "Products/{category}", 26 defaults: new { controller = "Products", action = "Index" } 27 ); 28 29 routes.MapRoute( 30 name: "ProductsIndex", 31 url: "Products", 32 defaults: new { controller = "Products", action = "Index" } 33 ); 34 35 routes.MapRoute( 36 name: "Default", 37 url: "{controller}/{action}/{id}", 38 defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } 39 ); 40 }
這條新的路由命名為ProductsIndex,它的目標是ProductsController控制器的Index動作方法。當使用URL鏈接和表單時,我們創建的這條路由會讓應用程序調用ProductsController控制器的Index動作方法。
5.3.2 在窗體中使用路由
當前,在產品索引(Index)頁面中的,按分類過濾產品信息的表單,其工作是不正確的,因為它依然被配置為調用ProductsController控制器類的Index方法。(譯者注:其實,之所以工作不正確,是因為該表單的目標URL被ProductsbyCategory路由所匹配,因此,當形如http://localhost:58735/Products/Sleeping?Category=Feeding&SortBy=&Search=的URL向服務器發送請求時,會匹配ProductsbyCategory路由,該路由會將Sleeping賦值給category變量,該變量會傳遞給Index動作方法的category參數,因此,會導致依然是查詢的Sleeping分類的產品,而不是Feeding分類的產品)。例如,當點擊分類鏈接,然后再點擊Sleeping鏈接,之后再在分類下拉列表中選擇Feeding,最后點擊Filter按鈕提交表單,這時的URL格式為:
http://localhost:5073/Products/Sleeping?Category=Feeding&SortBy=&Search=
這個URL包含兩個category參數:Sleeping和Feeding,因為應用程序只是簡單地按照第一個匹配的參數進行過濾,因此,依然按照Sleeping分類進行產品信息的過濾。為了解決這個問題,包含按照分類過濾產品的表單應該使用ProductsIndex路由,該路由會將/Sleeping?Category=Feeding&SortBy=&Search=中的/Sleeping前綴移除,並將?Category=Feeding&SortBy=&Search=作為查詢字符串的參數提交給Index動作方法,這個時候只有Category=Feeding才作為category參數傳遞給Index動作方法的category參數。
為了讓表單使用ProductsIndex路由,修改Views\Products\Index.cshtml文件,將其中的
1 @using (Html.BeginForm("Index", "Products", FormMethod.Get))
修改為
1 @using (Html.BeginRouteForm("ProductsIndex", FormMethod.Get))
不調試啟動應用程序,然后點擊分類鏈接,然后再點擊分類索引頁中的Sleeping鏈接,然后再點擊分頁控件中的頁碼2,這時應該如5-8所示。然后再分類下拉列表中選擇Feeding,然后點擊Filter按鈕,這個時候的結果應該就是按照Feeding分類過濾的產品信息。
提示:HTML表單只能被配置為提交給路由,它們不會按路由格式提交值,比如,過濾表單提交的URL依然是Products?Category=Feeding&SortBy=&Search=格式,而不是Products/Feeding。這是因為HTML表單的默認行為就是按這種格式提交URL,輸入元素被追加到URL的查詢字符串中。
和過濾表單一樣,搜索表單也存在這樣的問題,因此,按下列代碼更新/Views/Shared/_Layout.cshtml文件,將
1 @using (Html.BeginForm("Index", "Products", FormMethod.Get, new { @class = "navbar-form navbar-left" }))
更改為
1 @using (Html.BeginRouteForm("ProductsIndex", FormMethod.Get, new { @class = "navbar-form navbar-left" }))
5.3.3 在超鏈接中使用路由
需要解決的最后一個問題是當用戶在分類索引(Index)頁面中點擊某個分類的鏈接之后,這會導航到產品索引(Index)頁面,在這個時候再點擊導航欄中的產品鏈接,不會顯示全部產品信息。這個問題和我們上面解決的問題類似,為了解決這個問題,我們需要將輸出URL鏈接更新為路由,而不是動作方法。修改\Views\Shared\_Layout.cshtml中的代碼由
1 <li>@Html.ActionLink("產品", "Index", "Products")</li>
修改為
1 <li>@Html.RouteLink("產品", "ProductsIndex")</li>
為了將額外的參數由URL傳遞給目標路由,我們可以給它們傳遞一個匿名對象。例如,使一個鏈接的目標僅僅是Clothes分類,我們可以使用下列代碼:
1 <li>@Html.RouteLink("View all Clothes", "ProductsbyCategory", new { category = "Clothes" })</li>
5.4 設置項目的起始URL
我們已經添加了一些路由,現在,我們有必要停止Visual Studio自動加載我們當前正在編輯的視圖作為項目的啟動視圖。在Visual Studio中,點擊【項目】->【BabyStore屬性】打開項目屬性窗體(或者右鍵點擊解決方案資源管理器中的項目,然后選擇屬性)。然后選擇Web標簽,設置啟動操作為特定頁,如圖5-9所示。不需要輸入值,僅僅設置這個選擇即可以使得項目加載Home頁(主頁)。
圖5-9:設置項目的啟動URL為特定頁
5.5 小節
在這一章中,我們首先使用Entity Framework添加了對搜索結果的排序功能,然后又在視圖中添加了一個下列列表,以便用戶使用它來對產品信息按照價格進行排序。其次,我們使用PagedList.Mvc包向產品的索引(Index)頁面添加了分頁功能。最后,我們使用了ASP.NET的路由特性以使我們的站點對於分類和分頁信息顯示更加友好的URL。