教學課程:新增排序、篩選及分頁 - ASP.NET MVC 搭配 EF Core
在上一個教學課程中,您已針對學生實體的基本 CRUD 作業實作一組網頁。 在本教學課程中,您要將排序、篩選和分頁功能新增至 Students 的 [索引] 頁面。 此外,還要建立將執行簡易群組的頁面。
下圖顯示當您完成時的頁面外觀。 資料行標題是使用者可以按一下以依據該資料行排序的連結。 重覆按一下資料行標題,可切換遞增和遞減排序次序。
在本教學課程中,您已:
- 新增資料行排序連結
- 新增 [搜尋] 方塊
- 為 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
參數。 查詢字串值是由 ASP.NET Core MVC 提供,作為動作方法的參數。 該參數是 "Name" 或 "Date" 的字串,後面可選擇性地接著底線和字串 "desc" 來指定遞減順序。 預設排序順序為遞增。
第一次要求 [索引] 頁面時,沒有任何查詢字串。 學生會依姓氏的遞增順序顯示,這是 switch
陳述式中失敗案例所建立的預設值。 使用者按一下資料行標題超連結時,適當的 sortOrder
值將會在查詢字串值中提供。
檢視會使用兩個 ViewData
項目 (NameSortParm 和 DateSortParm),以適當的查詢字串值設定資料行標題超連結。
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());
}
這些是三元陳述式。 第一個陳述式指定當 sortOrder
參數為 null 或是空的時,NameSortParm 應該設定為 "name_desc";否則它應該設定為空字串。 這兩個陳述式會啟動設定資料行標題超連結的檢視,如下所示:
目前排序次序 | 姓氏超連結 | 日期超連結 |
---|---|---|
姓氏遞增 | 遞減 | ascending |
姓氏遞減 | ascending | ascending |
日期遞增 | ascending | descending |
日期遞減 | ascending | ascending |
這個方法會使用 LINQ to Entities 來指定排序所依據的資料行。 此程式碼會在 switch 陳述式之前建立 IQueryable
變數、在 switch 陳述式中修改它,並在 switch
陳述式之後呼叫 ToListAsync
方法。 當您建立和修改 IQueryable
變數時,沒有查詢會傳送至資料庫。 在您呼叫 ToListAsync
等方法以將 IQueryable
物件轉換成集合之前,不會執行查詢。 因此,此程式碼會產生一個直到 return View
陳述式才會執行的單一查詢。
此程式碼可取得使用大量資料行數目的詳細資訊。 本系列中的最後一個教學課程示範如何撰寫程式碼,讓您以字串變數傳遞 OrderBy
資料行的名稱。
將資料行標題超連結新增至 Student [索引] 檢視
以下列程式碼取代 Views/Students/Index.cshtml
中的程式碼,以新增資料行標題超連結。 變更的行已醒目提示。
@model IEnumerable<ContosoUniversity.Models.Student>
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<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>
<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>
此程式碼使用 ViewData
屬性中的資訊,以適當的查詢字串值設定超連結。
執行應用程式,選取 [Students] 索引標籤,然後按一下 [姓氏] 和 [註冊日期] 資料行標題,以確認排序的運作正常。
新增 [搜尋] 方塊
若要將篩選新增至 Students 的 [索引] 頁面,您要將文字方塊和提交按鈕新增至檢視,並在 Index
方法中進行對應的變更。 文字方塊可讓您輸入要在名字和姓氏欄位中搜尋的字串。
將篩選功能新增至 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());
}
您已將 searchString
參數新增至 Index
方法。 從將新增至 [索引] 檢視的文字方塊中接收搜尋字串值。 您也已在 LINQ 陳述式中新增 where 子句,該子句只會選取其名字或姓氏包含搜尋字串的學生。 唯有當具有要搜尋的值時,才會執行新增 where 子句的陳述式。
注意
在這裡,您可以在 IQueryable
物件上呼叫 Where
方法,而篩選將會在伺服器上處理。 在某些情況下,您可能會呼叫 Where
方法在記憶體內部集合上作為擴充方法。 (例如,假設您變更了 _context.Students
的參考,以便它參考傳回 IEnumerable
集合的存放庫方法,而不是參考 EF DbSet
)。結果通常都是一樣的,但在某些情況下可能會不同。
例如,.NET Framework 實作的 Contains
方法預設會執行區分大小寫的比較,但在 SQL Server 中,這取決於 SQL Server 執行個體的定序設定。 該設定預設為不區分大小寫。 您可以呼叫 ToUpper
方法,使測試明確地不區分大小寫:Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())。 如果您稍後變更程式碼,以使用傳回 IEnumerable
集合 (而不是 IQueryable
物件) 的存放庫,這會確保結果保持不變。 (當您在 IEnumerable
集合上呼叫 Contains
方法時,將取得 .NET Framework 實作;當您在 IQueryable
物件上呼叫它時,則會取得資料庫提供者實作。)不過,此解決方案會對效能帶來負面影響。 ToUpper
程式碼會將一個函式置於 TSQL SELECT 陳述式的 WHERE 子句中。 這會防止最佳化工具使用索引。 假設 SQL 大部分安裝為不區分大小寫,最好避免使用 ToUpper
程式碼,直到您移轉至區分大小寫的資料存放區為止。
將搜尋方塊新增至學生的 [索引] 檢視
在 Views/Student/Index.cshtml
中,於開始表格標記之前立即新增反白顯示的程式碼,以建立標題、文字方塊及 [搜尋] 按鈕。
<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">
此程式碼會使用 <form>
標籤協助程式 來新增搜尋文字方塊和按鈕。 <form>
標籤協助程式預設會使用 POST 提交表單資料,這表示參數會以 HTTP 訊息本文傳遞,而不是以 URL 作為查詢字串傳遞。 當您指定 HTTP GET 時,表單資料會以 URL 中作為查詢字串傳遞,這可讓使用者為該 URL 加上書籤。 W3C 指導方針建議,只有在動作不會產生更新時才應使用 GET。
執行應用程式,選取 [Students] 索引標籤,輸入搜尋字串,然後按一下 [搜尋] 以確認篩選可以運作。
請注意 URL 中包含了搜尋字串。
http://localhost:5813/Students?SearchString=an
如果您為此頁面加上書籤,則會在使用書籤時取得篩選的清單。 將 method="get"
新增至 form
標籤會導致查詢字串的產生。
在這個階段,如果您按一下資料行標題排序連結,將會遺失您在 [搜尋] 方塊中輸入的篩選值。 您將在下節修正該問題。
為 Students 索引新增分頁
若要將分頁新增至 Students 的 [索引] 頁面,您要建立使用 Skip
和 Take
陳述式的 PaginatedList
類別來篩選伺服器上的資料,而不是一直擷取資料表的所有資料列。 然後,您要在 Index
方法中進行其他變更,並將分頁按鈕新增至 Index
檢視。 下圖顯示分頁按鈕。
在專案資料夾中,建立 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 => PageIndex > 1;
public bool HasNextPage => 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
方法會採用頁面大小和頁面數,並會將適當的 Skip
和 Take
陳述式套用至 IQueryable
。 在 IQueryable
上呼叫 ToListAsync
時,會傳回僅包含所要求頁面的清單。 HasPreviousPage
和 HasNextPage
屬性可用來啟用或停用 [上一頁] 和 [下一頁] 分頁按鈕。
CreateAsync
方法用來建立 PaginatedList<T>
物件而不是建構函式,因為建構函式無法執行非同步程式碼。
為 Index 方法新增分頁
在 StudentsController.cs
中,以下列程式碼取代 Index
方法。
public async Task<IActionResult> Index(
string sortOrder,
string currentFilter,
string searchString,
int? pageNumber)
{
ViewData["CurrentSort"] = sortOrder;
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
if (searchString != null)
{
pageNumber = 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(), pageNumber ?? 1, pageSize));
}
此程式碼會將頁碼參數、目前排序次序參數和目前篩選參數新增至方法簽章。
public async Task<IActionResult> Index(
string sortOrder,
string currentFilter,
string searchString,
int? pageNumber)
第一次顯示頁面,或是使用者尚未按一下分頁或排序連結時,所有參數都會是 null。 如果按一下分頁連結,頁面變數將包含要顯示的頁碼。
名為 CurrentSort 的 ViewData
項目會提供使用目前排序次序的檢視,因為這必須包含在分頁連結中,才能保持與分頁時相同的排序順序。
名為 CurrentFilter 的 ViewData
項目則提供使用目前篩選字串的檢視。 此值必須包含在分頁連結中,才能維護分頁期間的篩選設定,而且它必須在頁面重新顯示時還原為文字方塊。
如果搜尋字串在分頁期間變更,頁面必須重設為 1,因為新的篩選可能會導致顯示不同的資料。 在文字方塊中輸入值並按下 [提交] 按鈕時,即會變更搜尋字串。 在此情況下,searchString
參數不是 null。
if (searchString != null)
{
pageNumber = 1;
}
else
{
searchString = currentFilter;
}
在 Index
方法的結尾處,PaginatedList.CreateAsync
方法會以支援分頁的集合類型,將學生查詢轉換成學生單一頁面。 該學生單一頁面接著會傳遞至檢視。
return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), pageNumber ?? 1, pageSize));
PaginatedList.CreateAsync
方法會採用頁面數。 兩個問號代表 null 聯合運算子。 Null 聯合運算子將針對可為 Null 的型別定義預設值;(pageNumber ?? 1)
運算式表示在它含有值時會傳回值 pageNumber
,或在 pageNumber
為 null 時傳回 1。
新增分頁連結
在 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-pageNumber="@(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-pageNumber="@(Model.PageIndex + 1)"
asp-route-currentFilter="@ViewData["CurrentFilter"]"
class="btn btn-default @nextDisabled">
Next
</a>
頁面頂端的 @model
陳述式指定檢視現在會取得 PaginatedList<T>
物件,而不是 List<T>
物件。
資料行標頭連結會使用查詢字串,將目前的搜尋字串傳遞至控制器,讓使用者可以在篩選結果內排序:
<a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asp-route-currentFilter ="@ViewData["CurrentFilter"]">Enrollment Date</a>
分頁按鈕會由標籤協助程式來顯示:
<a asp-action="Index"
asp-route-sortOrder="@ViewData["CurrentSort"]"
asp-route-pageNumber="@(Model.PageIndex - 1)"
asp-route-currentFilter="@ViewData["CurrentFilter"]"
class="btn btn-default @prevDisabled">
Previous
</a>
執行應用程式並移至 Students 頁面。
以不同排序次序按一下分頁連結,以確定分頁運作正常。 然後輸入搜尋字串並再次嘗試分頁,以確認分頁的排序和篩選能正確運作。
建立 [關於] 頁面
對於 Contoso 大學網站的 About 頁面,您將顯示每個註冊日期已有多少學生註冊。 這需要對群組進行分組和簡單計算。 若要完成此工作,您需要執行下列作業:
- 針對您需要傳遞至檢視的資料,建立檢視模型類別。
- 在 Home 控制器中建立 About 方法。
- 建立 About 檢視。
建立檢視模型
在 Models 資料夾中建立 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; }
}
}
修改 Home 控制器
在 HomeController.cs
中,於檔案最上方加入下列 using 陳述式:
using Microsoft.EntityFrameworkCore;
using ContosoUniversity.Data;
using ContosoUniversity.Models.SchoolViewModels;
using Microsoft.Extensions.Logging;
在類別的左大括弧之後立即新增資料庫內容的類別變數,並從 ASP.NET Core DI 取得內容的執行個體:
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly SchoolContext _context;
public HomeController(ILogger<HomeController> logger, SchoolContext context)
{
_logger = logger;
_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 陳述式會依註冊日期將學生實體組成群組、計算每個群組中的實體數目、將結果儲存在 EnrollmentDateGroup
檢視模型物件的集合中。
建立 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>
執行應用程式並移至 About 頁面。 每個註冊日期的學生人數將會顯示在資料表中。
取得程式碼
下一步
在本教學課程中,您已:
- 新增資料行排序連結
- 新增 [搜尋] 方塊
- 為 Students 索引新增分頁
- 為 Index 方法新增分頁
- 新增分頁連結
- 建立 [關於] 頁面
若要了解如何使用移轉來處理資料模型變更,請前往下一個教學課程。
意見反應
https://aka.ms/ContentUserFeedback。
即將登場:在 2024 年,我們將逐步淘汰 GitHub 問題作為內容的意見反應機制,並將它取代為新的意見反應系統。 如需詳細資訊,請參閱:提交並檢視相關的意見反應