在上一個教學課程中,您已為學生實體的基本 CRUD 作業實作了一套網頁。 在這個教學中,你會為學生索引頁面新增排序、篩選和分頁功能。 你還會建立一個用於簡單分組的頁面。
在這個練習中,你會為學生索引頁面新增排序、篩選和分頁功能。 你也會建立一個用於簡單分組的頁面。 以下圖示展示了完成後頁面的樣貌。 欄位標題是使用者可以選擇以該欄位排序的連結。 反覆選擇欄位標題會使排序順序在升降之間切換。
在本教學課程中,您已:
- 新增支援欄位排序的連結
- 新增搜尋框以支援搜尋
- 為學生索引添加分頁功能
- 新增連結以支援分頁動作
- 為網站建立關於頁面
必要條件
- 完成上一個教學,實現基本的 CRUD 功能 - ASP.NET MVC EF Core
新增欄位排序連結
要在 學生索引 頁面新增排序功能,你要更改 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 參數為空或空,該 NameSortParm 元素被設為「name_desc」。否則,該元素將被設定為空字串。 這兩個陳述式會啟動設定資料行標題超連結的檢視,如下所示:
| 目前排序次序 | 姓氏超連結 | 日期超連結 |
|---|---|---|
| 姓氏按字母升序排列 | 遞減 | 遞增 |
| 姓氏遞減 | 遞增 | 遞增 |
| 日期遞增 | 遞增 | 遞減 |
| 日期遞減 | 遞增 | 遞增 |
這個方法會使用 LINQ to Entities 來指定排序所依據的資料行(欄位)。 程式碼會在IQueryable陳述式之前建立變數,並在switch陳述式中修改該變數,之後在switch陳述式後呼叫ToListAsync方法。 當您建立和修改 IQueryable 變數時,沒有查詢會傳送至資料庫。 在您呼叫 IQueryable 等方法以將 ToListAsync 物件轉換成集合之前,不會執行查詢。 因此,這段程式碼會產生一個查詢,而該查詢直到執行return View陳述句時才會執行。
當你處理大量欄位時,這些程式碼可能會變得冗長。 本系列的最後一個教學《 學習進階情境 > 使用動態 LINQ 簡化程式碼》展示了如何撰寫能在字串變數中傳遞欄位名稱 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 資訊來設定帶有適當查詢字串值的超連結。
啟動應用程式,選擇 學生 標籤,並選擇 姓氏 和 入學日期 欄位標題以確認排序功能。
新增 [搜尋] 方塊
要在 學生索引 頁面新增篩選功能,你可以在檢視中加入文字框和 提交 按鈕,並在方法中做出相應的調整 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參數。 搜尋字串的值是從你加入索引檢視的文字框中獲得的。 你還在陳述中加入whereLINQ了一條條款,只選擇名字或姓氏與搜尋字串相符的學生。 加入子 where 句的陳述只有在存在可搜尋值時才會執行。
確定如何使用 Where 方法
在此情境中,在Where 方法作為延伸方法。 假設你將參考改為 _context.Students。 它不是呼叫 Entity Framework DbSet 方法,而是引用一個回傳 IEnumerable 集合的 repository 方法。 結果通常相同,但某些情況可能不同。
例如,.NET Framework 的 Contains 方法的預設行為是執行大小寫區分的比較。 在 SQL Server 中,判定是透過 SQL Server 實例的整合設定來完成。 設定預設為不區分大小寫。 你可以呼叫 ToUpper 方法來使檢測明確不區分大小寫:Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())。 這個選項確保即使你後來更改程式碼,使用回傳 IEnumerable 集合而非 IQueryable 物件的儲存庫,結果會保持不變。 (當你呼叫 Contains 集合中的 IEnumerable 方法時,你會得到 .NET Framework 的實作。當你呼叫 IQueryable 物件時,你會得到資料庫提供者的實作。)
不過,這個方案會帶來效能上的損失。
ToUpper 程式碼將函式放入WHERE子句中的TSQL SELECT陳述式。 此行為阻止優化器使用索引。 鑒於 SQL 大多安裝為不區分大小寫,因此最好在遷移到不區分大小寫的數據存儲之前避免使用 ToUpper 代碼。
在學生索引檢視中新增搜尋框
在 Views/Student/Index.cshtml 檔案中,請在開啟表格標籤 <table> 之前立即加入強調的程式碼,以便建立表格標題、一個文字框以及一個 搜尋 按鈕。
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-action="Index" method="get">
<div class="form-actions no-color">
<p>
<label>Find by name: <input type="text" name="SearchString" value="@ViewData["CurrentFilter"]" /></label>
<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 。
啟動應用程式,選擇 「學生 」標籤,輸入搜尋字串,然後選擇 「搜尋 」以確認過濾功能是否有效。
請注意 URL 中包含了搜尋字串。
http://localhost:5813/Students?SearchString=an
如果你將此頁面加入書籤,使用書籤時會看到篩選後的清單。 將子句加入method="get"form標籤後,會產生查詢字串。
在開發的這個階段,如果你選擇欄位標題排序連結,你會失去 搜尋框中 輸入的篩選值。 你將在下一節了解如何處理這種情況。
將分頁功能加入學生索引
要在 學生索引 頁面新增分頁功能,你可以建立 PaginatedList 一個類別,使用 Skip 和 Take 語句來過濾伺服器上的資料,而不是每次都檢索所有資料表列。 接著,你進一步修改 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 and Take 語句給物件 IQueryable 。 當在 IQueryable 物件上呼叫 ToListAsync 方法時,該方法會回傳一個只包含請求頁面的 List。
HasPreviousPage和HasNextPage屬性可用來啟用或停用「上一頁」和「下一頁」分頁按鈕。
CreateAsync 方法用來建立 PaginatedList<T> 物件而不是建構函式,因為建構函式無法執行非同步程式碼。
在索引方法中新增分頁
在 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)
頁面第一次顯示,或尚未選取分頁或排序連結時,所有參數都是空的。 若選擇分頁連結,頁面變數會包含要顯示的頁碼。
ViewData 元素(命名為 CurrentSort)提供當前的排序順序視圖。 這個值必須包含在分頁連結中,這樣分頁時排序順序才會保持一致。
名稱為CurrentFilter的ViewData元素為該視圖提供當前的過濾字串。 此值必須包含在分頁連結中,以維持分頁時的過濾器設定。 當頁面顯示時,必須將該值還原到文字框中。
若分頁時搜尋字串被更改,頁面必須重設為 1,因為新篩選器可能導致顯示不同資料。 當在文字框輸入一個值並選擇 「提交」時,搜尋字串會被更改。 在這種情況下,參數 searchString 並非空。
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 的型別定義一個預設值。 此表達 (pageNumber ?? 1) 式的意思是如果有值則回傳 的 pageNumber 值;若 pageNumber 為空,則回傳 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>
<label>Find by name: <input type="text" name="SearchString" value="@ViewData["CurrentFilter"]" /></label>
<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>
啟動應用程式,然後前往 學生 頁面。
選擇不同排序順序的分頁連結,並確保分頁功能正常。 輸入搜尋字串,再試一次分頁,確認分頁是否也能正常進行排序和篩選。
建立 [關於] 頁面
在Contoso大學網站的 「關於」 頁面,你會顯示每個註冊日期的學生人數。 此功能需要分組並對分組進行簡單計算。 為了支持這種行為,你要完成以下任務:
- 針對您需要傳遞至檢視的資料,建立檢視模型類別。
- 在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 檢視模型物件中。
建立關於我檢視
新增一個包含以下程式碼的 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>
啟動應用程式,然後進入 關於頁面 。 每個註冊日期的學生人數將會顯示在資料表中。