Phần 3: Razor Pages với EF Core trong ASP.NET Core - Sắp xếp, Lọc, Phân trang và Nhóm
Hướng dẫn này thêm chức năng sắp xếp (sorting), lọc (filtering), nhóm (grouping) và phân trang (paging).
Thêm sắp xếp vào trang Index
Cập nhật Students/Index.cshtml.cs với code sau để thêm sắp xếp:
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; }
public IList<Student> Students { get; set; }
public async Task OnGetAsync(string sortOrder)
{
// using System;
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
IQueryable<Student> studentsIQ = from s in _context.Students
select s;
switch (sortOrder)
{
case "name_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentsIQ = studentsIQ.OrderBy(s => s.LastName);
break;
}
Students = await studentsIQ.AsNoTracking().ToListAsync();
}
}Code trên:
- Yêu cầu thêm
using System;. - Thêm các thuộc tính để chứa các tham số sắp xếp.
- Đổi tên thuộc tính
StudentthànhStudents. - Thay thế code trong phương thức
OnGetAsync.
Phương thức OnGetAsync nhận tham số sortOrder từ query string (chuỗi truy vấn) trong URL. URL và query string được tạo bởi Anchor Tag Helper (trình trợ giúp thẻ neo).
Tham số sortOrder là Name hoặc Date. Tham số sortOrder tùy chọn theo sau bởi _desc để chỉ định thứ tự giảm dần. Thứ tự sắp xếp mặc định là tăng dần.
NameSort và DateSort được sử dụng bởi Razor Page để cấu hình các hyperlink (liên kết) tiêu đề cột với các giá trị query string thích hợp:
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : ""; DateSort = sortOrder == "Date" ? "date_desc" : "Date";
Code sử dụng toán tử điều kiện ?: trong C#. Hai câu lệnh này cho phép trang thiết lập các hyperlink tiêu đề cột như sau:
| Thứ tự sắp xếp hiện tại | Hyperlink Last Name | Hyperlink Date |
|---|---|---|
| Last Name tăng dần | giảm dần | tăng dần |
| Last Name giảm dần | tăng dần | tăng dần |
| Date tăng dần | tăng dần | giảm dần |
| Date giảm dần | tăng dần | tăng dần |
Phương thức sử dụng LINQ to Entities để chỉ định cột cần sắp xếp. Code khởi tạo IQueryable<Student> trước câu lệnh switch, và sửa đổi nó trong câu lệnh switch:
IQueryable<Student> studentsIQ = from s in _context.Students
select s;
switch (sortOrder)
{
case "name_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentsIQ = studentsIQ.OrderBy(s => s.LastName);
break;
}
Students = await studentsIQ.AsNoTracking().ToListAsync();Khi IQueryable được tạo hoặc sửa đổi, không có truy vấn nào được gửi đến database. Truy vấn không được thực thi cho đến khi đối tượng IQueryable được chuyển đổi thành collection (bộ sưu tập). IQueryable được chuyển đổi thành collection bằng cách gọi một phương thức như ToListAsync.
Thêm hyperlink tiêu đề cột vào trang Students Index
Thay thế code trong Students/Index.cshtml bằng code sau:
@page
@model ContosoUniversity.Pages.Students.IndexModel
@{
ViewData["Title"] = "Students";
}
<h2>Students</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.Students[0].LastName)
</a>
</th>
<th>
@Html.DisplayNameFor(model => model.Students[0].FirstMidName)
</th>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.DateSort">
@Html.DisplayNameFor(model => model.Students[0].EnrollmentDate)
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Students)
{
<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>Thêm lọc (filtering)
Để thêm filtering (lọc) vào trang Students Index:
- Một ô text và nút submit được thêm vào Razor Page. Ô text cung cấp chuỗi tìm kiếm theo tên họ hoặc tên.
- Page model được cập nhật để sử dụng giá trị ô text.
Cập nhật phương thức OnGetAsync
Thay thế code trong Students/Index.cshtml.cs bằng code sau để thêm filtering:
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; }
public IList<Student> Students { get; set; }
public async Task OnGetAsync(string sortOrder, string searchString)
{
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
CurrentFilter = searchString;
IQueryable<Student> studentsIQ = from s in _context.Students
select s;
if (!String.IsNullOrEmpty(searchString))
{
studentsIQ = studentsIQ.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
switch (sortOrder)
{
case "name_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentsIQ = studentsIQ.OrderBy(s => s.LastName);
break;
}
Students = await studentsIQ.AsNoTracking().ToListAsync();
}
}Code trên:
- Thêm tham số
searchStringvào phương thứcOnGetAsync, và lưu giá trị tham số trong thuộc tínhCurrentFilter. Giá trị chuỗi tìm kiếm được nhận từ ô text được thêm ở phần tiếp theo. - Thêm mệnh đề
Wherevào câu lệnh LINQ. Mệnh đềWherechỉ chọn những sinh viên có tên hoặc họ chứa chuỗi tìm kiếm.
IQueryable vs. IEnumerable
Code gọi phương thức Where trên đối tượng IQueryable, và filter (bộ lọc) được xử lý trên server. Trong một số tình huống, ứng dụng có thể gọi phương thức Where như extension method (phương thức mở rộng) trên in-memory collection. Ví dụ, giả sử _context.Students thay đổi từ EF Core DbSet sang phương thức repository trả về collection IEnumerable. Kết quả thường giống nhau nhưng trong một số trường hợp có thể khác nhau.
Ví dụ, việc triển khai Contains trong .NET Framework thực hiện so sánh phân biệt chữ hoa/thường theo mặc định. Trong SQL Server, độ nhạy chữ hoa/thường của Contains được xác định bởi thiết lập collation (đối chiếu) của phiên bản SQL Server. SQL Server mặc định không phân biệt chữ hoa/thường. SQLite mặc định phân biệt chữ hoa/thường.
Khi Contains được gọi trên collection IEnumerable, triển khai .NET được sử dụng. Khi Contains được gọi trên đối tượng IQueryable, triển khai database được sử dụng.
Gọi Contains trên IQueryable thường được ưu tiên hơn vì lý do hiệu năng. Với IQueryable, việc lọc được thực hiện bởi database server.
Cập nhật Razor Page
Thay thế code trong Pages/Students/Index.cshtml để thêm nút Search:
@page
@model ContosoUniversity.Pages.Students.IndexModel
@{
ViewData["Title"] = "Students";
}
<h2>Students</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-primary" /> |
<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">
@Html.DisplayNameFor(model => model.Students[0].LastName)
</a>
</th>
<th>
@Html.DisplayNameFor(model => model.Students[0].FirstMidName)
</th>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.DateSort">
@Html.DisplayNameFor(model => model.Students[0].EnrollmentDate)
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Students)
{
<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>Code trên sử dụng <form> tag helper (trình trợ giúp thẻ) để thêm ô text tìm kiếm và nút. Theo mặc định, tag helper <form> gửi dữ liệu form với POST. Với GET, dữ liệu form được truyền trong URL dưới dạng query string. Việc truyền dữ liệu với query string cho phép người dùng đánh dấu (bookmark) URL. Hướng dẫn W3C khuyến nghị nên dùng GET khi action không dẫn đến cập nhật.
Lưu ý rằng URL chứa chuỗi tìm kiếm. Ví dụ: https://localhost:5001/Students?SearchString=an
Thêm phân trang (paging)
Trong phần này, lớp PaginatedList được tạo để hỗ trợ phân trang. Lớp PaginatedList sử dụng câu lệnh Skip và Take để lọc dữ liệu trên server thay vì lấy tất cả các hàng của bảng.
Tạo lớp PaginatedList
Tạo PaginatedList.cs trong thư mục project với code sau:
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);
}
}
}Phương thức CreateAsync trong code trên nhận page size (kích thước trang) và số trang, và áp dụng các câu lệnh Skip và Take thích hợp vào IQueryable. Các thuộc tính HasPreviousPage và HasNextPage được sử dụng để bật hoặc tắt các nút phân trang Previous và Next.
Phương thức CreateAsync được sử dụng để tạo PaginatedList<T>. Constructor không thể tạo đối tượng PaginatedList<T>; constructor không thể chạy code bất đồng bộ.
Thêm page size vào configuration
Thêm PageSize vào file appsettings.json:
{
"PageSize": 3,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"SchoolContext": "Server=(localdb)\\mssqllocaldb;Database=CU-1;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}Thêm phân trang vào IndexModel
Thay thế code trong Students/Index.cshtml.cs để thêm phân trang:
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Students
{
public class IndexModel : PageModel
{
private readonly SchoolContext _context;
private readonly IConfiguration Configuration;
public IndexModel(SchoolContext context, IConfiguration configuration)
{
_context = context;
Configuration = configuration;
}
public string NameSort { get; set; }
public string DateSort { get; set; }
public string CurrentFilter { get; set; }
public string CurrentSort { get; set; }
public PaginatedList<Student> Students { get; set; }
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> studentsIQ = from s in _context.Students
select s;
if (!String.IsNullOrEmpty(searchString))
{
studentsIQ = studentsIQ.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
switch (sortOrder)
{
case "name_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentsIQ = studentsIQ.OrderBy(s => s.LastName);
break;
}
var pageSize = Configuration.GetValue("PageSize", 4);
Students = await PaginatedList<Student>.CreateAsync(
studentsIQ.AsNoTracking(), pageIndex ?? 1, pageSize);
}
}
}Code trên:
- Thay đổi kiểu thuộc tính
StudentstừIList<Student>thànhPaginatedList<Student>. - Thêm page index,
sortOrderhiện tại, vàcurrentFiltervào chữ ký phương thứcOnGetAsync. - Lưu sort order trong thuộc tính
CurrentSort. - Reset page index về 1 khi có chuỗi tìm kiếm mới.
- Sử dụng lớp
PaginatedListđể lấy các entity Student.
Thuộc tính CurrentSort cung cấp cho Razor Page sort order (thứ tự sắp xếp) hiện tại. Sort order hiện tại phải được bao gồm trong các link phân trang để duy trì sort order trong khi phân trang.
Thêm link phân trang
Thay thế code trong Students/Index.cshtml bằng code sau:
@page
@model ContosoUniversity.Pages.Students.IndexModel
@{
ViewData["Title"] = "Students";
}
<h2>Students</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-primary" /> |
<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.Students[0].LastName)
</a>
</th>
<th>
@Html.DisplayNameFor(model => model.Students[0].FirstMidName)
</th>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.DateSort"
asp-route-currentFilter="@Model.CurrentFilter">
@Html.DisplayNameFor(model => model.Students[0].EnrollmentDate)
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Students)
{
<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.Students.HasPreviousPage ? "disabled" : "";
var nextDisabled = !Model.Students.HasNextPage ? "disabled" : "";
}
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Students.PageIndex - 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-primary @prevDisabled">
Previous
</a>
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Students.PageIndex + 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-primary @nextDisabled">
Next
</a>Các link tiêu đề cột sử dụng query string để truyền chuỗi tìm kiếm hiện tại vào phương thức OnGetAsync.
Chạy ứng dụng và điều hướng đến trang students:
- Để đảm bảo phân trang hoạt động, hãy nhấp vào các link phân trang theo các thứ tự sắp xếp khác nhau.
- Để xác minh phân trang hoạt động đúng với sắp xếp và lọc, hãy nhập chuỗi tìm kiếm và thử phân trang.
Nhóm (Grouping)
Phần này tạo trang About hiển thị số lượng sinh viên đã đăng ký cho mỗi ngày đăng ký. Bản cập nhật sử dụng grouping và bao gồm các bước sau:
- Tạo view model cho dữ liệu được sử dụng bởi trang
About. - Cập nhật trang
Aboutđể sử dụng view model.
Tạo view model
Tạo thư mục Models/SchoolViewModels.
Tạo SchoolViewModels/EnrollmentDateGroup.cs với code sau:
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; }
}
}Tạo Razor Page
Tạo file Pages/About.cshtml với code sau:
@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.Students)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
@item.StudentCount
</td>
</tr>
}
</table>Tạo page model
Cập nhật file Pages/About.cshtml.cs với code sau:
using ContosoUniversity.Models.SchoolViewModels;
using ContosoUniversity.Data;
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> Students { get; set; }
public async Task OnGetAsync()
{
IQueryable<EnrollmentDateGroup> data =
from student in _context.Students
group student by student.EnrollmentDate into dateGroup
select new EnrollmentDateGroup()
{
EnrollmentDate = dateGroup.Key,
StudentCount = dateGroup.Count()
};
Students = await data.AsNoTracking().ToListAsync();
}
}
}Câu lệnh LINQ nhóm các entity sinh viên theo ngày đăng ký, tính số lượng entity trong mỗi nhóm, và lưu kết quả trong collection của các đối tượng view model EnrollmentDateGroup.
Chạy ứng dụng và điều hướng đến trang About. Số lượng sinh viên cho mỗi ngày đăng ký được hiển thị trong bảng.