Thêm Sắp xếp, Lọc, Phân trang - ASP.NET MVC với EF Core
Trong hướng dẫn trước, bạn đã triển khai một tập hợp các trang web cho các thao tác CRUD (Create, Read, Update, Delete - Tạo, Đọc, Cập nhật, Xóa) cơ bản cho các đối tượng Student. Trong hướng dẫn này, bạn thêm chức năng sắp xếp (sorting), lọc (filtering) và phân trang (paging) vào trang chỉ mục Students. Bạn cũng tạo một trang thực hiện nhóm đơn giản.
Trong bài tập này, bạn thêm chức năng sắp xếp, lọc và phân trang cho trang Students Index. Bạn cũng tạo một trang thực hiện nhóm đơn giản. Hình minh họa sau đây cho thấy trang trông như thế nào khi bạn hoàn thành. Các tiêu đề cột là các liên kết mà người dùng có thể chọn để sắp xếp theo cột đó. Chọn tiêu đề cột nhiều lần sẽ chuyển đổi thứ tự sắp xếp giữa tăng dần và giảm dần.
Trong hướng dẫn này, bạn:
- Thêm liên kết để hỗ trợ sắp xếp theo cột
- Thêm ô tìm kiếm để hỗ trợ tìm kiếm
- Thêm phân trang vào Students Index
- Thêm liên kết để hỗ trợ hành động phân trang
- Tạo trang About cho trang web
Điều kiện tiên quyết
- Hoàn thành hướng dẫn trước, Implement basic CRUD functionality - ASP.NET MVC with EF Core
Thêm liên kết sắp xếp cột
Để thêm sắp xếp vào trang Students Index, bạn thay đổi phương thức Index của Students controller và thêm mã vào Student Index view.
Thêm chức năng sắp xếp vào phương thức Index
Trong file StudentsController.cs, thay thế phương thức Index bằng đoạn mã sau:
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());
}Mã này nhận tham số sortOrder từ query string (chuỗi truy vấn) trong URL. Giá trị query string được ASP.NET Core MVC cung cấp dưới dạng tham số cho phương thức action. Tham số là một chuỗi: "Name" hoặc "Date". Giá trị này tùy chọn được theo sau bởi dấu gạch dưới và chuỗi "desc" để chỉ định thứ tự giảm dần. Thứ tự sắp xếp mặc định là tăng dần.
Lần đầu tiên trang Index được yêu cầu, không có query string. Các sinh viên được hiển thị theo thứ tự tăng dần của họ tên, đây là mặc định được thiết lập bởi trường hợp fall-through trong câu lệnh switch. Khi người dùng nhấp vào liên kết tiêu đề cột, giá trị sortOrder thích hợp được cung cấp trong query string.
Hai phần tử ViewData (NameSortParm và DateSortParm) được view sử dụng để cấu hình các liên kết tiêu đề cột với các giá trị query string thích hợp.
Các thay đổi mã này bao gồm các câu lệnh ternary (tam nguyên). Câu lệnh đầu tiên chỉ định rằng nếu tham số sortOrder là null hoặc rỗng, phần tử NameSortParm được đặt thành "name\_desc". Ngược lại, phần tử được đặt thành chuỗi rỗng. Hai câu lệnh này cho phép view thiết lập các liên kết tiêu đề cột như sau:
| Thứ tự sắp xếp hiện tại | Liên kết Họ tên | Liên kết Ngày |
|---|---|---|
| Họ tên tăng dần | Giảm dần | Tăng dần |
| Họ tên giảm dần | Tăng dần | Tăng dần |
| Ngày tăng dần | Tăng dần | Giảm dần |
| Ngày giảm dần | Tăng dần | Tăng dần |
Phương thức sử dụng LINQ to Entities (Language Integrated Query đến các đối tượng) để chỉ định cột cần sắp xếp theo. Mã tạo một biến IQueryable trước câu lệnh switch, sửa đổi nó trong câu lệnh switch, và gọi phương thức ToListAsync sau câu lệnh switch. Khi bạn tạo và sửa đổi các biến IQueryable, không có truy vấn nào được gửi đến cơ sở dữ liệu. Truy vấn không được thực thi cho đến khi bạn chuyển đổi đối tượng IQueryable thành một collection bằng cách gọi một phương thức như ToListAsync. Do đó, mã này dẫn đến một truy vấn duy nhất không được thực thi cho đến câu lệnh return View.
Thêm liên kết tiêu đề cột vào Student Index view
Thay thế mã trong file Views/Students/Index.cshtml bằng đoạn mã sau để thêm các liên kết tiêu đề cột:
@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>Mã này sử dụng thông tin trong các thuộc tính ViewData để thiết lập các siêu liên kết với các giá trị query string thích hợp.
Chạy ứng dụng, chọn tab Students và nhấp vào tiêu đề cột Last Name và Enrollment Date để kiểm tra sắp xếp hoạt động.
Thêm ô tìm kiếm
Để thêm lọc vào trang Students Index, bạn thêm một ô text và nút Submit vào view và thực hiện các thay đổi tương ứng trong phương thức Index. Trong ô text, bạn có thể nhập một chuỗi để tìm kiếm trong trường tên và họ của sinh viên.
Thêm hỗ trợ lọc vào phương thức Index
Trong file StudentsController.cs, thay thế phương thức Index bằng đoạn mã sau (các thay đổi được highlight):
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());
}Bạn đã thêm tham số searchString vào phương thức Index. Giá trị chuỗi tìm kiếm được nhận từ một ô text mà bạn thêm vào Index view. Bạn cũng đã thêm mệnh đề where vào câu lệnh LINQ chỉ chọn sinh viên có tên hoặc họ khớp với chuỗi tìm kiếm. Câu lệnh thêm mệnh đề where chỉ được thực thi nếu có một giá trị cần tìm kiếm.
Xác định cách sử dụng phương thức Where
Trong tình huống này, bạn gọi phương thức Where trên một đối tượng IQueryable, và bộ lọc được xử lý trên máy chủ. Trong một số tình huống, bạn có thể gọi phương thức Where như một phương thức extension trên một collection trong bộ nhớ. Giả sử bạn thay đổi tham chiếu đến _context.Students. Thay vì gọi phương thức Entity Framework DbSet, nó tham chiếu đến một phương thức repository trả về một collection IEnumerable. Kết quả thường giống nhau, nhưng một số trường hợp có thể khác nhau.
Ví dụ, việc triển khai .NET Framework của phương thức Contains thực hiện so sánh phân biệt chữ hoa chữ thường theo mặc định. Trong SQL Server, điều này được xác định bởi cài đặt collation (phân loại) của phiên bản SQL Server. Cài đặt mặc định là không phân biệt chữ hoa chữ thường. Bạn có thể gọi phương thức ToUpper để kiểm tra không phân biệt chữ hoa chữ thường một cách rõ ràng: Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())). Điều này đảm bảo kết quả không thay đổi nếu bạn thay đổi mã sau này để sử dụng repository trả về collection IEnumerable thay vì đối tượng IQueryable.
Tuy nhiên, giải pháp này có ảnh hưởng đến hiệu năng. Mã ToUpper đặt một hàm trong mệnh đề WHERE của câu lệnh TSQL SELECT. Điều này ngăn optimizer sử dụng index. Vì SQL chủ yếu được cài đặt là không phân biệt chữ hoa chữ thường, tốt nhất nên tránh mã ToUpper cho đến khi bạn chuyển sang data store không phân biệt chữ hoa chữ thường.
Thêm ô tìm kiếm vào Student Index view
Trong file Views/Student/Index.cshtml, thêm mã highlight ngay trước thẻ mở bảng <table> để tạo caption, ô text và nút Search:
<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">Mã này sử dụng tag helper <form> để 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, nghĩa là các tham số được truyền trong thân HTTP message chứ không phải trong URL như các query string. Khi bạn chỉ định HTTP GET, dữ liệu form được truyền trong URL dưới dạng query string, cho phép người dùng đánh dấu URL. Hướng dẫn W3C khuyến nghị sử dụng GET khi hành động không dẫn đến cập nhật.
Chạy ứng dụng, chọn tab Students, nhập một chuỗi tìm kiếm và nhấp Search để kiểm tra lọc hoạt động.
Lưu ý rằng URL chứa chuỗi tìm kiếm:
http://localhost:5813/Students?SearchString=an
Nếu bạn đánh dấu trang này, bạn sẽ nhận được danh sách đã lọc khi sử dụng bookmark. Thêm mệnh đề method="get" vào thẻ form gây ra việc tạo query string.
Thêm phân trang vào Students Index
Để thêm phân trang vào trang Students Index, bạn tạo một class PaginatedList sử dụng các câu lệnh Skip và Take để lọc dữ liệu trên máy chủ thay vì luôn truy xuất tất cả các hàng trong bảng. Sau đó bạn thực hiện thêm các thay đổi trong phương thức Index và thêm các nút phân trang vào Index view.
Trong thư mục dự án, tạo file PaginatedList.cs. Thay thế mã template bằng mã 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 mã này nhận 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 cho đối tượng IQueryable. Khi phương thức ToListAsync được gọi trên đối tượng IQueryable, phương thức trả về một List chỉ chứa trang được yêu cầu. Các thuộc tính HasPreviousPage và HasNextPage có thể được sử dụng để bật hoặc tắt các nút phân trang Previous (Trước) và Next (Tiếp theo).
Phương thức CreateAsync được sử dụng thay vì constructor (hàm tạo) để tạo đối tượng PaginatedList<T> vì các constructor không thể chạy mã bất đồng bộ.
Thêm phân trang vào phương thức Index
Trong file StudentsController.cs, thay thế phương thức Index bằng mã sau:
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));
}Mã này thêm tham số số trang, tham số thứ tự sắp xếp hiện tại và tham số bộ lọc hiện tại vào chữ ký phương thức.
Phần tử ViewData có tên CurrentSort cung cấp cho view thứ tự sắp xếp hiện tại. Giá trị này phải được bao gồm trong các liên kết phân trang để thứ tự sắp xếp được duy trì trong khi phân trang.
Phần tử ViewData có tên CurrentFilter cung cấp cho view chuỗi bộ lọc hiện tại. Giá trị này phải được bao gồm trong các liên kết phân trang để duy trì các cài đặt bộ lọc trong khi phân trang.
Nếu chuỗi tìm kiếm thay đổi trong khi phân trang, trang phải được đặt lại về 1 vì bộ lọc mới có thể dẫn đến dữ liệu hiển thị khác. Chuỗi tìm kiếm thay đổi khi một giá trị được nhập vào ô text và người dùng nhấp Submit. Trong trường hợp này, tham số searchString không phải là null.
if (searchString != null)
{
pageNumber = 1;
}
else
{
searchString = currentFilter;
}Ở cuối phương thức Index, phương thức PaginatedList.CreateAsync chuyển đổi truy vấn sinh viên thành một trang sinh viên trong một loại collection hỗ trợ phân trang.
return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), pageNumber ?? 1, pageSize));
Phương thức PaginatedList.CreateAsync nhận số trang. Hai dấu hỏi ?? đại diện cho toán tử null-coalescing (gộp null). Toán tử null-coalescing định nghĩa một giá trị mặc định cho kiểu nullable. Biểu thức (pageNumber ?? 1) có nghĩa là trả về giá trị của pageNumber nếu nó có giá trị, hoặc trả về 1 nếu pageNumber là null.
Thêm liên kết phân trang
Trong file Views/Students/Index.cshtml, thay thế mã hiện có bằng mã sau:
@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>Câu lệnh @model ở đầu trang chỉ định rằng view bây giờ nhận một đối tượng PaginatedList<T> thay vì đối tượng List<T>.
Các liên kết tiêu đề cột sử dụng query string để truyền chuỗi tìm kiếm hiện tại cho controller để người dùng có thể sắp xếp trong kết quả bộ lọc.
Tag helper hiển thị các nút phân trang.
Chạy ứng dụng và đến trang Students.
Chọn các liên kết phân trang theo các thứ tự sắp xếp khác nhau và đảm bảo phân trang hoạt động. Nhập một chuỗi tìm kiếm và thử phân trang lại để xác minh phân trang cũng hoạt động đúng với sắp xếp và lọc.
Tạo trang About
Đối với trang About của trang web Contoso University, bạn hiển thị số lượng sinh viên đăng ký cho mỗi ngày đăng ký. Chức năng này yêu cầu nhóm và tính toán đơn giản trên các nhóm. Để hỗ trợ hành vi này, bạn hoàn thành các nhiệm vụ sau:
- Tạo một class view model (mô hình hiển thị) cho dữ liệu bạn cần truyền cho view.
- Tạo phương thức
Abouttrong Home controller. - Tạo About view.
Tạo view model
Tạo thư mục SchoolViewModels trong thư mục Models.
Trong thư mục mới, thêm file class EnrollmentDateGroup.cs và thay thế mã template bằng mã 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; }
}
}Sửa đổi Home Controller
Trong file HomeController.cs, thêm các câu lệnh using sau ở đầu file:
using Microsoft.EntityFrameworkCore; using ContosoUniversity.Data; using ContosoUniversity.Models.SchoolViewModels; using Microsoft.Extensions.Logging;
Thêm biến class cho database context (ngữ cảnh cơ sở dữ liệu) ngay sau dấu ngoặc nhọn mở { cho class, và nhận instance (thực thể) của context từ ASP.NET Core DI (Dependency Injection - Tiêm phụ thuộc):
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly SchoolContext _context;
public HomeController(ILogger<HomeController> logger, SchoolContext context)
{
_logger = logger;
_context = context;
}Thêm phương thức About với mã sau:
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());
}Câu lệnh LINQ nhóm các thực thể student theo ngày đăng ký. Nó tính toán số lượng thực thể trong mỗi nhóm và lưu trữ kết quả trong một collection của các đối tượng view model EnrollmentDateGroup.
Tạo About view
Thêm file Views/Home/About.cshtml với mã sau:
@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>Chạy ứng dụng và đến trang About. Số lượng sinh viên cho mỗi ngày đăng ký được hiển thị trong bảng.