排序、過濾、分頁、分組
Contoso 大學(xué)示例 Web 應(yīng)用程序演示如何使用實體框架(EF)Core 2.0 和 Visual Studio 2017 創(chuàng)建 ASP.NET Core 2.0 MVC Web 應(yīng)用程序。 如欲了解更多本教程相關(guān)信息,請參閱 入門
在前面的教程,你實現(xiàn)了一組 Student 實體的基本 CRUD 頁面。 在本節(jié)中,您將向 Student 列表頁添加排序、 篩選和分頁功能, 還將創(chuàng)建一個進行簡單分組的頁面。
下圖顯示本節(jié)中將會完成的頁面。 用戶可以點擊列標(biāo)題進行排序。 重復(fù)點擊列標(biāo)題將排序在升序和降序之間切換。
將列排序鏈接添加到學(xué)生索引頁 (Student Index)
要在學(xué)生索引頁中添加排序,需要更改 Students 控制器中的 Index 的方法,并添加代碼到 Students Index 視圖。
在 Index 方法中添加排序功能
在 StudentsController.cs,替換 Index 方法為以下代碼:
public async Task<IActionResult> Index(string sortOrder)
{
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
var students = from s in _context.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(await students.AsNoTracking().ToListAsync());
}
代碼從 URL 中接收 sortOrder 查詢參數(shù),此查詢參數(shù)由 ASP.NET Core MVC 提供。參數(shù)是值為 "Name" 或 "Date" 的字符串,有時候后面會帶有下劃線和字符串 "desc" 來指定降序順序。 默認(rèn)排序順序為升序。
第一次請求索引頁時,沒有附加查詢字符串。 在默認(rèn)的 Switch default 方法中按 LastName 排序。 當(dāng)用戶單擊列標(biāo)題,相應(yīng)的 sortOrder 將會出現(xiàn)在查詢字符串中。
兩個 ViewData 元素 ( NameSortParm 和 DateSortParm )供視圖用于配置列標(biāo)題超鏈接查詢字符串。
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
當(dāng)前排序情況 | LastName 鏈接 | Date 鏈接 |
---|---|---|
LastName 升序 | 降序 | 升序 |
LastName 降序 | 升序 | 升序 |
Date 升序 | 升序 | 降序 |
Date 降序 | 升序 | 升序 |
這是三元選擇語句。 如果 sortOrder 參數(shù)為 null 或為空,NameSortParm 應(yīng)設(shè)置為 "name_desc"; 否則,設(shè)置為一個空字符串。 這兩個語句在視圖中用于設(shè)置列標(biāo)題超鏈接,如下所示:
當(dāng)前排序情況 | LastName 鏈接 | Date 鏈接 |
---|---|---|
LastName 升序 | 降序 | 升序 |
LastName 降序 | 升序 | 升序 |
Date 升序 | 升序 | 降序 |
Date 降序 | 升序 | 升序 |
方法中使用 LINQ to Entities 指定排序列。 在進行 Switch 判斷前, 創(chuàng)建 IQueryables 變量, 在判斷之后, 調(diào)用 ToListAsync 方法。 在創(chuàng)建和修改 IQueryable 變量過程中,查詢并不會真正發(fā)送到數(shù)據(jù)庫,直到你通過調(diào)用一個類似 ToListAsync 的方法將 IQueryable 變量轉(zhuǎn)化為一個集合。 因此,在這段代碼中,只當(dāng)返回 View 語句執(zhí)行時,查詢才真正發(fā)生。
這樣的代碼可能導(dǎo)致出現(xiàn)非常多的列變量,在本系列最后一個教程中將告訴你如何在變量中傳遞排序列名。
在學(xué)生索引視圖中添加列標(biāo)題超鏈接
為了添加列標(biāo)題超鏈接,替換 Views/Students/Index.cshtml 文件中的代碼為如下代碼:
<th>
<a asp-action="Index" asp-route-sortOrder = "@ViewData["NameSortParm"]"> @Html.DisplayNameFor(model => model.LastName) </a>
</th>
<th>
@Html.DisplayNameFor(model => model.FirstMidName)
</th>
<th>
<a asp-action="Index" asp-route-sortOrder = "@ViewData["DateSortParm"]"> @Html.DisplayNameFor(model => model.EnrollmentDate) </a>
</th>
代碼使用 ViewData 屬性中的信息建立超鏈接中的查詢字符串。
運行應(yīng)用程序中,選擇 Student 菜單,然后單擊 Last name 和 Enrollement Date 列標(biāo)題,以驗證排序是否生效。
在學(xué)生索引視圖中添加搜索框
要在學(xué)生索引頁面中添加過濾功能,您需要在視圖中添加一個文本框和一個提交按鈕,并在 Index 方法中做相應(yīng)修改。 文本框中,你將輸入要在名字和姓氏字段中搜索的字符串。
在 Index 方法中添加過濾功能
在StudentsController.cs,替換 Index 方法替換為以下代碼
public async Task<IActionResult> Index(string sortOrder, string searchString)
{
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
ViewData["CurrentFilter"] = searchString;
var students = from s in _context.Students
select s;
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
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(await students.AsNoTracking().ToListAsync());
}
在 Index 方法中添加 searchString 參數(shù),此參數(shù)值來自剛剛加入視圖中的文本框。同時,在 LINQ 語句中添加一個 Where 子句來選擇名字 (first name 和 last name)中包含查詢字符串的學(xué)生。Where 子句僅當(dāng)查詢字符串中有值時才生效。
備注
在這里, 您在 IQueryable 對象上調(diào)用 Where 方法, 過濾將在服務(wù)器上進行。某些情況下,您也可能是對內(nèi)存集合調(diào)用 Where 方法。(例如,假設(shè)你將 _context.Students 的引用,從 EF Dataset 修改為一個返回 IEnumerable 的倉儲方法。)查詢結(jié)果通常是相同的,但在某些情況下可能會有所不同。
例如,.NET Framework 實現(xiàn)的 Contains 方法默認(rèn)區(qū)分大小寫。但 SQL Server 中這取決于 SQL Server 實例的排序規(guī)則設(shè)置,該設(shè)置默認(rèn)為不區(qū)分大小寫。 您可以調(diào)用 ToUpper 來進行測試顯式不區(qū)分大小寫的方法:Where (s = > s.LastName.ToUpper()。Contains(searchString.ToUpper())
。 這將確保如果稍后你修改代碼為使用返回 IEnumerable 對象的倉儲 Repository,而不是返回 IQueryable 對象時,結(jié)果保持相同。 (當(dāng)您在 IEnumerable 集合上調(diào)用 Contains 方法時,使用的是 .NET Framework 實現(xiàn); 而在 IQueryable 對象上,則使用 database provider 實現(xiàn)。) 但是,這個解決方案將對性能產(chǎn)生負(fù)面影響。ToUpper
代碼將在 TSQL 查詢語句的 Where 條件中加入函數(shù)調(diào)用,進而導(dǎo)致 SQL 優(yōu)化器停止使用索引。 假設(shè) SQL 主要安裝為不區(qū)分大小寫,最好是避免 ToUpper 代碼,直到您遷移到區(qū)分大小寫的數(shù)據(jù)存儲區(qū)。
在 Index 視圖中添加搜索框
在Views/Student/Index.cshtml,在 <Table>
標(biāo)簽前加入如下代碼,創(chuàng)建一個標(biāo)題、一個文本框和一個搜索按鈕。
<form asp-action="Index" method="get">
<div class="form-actions no-color">
<p>
Find by name: <input type="text" name="SearchString" value="@ViewData["currentFilter"]" />
<input type="submit" value="Search" class="btn btn-default" /> |
<a asp-action="Index">Back to Full List</a>
</p>
</div>
</form>
代碼使用 <form>
標(biāo)簽,添加搜索文本框和按鈕。默認(rèn)情況下,<form>
標(biāo)簽使用 POST
方法進行數(shù)據(jù)提交,參數(shù)在消息正文而不是 URL 查詢字符串中傳遞。通過指定使用 GET
方法,窗體數(shù)據(jù)通過 URL 查詢字符串進行傳遞,這是的用戶可以對 URL 創(chuàng)建書簽。 W3C 準(zhǔn)則建議,在未導(dǎo)致更新的操作中,使用 GET
方法。
運行應(yīng)用程序中,選擇 Student 菜單,輸入任意搜索字符,并點擊“搜索”按鈕,以驗證過濾功能生效。
請注意在 URL 中包含了搜索字符串。
http://localhost:5813/Students?SearchString=an
如果您將本頁面加入書簽,下次使用書簽時,您將得到過濾后的列表。在 Form
標(biāo)簽中添加的 method="get"
是產(chǎn)生查詢字符串的原因。
在此階段,如果您單擊列標(biāo)題進行排序,你將丟失搜索框中輸入的過濾查詢。 在下一部分中將修復(fù)此問題。
在學(xué)生索引視圖中添加分頁功能
要在學(xué)生索引頁中添加分頁功能,您將創(chuàng)建一個 PaginatedList
類,在類中使用 Skip
和 Take
語句實現(xiàn)在服務(wù)器過濾數(shù)據(jù),而不是獲取數(shù)據(jù)表的所有數(shù)據(jù)行。然后再對 Index
做一些更改,再 Index
視圖中添加分頁按鈕。下圖中展示了分頁按鈕。
在項目文件夾中,創(chuàng)建
PaginatedList.cs
,然后鍵入下面的代碼。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity
{
public class PaginatedList<T> : List<T>
{
public int PageIndex { get; private set; }
public int TotalPages { get; private set; }
public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)
{
PageIndex = pageIndex;
TotalPages = (int)Math.Ceiling(count / (double)pageSize);
this.AddRange(items);
}
public bool HasPreviousPage
{
get
{
return (PageIndex > 1);
}
}
public bool HasNextPage
{
get
{
return (PageIndex < TotalPages);
}
}
public static async Task<PaginatedList<T>> CreateAsync(IQueryable<T> source, int pageIndex, int pageSize)
{
var count = await source.CountAsync();
var items = await source.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
return new PaginatedList<T>(items, count, pageIndex, pageSize);
}
}
}
代碼中,CreateAsync
方法獲取分頁大小及頁碼,再 IQueryable
對象上使用相應(yīng)的 Skip
和 Take
語句。 在 IQueryable
上調(diào)用 ToListAsync
后, 返回一個只包含請求頁的列表。 屬性 HasPreviousPage
及 HasNextPage
用于啟用或禁用 “上一頁” 和 “下一頁” 按鈕。
在 PaginatedList<T>
中使用 CreateAsync
方法而不是構(gòu)造函數(shù)的原因是構(gòu)造函數(shù)無法運行異步代碼。
ACreateAsync方法用于而不是一個構(gòu)造函數(shù)創(chuàng)建PaginatedList<T>對象,因為構(gòu)造函數(shù)不能運行異步代碼。
在 Index
方法中添加分頁功能
在 StudentsController.cs
,替換 Index
方法替換為以下代碼。
public async Task<IActionResult> Index(
string sortOrder,
string currentFilter,
string searchString,
int? page)
{
ViewData["CurrentSort"] = sortOrder;
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
if (searchString != null)
{
page = 1;
}
else
{
searchString = currentFilter;
}
ViewData["CurrentFilter"] = searchString;
var students = from s in _context.Students
select s;
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
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;
return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), page ?? 1, pageSize));
}
代碼在方法中添加了 page, sortOrder, currentFilter 三個參數(shù)。
第一次顯示頁面,或如果用戶未單擊分頁或排序鏈接,則所有參數(shù)將都為 null
。 單擊分頁鏈接時,如果頁變量將包含要顯示的頁碼。
ViewData("CurrentSort") 保存當(dāng)前排序以供視圖使用。在視圖的分頁鏈接中包含排序,翻頁的時候才能保持排序不變。
ViewData("CurrentFilter")保存當(dāng)前過濾字符串以供視圖使用。在視圖的分頁鏈接中包含過濾字符串,翻頁額時候才能保持過濾不變。
如果在分頁期間,搜索字符串被更改,因為新的過濾導(dǎo)致顯示不同的數(shù)據(jù),頁碼必須被重置為第一頁。在文本框中輸入并按下提交按鈕時,搜索字符串改變。在這種情況下,searchString 參數(shù)不為空。
if (searchString != null)
{
page = 1;
}
else
{
searchString = currentFilter;
}
在 Index
方法結(jié)尾, PaginatedList.CreateAsync
方法轉(zhuǎn)化學(xué)生查詢至一個支持分頁功能的單頁學(xué)生集合,然后這個集合被傳遞給視圖。
return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), page ?? 1, pageSize));
PaginatedList.CreateAsync
方法使用參數(shù) page
(頁碼)和pageSize
(頁大小)作為參數(shù)。 page
參數(shù)后的兩個 ?
代表 null 合并運算符
。null 合并運算符
定義了可為空類型的默認(rèn)值;page ?? 1
表達(dá)式意味著,如果 page
具有一個值(不為空),則返回 page
, 如果為空則返回 1
。
在 Index
視圖中添加分頁鏈接
在 Views/Students/Index.cshtml
,替換為以下代碼。
@model PaginatedList<ContosoUniversity.Models.Student>
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-action="Index" method="get">
<div class="form-actions no-color">
<p>
Find by name: <input type="text" name="SearchString" value="@ViewData["currentFilter"]" />
<input type="submit" value="Search" class="btn btn-default" /> |
<a asp-action="Index">Back to Full List</a>
</p>
</div>
</form>
<table class="table">
<thead>
<tr>
<th>
<a asp-action="Index" asp-route-sortOrder="@ViewData["NameSortParm"]" asp-route-currentFilter="@ViewData["CurrentFilter"]">Last Name</a>
</th>
<th>
First Name
</th>
<th>
<a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asp-route-currentFilter="@ViewData["CurrentFilter"]">Enrollment Date</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@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>
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
@{
var prevDisabled = !Model.HasPreviousPage ? "disabled" : "";
var nextDisabled = !Model.HasNextPage ? "disabled" : "";
}
<a asp-action="Index"
asp-route-sortOrder="@ViewData["CurrentSort"]"
asp-route-page="@(Model.PageIndex - 1)"
asp-route-currentFilter="@ViewData["CurrentFilter"]"
class="btn btn-default @prevDisabled">
Previous
</a>
<a asp-action="Index"
asp-route-sortOrder="@ViewData["CurrentSort"]"
asp-route-page="@(Model.PageIndex + 1)"
asp-route-currentFilter="@ViewData["CurrentFilter"]"
class="btn btn-default @nextDisabled">
Next
</a>
譯者注:Markdown 語法無法實現(xiàn)代碼內(nèi)高亮,如不清楚修改的位置,請參考微軟原文。
頁面頂部的 @model
指定視圖現(xiàn)在獲取 PaginatedList<T>
對象而不是 List<T>
對象。
列標(biāo)題上的鏈接使用查詢字符串將當(dāng)前的搜索字符串傳遞到控制器,以便用戶可以在過濾后的結(jié)果中進行排序:
<a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asp-route-currentFilter ="@ViewData["CurrentFilter"]">Enrollment Date</a>
The paging buttons are displayed by tag helpers:
分頁按鈕使用 tag helpers
進行顯示
<a asp-action="Index"
asp-route-sortOrder="@ViewData["CurrentSort"]"
asp-route-page="@(Model.PageIndex - 1)"
asp-route-currentFilter="@ViewData["CurrentFilter"]"
class="btn btn-default @prevDisabled">
Previous
</a>
運行應(yīng)用并轉(zhuǎn)到 Student 頁面。
在不同排序狀態(tài)下點擊分頁鏈接,以確認(rèn)分頁功能正常工作。然后嘗試搜索后再分頁,驗證分頁功能在不同排序和過濾條件下都正常工作。
創(chuàng)建一個顯示學(xué)生統(tǒng)計信息的關(guān)于頁面
在 Contoso 大學(xué)網(wǎng)站的 About
頁面, 將顯示每天有多少學(xué)生進行注冊,這需要對數(shù)據(jù)進行分組,并在分組上做計算。要完成此任務(wù),您需要執(zhí)行以下操作:
- 創(chuàng)建一個用于傳遞數(shù)據(jù)到視圖的 ViewModel 類。
- 修改
HomeController
中的About
方法。 - 修改
About
視圖。
創(chuàng)建 ViewModel 類
在 Models
文件夾中創(chuàng)建一個 SchoolViewModels
文件夾
在這個新的文件夾中,添加一個文件名為 EnrollmentDateGroup.cs
的類,并輸入以下代碼:
using System;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models.SchoolViewModels
{
public class EnrollmentDateGroup
{
[DataType(DataType.Date)]
public DateTime? EnrollmentDate { get; set; }
public int StudentCount { get; set; }
}
}
修改 HomeController
在 HomeController.cs
文件, 頂部加入如下語句:
using Microsoft.EntityFrameworkCore;
using ContosoUniversity.Data;
using ContosoUniversity.Models.SchoolViewModels;
在類中添加一個數(shù)據(jù)庫上下文變量 _context, ASP.NET Core 依賴注入將為此變量提供實例。
public class HomeController : Controller
{
private readonly SchoolContext _context;
public HomeController(SchoolContext context)
{
_context = context;
}
將 About 方法替換為以下代碼:
public async Task<ActionResult> About()
{
IQueryable<EnrollmentDateGroup> data =
from student in _context.Students
group student by student.EnrollmentDate into dateGroup
select new EnrollmentDateGroup()
{
EnrollmentDate = dateGroup.Key,
StudentCount = dateGroup.Count()
};
return View(await data.AsNoTracking().ToListAsync());
}
LINQ 語句將 Student 實體進行分組,計算每個分組中的實體數(shù)量,并將結(jié)果存放在 EnrollmentDateGroup
ViewModel 對象中。
備注
在 EF Core 1.0 版本中, 整個結(jié)果集返回到客戶端,并在客戶端上進行分組。在某些情況下,這會導(dǎo)致性能問題。請使用實際生產(chǎn)環(huán)境規(guī)模的數(shù)據(jù)測試性能,如有必要,使用原始 SQL 在服務(wù)器進行分組。 有關(guān)如何使用原始的 SQL 的信息,請參閱本系列最后一個教程。
修改 About
視圖
替換 Views/Home/About.cshtml 為如下代碼:
@model IEnumerable<ContosoUniversity.Models.SchoolViewModels.EnrollmentDateGroup>
@{
ViewData["Title"] = "Student Body Statistics";
}
<h2>Student Body Statistics</h2>
<table>
<tr>
<th>
Enrollment Date
</th>
<th>
Students
</th>
</tr>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
@item.StudentCount
</td>
</tr>
}
</table>
運行應(yīng)用,轉(zhuǎn)至 About 頁面。 每個日期的學(xué)生注冊數(shù)量顯示于表格中。
小結(jié)
在本教程中,你已了解如何執(zhí)行排序、 篩選、 分頁和分組。 在下一步的教程中,你將了解如何通過使用遷移來處理數(shù)據(jù)模型更改。