Nguon: Microsoft Learn · .NET 8.0

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

Nguồn: Part 3: Razor Pages - EF Core Sort, Filter, Page

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:

csharp
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:

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ố sortOrderName 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.

NameSortDateSort đượ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:

csharp
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ạiHyperlink Last NameHyperlink Date
Last Name tăng dầngiảm dầntăng dần
Last Name giảm dầntăng dầntăng dần
Date tăng dầntăng dầngiảm dần
Date giảm dầntăng dầntă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:

csharp
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.

Thay thế code trong Students/Index.cshtml bằng code sau:

cshtml
@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:

Cập nhật phương thức OnGetAsync

Thay thế code trong Students/Index.cshtml.cs bằng code sau để thêm filtering:

csharp
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:

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:

cshtml
@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 SkipTake để 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:

csharp
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 SkipTake thích hợp vào IQueryable. Các thuộc tính HasPreviousPageHasNextPage được sử dụng để bật hoặc tắt các nút phân trang PreviousNext.

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:

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:

csharp
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:

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.

Thay thế code trong Students/Index.cshtml bằng code sau:

cshtml
@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:

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

Tạo thư mục Models/SchoolViewModels.

Tạo SchoolViewModels/EnrollmentDateGroup.cs với code sau:

csharp
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:

cshtml
@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:

csharp
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.