這是微軟官方教程Getting Started with Entity Framework 6 Code First using MVC 5 系列的翻譯,這里是第三篇:排序、篩選和分頁
原文:Sorting, Filtering, and Paging with the Entity Framework in an ASP.NET MVC Application
譯文版權所有,謝絕全文轉載——但你可以在你的網站上添加到該教程的鏈接。
在之前的教程中你實現了一組使用Web頁面對Student實體的的基本CRUD操作。在本教程中,您將為索引頁添加排序、篩選和分頁的功能。您還會創建一個簡單的分組頁面。
下圖顯示了當你完成本教程后的頁面截屏。用戶可以點擊行標題來進行排序,並且多次點擊可以讓你在升序和降序之間切換。
將排序鏈接添加到學生的索引頁
要為學生索引頁添加排序功能,你需要往學生控制器中的索引方法和學生索引視圖添加代碼。
在索引方法中添加排序功能
使用下面的代碼替換學生控制器的索引方法:
public ActionResult Index(string sortOrder) { ViewBag.NameSortParm = string.IsNullOrEmpty(sortOrder) ? "name_desc" : ""; ViewBag.DateSortParm = sortOrder == "Date" ? "date_desc" : "Date"; var students = from s in db.Students select s; switch (sortOrder) { case "name_desc": students = students.OrderByDescending(s => s.LastName); break; case "Date": students = students.OrderBy(s => s.EnrollmentDate); break; case "date_desc": students = students.OrderByDescending(s => s.EnrollmentDate); break; default: students = students.OrderBy(s => s.LastName); break; } return View(students.ToList()); }
這段代碼從URL中接收sortOrder查詢字符串,該字符串是由ASP.NET MVC作為參數傳遞給動作方法的。該參數將是"Name"或"Date"之一,這是作為升序的缺省的排序規則。還可能有一條下划線和"desc"來指示這是一個降序排序。
索引頁面第一次請求時,沒有任何查詢字符串被傳遞,學生們按照LastName的升序排序顯示。這是switch語句中的default指定的,當用戶點擊某列的標題超鏈接時,相應的sortOrder值通過查詢字符串傳遞到控制器中。
兩個ViewBag變量被用於為視圖提供合適的查詢字符串值。
ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "name_desc" : ""; ViewBag.DateSortParm = sortOrder == "Date" ? "date_desc" : "Date";
這里使用了三元選擇語句。第一個指定假如sortOrder參數為null或為空,則NameSortParm應設置為"name_desc",否則將其設置為空字符串。這兩個語句為視圖中列標題的超鏈接提供下列排序規則:
當前排序順序 | Last Name超鏈接 | Date 超鏈接 |
Last Name 升序 | 降序 | 升序 |
Last Name 降序 | 升序 | 升序 |
Date 升序 | 升序 | 降序 |
Date 降序 | 升序 | 升序 |
該方法使用LINQ to Entities來指定要作為排序依據的列。代碼在switch語句前創建了一個 IQueryable變量,然后在switch中修改它,並在switch語句后調用ToList方法。當您創建和修改IQueryable變量時,沒有查詢被實際發送到數據庫執行。直到您將IQueryable 對象通過調用一種方法如ToList轉換為一個集合時才進行真正的查詢。因此,直到return View語句之前,這段代碼的查詢都不會執行。
作為為每個排序順序編寫不同的LINQ語句的替代方法,您可以動態地創建LINQ句。有關動態LINQ的信息,請參閱Dynamic LINQ。
為學生索引視圖添加行標題超鏈接
在Views\Student\Index.cshtml中,使用下面的代碼替換標題行的<tr>和<th>元素。
<tr> <th> @Html.ActionLink("Last Name", "Index", new { sortOrder = ViewBag.NameSortParm}) </th> <th> First Name </th> <th> @Html.ActionLink("Last Name", "Index", new { sortOrder = ViewBag.DateSortParm }) </th> <th></th> </tr>
這段代碼使用ViewBag的屬性來設置超鏈接和查詢字符串值。
運行該頁面,點擊Last Name和Enrollment Date行標題,觀察排序的變化。
點擊Last Name,學生排序將變為按照Last Name的降序排列。
向學生索引頁中添加搜索框
要在索引頁中增加搜索功能,你需要向視圖中添加一個文本框及一個提交按鈕並在Index方法中做相應的修改。文本框允許你輸入要在名字和姓氏中檢索的字符串。
向索引方法中添加篩選功能
在學生控制器中,使用下面的代碼替換Index方法(高亮部分是我們所做出的修改):
public ActionResult Index(string sortOrder, string searchString) { ViewBag.NameSortParm = string.IsNullOrEmpty(sortOrder) ? "name_desc" : ""; ViewBag.DateSortParm = sortOrder == "Date" ? "date_desc" : "Date"; var students = from s in db.Students select s; if (!string.IsNullOrEmpty(searchString)) { students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper()) || s.FirstMidName.ToUpper().Contains(searchString.ToUpper())); } switch (sortOrder) { case "name_desc": students = students.OrderByDescending(s => s.LastName); break; case "Date": students = students.OrderBy(s => s.EnrollmentDate); break; case "date_desc": students = students.OrderByDescending(s => s.EnrollmentDate); break; default: students = students.OrderBy(s => s.LastName); break; } return View(students.ToList()); }
您已經將searchString參數添加到Index方法,您也已經添加用於在姓名中搜索指定字符串的Linq語句。搜索字符串是從文本框中接受的,稍后您將在視圖中添加它。只有在搜索字符串有值時,搜索部分的語句才會執行。
注意:在大部分情況下,你在實體框架實體集合上或作為擴展方法去執行同一個的方法結果都是相同的,但某些情況下可能不同。
例如,.Net框架中的Contains方法實現為當你傳遞一個空字符串作為參數時,將返回所有行。但實體框架的SQL Server Compact 4.0提供程序將不返回任何行。因此示例中的代碼(將Where語句放入一個if語句中)可以確保您在所有版本的SQL Server都能得到相同的結果。此外,Contains方法在.Net框架下默認是執行區分大小寫的比較,而實體框架的SQL Server提供程序在默認情況下執行不區分大小寫的比較。因此我們調用ToUpper方法使這里明確進行不區分大小寫的比較。這樣,將來如果您使用倉儲庫時,可以不需要進行多余的代碼修正。那時將返回IEnumerable集合而不是IQueryable對象。(當你在IEnumerable集合上調用Contains方法,你將得到.Net框架版的Contains實現;當你在IQueryable對象上調用時,你將得到對應的數據庫提供程序的方法實現。)
當你使用不同的數據庫提供程序或使用IQueryable對象和IEnumable集合比較時,空值處理也可能不同。例如,在某些情況下一個where條件比如table.Column !=0可能不會返回包含NULL值的行。更多的信息,請參閱Incorrect handling of null variables in 'where' clause。
向學生索引視圖中添加一個搜索框
在Views\Student\Index.cshtml中,在table元素之前添加下面高亮的代碼以創建一個標題、一個文本框及一個搜索按鈕。
<p> @Html.ActionLink("Create New", "Create") </p> @using (Html.BeginForm()) { <p> Find By Name: @Html.TextBox("SearchString") <input type="submit" value="Search" /> </p> } <table class="table"> <tr> <th>
運行索引頁面,輸入搜索字符串並提交,檢查搜索功能是否正常工作。
注意該URL中並不包含搜索字符串,這意味着如果您將查詢結果頁面加入書簽,通過使用書簽打開該頁面將無法得到篩選后的列表結果。稍后在本教程中我們將更改搜索按鈕改用搜索字符串來過濾結果。
向學生索引頁面添加分頁
要向索引頁面添加分頁,你需要安裝PagedList.Mvc NuGet包,然后你在索引方法及視圖中進行相應的修改。PagedList.Mvc是一個很好的分頁排序包,在此我們僅使用它來進行演示,下圖顯示附加了分頁連接的索引頁面。
安裝PagedList.Mvc NuGet包
PagedList包作為PagedList.Mvc包的依賴項會自動安裝到項目中。PagedList對IQueryable和IEnumable集合添加了PagedList集合類型及相應的擴展方法。這些擴展方法讓你在使用這些集合時能方便地處理分頁功能。
在程序包管理器控制台中輸入以下命令來安裝PagedList.Mvc包
Install-Package PagedList.Mvc
將分頁功能添加到索引方法中
在學生控制器中,添加PagedList命名空間:
using PagedList;
使用以下代碼替換Index方法:
public ActionResult Index(string sortOrder, string currentFilter, string searchString, int? page) { ViewBag.CurrentSort = sortOrder; ViewBag.NameSortParm = string.IsNullOrEmpty(sortOrder) ? "name_desc" : ""; ViewBag.DateSortParm = sortOrder == "Date" ? "date_desc" : "Date"; if (searchString != null) { page = 1; } else { searchString = currentFilter; } ViewBag.CurrentFilter = searchString; var students = from s in db.Students select s; if (!string.IsNullOrEmpty(searchString)) { students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper()) || s.FirstMidName.ToUpper().Contains(searchString.ToUpper())); } switch (sortOrder) { case "name_desc": students = students.OrderByDescending(s => s.LastName); break; case "Date": students = students.OrderBy(s => s.EnrollmentDate); break; case "date_desc": students = students.OrderByDescending(s => s.EnrollmentDate); break; default: students = students.OrderBy(s => s.LastName); break; } int pageSize = 3; int pageNumber = (page ?? 1); return View(students.ToPagedList(pageNumber, pageSize)); }
這段代碼添加了一個page參數,一個當前排序順序參數,添加了一個當前篩選參數到方法簽名中:
public ActionResult Index(string sortOrder, string currentFiler, string searchString, int? page)
頁面第一次顯示時,或者用戶還沒有點擊分頁或排序鏈接,所有參數都為null。如果點擊分頁鏈接,page變量將包含要顯示的頁面編號。
一個ViewBag屬性被提供給視圖用於指示當前的排序順序,因為在點擊了分頁鏈接后必須要保持當前的排序順序才能正確的對結果進行分頁。
ViewBag.CurrentSort = sortOrder;
另一個屬性ViewBag.CurrentFiler被提供給視圖用於指示當前的搜索字符串,分頁連接同樣必須包含此值以保持針對搜索結果進行分頁。同時字符串還必須還原到搜索框中。如果在分頁的過程中修改了搜索字符串,頁碼被重置為1,因為新的搜索字符串可能會導致不同的搜索結果集合。這一改變是在文本框中輸入值並提交時。在這種情況下,searchString參數不為空。
if (searchString != null) { page = 1; } else { searchString = currentFiler; }
在方法的結尾,學生IQueryable對象的ToPagedList擴展方法將學生查詢轉換為一個包含了單頁的支持分頁的集合類型。該學生集合的單頁被傳遞給視圖用於顯示:
int pageSize = 3; int pageNumber = (page ?? 1); return View(students.ToPagedList(pageNumber, pageSize));
ToPagedList方法需要一個頁碼,兩個問號表示null合成運算符,Null合成運算符定義了可為空類型的缺省值。在本例中,如果page的值不為空,則返回該值,如果page的值為空,則返回1。
向學生索引視圖添加分頁鏈接
在Views\Student\Index.cshtml中,使用下面的代碼替換原來的,高亮部分顯示了我們所做的更改:
@using PagedList.Mvc; @model PagedList.IPagedList<ContosoUniversity.Models.Student> <link href="~/Content/PagedList.css" type="text/css" rel="stylesheet" /> @{ ViewBag.Title = "Students"; } <h2>Students</h2> <p> @Html.ActionLink("Create New", "Create") </p> @using (Html.BeginForm("Index","Student", FormMethod.Get)) { <p> Find By Name: @Html.TextBox("SearchString",ViewBag.CurrentFilter as string) <input type="submit" value="Search" /> </p> } <table class="table"> <tr> <th> @Html.ActionLink("Last Name", "Index", new { sortOrder = ViewBag.NameSortParm, currentFilter = ViewBag.CurrentFilter }) </th> <th> First Name </th> <th> @Html.ActionLink("Enrollment Date", "Index", new { sortOrder = ViewBag.DateSortParm, currentFilter = ViewBag.CurrentFilter }) </th> <th></th> </tr> @foreach (var item in Model) { <tr> <td> @Html.DisplayFor(modelItem => item.LastName) </td> <td> @Html.DisplayFor(modelItem => item.FirstMidName) </td> <td> @Html.DisplayFor(modelItem => item.EnrollmentDate) </td> <td> @Html.ActionLink("Edit", "Edit", new { id = item.ID }) | @Html.ActionLink("Details", "Details", new { id = item.ID }) | @Html.ActionLink("Delete", "Delete", new { id = item.ID }) </td> </tr> } </table> <br /> Page @(Model.PageCount < Model.PageNumber ? 0 : Model.PageNumber) of @Model.PageCount @Html.PagedListPager(Model, page => Url.Action("Index", new { page , sortOrder = ViewBag.CurrentSort , currentFilter = ViewBag.CurrentFilter }))
頁面頂部的@model語句指示視圖現在獲取PagedList對象而不是List對象。
PagedList.Mvc的using語句使MVC幫助器可以使用PagedListPager擴展方法來生成分頁按鈕。
下面的代碼使用了指定了FormMethod.Get的BeginForm的重載。
@using (Html.BeginForm("Index","Student", FormMethod.Get)) { <p> Find By Name: @Html.TextBox("SearchString",ViewBag.CurrentFilter as string) <input type="submit" value="Search" /> </p> }
默認情況下表單使用POST方式提交數據,這意味着參數使用HTTP消息體傳遞,而不是URL。當你指定使用HTTP GET時,表單數據通過URL來傳遞,這樣用戶就可以創建該URL的書簽並重復使用。W3C guidelines for the use of HTTP GET推薦你在不會導致結果更新時使用GET方式來進行操作。
文本框使用當前的搜索字符串進行初始化,以便在分頁的時候不會丟失搜索字符串。
Find by name: @Html.TextBox("SearchString", ViewBag.CurrentFilter as string)
列表器鏈接使用查詢字符串將當前的搜索字符串傳遞給控制器,以便用戶可以對搜索結果進行排序:
@Html.ActionLink("Last Name", "Index", new { sortOrder = ViewBag.NameSortParm, currentFilter = ViewBag.CurrentFilter })
顯示當前頁數和總頁數。
Page @(Model.PageCount < Model.PageNumber ? 0 : Model.PageNumber) of @Model.PageCount
如果沒有要顯示的頁,則顯示"Page 0 of 0"。(在這種情況下頁面數字會大於總頁數,因為PageNumber是1,而PageCount是0)
由PagedListPager幫助器顯示分頁按鈕:
@Html.PagedListPager(Model, page => Url.Action("Index", new { page , sortOrder = ViewBag.CurrentSort , currentFilter = ViewBag.CurrentFilter }))
PagedListPager幫助器提供了很多選項,您可以自定義Url及樣式,更多的信息請參閱TroyGoode / PagedList。
運行頁面。
點擊不同的排序順序,並跳轉到不同的頁碼,然后輸入搜索字符串,並再次分頁並驗證排序及搜索過濾還可以正常工作。
創建關於我們頁面來顯示學生的統計信息
為Contoso大學的關於頁面添加每日有多少個學生注冊,需要用到分組及簡單的計算,要做到這些,您需要執行下列操作:
- 創建一個ViewModel用來傳遞數據給視圖
- 修改Home控制器中的About方法
- 修改About視圖
創建ViewModel
在項目文件夾中創建一個ViewModels文件夾,在該文件夾中添加一個新類,命名為EnrollmentDataGroup.cs,使用下面的代碼替換自動生成的:
1 using System; 2 using System.ComponentModel.DataAnnotations; 3 4 namespace ContosoUniversity.ViewModels 5 { 6 public class EnrollmentDateGroup 7 { 8 [DataType(DataType.Date)] 9 public DateTime? EnrollmentDate { get; set; } 10 11 public int StudentCount { get; set; } 12 } 13 }
修改Home控制器
在Home控制器中,在文件的頂部添加以下using語句:
using ContosoUniversity.DAL; using ContosoUniversity.ViewModels;
在類定義后添加數據庫上下文的類變量:
public class HomeController : Controller { private SchoolContext db = new SchoolContext();
使用下面的代碼替換About方法:
public ActionResult About() { IQueryable<EnrollmentDateGroup> data = from student in db.Students group student by student.EnrollmentDate into dateGroup select new EnrollmentDateGroup() { EnrollmentDate = dateGroup.Key, StudentCount = dateGroup.Count() }; return View(data.ToList()); }
使用LINQ對學生實體按照注冊日期進行分組,計算每個分組的實體數量並將結果存儲在EnrollmentDateGroup視圖模型對象的集合中。
添加Dispose方法:
protected override void Dispose(bool disposing) { db.Dispose(); base.Dispose(disposing); }
修改關於視圖
將About.cshtml替換為如下代碼:
1 @model IEnumerable<ContosoUniversity.ViewModels.EnrollmentDateGroup> 2 @{ 3 ViewBag.Title = "Student Body Statistics"; 4 } 5 <h2>Student Body Statistics</h2> 6 <table> 7 <tr> 8 <th>Enrollment Date</th> 9 <th>Students</th> 10 </tr> 11 @foreach (var item in Model) 12 { 13 <tr> 14 <td>@Html.DisplayFor(o => item.EnrollmentDate)</td> 15 <td>@item.StudentCount</td> 16 </tr> 17 } 18 </table>
運行程序,點擊關於鏈接,你可以看到學生的統計信息了。
總結
到目前為止,你已經實現了基本的CRUD和排序、篩選、分頁及分組功能,下一節中我們將通過擴展數據模型來介紹更高級的主題。
作者信息
Tom Dykstra - Tom Dykstra是微軟Web平台及工具團隊的高級程序員,作家。