Contoso University Web 應(yīng)用演示了如何使用 EF Core 和 Visual Studio 創(chuàng)建 Razor 頁面 Web 應(yīng)用。若要了解系列教程,請參閱第一個教程。
本教程將添加排序、篩選、分組和分頁功能。
下圖顯示完整的頁面。 列標題是可單擊的鏈接,可用于對列進行排序。 重復(fù)單擊列標題可在升降和降序排序順序之間切換。
如果遇到無法解決的問題,請下載已完成應(yīng)用。
向 Students/Index.cshtml.cs PageModel 添加字符串,使其包含排序參數(shù):
C#
public class IndexModel : PageModel
{
private readonly SchoolContext _context;
public IndexModel(SchoolContext context)
{
_context = context;
}
public string NameSort { get; set; }
public string DateSort { get; set; }
public string CurrentFilter { get; set; }
public string CurrentSort { get; set; }
用以下代碼更新 Students/Index.cshtml.cs OnGetAsync:
C#
public async Task OnGetAsync(string sortOrder)
{
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
IQueryable<Student> studentIQ = from s in _context.Student
select s;
switch (sortOrder)
{
case "name_desc":
studentIQ = studentIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentIQ = studentIQ.OrderBy(s => s.LastName);
break;
}
Student = await studentIQ.AsNoTracking().ToListAsync();
}
上述代碼接收來自 URL 中的查詢字符串的 sortOrder 參數(shù)。 該 URL(包括查詢字符串)由定位點標記幫助器生成
sortOrder 參數(shù)為“名稱”或“日期”。 sortOrder 參數(shù)后面可跟“_desc”以指定降序(可選)。 默認排序順序為升序。
如果通過“學(xué)生”鏈接對“索引”頁發(fā)起請求,則不會有任何查詢字符串。 學(xué)生按姓氏升序顯示。 按姓氏升序是 switch 語句中的默認順序 (fall-through case)。 用戶單擊列標題鏈接時,查詢字符串值中會提供相應(yīng)的 sortOrder 值。
Razor 頁面使用 NameSort 和 DateSort 為列標題超鏈接配置相應(yīng)的查詢字符串值:
C#
public async Task OnGetAsync(string sortOrder)
{
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
IQueryable<Student> studentIQ = from s in _context.Student
select s;
switch (sortOrder)
{
case "name_desc":
studentIQ = studentIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentIQ = studentIQ.OrderBy(s => s.LastName);
break;
}
Student = await studentIQ.AsNoTracking().ToListAsync();
}
以下代碼包含 C# 條件 ?: 運算符:
C#
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
第一行指定當 sortOrder 為 NULL 或為空時,NameSort 設(shè)置為“name_desc”。 如果 sortOrder 不為 NULL 或不為空,則 NameSort 設(shè)置為空字符串。
?: operator 也稱為三元運算符。
通過這兩個語句,頁面可如下設(shè)置列標題超鏈接:
當前排序順序 | 姓氏超鏈接 | 日期超鏈接 |
---|---|---|
姓氏升序 | descending | ascending |
姓氏降序 | ascending | ascending |
日期升序 | ascending | descending |
日期降序 | ascending | ascending |
該方法使用 LINQ to Entities 指定要作為排序依據(jù)的列。 此代碼會初始化 switch 語句前面的 IQueryable<Student>,并在 switch 語句中對其進行修改:
C#
public async Task OnGetAsync(string sortOrder)
{
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
IQueryable<Student> studentIQ = from s in _context.Student
select s;
switch (sortOrder)
{
case "name_desc":
studentIQ = studentIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentIQ = studentIQ.OrderBy(s => s.LastName);
break;
}
Student = await studentIQ.AsNoTracking().ToListAsync();
}
創(chuàng)建或修改 IQueryable 時,不會向數(shù)據(jù)庫發(fā)送任何查詢。 將 IQueryable 對象轉(zhuǎn)換成集合后才能執(zhí)行查詢。 通過調(diào)用 IQueryable 等方法可將 ToListAsync 轉(zhuǎn)換成集合。 因此,IQueryable 代碼會生成單個查詢,此查詢直到出現(xiàn)以下語句才執(zhí)行:
C#
Student = await studentIQ.AsNoTracking().ToListAsync();
OnGetAsync 可能獲得包含大量可排序列的詳細信息。
將 Students/Index.cshtml 中的代碼替換為以下突出顯示的代碼:
HTML
@page
@model ContosoUniversity.Pages.Students.IndexModel
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort">
@Html.DisplayNameFor(model => model.Student[0].LastName)
</a>
</th>
<th>
@Html.DisplayNameFor(model => model.Student[0].FirstMidName)
</th>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.DateSort">
@Html.DisplayNameFor(model => model.Student[0].EnrollmentDate)
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Student)
{
<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-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
前面的代碼:
若要驗證排序是否生效:
若要更好地了解此代碼:
單步執(zhí)行調(diào)試程序。
若要向“學(xué)生索引”頁添加篩選:
用以下代碼更新 Students/Index.cshtml.cs OnGetAsync:
C#
public async Task OnGetAsync(string sortOrder, string searchString)
{
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
CurrentFilter = searchString;
IQueryable<Student> studentIQ = from s in _context.Student
select s;
if (!String.IsNullOrEmpty(searchString))
{
studentIQ = studentIQ.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
switch (sortOrder)
{
case "name_desc":
studentIQ = studentIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentIQ = studentIQ.OrderBy(s => s.LastName);
break;
}
Student = await studentIQ.AsNoTracking().ToListAsync();
}
前面的代碼:
注意:上述代碼調(diào)用 IQueryable 對象上的 Where 方法,且在服務(wù)器上處理該篩選器。 在某些情況下,應(yīng)用可能會對內(nèi)存中的集合調(diào)用 Where 方法作為擴展方法。 例如,假設(shè) _context.Students 從 EF Core DbSet 更改為可返回 IEnumerable 集合的存儲庫方法。 結(jié)果通常是相同的,但在某些情況下可能不同。
例如,Contains 的 .NET Framework 實現(xiàn)會默認執(zhí)行區(qū)分大小寫的比較。 在 SQL Server 中,Contains 區(qū)分大小寫由 SQL Server 實例的排序規(guī)則設(shè)置決定。 SQL Server 默認為不區(qū)分大小寫??烧{(diào)用 ToUpper,進行不區(qū)分大小寫的顯式測試:
Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
如果上述代碼改為使用 IEnumerable,則該代碼會確保結(jié)果區(qū)分大小寫。 如果在 IEnumerable 集合上調(diào)用 Contains,則使用 .NET Core 實現(xiàn)。 如果在 IQueryable 對象上調(diào)用 Contains,則使用數(shù)據(jù)庫實現(xiàn)。 從存儲庫返回 IEnumerable 可能會大幅降低性能:
調(diào)用 ToUpper 不會對性能產(chǎn)生負面影響。 ToUpper 代碼會在 TSQL SELECT 語句的 WHERE 子句中添加一個函數(shù)。 添加的函數(shù)會防止優(yōu)化器使用索引。 如果安裝的 SQL 區(qū)分大小寫,則最好避免在不必要時調(diào)用 ToUpper。
在 Pages/Students/Index.cshtml中,添加以下突出顯示的代碼以創(chuàng)建“搜索”按鈕和各種 chrome。
HTML
@page
@model ContosoUniversity.Pages.Students.IndexModel
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<form asp-page="./Index" method="get">
<div class="form-actions no-color">
<p>
Find by name:
<input type="text" name="SearchString" value="@Model.CurrentFilter" />
<input type="submit" value="Search" class="btn btn-default" /> |
<a asp-page="./Index">Back to full List</a>
</p>
</div>
</form>
<table class="table">
上述代碼使用 <form> 標記幫助器來添加搜索文本框和按鈕。 默認情況下,<form> 標記幫助器利用 POST 提交表單數(shù)據(jù)。 借助 POST,會在 HTTP 消息正文中而不是在 URL 中傳遞參數(shù)。 使用 HTTP GET 時,表單數(shù)據(jù)作為查詢字符串在 URL 中進行傳遞。 通過查詢字符串傳遞數(shù)據(jù)時,用戶可對 URL 添加書簽。 W3C 指南建議應(yīng)在操作不引起更新的情況下使用 GET。
測試應(yīng)用:
請注意,該 URL 包含搜索字符串。
HTML
http://localhost:5000/Students?SearchString=an
如果頁面具有書簽,該書簽將包含該頁面的 URL 和 SearchString 查詢字符串。 form 標記中的 method="get" 會導(dǎo)致生成查詢字符串。
目前,選中列標題排序鏈接時,“搜索”框中的篩選值會丟失。 丟失的篩選值在下一部分進行修復(fù)。
本部分將創(chuàng)建一個 PaginatedList 類來支持分頁。 PaginatedList 類使用 Skip 和 Take 語句在服務(wù)器上篩選數(shù)據(jù),而不是檢索所有表格行。 下圖顯示了分頁按鈕。
在項目文件夾中,使用以下代碼創(chuàng)建 PaginatedList.cs:
C#
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 方法會提取頁面大小和頁碼,并將相應(yīng)的 Skip 和 Take 語句應(yīng)用于 IQueryable。 當在 IQueryable 上調(diào)用 ToListAsync 時,它將返回僅包含所請求頁的列表。 屬性 HasPreviousPage 和 HasNextPage 用于啟用或禁用“上一頁”和“下一頁”分頁按鈕。
CreateAsync 方法用于創(chuàng)建 PaginatedList<T>。 構(gòu)造函數(shù)不能創(chuàng)建 PaginatedList<T> 對象;構(gòu)造函數(shù)不能運行異步代碼。
在 Students/Index.cshtml.cs 中,將 Student 的類型從 IList<Student> 更新到 PaginatedList<Student>:
C#
public PaginatedList<Student> Student { get; set; }
用以下代碼更新 Students/Index.cshtml.cs OnGetAsync:
C#
public async Task OnGetAsync(string sortOrder,
string currentFilter, string searchString, int? pageIndex)
{
CurrentSort = sortOrder;
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
if (searchString != null)
{
pageIndex = 1;
}
else
{
searchString = currentFilter;
}
CurrentFilter = searchString;
IQueryable<Student> studentIQ = from s in _context.Student
select s;
if (!String.IsNullOrEmpty(searchString))
{
studentIQ = studentIQ.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
switch (sortOrder)
{
case "name_desc":
studentIQ = studentIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentIQ = studentIQ.OrderBy(s => s.LastName);
break;
}
int pageSize = 3;
Student = await PaginatedList<Student>.CreateAsync(
studentIQ.AsNoTracking(), pageIndex ?? 1, pageSize);
}
上述代碼會向方法簽名添加頁面索引、當前的 sortOrder 和 currentFilter。
C#
public async Task OnGetAsync(string sortOrder,
string currentFilter, string searchString, int? pageIndex)
出現(xiàn)以下情況時,所有參數(shù)均為 NULL:
單擊分頁鏈接后,頁面索引變量將包含要顯示的頁碼。
CurrentSort 為 Razor 頁面提供當前排序順序。 必須在分頁鏈接中包含當前排序順序才能在分頁時保留排序順序。
CurrentFilter 為 Razor 頁面提供當前的篩選字符串。 CurrentFilter 值:
如果在分頁時更改搜索字符串,頁碼會重置為 1。 頁面必須重置為 1,因為新的篩選器會導(dǎo)致顯示不同的數(shù)據(jù)。 輸入搜索值并選擇“提交”時:
C#
if (searchString != null)
{
pageIndex = 1;
}
else
{
searchString = currentFilter;
}
PaginatedList.CreateAsync 方法會將學(xué)生查詢轉(zhuǎn)換為支持分頁的集合類型中的單個學(xué)生頁面。 單個學(xué)生頁面會傳遞到 Razor 頁面。
C#
Student = await PaginatedList<Student>.CreateAsync(
studentIQ.AsNoTracking(), pageIndex ?? 1, pageSize);
PaginatedList.CreateAsync 中的兩個問號表示 NULL 合并運算符。 NULL 合并運算符定義可為 NULL 的類型的默認值。 (pageIndex ?? 1) 表達式表示返回 pageIndex 的值(若帶有值)。 如果 pageIndex 沒有值,則返回 1。
更新 Students/Index.cshtml 中的標記。 突出顯示所作更改:
HTML
@page
@model ContosoUniversity.Pages.Students.IndexModel
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<form asp-page="./Index" method="get">
<div class="form-actions no-color">
<p>
Find by name: <input type="text" name="SearchString" value="@Model.CurrentFilter" />
<input type="submit" value="Search" class="btn btn-default" /> |
<a asp-page="./Index">Back to full List</a>
</p>
</div>
</form>
<table class="table">
<thead>
<tr>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
asp-route-currentFilter="@Model.CurrentFilter">
@Html.DisplayNameFor(model => model.Student[0].LastName)
</a>
</th>
<th>
@Html.DisplayNameFor(model => model.Student[0].FirstMidName)
</th>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.DateSort"
asp-route-currentFilter="@Model.CurrentFilter">
@Html.DisplayNameFor(model => model.Student[0].EnrollmentDate)
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Student)
{
<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-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
@{
var prevDisabled = !Model.Student.HasPreviousPage ? "disabled" : "";
var nextDisabled = !Model.Student.HasNextPage ? "disabled" : "";
}
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Student.PageIndex - 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-default @prevDisabled">
Previous
</a>
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Student.PageIndex + 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-default @nextDisabled">
Next
</a>
列標題鏈接使用查詢字符串將當前搜索字符串傳遞到 OnGetAsync 方法,讓用戶可對篩選結(jié)果進行排序:
HTML
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
asp-route-currentFilter="@Model.CurrentFilter">
@Html.DisplayNameFor(model => model.Student[0].LastName)
</a>
分頁按鈕由標記幫助器顯示:
HTML
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Student.PageIndex - 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-default @prevDisabled">
Previous
</a>
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Student.PageIndex + 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-default @nextDisabled">
Next
</a>
運行應(yīng)用并導(dǎo)航到學(xué)生頁面。
若要更好地了解此代碼:
單步執(zhí)行調(diào)試程序。
此步驟將更新 Pages/About.cshtml,顯示每個注冊日期的已注冊學(xué)生的數(shù)量。 更新需使用分組并包括以下步驟:
在 Models 文件夾中創(chuàng)建一個 SchoolViewModels 文件夾。
在 SchoolViewModels 文件夾中,使用以下代碼添加 EnrollmentDateGroup.cs:
C#
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; }
}
}
ASP.NET Core 2.2 中的 Web 模板不包含“關(guān)于”頁面。 如果使用的是 ASP.NET Core 2.2,請創(chuàng)建“關(guān)于 Razor”頁面。
用以下代碼更新 Pages/About.cshtml.cs 文件:
C#
using ContosoUniversity.Models.SchoolViewModels;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ContosoUniversity.Models;
namespace ContosoUniversity.Pages
{
public class AboutModel : PageModel
{
private readonly SchoolContext _context;
public AboutModel(SchoolContext context)
{
_context = context;
}
public IList<EnrollmentDateGroup> Student { get; set; }
public async Task OnGetAsync()
{
IQueryable<EnrollmentDateGroup> data =
from student in _context.Student
group student by student.EnrollmentDate into dateGroup
select new EnrollmentDateGroup()
{
EnrollmentDate = dateGroup.Key,
StudentCount = dateGroup.Count()
};
Student = await data.AsNoTracking().ToListAsync();
}
}
}
LINQ 語句按注冊日期對學(xué)生實體進行分組,計算每組中實體的數(shù)量,并將結(jié)果存儲在 EnrollmentDateGroup 視圖模型對象的集合中。
將 Pages/About.cshtml 文件中的代碼替換為以下代碼:
HTML
@page
@model ContosoUniversity.Pages.AboutModel
@{
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.Student)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
@item.StudentCount
</td>
</tr>
}
</table>
運行應(yīng)用并導(dǎo)航到“關(guān)于”頁面。 表格中會顯示每個注冊日期的學(xué)生計數(shù)。
如果遇到無法解決的問題,請下載本階段的已完成應(yīng)用。
更多建議: