在這一章中,我們首先添加一個搜索產品的模塊以增強站點的功能,然后使用視圖模型而不是ViewBag向視圖傳遞復雜數據。
注意:如果你想按照本章的代碼編寫示例,你必須完成第二章或者直接從www.apress.com下載第二章的源代碼。
3.1 添加產品搜索
為了執行產品搜索,我們將添加一些功能使其能夠按照產品名稱、描述和分類進行搜索,從而讓用戶有一個更好的選擇來查找相關結果。
之所以將分類也包含在內,是因為如果用戶輸入的是“clothes”,而不是一件特定的衣服,那么所有的衣服都會被搜索到。如果我們不將分類作為搜索條件,用戶可能不會搜索到任何結果,這是因為產品的名稱或描述字段中可能沒有包含“clothes”這個單詞。
我們將使用LINQ to Entities中的方法來搜索相關產品信息。
3.1.1 修改控制器以進行產品搜索
修改Controllers\ProductsController.cs文件中的Index方法以添加產品搜索功能,代碼如下所示:
1 public ActionResult Index(string category, string search) 2 { 3 var products = db.Products.Include(p => p.Category); 4 5 if (!string.IsNullOrEmpty(category)) 6 { 7 products = products.Where(p => p.Category.Name == category); 8 } 9 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 } 14 15 return View(products.ToList()); 16 }
首先,在Index方法中添加了一個名為search的參數,然后,判斷該參數是否為null或空,如果不為null或空,則我們使用下面的代碼來進行產品的搜索:
1 if (!string.IsNullOrEmpty(search)) 2 { 3 products = products.Where(p => p.Name.Contains(search) || p.Description.Contains(search) || p.Category.Name.Contains(search)); 4 }
按照直白的說法,上面的代碼表示的含義是:如果產品的名稱、產品的描述以及產品的分類名稱中含有search,則這些產品都會被作為結果返回。這段代碼也使用了lambda表達式,但是這個表達式更加復雜,並且使用了邏輯或(||)操作符。注意,在lambda操作符(=>)左邊依然只有一個參數,雖然在其右邊的代碼中有多條語句。當這條語句提交到數據庫時,Contains方法被轉換為SQL的LIKE操作,並且它不是大小寫敏感的。
3.1.2 測試產品搜索
點擊【開始調試(不執行)】菜單項啟動應用程序以測試新的搜索功能,在首頁中點擊產品鏈接,打開產品索引(Index)頁面,在URL后面手動添加?search=red,然后回車,我們將會看到如圖3-1所示的結果。

圖3-1:通過URL手動搜索含有“red”的產品
為了測試按照分類名稱搜索產品是否能夠正確工作,我們修改URL為/Products?search=clothes。這個查詢現在將匹配在產品名稱或產品描述或分類名稱中含有clothes這個單詞的所有產品,結果如圖3-2所示。

圖3-2:通過修改URL搜索屬於clothes分類的產品
3.1.3 在站點主導航欄中添加搜索框
我們不應該期望用戶能夠手動輸入URL以搜索產品,因此,我們有必要在站點中添加一個搜索框。我們將在站點的主導航欄中添加這個搜索框,以便用戶可以一直看到它,並可以在任何頁面中使用它。
就像在第一章講述的那樣,主導航欄是站點布局頁的一部分,因此它包含在Views\Shared\_Layout.cshtml文件中。
首先,刪除站點中的關於和聯系方式連接。在這個例子中不會使用到它們,並且它們還占用空間。為了完成此工作,從Views\Shared\_Layout.cshtml文件中刪除以下代碼:
1 <li>@Html.ActionLink("關於", "About", "Home")</li> 2 <li>@Html.ActionLink("聯系方式", "Contact", "Home")</li>
為了添加一個搜索框,編輯Views\Shared\_Layout.cshtml文件中class屬性為“navbar-collapse collapse”的div,將其代碼修改為如下高亮顯示的代碼:
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 5 <meta charset="utf-8" /> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 <title>@ViewBag.Title - 我的 ASP.NET 應用程序</title> 8 @Styles.Render("~/Content/css") 9 @Scripts.Render("~/bundles/modernizr") 10 11 </head> 12 <body> 13 <div class="navbar navbar-inverse navbar-fixed-top"> 14 <div class="container"> 15 <div class="navbar-header"> 16 <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> 17 <span class="icon-bar"></span> 18 <span class="icon-bar"></span> 19 <span class="icon-bar"></span> 20 </button> 21 @Html.ActionLink("Baby Store", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" }) 22 </div> 23 <div class="navbar-collapse collapse"> 24 <ul class="nav navbar-nav"> 25 <li>@Html.ActionLink("主頁", "Index", "Home")</li> 26 <li>@Html.ActionLink("分類", "Index", "Categories")</li> 27 <li>@Html.ActionLink("產品", "Index", "Products")</li> 28 </ul> 29 @using (Html.BeginForm("Index", "Products", FormMethod.Get, new { @class = "navbar-form navbar-left" })) 30 { 31 <div class="form-group"> 32 @Html.TextBox("Search", null, new { @class = "form-control", @placeholder = "Search Products" }) 33 </div> 34 <button type="submit" class="btn btn-default">Submit</button> 35 } 36 @Html.Partial("_LoginPartial") 37 </div> 38 </div> 39 </div> 40 <div class="container body-content"> 41 @RenderBody() 42 <hr /> 43 <footer> 44 <p>© @DateTime.Now.Year - 我的 ASP.NET 應用程序</p> 45 </footer> 46 </div> 47 48 @Scripts.Render("~/bundles/jquery") 49 @Scripts.Render("~/bundles/bootstrap") 50 @RenderSection("scripts", required: false) 51 </body> 52 </html>
表單使用GET而不是POST請求以便搜索條件可以在URL中看到。因此,用戶可以復制它然后使用其他方式分享它,比如e-mail或社交媒體。按照慣例,GET請求用於查詢數據庫中的數據,而不是修改數據庫中的數據。
在這段代碼中還涉及了前面我們沒有使用過的HTML5的placeholder特性,它主要用於在頁面首次加載時,在搜索框架中顯示“Search Products”文本,以向用戶提示該搜索框的用途。這主要通過在代碼@Html.TextBox("Search", null, new { @class = "form-control", @placeholder = "Search Products"})中向htmlAttributes參數對象傳遞額外的條目完成的。文本框的name屬性被指定為Search,MVC框架將其與搜索參數Search匹配。圖3-3顯示了導航欄的最終效果。

圖3-3:完成搜索框之后的導航欄
使用搜索框執行一些搜索,結果應該和手動在URL中輸入搜索條件的效果一樣。
3.1.4 如何使用Bootstrap添加樣式
Bootstrap是一個HTML、CSS和JavaScript的框架,起初由Twitter創建。使用基架的ASP.NET項目默認使用它來樣式化站點的外觀。在網上有一大堆關於Bootstrap的可用信息,因此,我們不會討論關於它的任何細節,但是,當我們引入新的知識時,我們會解釋為什么我們會那樣樣式化它。
那么,我們如何知道搜索框的樣式?答案在於Thomas Park創建的站點http://www.bootswatch.com。這個站點提供了幾個免費的Bootstrap主題,還提供了許多元素的HTML預覽。如果我們點擊Themes按鈕中的某個主題,然后往下拉動頁面,並且將鼠標懸停在某個元素上,這個時候<>符號將會出現在該元素的右上角,如圖3-4所示。如果點擊這個符號,HTML預覽就會出現,如圖3-5所示。

圖3-4:在bootswatch.com站點上出現在某個元素右上角的預覽符號<>

圖3-5:來自於bootswatch.com的HTML預覽。高亮部分顯示了如何樣式化一個搜索框以及如何在導航中加入表單
3.2 使用ViewBag按照分類過濾搜索結果
下面,我們添加一個新功能以便用戶可以按照分類過濾搜索結果。在這個例子中,我們將在搜索結果頁面中使用ViewBag來顯示一個含有分類信息的下拉框。我們將這個下拉框與用戶輸入的搜索條件相關聯,以便在下拉框中只顯示與搜索結果相關的分類,同時不允許用戶選擇一個空的分類。
3.2.1 修改ProductsController控制器的Index方法以實現按照分類進行過濾
按下面的代碼修改Controllers\ProductsController.cs文件中的Index方法,以便在ViewBag中存儲當前搜索的條件,並且生成一個去除重復值的分類列表,並將其作為SelectList對象存儲在ViewBag中。
1 public ActionResult Index(string category, string search) 2 { 3 var products = db.Products.Include(p => p.Category); 4 5 if (!string.IsNullOrEmpty(category)) 6 { 7 products = products.Where(p => p.Category.Name == category); 8 } 9 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 ViewBag.Search = search; 14 } 15 16 var categories = products.OrderBy(p => p.Category.Name).Select(p => p.Category.Name).Distinct(); 17 18 ViewBag.Category = new SelectList(categories); 19 20 return View(products.ToList()); 21 }
將搜索條件保存在VeiwBag中,以便當用戶點擊分類過濾時可以重用該搜索條件。如果我們不保存該搜索條件,那么這個搜索條件將會被丟棄,產品信息將不會被正確地被過濾。
代碼var categories = products.OrderBy(p => p.Category.Name).Select(p => p.Category.Name).Distinct();生成了一個按字母排序並且去除重復值的分類列表。這個分類列表是不全面的,它只包含與搜索條件相關的產品所包含的分類信息。
最后,我們根據categories變量生成了一個SelectList對象,並將其保存在ViewBag中,以便在視圖中使用。
3.2.2 向產品的索引(Index)頁面添加過濾功能
為了使\Products\Index.cshtml文件生成的HTML頁面具有按分類過濾的功能,我們需要添加一個新的HTML表單以提交過濾請求。我們在Views\Products\Index.cshtml文件的Create New鏈接后面添加一個帶有下列列表的新表單,代碼如下所示:
1 <p> 2 @Html.ActionLink("Create New", "Create") 3 @using(Html.BeginForm("Index", "Products", FormMethod.Get)) 4 { 5 <label>Filter by category:</label> @Html.DropDownList("Category", "All") 6 <input type="submit" value="Filter"/> 7 <input type="hidden" name="Search" id="Search" value="@ViewBag.Search"/> 8 } 9 </p>
這段代碼添加了一個表單,該表單使用GET方法向ProductsController控制器的Index動作方法提交請求,因此,查詢字符串所包含的值一並被提交到服務端。使用代碼@Html.DropDownList("Category", "All")生成一個下拉列表,該下拉列表使用了ViewBag.Category屬性所提供的值,參數“All”對下拉列表指定了一個默認值。添加的提交按鈕Filter可以讓用戶提交表單並執行過濾。一個隱藏的HTML元素被用來存儲當前的搜索條件,當按分類過濾產品信息並提交表單時,可以保證用戶最初輸入的搜索條件被保存下來。
不調試啟動站點,圖3-6顯示了搜索“Red”單詞后的產品的索引(Index)頁面的樣子,注意All是過濾下拉框的默認值。

圖3-6:帶有過濾功能的產品索引(Index)頁面
看起來代碼都像預期的一樣被正確執行,然而,代碼存在一個小問題。圖3-7顯示了搜索“Red”之后,又按照Clothes分類過濾之后的效果。

圖3-7:搜索“Red”后按照Clothes過濾后的頁面
這個問題是在在按照Clothes分類過濾后,在分類列表中的Toys分類消息了。修正這個問題很簡單,但是它強調了需要按正確的順序來創建我們的查詢。為了修正這個問題,向如下所示的代碼一樣修改\Controllers\ProductsController.cs文件中的Index方法,以使按照分類過濾產品的代碼放在categories變量已被賦值的后面:
1 public ActionResult Index(string category, string search) 2 { 3 var products = db.Products.Include(p => p.Category); 4 5 if (!string.IsNullOrEmpty(search)) 6 { 7 products = products.Where(p => p.Name.Contains(search) || p.Description.Contains(search) || p.Category.Name.Contains(search)); 8 ViewBag.Search = search; 9 } 10 11 var categories = products.OrderBy(p => p.Category.Name).Select(p => p.Category.Name).Distinct(); 12 13 if (!string.IsNullOrEmpty(category)) 14 { 15 products = products.Where(p => p.Category.Name == category); 16 } 17 18 ViewBag.Category = new SelectList(categories); 19 20 return View(products.ToList()); 21 }
不調試啟動站點,如果我們現在執行一個搜索,然后按照分類進行過濾,我們會發現在分類下拉列表中的Toys分類依然存在,如圖3-8所示。

圖3-8:搜索“Red”,然后按Clothes分類過濾之后,Toys分類依然可以讓用戶進行選擇
3.3 使用視圖模型實現更加復雜的過濾
一旦我們使用ViewBag將多個數據由控制器傳遞給視圖時,我們的代碼會變得難以維護,這主要歸咎於ViewBag的動態特性,Visual Studio沒有對ViewBag可用的屬性提供智能提示功能,這很容易造成ViewBag屬性的拼寫錯誤。
由控制器向視圖傳遞信息更好的一種方法是使用視圖模型,而不是ViewBag。然后視圖基於這種模型,而不是基於域模型(到目前為止,我們的視圖都是基於域模型的)。一些開發人員將這種概念進一步細分,將他們所有的視圖都僅僅基於視圖模型。在本書中,我們混用視圖模型和域模型。
在這個例子中,我們將學習對於每一個分類,如何實現在下拉列表中同時顯示該分類所對的產品數量。為了實現這個功能,我們需要一個視圖模型保存我們傳遞給視圖的所有信息。
3.3.1 創建視圖模型
在BabyStore項目下創建一個名為“ViewModels”的新文件夾,然后在其添加一個名為ProductIndexViewModel的類,然后在該類中編寫如下代碼:
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 28 public IEnumerable<SelectListItem> CatFilterItems 29 { 30 get 31 { 32 var allCats = CatsWithCount.Select(cc => new SelectListItem 33 { 34 Value = cc.CategoryName, 35 Text = cc.CatNameWithCount 36 }); 37 38 return allCats; 39 } 40 } 41 } 42 }
這個類文件看起來比我們之前使用的代碼都要復雜,因此,我們將一步一步地解釋每個屬性的用途。
首先,這個文件包含兩個類,分別為CategoryWithCount和ProductIndexViewModel。
CategoryWithCount類比較簡單,主要用於保存分類的名字和該分類所包含該的產品數量。
第一個屬性ProductCount用於保存該分類中所包含的產品數量。
第二個屬性CategoryName用於保存分類的名稱。
第三個屬性CatNameWithCount返回ProductCount和CategoryName合並之后的格式化字符串,例如:Clothes (2)。
ProductIndexViewModel類需要保存先前使用ViewBag傳遞給視圖的信息組合以及模型IEnumerable<BabyStore.Models.Product>(該模型出現在/Views/Products/Index.cshtml文件的頂部)。
在這個類中的第一個屬性public IQueryable<Product> Products { get; set; },將會用於替換在當前視圖中使用的模型。
第二個屬性是public string Search { get; set; },將會用於替換在當前ProductsController類中使用的ViewBag.Search。
第三個屬性是public IEnumerable<CategoryWithCount> CatsWithCount { get; set; },將會用於存儲CategoryWithCount類型的集合,該集合用於視圖中的下拉列表中。
第四個屬性Category將會用作視圖中的下拉列表的名稱。
最后一個屬性public IEnumerable<SelectListItem> CatFilterItems用於返回一個SelectListItem類型的列表,該屬性將會利用第三個屬性CatsWithCount生成SelectListItem對象,該對象的Value對應CategoryWithCount的CategoryName,Text對應CategoryWithCount的CatNameWithCount;Value用於視圖中下拉列表的值,Text用於視圖中下拉列表的顯示文本。
3.3.2 修改ProductsController控制器的Index方法以使用視圖模型
更新\Controllers\ProductsController.cs文件的Index方法以便它匹配下列代碼。改動的代碼使用粗體高亮顯示:使用視圖模型而不是ViewBag返回項目的計數。首先,在該文件的上方添加一個using語句,以使該類可以訪問我們剛剛創建的ProductIndexViewModel類。
1 using BabyStore.ViewModels; 2 3 public ActionResult Index(string category, string search) 4 { 5 // instantiate a new view model 6 ProductIndexViewModel viewModel = new ProductIndexViewModel(); 7 8 var products = db.Products.Include(p => p.Category); 9 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 viewModel.Products = products; 32 33 return View(viewModel); 34 }
在這個方法內的第一處改動的代碼是創建一個視圖模型:ProductIndexViewModel viewModel = new ProductIndexViewModel();
代碼viewModel.Search = search;將變量search賦值給viewModel而不是ViewBag。
第三處代碼改動是LINQ語句,該語句將一個CategoryWithCount對象的列表賦值給viewModel的CatsWithCount屬性。在這個例子中,因為這個查詢比較復雜,我們使用了另外一種形式的LINQ,該形成稱之為查詢語法(query syntax),該語法使得查詢語句更容易閱讀。
該語句的下列代碼,首先使用where子句過濾掉分類ID為null的產品,然后按照分類名稱進行分組。
from matchinngProducts in products
where matchinngProducts.CategoryID != null
group matchinngProducts by matchinngProducts.Category.Name into catGroup
對於每一個分組,分類的名稱和該分類所對應的產品數量被賦值給一個CategoryWithCount對象。
select new CategoryWithCount()
{
CategoryName = catGroup.Key,
ProductCount = catGroup.Count()
};
最后一處改動的代碼將products變量賦值給viewModel的Products屬性,而不是將它傳遞給視圖。緊接着,我們將viewModel傳遞給視圖,代碼如下:
viewModel.Products = products;
return View(viewModel);
注意,不屬於任何分類的的產品將不會顯示在分類的下拉列表中,但是依然能夠被搜索到。
3.3.3 修改視圖以便使用視圖模型完成新過濾功能
更新\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 <input type="submit" value="Filter"/> 15 <input type="hidden" name="Search" id="Search" value="@Model.Search"/> 16 } 17 </p> 18 <table class="table"> 19 <tr> 20 <th> 21 @Html.DisplayNameFor(model => model.Category) 22 </th> 23 <th> 24 @Html.DisplayNameFor(model => model.Products.First().Name) 25 </th> 26 <th> 27 @Html.DisplayNameFor(model => model.Products.First().Description) 28 </th> 29 <th> 30 @Html.DisplayNameFor(model => model.Products.First().Price) 31 </th> 32 <th></th> 33 </tr> 34 35 @foreach (var item in Model.Products) { 36 <tr> 37 <td> 38 @Html.DisplayFor(modelItem => item.Category.Name) 39 </td> 40 <td> 41 @Html.DisplayFor(modelItem => item.Name) 42 </td> 43 <td> 44 @Html.DisplayFor(modelItem => item.Description) 45 </td> 46 <td> 47 @Html.DisplayFor(modelItem => item.Price) 48 </td> 49 <td> 50 @Html.ActionLink("Edit", "Edit", new { id=item.ID }) | 51 @Html.ActionLink("Details", "Details", new { id=item.ID }) | 52 @Html.ActionLink("Delete", "Delete", new { id=item.ID }) 53 </td> 54 </tr> 55 } 56 57 </table>
對此文件所做的代碼修改十分簡單,但意義重大。第一處改動@model BabyStore.ViewModels.ProductIndexViewModel只是簡單地告訴視圖使用ProductIndexViewModel作為該視圖基於的模型。注意該模型現在是一個單一的類,而不是一個可枚舉的集合。
第二處改動@Html.DropDownListFor(vm => vm.Category, Model.CatFilterItems, "All");基於視圖模型的CatFilterItems屬性生成一個下拉列表,第二個參數用於生成下拉列表中的顯示數據以及值。第一個參數vm => vm.Category指定該下拉列表的名稱,當提交該表單時,該下拉列表的名稱將出現在URL的查詢字符串部分。因為該控件的名稱是Category,因此,我們先前在URL中查找Category參數的代碼將會繼續正確地工作。
第三次改動保確保隱藏域現在引用的是視圖模型而不是ViewBag:
<input type="hidden" name="Search" id="Search" value="@Model.Search" />
當我們需要生成表格標題時,不像看起來那么簡單,因為HTML輔助器的DisplayNameFor方法不適用於集合,因此,我們基於視圖模型的Products屬性來顯示標題。分類標題十分簡單,因為這是視圖模型的一個屬性。但是,顯示Product類的Description屬性的名字不能使用類似這樣的代碼@Html.DisplayNameFor(model => model.Products.Description),而是要求我們使用First()方法,強制輔助器使用自於products集合中的,一個實際的Product實體,代碼如下:
@Html.DisplayNameFor(model => model.Products.First().Description)
這段代碼將會生成一個基於Product類的Description屬性的表格標題。如果在數據庫中的Products表中沒有數據,該代碼也會正確工作。
提示:當HTML輔助器的DisplayNameFor方法用於集合而不是單一對象時,使用First()方法以便訪問我們要想顯示的屬性的名稱。
最后一處改動是@foreach (var item in Model.Products) {,確保我們現在使用視圖模型的Products屬性來顯示產品信息。
不調試啟動站點,然后點擊產品鏈接,我們會發現在分類下拉列表中的每個分類都帶有一個數量,這反映出每個分類所包含的產品的數量。圖3-9顯示了我們在搜索框中輸入“Red”后,下拉列表所顯示的條目。

圖3-9:在下拉列表中的每一個分類都包含所匹配的產品數量
3.4 小節
在這一章中,我們學習了如何添加搜索功能,包括如何使用Bootstrap添加一個搜索框,然后學習了怎樣發現更多關於使用Boootstrap如何樣式化的站點的信息。我們還演示了如何使用ViewBag添加一個對於搜索結果的過濾,以及如何使用視圖模型添加一個更加復雜的過濾。
