3부. ASP.NET Core에서 EF Core를 사용한 Razor Pages - 정렬, 필터, 페이징

작성자 : Tom Dykstra, Jeremy Likness, Jon P Smith

Contoso University 웹앱은 EF Core 및 Visual Studio를 사용하여 Razor Pages 웹앱을 만드는 방법을 보여줍니다. 자습서 시리즈에 대한 정보는 첫 번째 자습서를 참조합니다.

해결할 수 없는 문제가 발생한 경우 완성된 앱을 다운로드하고 자습서를 따라 만든 코드와 해당 코드를 비교합니다.

이 자습서에서는 학생 페이지에 정렬, 필터링 및 페이징 기능을 추가합니다.

다음 그림은 완료된 페이지를 보여 줍니다. 열 제목은 열을 정렬하는 클릭할 수 있는 링크입니다. 열 제목을 반복적으로 클릭하여 오름차순 및 내림차순으로 정렬 순서를 전환합니다.

Students index page

정렬 추가

코드를 다음 코드 Pages/Students/Index.cshtml.cs 로 바꿔 정렬을 추가합니다.

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();
    }
}

앞의 코드가 하는 역할은 다음과 같습니다.

  • using System;을 추가해야 합니다.
  • 정렬 매개 변수를 포함할 속성을 추가합니다.
  • Student 속성의 이름을 Students로 변경합니다.
  • OnGetAsync 메서드의 코드를 바꿉니다.

OnGetAsync 메서드는 URL의 쿼리 문자열에서 sortOrder 매개 변수를 받습니다. URL 및 쿼리 문자열은 앵커 태그 도우미에서 생성됩니다.

sortOrder 매개 변수는 Name 또는 Date입니다. 내림차순을 지정하려면 필요에 따라 sortOrder 매개 변수 뒤에 _desc를 추가합니다. 기본 정렬 순서는 오름차순입니다.

인덱스 페이지가 학생 링크에서 요청되는 경우 쿼리 문자열이 없습니다. 학생은 성 기준 오름차순으로 표시됩니다. switch 문에서는 성 기준 오름차순이 default입니다. 사용자가 열 제목 링크를 클릭하면 적절한 sortOrder 값이 쿼리 문자열 값에 제공됩니다.

Razor Page에서 열 제목 하이퍼링크를 적절한 쿼리 문자열 값으로 구성하기 위해 NameSortDateSort가 사용됩니다.

NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";

이 코드는 C# 조건 연산자 ?:을 사용합니다. ?: 연산자는 피연산자 3개를 사용하는 3개로 구성된 연산자입니다. 첫 번째 줄은 sortOrder가 null이거나 비어 있는 경우 NameSortname_desc로 설정되도록 지정합니다. sortOrder가 null도 아니고 비어 있지도 않은 경우 NameSort는 빈 문자열로 설정됩니다.

이러한 두 명령문을 사용하면 페이지에서 다음과 같이 열 제목 하이퍼링크를 설정할 수 있습니다.

현재 정렬 순서 성 하이퍼링크 날짜 하이퍼링크
성 오름차순 내림차순 ascending
성 내림차순 ascending ascending
날짜 오름차순 ascending 내림차순
날짜 내림차순 ascending ascending

메서드는 LINQ to Entities를 사용하여 정렬할 기준이 되는 열을 지정합니다. 코드는 IQueryable<Student>를 switch 문 앞에서 초기화하고 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();

IQueryable을 만들거나 수정하는 경우 데이터베이스에 쿼리가 전송되지 않습니다. 쿼리는 IQueryable 개체가 컬렉션으로 변환될 때까지 실행되지 않습니다. ToListAsync와 같은 메서드를 호출하면 IQueryable이 컬렉션으로 변환됩니다. 따라서 IQueryable 코드는 다음 명령문까지 실행되지 않는 단일 쿼리가 됩니다.

Students = await studentsIQ.AsNoTracking().ToListAsync();

OnGetAsync는 많은 수의 정렬 가능한 열이 포함된 자세한 정보를 가져올 수 있습니다. 이 기능을 코딩하는 다른 방법에 대한 자세한 내용은 이 자습서 시리즈의 MVC 버전에서 동적 LINQ를 사용하여 코드 단순화를 참조하세요.

Students/Index.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>

앞의 코드가 하는 역할은 다음과 같습니다.

  • 하이퍼링크를 LastNameEnrollmentDate 열 머리글에 추가합니다.
  • NameSortDateSort의 정보를 사용하여 현재 정렬 순서 값으로 하이퍼링크를 설정합니다.
  • 페이지 제목을 인덱스에서 학생으로 변경합니다.
  • Model.StudentModel.Students로 변경합니다.

정렬이 작동하는지 확인하려면 다음을 수행합니다.

  • 앱을 실행하고 학생 탭을 선택합니다.
  • 열 제목을 클릭합니다.

필터링 추가

학생 인덱스 페이지에 필터링을 추가하려면 다음을 수행합니다.

  • 텍스트 상자 및 전송 단추가 Razor 페이지에 추가됩니다. 텍스트 상자는 첫 번째 또는 마지막 이름에 검색 문자열을 제공합니다.
  • 페이지 모델이 텍스트 상자 값을 사용하도록 업데이트됩니다.

OnGetAsync 메서드 업데이트

코드를 다음 코드 Students/Index.cshtml.cs 로 바꿔 필터링을 추가합니다.

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();
    }
}

앞의 코드가 하는 역할은 다음과 같습니다.

  • searchString 매개 변수를 OnGetAsync 메서드에 추가하고 매개 변수 값을 CurrentFilter 속성에 저장합니다. 다음 섹션에서 추가되는 텍스트 상자에서 검색 문자열 값이 수신됩니다.
  • Where 절을 LINQ 문에 추가합니다. Where 절은 이름 또는 성에 검색 문자열이 포함된 학생만 선택합니다. LINQ 문은 검색할 값이 있는 경우에만 실행됩니다.

IQueryable 및 IEnumerable

이 코드는 IQueryable 개체에서 Where 메서드를 호출하고, 필터는 서버에서 처리됩니다. 일부 시나리오에서 앱은 메모리 내 컬렉션에서 확장 메서드로 Where 메서드를 호출할 수 있습니다. 예를 들어, _context.Students가 EF CoreDbSet에서 IEnumerable 컬렉션을 반환하는 리포지토리 메서드로 변경되었다고 가정합니다. 결과는 일반적으로 동일하지만 경우에 따라 다를 수 있습니다.

예를 들어 Contains의 .NET Framework 구현은 기본적으로 대/소문자 구분 비교를 수행합니다. SQL Server에서 Contains 대/소문자 구분은 SQL Server 인스턴스의 컬렉션 설정에 의해 결정됩니다. SQL Server는 기본적으로 대/소문자를 구분하지 않습니다. SQLite는 기본적으로 대/소문자를 구분합니다. ToUpper는 테스트가 명시적으로 대/소문자를 구분하지 않도록 하기 위해 호출됩니다.

Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())`

앞의 코드는 Where 메서드가 IEnumerable에서 호출되거나 SQLite에서 실행되는 경우에도 필터가 대/소문자를 구분하지 않도록 합니다.

ContainsIEnumerable 컬렉션에서 호출된 경우 .NET Core 구현이 사용됩니다. ContainsIQueryable 개체에서 호출된 경우 데이터베이스 구현이 사용됩니다.

성능상 이유로, 일반적으로 IQueryable에 대해 Contains를 호출하는 것이 더 좋습니다. IQueryable을 사용하면 필터링이 데이터베이스 서버에서 수행됩니다. IEnumerable이 먼저 만들어진 경우에는 먼저 데이터베이스 서버에서 모든 행이 반환되어야 합니다.

ToUpper 호출에 대한 성능 저하가 발생합니다. ToUpper 코드는 TSQL SELECT 문의 WHERE 절에 함수를 추가합니다. 추가된 함수는 최적화 프로그램이 인덱스를 사용하지 않도록 합니다. SQL은 대/소문자를 구분하지 않도록 설치되어 있으므로 필요하지 않은 경우 ToUpper 호출을 피하는 것이 가장 좋습니다.

자세한 내용은 SQLite 공급자를 사용하여 대/소문자를 구분하지 않는 쿼리를 사용하는 방법을 참조하세요.

Razor 페이지 업데이트

코드를 Pages/Students/Index.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>

이전 코드는 <form>태그 도우미를 사용하여 검색 텍스트 상자 및 단추를 추가합니다. 기본적으로 <form> 태그 도우미는 POST로 양식 데이터를 전송합니다. POST를 사용하면 매개 변수가 URL에 없는 HTTP 메시지 본문에 전달됩니다. HTTP GET을 사용하는 경우 양식 데이터가 URL에 쿼리 문자열로 전달됩니다. 사용자는 쿼리 문자열로 데이터를 전달하여 URL을 책갈피로 표시할 수 있습니다. W3C 지침에 따라 작업이 업데이트되지 않을 때 GET을 사용해야 합니다.

앱을 테스트합니다.

  • 학생 탭을 선택하고 검색 문자열을 입력합니다. SQLite를 사용하는 경우 필터는 앞에 표시된 선택적 ToUpper 코드를 구현한 경우에만 대/소문자를 구분하지 않습니다.

  • 검색을 선택합니다.

URL에 검색 문자열이 포함되어 있음을 확인하세요. 예시:

https://localhost:5001/Students?SearchString=an

페이지가 책갈피로 표시된 경우 책갈피에는 페이지에 대한 URL 및 SearchString 쿼리 문자열이 포함됩니다. form 태그의 method="get"으로 인해 쿼리 문자열이 생성됩니다.

현재 열 제목 정렬 링크를 선택하면 검색 상자에서 필터 값이 손실됩니다. 손실된 필터 값은 다음 섹션에서 수정됩니다.

페이징 추가

이 섹션에서는 페이징을 지원하기 위해 PaginatedList 클래스를 만듭니다. PaginatedList 클래스는 SkipTake 문을 사용하여 테이블의 모든 행을 검색하는 대신 서버에 있는 데이터를 필터링합니다. 다음 그림에는 페이징 단추가 나와 있습니다.

Students index page with paging links

PaginatedList 클래스 만들기

프로젝트 폴더에서 다음 코드로 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 메서드는 페이지 크기 및 페이지 번호를 사용하고 적절한 SkipTake 문을 IQueryable에 적용합니다. IQueryable에서 ToListAsync를 호출하면 요청된 페이지만 포함하는 목록을 반환합니다. 이전다음 페이징 단추를 사용 또는 사용하지 않도록 설정하는 데 HasPreviousPageHasNextPage 속성을 사용합니다.

CreateAsync 메서드는 PaginatedList<T>를 만드는 데 사용합니다. 생성자는 PaginatedList<T> 개체를 만들 수 없고 비동기 코드를 실행할 수 없습니다.

구성에 페이지 크기 추가

appsettings.json구성 파일에 PageSize를 추가합니다.

{
  "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"
  }
}

IndexModel에 페이징 추가

코드를 Students/Index.cshtml.cs 바꿔 페이징을 추가합니다.

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);
        }
    }
}

앞의 코드가 하는 역할은 다음과 같습니다.

  • Students 속성의 형식을 IList<Student>에서 PaginatedList<Student>로 변경합니다.
  • 페이지 인덱스, 현재 sortOrdercurrentFilterOnGetAsync 메서드 시그니처에 추가합니다.
  • CurrentSort 속성에 정렬 순서를 저장합니다.
  • 새 검색 문자열이 있는 경우 페이지 인덱스를 1로 다시 설정합니다.
  • PaginatedList 클래스를 사용하여 Student 엔터티를 가져옵니다.
  • 구성에서 pageSize을 3으로 설정하고, 구성이 실패하는 경우 4로 설정합니다.

다음과 같은 경우 OnGetAsync가 수신하는 모든 매개 변수는 Null입니다.

  • 페이지가 학생 링크에서 호출됩니다.
  • 사용자가 페이징 또는 정렬 링크를 클릭하지 않았습니다.

페이징 링크를 클릭하면 페이지 인덱스 변수에 표시할 페이지 번호가 포함됩니다.

CurrentSort 속성은 Razor Page에 현재 정렬 순서를 제공합니다. 현재 정렬 순서는 페이징하는 동안 정렬 순서를 유지하기 위해 페이징 링크에 포함되어야 합니다.

CurrentFilter 속성은 Razor Page에 현재 필터 문자열을 제공합니다. CurrentFilter 값은:

  • 페이징하는 동안 필터 설정을 유지하기 위해 페이징 링크에 포함되어야 합니다.
  • 페이지를 다시 표시하는 경우 텍스트 상자에 복원되어야 합니다.

검색 문자열이 페이징하는 동안 변경되면 페이지가 1로 다시 설정됩니다. 새 필터로 인해 다른 데이터가 표시될 수 있으므로 페이지는 1로 재설정되어야 합니다. 검색 값을 입력하는 경우 및 전송을 선택한 경우:

  • 검색 문자열이 변경됩니다.
  • searchString 매개 변수가 null이 아닙니다.

PaginatedList.CreateAsync 메서드가 학생 쿼리를 페이징을 지원하는 컬렉션 형식의 단일 학생 페이지로 변환합니다. 해당 단일 학생 페이지가 Razor 페이지에 전달됩니다.

PaginatedList.CreateAsync에서 pageIndex 뒤에 있는 두 개의 물음표는 Null 병합 연산자를 나타냅니다. Null 병합 연산자는 null 허용 형식에 대한 기본값을 정의합니다. pageIndex ?? 1 식은 값이 있으면 pageIndex의 값을 반환하고, 그렇지 않으면 1을 반환합니다.

Students/Index.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>

열 머리글 링크는 쿼리 문자열을 사용하여 현재 검색 문자열을 OnGetAsync 메서드에 전달합니다.

<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
   asp-route-currentFilter="@Model.CurrentFilter">
    @Html.DisplayNameFor(model => model.Students[0].LastName)
</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 @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>

앱을 실행하고 학생 페이지로 이동합니다.

  • 페이징이 작동하는지 확인하려면 다른 정렬 순서의 페이징 링크를 클릭합니다.
  • 정렬 및 필터링을 통해 페이징이 제대로 작동하는지 확인하려면 검색 문자열을 입력하고 페이징을 다시 시도합니다.

students index page with paging links

그룹화

이 섹션에서는 각 등록 날짜에 대해 등록한 학생 수를 표시하는 About 페이지를 만듭니다. 업데이트는 그룹화를 사용하며 다음 단계를 포함합니다.

  • 페이지에서 사용하는 데이터에 대한 뷰 모델을 만듭니다 About .
  • About 페이지를 업데이트하여 뷰 모델을 사용합니다.

뷰 모델 만들기

Models/SchoolViewModels 폴더를 만듭니다.

다음 코드를 사용하여 만듭니 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; }
    }
}

Razor Page 만들기

다음 코드를 사용하여 Pages/About.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>

페이지 모델 만들기

Pages/About.cshtml.cs 파일을 다음 코드로 업데이트합니다.

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();
        }
    }
}

LINQ 문은 등록 날짜별로 학생 엔터티를 그룹화하고 각 그룹의 엔터티 수를 계산하며 결과를 EnrollmentDateGroup 뷰 모델 개체의 컬렉션에 저장합니다.

앱을 실행하고 정보 페이지로 이동합니다. 각 등록 날짜에 대한 학생 수가 테이블에 표시됩니다.

About page

다음 단계

다음 자습서에서는 앱은 마이그레이션을 사용하여 데이터 모델을 업데이트합니다.

이 자습서에서는 정렬, 필터링, 그룹화 및 페이징 기능이 추가됩니다.

다음 그림은 완료된 페이지를 보여 줍니다. 열 제목은 열을 정렬하는 클릭할 수 있는 링크입니다. 열 제목을 반복적으로 클릭하면 오름차순과 내림차순 정렬 순서 사이로 전환됩니다.

Students index page

해결할 수 없는 문제가 발생한 경우 완성된 앱을 다운로드합니다.

인덱스 페이지에 정렬 추가

정렬 매개 변수를 포함할 Students/Index.cshtml.csPageModel에 문자열을 추가합니다.

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.csOnGetAsync를 다음 코드로 업데이트합니다.

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 매개 변수를 받습니다. URL(쿼리 문자열 포함)이 앵커 태그 도우미에서 생성됩니다.

sortOrder 매개 변수는 “Name” 또는 “Date”입니다. 필요에 따라 내림차순을 지정하기 위해 sortOrder 매개 변수 다음에 “_desc”가 옵니다. 기본 정렬 순서는 오름차순입니다.

인덱스 페이지가 학생 링크에서 요청되는 경우 쿼리 문자열이 없습니다. 학생은 성 기준 오름차순으로 표시됩니다. switch 문에서 성 기준 오름차순이 기본값(제어 이동 사례)입니다. 사용자가 열 제목 링크를 클릭하면 적절한 sortOrder 값이 쿼리 문자열 값에 제공됩니다.

Razor Page에서 열 제목 하이퍼링크를 적절한 쿼리 문자열 값으로 구성하기 위해 NameSortDateSort가 사용됩니다.

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# 조건적 ?: 연산자를 포함합니다.

NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";

첫 번째 줄은 sortOrder가 null이거나 비어 있는 경우 NameSort가 “name_desc”로 설정되도록 지정합니다. sortOrder가 null도 아니고 비어 있지 않은 경우 NameSort가 빈 문자열로 설정됩니다.

?: operator는 삼진 연산자라고도 합니다.

이러한 두 명령문을 사용하면 페이지에서 다음과 같이 열 제목 하이퍼링크를 설정할 수 있습니다.

현재 정렬 순서 성 하이퍼링크 날짜 하이퍼링크
성 오름차순 내림차순 ascending
성 내림차순 ascending ascending
날짜 오름차순 ascending 내림차순
날짜 내림차순 ascending ascending

메서드는 LINQ to Entities를 사용하여 정렬할 기준이 되는 열을 지정합니다. 코드는 IQueryable<Student>를 switch 문 앞에서 초기화하고 switch 문에서 수정합니다.

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();
}

IQueryable을 만들거나 수정하는 경우 데이터베이스에 쿼리가 전송되지 않습니다. 쿼리는 IQueryable 개체가 컬렉션으로 변환될 때까지 실행되지 않습니다. ToListAsync와 같은 메서드를 호출하면 IQueryable이 컬렉션으로 변환됩니다. 따라서 IQueryable 코드는 다음 명령문까지 실행되지 않는 단일 쿼리가 됩니다.

Student = await studentIQ.AsNoTracking().ToListAsync();

OnGetAsync는 많은 수의 정렬 가능한 열이 포함된 자세한 정보를 가져올 수 있습니다.

코드를 Students/Index.cshtml강조 표시된 다음 코드로 바꿉다.

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

앞의 코드가 하는 역할은 다음과 같습니다.

  • 하이퍼링크를 LastNameEnrollmentDate 열 머리글에 추가합니다.
  • NameSortDateSort의 정보를 사용하여 현재 정렬 순서 값으로 하이퍼링크를 설정합니다.

정렬이 작동하는지 확인하려면 다음을 수행합니다.

  • 앱을 실행하고 학생 탭을 선택합니다.
  • 을 클릭합니다.
  • 등록 날짜를 클릭합니다.

코드를 더 잘 이해하려면 다음을 수행합니다.

  • Students/Index.cshtml.cs에서 switch (sortOrder)에 중단점을 설정합니다.
  • NameSortDateSort에 대한 조사식을 추가합니다.
  • Students/Index.cshtml에서 @Html.DisplayNameFor(model => model.Student[0].LastName)에 중단점을 설정합니다.

디버거를 단계별로 실행합니다.

학생 인덱스 페이지에 검색 상자 추가

학생 인덱스 페이지에 필터링을 추가하려면 다음을 수행합니다.

  • 텍스트 상자 및 전송 단추가 Razor 페이지에 추가됩니다. 텍스트 상자는 첫 번째 또는 마지막 이름에 검색 문자열을 제공합니다.
  • 페이지 모델이 텍스트 상자 값을 사용하도록 업데이트됩니다.

인덱스 메서드에 필터링 기능 추가

Students/Index.cshtml.csOnGetAsync를 다음 코드로 업데이트합니다.

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();
}

앞의 코드가 하는 역할은 다음과 같습니다.

  • searchString 매개 변수를 OnGetAsync 메서드에 추가합니다. 다음 섹션에서 추가되는 텍스트 상자에서 검색 문자열 값이 수신됩니다.
  • LINQ 문 및 Where 절을 추가했습니다. Where 절은 이름 또는 성에 검색 문자열이 포함된 학생만 선택합니다. LINQ 문은 검색할 값이 있는 경우에만 실행됩니다.

참고: 이전 코드는 IQueryable 개체에서 Where 메서드를 호출하고, 필터는 서버에서 처리됩니다. 일부 시나리오에서 앱은 메모리 내 컬렉션에서 확장 메서드로 Where 메서드를 호출할 수 있습니다. 예를 들어, _context.Students가 EF CoreDbSet에서 IEnumerable 컬렉션을 반환하는 리포지토리 메서드로 변경되었다고 가정합니다. 결과는 일반적으로 동일하지만 경우에 따라 다를 수 있습니다.

예를 들어 Contains의 .NET Framework 구현은 기본적으로 대/소문자 구분 비교를 수행합니다. SQL Server에서 Contains 대/소문자 구분은 SQL Server 인스턴스의 컬렉션 설정에 의해 결정됩니다. SQL Server는 기본적으로 대/소문자를 구분하지 않습니다. ToUpper는 테스트가 명시적으로 대/소문자를 구분하지 않도록 하기 위해 호출됩니다.

Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())

위의 코드는 코드가 IEnumerable을 사용하도록 변경된 경우 결과가 대/소문자를 구분하지 않는지 확인합니다. ContainsIEnumerable 컬렉션에서 호출된 경우 .NET Core 구현이 사용됩니다. ContainsIQueryable 개체에서 호출된 경우 데이터베이스 구현이 사용됩니다. 리포지토리에서 IEnumerable을 반환하면 상당한 성능 저하가 발생할 수 있습니다.

  1. 모든 행이 DB 서버에서 반환됩니다.
  2. 필터는 애플리케이션에서 반환된 모든 행에 적용됩니다.

ToUpper 호출에 대한 성능 저하가 발생합니다. ToUpper 코드는 TSQL SELECT 문의 WHERE 절에 함수를 추가합니다. 추가된 함수는 최적화 프로그램이 인덱스를 사용하지 않도록 합니다. SQL은 대/소문자를 구분하지 않도록 설치되어 있으므로 필요하지 않은 경우 ToUpper 호출을 피하는 것이 가장 좋습니다.

학생 인덱스 페이지에 검색 상자 추가

에서 Pages/Students/Index.cshtml다음 강조 표시된 코드를 추가하여 검색 단추 및 다양한 크롬을 만듭니다.

@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로 양식 데이터를 전송합니다. POST를 사용하면 매개 변수가 URL에 없는 HTTP 메시지 본문에 전달됩니다. HTTP GET을 사용하는 경우 양식 데이터가 URL에 쿼리 문자열로 전달됩니다. 사용자는 쿼리 문자열로 데이터를 전달하여 URL을 책갈피로 표시할 수 있습니다. W3C 지침에 따라 작업이 업데이트되지 않을 때 GET을 사용해야 합니다.

앱을 테스트합니다.

  • 학생 탭을 선택하고 검색 문자열을 입력합니다.
  • 검색을 선택합니다.

URL에 검색 문자열이 포함되어 있음을 확인하세요.

http://localhost:5000/Students?SearchString=an

페이지가 책갈피로 표시된 경우 책갈피에는 페이지에 대한 URL 및 SearchString 쿼리 문자열이 포함됩니다. form 태그의 method="get"으로 인해 쿼리 문자열이 생성됩니다.

현재 열 제목 정렬 링크를 선택하면 검색 상자에서 필터 값이 손실됩니다. 손실된 필터 값은 다음 섹션에서 수정됩니다.

학생 인덱스 페이지에 페이징 기능 추가

이 섹션에서는 페이징을 지원하기 위해 PaginatedList 클래스를 만듭니다. PaginatedList 클래스는 SkipTake 문을 사용하여 테이블의 모든 행을 검색하는 대신 서버에 있는 데이터를 필터링합니다. 다음 그림에는 페이징 단추가 나와 있습니다.

Students index page with paging links

프로젝트 폴더에서 다음 코드로 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 메서드는 페이지 크기 및 페이지 번호를 사용하고 적절한 SkipTake 문을 IQueryable에 적용합니다. IQueryable에서 ToListAsync를 호출하면 요청된 페이지만 포함하는 목록을 반환합니다. 이전다음 페이징 단추를 사용 또는 사용하지 않도록 설정하는 데 HasPreviousPageHasNextPage 속성을 사용합니다.

CreateAsync 메서드는 PaginatedList<T>를 만드는 데 사용합니다. 생성자를 PaginatedList<T> 개체를 만들 수 없고, 생성자는 비동기 코드를 실행할 수 없습니다.

인덱스 메서드에 페이징 기능 추가

Students/Index.cshtml.cs에서 Student 유형을 IList<Student>에서 PaginatedList<Student>로 업데이트합니다.

public PaginatedList<Student> Student { get; set; }

Students/Index.cshtml.csOnGetAsync를 다음 코드로 업데이트합니다.

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);
}

이전 코드는 페이지 인덱스, 현재 sortOrdercurrentFilter를 메서드 서명에 추가합니다.

public async Task OnGetAsync(string sortOrder,
    string currentFilter, string searchString, int? pageIndex)

모든 매개 변수는 다음 경우에 null입니다.

  • 페이지가 학생 링크에서 호출됩니다.
  • 사용자가 페이징 또는 정렬 링크를 클릭하지 않았습니다.

페이징 링크를 클릭하면 페이지 인덱스 변수에 표시할 페이지 번호가 포함됩니다.

CurrentSort는 Razor 페이지를 현재 정렬 순서로 제공합니다. 현재 정렬 순서는 페이징하는 동안 정렬 순서를 유지하기 위해 페이징 링크에 포함되어야 합니다.

CurrentFilter는 Razor 페이지를 현재 필터 문자열로 제공합니다. CurrentFilter 값은:

  • 페이징하는 동안 필터 설정을 유지하기 위해 페이징 링크에 포함되어야 합니다.
  • 페이지를 다시 표시하는 경우 텍스트 상자에 복원되어야 합니다.

검색 문자열이 페이징하는 동안 변경되면 페이지가 1로 다시 설정됩니다. 새 필터로 인해 다른 데이터가 표시될 수 있으므로 페이지는 1로 재설정되어야 합니다. 검색 값을 입력하는 경우 및 전송을 선택한 경우:

  • 검색 문자열이 변경됩니다.
  • searchString 매개 변수가 null이 아닙니다.
if (searchString != null)
{
    pageIndex = 1;
}
else
{
    searchString = currentFilter;
}

PaginatedList.CreateAsync 메서드가 학생 쿼리를 페이징을 지원하는 컬렉션 형식의 단일 학생 페이지로 변환합니다. 해당 단일 학생 페이지가 Razor 페이지에 전달됩니다.

Student = await PaginatedList<Student>.CreateAsync(
    studentIQ.AsNoTracking(), pageIndex ?? 1, pageSize);

PaginatedList.CreateAsync에서 두 개의 물음표는 Null 병합 연산자를 나타냅니다. Null 병합 연산자는 null 허용 형식에 대한 기본값을 정의합니다. 식 (pageIndex ?? 1)은 값이 있는 경우 pageIndex의 값을 반환함을 의미합니다. pageIndex에 값이 없으면 1을 반환합니다.

에서 태그를 업데이트합니다 Students/Index.cshtml. 변경 내용이 강조 표시되어 있습니다.

@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 메서드에 전달하므로 사용자가 필터 결과 내에서 정렬할 수 있습니다.

<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
   asp-route-currentFilter="@Model.CurrentFilter">
    @Html.DisplayNameFor(model => model.Student[0].LastName)
</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 @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>

앱을 실행하고 학생 페이지로 이동합니다.

  • 페이징이 작동하는지 확인하려면 다른 정렬 순서의 페이징 링크를 클릭합니다.
  • 정렬 및 필터링을 통해 페이징이 제대로 작동하는지 확인하려면 검색 문자열을 입력하고 페이징을 다시 시도합니다.

students index page with paging links

코드를 더 잘 이해하려면 다음을 수행합니다.

  • Students/Index.cshtml.cs에서 switch (sortOrder)에 중단점을 설정합니다.
  • NameSort, DateSort, CurrentSortModel.Student.PageIndex에 대한 조사식을 추가합니다.
  • Students/Index.cshtml에서 @Html.DisplayNameFor(model => model.Student[0].LastName)에 중단점을 설정합니다.

디버거를 단계별로 실행합니다.

학생 통계를 표시하도록 정보 페이지를 업데이트

이 단계에서 Pages/About.cshtml 는 각 등록 날짜에 등록한 학생 수를 표시하도록 업데이트됩니다. 업데이트는 그룹화를 사용하며 다음 단계를 포함합니다.

  • 정보 페이지에서 사용되는 데이터에 대한 보기 모델을 만듭니다.
  • 정보 페이지를 업데이트하여 보기 모델을 사용합니다.

뷰 모델 만들기

Models 폴더에 SchoolViewModels 폴더를 만듭니다.

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; }
    }
}

페이지 모델 정보 업데이트

ASP.NET Core 2.2의 웹 템플릿에는 정보 페이지가 포함되지 않습니다. ASP.NET Core 2.2를 사용하는 경우 Razor 정보 페이지를 만드세요.

Pages/About.cshtml.cs 파일을 다음 코드로 업데이트합니다.

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 문은 등록 날짜별로 학생 엔터티를 그룹화하고 각 그룹의 엔터티 수를 계산하며 결과를 EnrollmentDateGroup 뷰 모델 개체의 컬렉션에 저장합니다.

Razor 페이지 정보 수정

Pages/About.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.Student)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.EnrollmentDate)
            </td>
            <td>
                @item.StudentCount
            </td>
        </tr>
    }
</table>

앱을 실행하고 정보 페이지로 이동합니다. 각 등록 날짜에 대한 학생 수가 테이블에 표시됩니다.

해결할 수 없는 문제가 발생한 경우 이 단계에 완성된 앱을 다운로드합니다.

About page

추가 리소스

다음 자습서에서는 앱은 마이그레이션을 사용하여 데이터 모델을 업데이트합니다.