자습서: 정렬, 필터링 및 페이징 추가 - ASP.NET MVC 및 EF Core 사용

이전 자습서에서는 학생 엔터티에 대한 기본적인 CRUD 작업을 위한 일련의 웹 페이지를 구현했습니다. 이 자습서에서는 학생 인덱스 페이지에 정렬, 필터링 및 페이징 기능을 추가합니다. 단순 그룹화를 수행하는 페이지도 만듭니다.

다음 그림에서는 작업이 완료되었을 때 페이지 모양을 보여 줍니다. 열 제목은 해당 열로 정렬하기 위해 사용자가 클릭할 수 있는 링크입니다. 열 제목을 반복해서 클릭하면 오름차순 및 내림차순으로 정렬 순서가 토글됩니다.

Students index page

이 자습서에서는 다음을 수행합니다.

  • 열 정렬 링크 추가
  • 검색 상자 추가
  • 학생 인덱스에 페이징 추가
  • Index 메서드에 페이징 추가
  • 페이징 링크 추가
  • 정보 페이지 만들기

필수 조건

Student 인덱스 페이지에 정렬을 추가하려면 Students 컨트롤러의 Index 메서드를 변경하고 Student 인덱스 뷰에 코드를 추가합니다.

Index 메서드에 정렬 기능 추가

StudentsController.cs에서 Index 메서드를 다음 코드로 바꿉니다.

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

이 코드는 URL의 쿼리 문자열에서 sortOrder 매개 변수를 받습니다. 쿼리 문자열 값은 매개 변수로 ASP.NET Core MVC에 의해 작업 메서드에 제공됩니다. 매개 변수는 "Name" 또는 "Date" 문자열이며 필요에 따라 밑줄과 내림차순을 지정하는 문자열 "desc"가 옵니다. 기본 정렬 순서는 오름차순입니다.

인덱스 페이지를 처음 요청하면 쿼리 문자열은 없습니다. 학생은 성을 기준으로 오름차순으로 표시되며 이것은 switch 문의 제어 이동 사례로 설정된 기본값입니다. 사용자가 열 제목 하이퍼링크를 클릭하면 쿼리 문자열에 해당 sortOrder 값이 제공됩니다.

열 제목 하이퍼링크를 적절한 쿼리 문자열 값으로 구성하기 위해 뷰에 두 ViewData 요소(NameSortParm 및 DateSortParm)가 사용됩니다.

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

이것은 3개로 구성된 문입니다. 첫 번째 문은 sortOrder 매개 변수는 Null인지, 비어 있는지 여부를 지정하고 Null이면 NameSortParm을 "name_desc"로 설정하고, 그렇지 않으면 빈 문자열로 설정합니다. 이러한 두 명령문을 사용하면 뷰에서 다음과 같이 열 제목 하이퍼링크를 설정할 수 있습니다.

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

메서드는 LINQ to Entities를 사용하여 정렬할 기준이 되는 열을 지정합니다. 이 코드는 switch 문 앞에 IQueryable 변수를 만들고 switch 문에서 변수를 수정한 다음, switch 문 다음에 ToListAsync 메서드를 호출합니다. IQueryable 변수를 작성하고 수정하면 데이터베이스에 쿼리가 보내지지 않습니다. IQueryable 개체를 ToListAsync와 같은 메서드를 호출하여 컬렉션으로 변환할 때까지 쿼리가 실행되지 않습니다. 따라서 이 코드는 return View 문까지 실행되지 않는 단일 쿼리가 됩니다.

많은 수의 열로 이 코드가 길어질 수 있습니다. 이 시리즈의 마지막 자습서에서는 문자열 변수에 OrderBy 열의 이름을 전달할 수 있는 코드를 작성하는 방법을 보여 줍니다.

코드를 다음 코드 Views/Students/Index.cshtml로 바꿔 열 머리글 하이퍼링크를 추가합니다. 변경된 선이 강조 표시됩니다.

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

이 코드는 ViewData 속성의 정보를 사용하여 하이퍼링크를 적절한 쿼리 문자열 값으로 설정합니다.

앱을 실행하여 Students(학생) 탭을 선택하고 Last Name(성)Enrollment Date(등록 날짜) 열 머리글을 클릭하여 제대로 정렬되는지 확인합니다.

Students index page in name order

학생 인덱스 페이지에 필터링을 추가하려면 뷰에 텍스트 상자와 [제출] 단추를 추가하고 Index 메서드에 해당 변경 내용을 적용합니다. 텍스트 상자를 통해 이름 및 성 필드에서 검색할 문자열을 입력할 수 있습니다.

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

StudentsController.cs에서 Index 메서드를 다음 코드로 바꿉니다(변경 내용이 강조 표시됨).

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

searchString 매개 변수를 Index 메서드에 추가했습니다. 검색 문자열 값은 Index 뷰에 추가할 텍스트 상자에서 가져옵니다. 또한 성과 이름에 검색 문자열이 포함된 학생만 선택하는 where 절이 LINQ 문에 추가되었습니다. where 절을 추가하는 문은 검색할 값이 있는 경우에만 실행됩니다.

참고 항목

여기에서는 IQueryable 개체에서 Where 메서드를 호출하고 있으며 필터는 서버에서 처리됩니다. 일부 시나리오에서는 메모리 내 컬렉션에서 확장 메서드로 Where 메서드를 호출할 수 있습니다. 예를 들어 EF DbSet 대신 컬렉션을 반환 IEnumerable 하는 리포지토리 메서드를 참조하도록 _context.Students 참조를 변경한다고 가정합니다. 결과는 일반적으로 동일하지만 경우에 따라 다를 수 있습니다.

예를 들어 Contains 메서드의 .NET Framework 구현은 기본적으로 대/소문자 구분 비교를 수행하지만 SQL Server에서는 SQL Server 인스턴스의 데이터 정렬 설정에 따라 결정됩니다. 이 설정은 기본적으로 대/소문자를 구분하지 않습니다. 테스트에서 대/소문자를 구분하지 않도록 명시적으로 지정하려면 ToUpper 메서드를 호출하면 됩니다. Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper()) 이렇게 하면 IQueryable 개체 대신 IEnumerable 컬렉션을 반환하는 리포지토리를 사용하도록 코드를 나중에 변경할 경우 결과가 동일하게 유지됩니다. (컬렉션에서 메서드를 ContainsIEnumerable 호출하면 .NET Framework 구현이 구현됩니다. 개체에서 메서드를 IQueryable 호출하면 데이터베이스 공급자 구현이 제공됩니다.) 그러나 이 솔루션에는 성능 저하가 있습니다. ToUpper 코드는 TSQL SELECT 문의 WHERE 절에 함수를 배치합니다. 그러면 최적화 프로그램이 인덱스를 사용할 수 없게 됩니다. SQL은 대부분 대/소문자를 구분하지 않도록 설치된다는 것을 고려하면 대/소문자 구분 데이터 저장소로 마이그레이션할 때까지 ToUpper 코드를 사용하지 않는 것이 좋습니다.

Students 인덱스 뷰에 검색 상자 추가

에서 Views/Student/Index.cshtml캡션, 텍스트 상자 및 검색 단추를 만들기 위해 강조 표시된 코드를 열기 테이블 태그 바로 앞에 추가합니다.

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-action="Index" method="get">
    <div class="form-actions no-color">
        <p>
            Find by name: <input type="text" name="SearchString" value="@ViewData["CurrentFilter"]" />
            <input type="submit" value="Search" class="btn btn-default" /> |
            <a asp-action="Index">Back to Full List</a>
        </p>
    </div>
</form>

<table class="table">

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

앱을 실행하고 Students(학생) 탭을 선택하며 검색 문자열을 입력한 후 [검색]을 클릭하여 필터링이 작동하는지 확인합니다.

Students index page with filtering

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

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

이 페이지를 책갈피로 지정하면 책갈피를 사용할 때 필터링된 목록을 얻게 됩니다. method="get"form 태그에 추가하면 쿼리 문자열이 생성됩니다.

이 단계에서 열 제목 정렬 링크를 클릭하면 검색 상자에 입력한 필터 값이 손실됩니다. 다음 섹션에서 이 문제를 해결합니다.

학생 인덱스에 페이징 추가

학생 인덱스 페이지에 페이징을 추가하려면 SkipTake 문을 사용하는 PaginatedList 클래스를 만들어 항상 테이블의 모든 행을 검색하는 대신, 서버에서 데이터를 필터링합니다. 그런 다음, Index 메서드를 추가로 변경하고 Index 뷰에 페이징 단추를 추가합니다. 다음 그림에는 페이징 단추가 나와 있습니다.

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> 개체를 만드는 데 사용됩니다.

Index 메서드에 페이징 추가

StudentsController.cs에서 Index 메서드를 다음 코드로 바꿉니다.

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

이 코드는 페이지 번호 매개 변수, 현재 정렬 순서 매개 변수 및 현재 필터 매개 변수를 메서드 시그니처에 추가합니다.

public async Task<IActionResult> Index(
    string sortOrder,
    string currentFilter,
    string searchString,
    int? pageNumber)

페이지가 처음 표시되거나 사용자가 페이징 또는 정렬 링크를 클릭하지 않으면 모든 매개 변수가 Null이 됩니다. 페이징 링크를 클릭하면 페이지 변수에 표시할 페이지 번호가 포함됩니다.

페이징 동안 정렬 순서를 동일하게 유지하기 위해 페이징 링크에 포함되어야 하므로 CurrentSort라는 ViewData 요소는 현재 정렬 순서의 뷰를 제공합니다.

CurrentFilter라는 ViewData 요소는 현재 필터 문자열이 포함된 뷰를 제공합니다. 이 값은 페이징 중 필터 설정을 유지하기 위해 페이징 링크에 포함되어야 하며 페이지를 다시 표시할 때 텍스트 상자에 복원되어야 합니다.

페이징 중에 검색 문자열이 변경되면 새 필터로 인해 다른 데이터가 표시될 수 있으므로 페이지는 1로 재설정되어야 합니다. 텍스트 상자에 값을 입력하고 [제출] 단추를 누르면 검색 문자열이 변경됩니다. 이 경우 searchString 매개 변수는 Null이 아닙니다.

if (searchString != null)
{
    pageNumber = 1;
}
else
{
    searchString = currentFilter;
}

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

return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), pageNumber ?? 1, pageSize));

PaginatedList.CreateAsync 메서드는 페이지 번호를 사용합니다. 두 개의 물음표는 Null 병합 연산자를 나타냅니다. Null 병합 연산자는 nullable 형식의 기본값을 정의합니다. (pageNumber ?? 1) 식은 값이 있는 경우 pageNumber 값을 반환하고 pageNumber가 Null이면 1일 반환합니다.

Views/Students/Index.cshtml에서 기존 코드를 다음 코드로 바꿉니다. 변경 내용은 강조 표시되어 있습니다.

@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>
            Find by name: <input type="text" name="SearchString" value="@ViewData["CurrentFilter"]" />
            <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>

페이지 맨 위에 @model 문은 뷰가 List<T> 개체 대신 PaginatedList<T> 개체를 가져오는 것을 지정합니다.

열 머리글 링크는 쿼리 문자열을 사용하여 현재 검색 문자열을 컨트롤러에 전달하므로 사용자가 필터 결과 내에서 정렬할 수 있습니다.

<a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asp-route-currentFilter ="@ViewData["CurrentFilter"]">Enrollment Date</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 @prevDisabled">
   Previous
</a>

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

Students index page with paging links

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

정보 페이지 만들기

Contoso University 웹 사이트의 정보 페이지에는 각 등록 날짜에 등록된 학생 수가 표시됩니다. 여기에는 그룹화와 그룹에 대한 간단한 계산이 필요합니다. 이 작업을 수행하기 위해 다음을 수행합니다.

  • 뷰에 전달해야 하는 데이터에 대해 뷰 모델 클래스를 만듭니다.
  • Home 컨트롤러에서 About 메서드를 만듭니다.
  • 정보 뷰를 만듭니다.

뷰 모델 만들기

Models 폴더에 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; }
    }
}

Home 컨트롤러 수정

HomeController.cs에서 파일 맨 위에 다음 using 문을 추가합니다.

using Microsoft.EntityFrameworkCore;
using ContosoUniversity.Data;
using ContosoUniversity.Models.SchoolViewModels;
using Microsoft.Extensions.Logging;

클래스의 여는 중괄호 바로 뒤에 데이터베이스 컨텍스트에 대한 클래스 변수를 추가하고 ASP.NET Core DI에서 컨텍스트 인스턴스를 가져옵니다.

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly SchoolContext _context;

    public HomeController(ILogger<HomeController> logger, SchoolContext context)
    {
        _logger = logger;
        _context = context;
    }

다음 코드를 통해 About 메서드를 추가합니다.

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

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

정보 뷰 만들기

다음 코드를 통해 Views/Home/About.cshtml 파일을 추가합니다.

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

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

코드 가져오기

완성된 애플리케이션을 다운로드하거나 확인합니다.

다음 단계

이 자습서에서는 다음을 수행합니다.

  • 열 정렬 링크 추가
  • 검색 상자 추가
  • 학생 인덱스에 페이징 추가
  • Index 메서드에 페이징 추가
  • 페이징 링크 추가
  • 정보 페이지 만들기

마이그레이션을 사용하여 데이터 모델 변경 내용을 처리하는 방법을 알아보려면 다음 자습서로 진행합니다.