パート 3、ASP.NET Core の Razor ページと EF Core - 並べ替え、フィルター、ページングPart 3, Razor Pages with EF Core in ASP.NET Core - Sort, Filter, Paging

作成者: Tom DykstraJeremy LiknessJon P SmithBy Tom Dykstra, Jeremy Likness, and Jon P Smith

Contoso 大学 Web アプリでは、EF Core と Visual Studio を使用して Razor Pages Web アプリを作成する方法を示します。The Contoso University web app demonstrates how to create Razor Pages web apps using EF Core and Visual Studio. チュートリアル シリーズについては、最初のチュートリアルを参照してください。For information about the tutorial series, see the first tutorial.

解決できない問題が発生した場合は、完成したアプリをダウンロードし、チュートリアルに従って作成した内容とコードを比較します。If you run into problems you can't solve, download the completed app and compare that code to what you created by following the tutorial.

このチュートリアルでは、Students ページに並べ替え、フィルター、ページング機能を追加します。This tutorial adds sorting, filtering, and paging functionality to the Students pages.

次の図は、完成したページを示しています。The following illustration shows a completed page. 列見出しはクリックできるリンクとなっており、クリックすると列が並べ替えられます。The column headings are clickable links to sort the column. 列見出しを繰り返しクリックすると、昇順と降順の並べ替え順序が切り替えられます。Click a column heading repeatedly to switch between ascending and descending sort order.

Students インデックス ページ

並べ替えの追加Add sorting

Pages/Students/Index.cshtml.cs のコードを次のコードに置き換え、並べ替えを追加します。Replace the code in Pages/Students/Index.cshtml.cs with the following code to add sorting.

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

上記のコードでは次の操作が行われます。The preceding code:

  • using System; の追加を求めます。Requires adding using System;.
  • プロパティを追加して、並べ替えパラメーターを含めます。Adds properties to contain the sorting parameters.
  • Student プロパティの名前を Students に変更します。Changes the name of the Student property to Students.
  • OnGetAsync メソッドのコードを置き換えます。Replaces the code in the OnGetAsync method.

OnGetAsync メソッドでは、URL 内のクエリ文字列から sortOrder パラメーターを受け取ります。The OnGetAsync method receives a sortOrder parameter from the query string in the URL. URL とクエリ文字列は、アンカー タグ ヘルパーによって生成されます。The URL and query string is generated by the Anchor Tag Helper.

sortOrder パラメーターは Name または Date のいずれかとなります。The sortOrder parameter is either Name or Date. sortOrder パラメーターの後には、必要に応じて、降順を指定する _desc が置かれます。The sortOrder parameter is optionally followed by _desc to specify descending order. 既定の並べ替え順序は昇順です。The default sort order is ascending.

インデックス ページが、Students リンクから要求された場合、クエリ文字列はありません。When the Index page is requested from the Students link, there's no query string. 学生は、姓の昇順で表示されます。The students are displayed in ascending order by last name. switch ステートメントでは、姓の昇順が default です。Ascending order by last name is the default in the switch statement. ユーザーが列見出しリンクをクリックすると、適切な sortOrder 値がクエリ文字列値で提供されます。When the user clicks a column heading link, the appropriate sortOrder value is provided in the query string value.

NameSort および DateSort は、Razor ページで、適切なクエリ文字列値を持つ列見出しのハイパーリンクを構成するために使用されます。NameSort and DateSort are used by the Razor Page to configure the column heading hyperlinks with the appropriate query string values:

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

このコードによって、C# の条件演算子 ?: が使用されています。The code uses the C# conditional operator ?:. ?: 演算子は三項演算子であり、3 つのオペランドを取ります。The ?: operator is a ternary operator, it takes three operands. 最初の行により、sortOrder が null または空の場合に、NameSortname_desc に設定することが指定されます。The first line specifies that when sortOrder is null or empty, NameSort is set to name_desc. sortOrder が null または空 ではない 場合、NameSort は空の文字列に設定されます。If sortOrder is not null or empty, NameSort is set to an empty string.

これらの 2 つのステートメントを使用して、次のようにページで列見出しのハイパーリンクを設定することができます。These two statements enable the page to set the column heading hyperlinks as follows:

既定の並べ替え順Current sort order 姓のハイパーリンクLast Name Hyperlink 日付のハイパーリンクDate Hyperlink
姓の昇順Last Name ascending descendingdescending ascendingascending
姓の降順Last Name descending ascendingascending ascendingascending
日付の昇順Date ascending ascendingascending descendingdescending
日付の降順Date descending ascendingascending ascendingascending

このメソッドは、並べ替える列を指定するのに LINQ to Entities を使用します。The method uses LINQ to Entities to specify the column to sort by. このコードは、switch ステートメントの前に IQueryable<Student> を初期化し、switch ステートメントでそれを変更します。The code initializes an IQueryable<Student> before the switch statement, and modifies it in the switch statement:

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 が作成または変更されるときには、クエリはデータベースに送信されません。When an IQueryable is created or modified, no query is sent to the database. クエリは、IQueryable オブジェクトがコレクションに変換されるまで実行されません。The query isn't executed until the IQueryable object is converted into a collection. IQueryable は、ToListAsync などのメソッドを呼び出すことで、コレクションに変換されます。IQueryable are converted to a collection by calling a method such as ToListAsync. そのため、IQueryable コードの結果として、次のステートメントまで実行されない 1 つのクエリになります。Therefore, the IQueryable code results in a single query that's not executed until the following statement:

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

並べ替え可能な列が多数ある場合、OnGetAsync は冗長になる可能性があります。OnGetAsync could get verbose with a large number of sortable columns. この機能をコーディングする別の方法については、このチュートリアル シリーズの MVC バージョンの「動的な LINQ を使ってコードを簡略化する」を参照してください。For information about an alternative way to code this functionality, see Use dynamic LINQ to simplify code in the MVC version of this tutorial series.

Students/Index.cshtml のコードを次のコードに置き換えます。Replace the code in Students/Index.cshtml, with the following code. 変更が強調表示されます。The changes are highlighted.

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

上記のコードでは次の操作が行われます。The preceding code:

  • LastNameEnrollmentDate 列見出しにハイパーリンクを追加します。Adds hyperlinks to the LastName and EnrollmentDate column headings.
  • この情報を NameSort および DateSort で使用して、現在の並べ替えの値を含むハイパーリンクを設定します。Uses the information in NameSort and DateSort to set up hyperlinks with the current sort order values.
  • ページ見出しを Index から Students に変更します。Changes the page heading from Index to Students.
  • Model.StudentModel.Students に変更します。Changes Model.Student to Model.Students.

並べ替えが動作することを確認するにはTo verify that sorting works:

  • アプリを実行し、 [Students] タブを選択します。Run the app and select the Students tab.
  • 列見出しをクリックします。Click the column headings.

フィルターの追加Add filtering

Students インデックス ページにフィルターを追加するにはTo add filtering to the Students Index page:

  • テキスト ボックスと [送信] ボタンが、Razor ページに追加されます。A text box and a submit button is added to the Razor Page. テキスト ボックスは、名と姓で検索文字列を指定します。The text box supplies a search string on the first or last name.
  • テキスト ボックスの値を使用するようにページ モデルが更新されます。The page model is updated to use the text box value.

OnGetAsync メソッドの更新Update the OnGetAsync method

Students/Index.cshtml.cs のコードを次のコードに置き換え、フィルターを追加します。Replace the code in Students/Index.cshtml.cs with the following code to add filtering:

public class IndexModel : PageModel
{
    private readonly SchoolContext _context;

    public IndexModel(SchoolContext context)
    {
        _context = context;
    }

    public string NameSort { get; set; }
    public string DateSort { get; set; }
    public string CurrentFilter { get; set; }
    public string CurrentSort { get; set; }

    public IList<Student> Students { get; set; }

    public async Task OnGetAsync(string sortOrder, string searchString)
    {
        NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
        DateSort = sortOrder == "Date" ? "date_desc" : "Date";

        CurrentFilter = searchString;
        
        IQueryable<Student> studentsIQ = from s in _context.Students
                                        select s;
        if (!String.IsNullOrEmpty(searchString))
        {
            studentsIQ = studentsIQ.Where(s => s.LastName.Contains(searchString)
                                   || s.FirstMidName.Contains(searchString));
        }

        switch (sortOrder)
        {
            case "name_desc":
                studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
                break;
            case "Date":
                studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
                break;
            case "date_desc":
                studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
                break;
            default:
                studentsIQ = studentsIQ.OrderBy(s => s.LastName);
                break;
        }

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

上記のコードでは次の操作が行われます。The preceding code:

  • searchString パラメーターを OnGetAsync メソッドに追加し、CurrentFilter プロパティのパラメーター値を保存します。Adds the searchString parameter to the OnGetAsync method, and saves the parameter value in the CurrentFilter property. 次のセクションで追加されるテキスト ボックスから検索する文字列値を受け取ります。The search string value is received from a text box that's added in the next section.
  • LINQ ステートメントに Where 句を追加します。Adds to the LINQ statement a Where clause. Where 句は、名または姓に検索文字列が含まれている学生のみを選択します。The Where clause selects only students whose first name or last name contains the search string. 検索する値がある場合にのみ LINQ ステートメントを実行します。The LINQ statement is executed only if there's a value to search for.

IQueryable/IEnumerableIQueryable vs. IEnumerable

このコードでは、IQueryable オブジェクトに対して Where メソッドを呼び出し、フィルターがサーバーで処理されます。The code calls the Where method on an IQueryable object, and the filter is processed on the server. 一部のシナリオでは、アプリが Where メソッドをメモリ内コレクションの拡張メソッドとして呼び出す場合があります。In some scenarios, the app might be calling the Where method as an extension method on an in-memory collection. たとえば、_context.Students が EF Core DbSet から IEnumerable コレクションを返すリポジトリ メソッドに変更されるとしますFor example, suppose _context.Students changes from EF Core DbSet to a repository method that returns an IEnumerable collection. 結果は、通常同じになりますが、場合によっては異なる場合があります。The result would normally be the same but in some cases may be different.

たとえば、.NET Framework の Contains の実装では、既定では大文字小文字を区別する比較を実行します。For example, the .NET Framework implementation of Contains performs a case-sensitive comparison by default. SQL Server で、Contains の大文字小文字の区別は、SQL Server インスタンスの照合順序の設定によって決まります。In SQL Server, Contains case-sensitivity is determined by the collation setting of the SQL Server instance. SQL Server は、既定では大文字小文字を区別しません。SQL Server defaults to case-insensitive. SQLite では、既定で大文字と小文字が区別されます。SQLite defaults to case-sensitive. ToUpper を呼び出して、テストを明示的に大文字小文字を区別しないようにすることができます。ToUpper could be called to make the test explicitly case-insensitive:

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

上記のコードでは、Where メソッドが IEnumerable で呼び出された場合でも、SQLite で実行された場合でも、フィルターの大文字と小文字が確実に区別されないようにします。The preceding code would ensure that the filter is case-insensitive even if the Where method is called on an IEnumerable or runs on SQLite.

ContainsIEnumerable コレクションで呼び出されたときには、.NET Core の実装が使用されます。When Contains is called on an IEnumerable collection, the .NET Core implementation is used. ContainsIQueryable オブジェクトで呼び出されたときには、データベースの実装が使用されます。When Contains is called on an IQueryable object, the database implementation is used.

通常、パフォーマンス上の理由から、IQueryable での Contains の呼び出しをお勧めします。Calling Contains on an IQueryable is usually preferable for performance reasons. IQueryable では、データベース サーバーによってフィルター処理が行われます。With IQueryable, the filtering is done by the database server. 最初に IEnumerable を作成する場合は、すべての行がデータベース サーバーから返される必要があります。If an IEnumerable is created first, all the rows have to be returned from the database server.

ToUpper を呼び出すとパフォーマンスが低下します。There's a performance penalty for calling ToUpper. ToUpper コードは、TSQL SELECT ステートメントの WHERE 句に関数を追加します。The ToUpper code adds a function in the WHERE clause of the TSQL SELECT statement. 関数が追加されると、オプティマイザーがインデックスを使用できなくなります。The added function prevents the optimizer from using an index. 大文字小文字を区別しないように SQL がインストールされている場合、不要な場合は ToUpper を呼び出さないようにすることをお勧めします。Given that SQL is installed as case-insensitive, it's best to avoid the ToUpper call when it's not needed.

詳細については、「Sqlite プロバイダーで大文字と小文字を区別しないクエリを使用する方法」を参照してください。For more information, see How to use case-insensitive query with Sqlite provider.

Razor ページを更新するUpdate the Razor page

Pages/Students/Index.cshtml 内のコードを置き換えて、 [検索] ボタンを追加します。Replace the code in Pages/Students/Index.cshtml to add a Search button.

@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> タグ ヘルパーを使用して、検索テキスト ボックスとボタンを追加します。The preceding code uses the <form> tag helper to add the search text box and button. 既定では、<form> タグ ヘルパーが POST を使用してフォーム データを送信します。By default, the <form> tag helper submits form data with a POST. POST を使用すると、URL ではなく HTTP メッセージの本文でパラメーターが渡されます。With POST, the parameters are passed in the HTTP message body and not in the URL. HTTP GET を使用すると、フォームのデータはクエリ文字列として URL で渡されます。When HTTP GET is used, the form data is passed in the URL as query strings. クエリ文字列を使用してデータを渡すことにより、ユーザーが URL にブックマークを設定できます。Passing the data with query strings enables users to bookmark the URL. アクションの結果として更新されない場合、W3C のガイドラインでは、Get の使用が推奨されています。The W3C guidelines recommend that GET should be used when the action doesn't result in an update.

アプリをテストします。Test the app:

  • [Students] タブを選択し、検索文字列を入力します。Select the Students tab and enter a search string. SQLite を使用している場合、前に示したオプションの ToUpper コードを実装した場合にのみ、フィルターの大文字と小文字が区別されません。If you're using SQLite, the filter is case-insensitive only if you implemented the optional ToUpper code shown earlier.

  • [Search] を選択します。Select Search.

URL に検索文字列が含まれることに注意してください。Notice that the URL contains the search string. 次に例を示します。For example:

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

ブックマークがブックマークに設定されている場合、ブックマークにページの URL と SearchString クエリ文字列が含まれます。If the page is bookmarked, the bookmark contains the URL to the page and the SearchString query string. form タグ内に method="get" があると、クエリ文字列が生成されます。The method="get" in the form tag is what caused the query string to be generated.

現時点では、列見出しの並べ替えリンクを選択すると、フィルター値が [Search] ボックスから失われます。Currently, when a column heading sort link is selected, the filter value from the Search box is lost. 次のセクションでは、失われたフィルター値は修正されます。The lost filter value is fixed in the next section.

ページングの追加Add paging

このセクションでは、ページングをサポートする PaginatedList クラスを作成します。In this section, a PaginatedList class is created to support paging. PaginatedList クラスは、SkipTake ステートメントを使用して、テーブルのすべての行を取得する代わりに、サーバー上のデータをフィルター処理します。The PaginatedList class uses Skip and Take statements to filter data on the server instead of retrieving all rows of the table. ページング ボタンを次の図に示します。The following illustration shows the paging buttons.

ページング リンクを含む Students インデックス ページ

PaginatedList クラスの作成Create the PaginatedList class

プロジェクト フォルダーに、次のコードを使用して PaginatedList.cs を作成します。In the project folder, create PaginatedList.cs with the following code:

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
        {
            get
            {
                return (PageIndex > 1);
            }
        }

        public bool HasNextPage
        {
            get
            {
                return (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 メソッドは、ページ サイズとページ番号を受け取り、適切な Skip および Take ステートメントを IQueryable に適用します。The CreateAsync method in the preceding code takes page size and page number and applies the appropriate Skip and Take statements to the IQueryable. IQueryableToListAsync が呼び出されると、要求されたページのみを含むリストを返します。When ToListAsync is called on the IQueryable, it returns a List containing only the requested page. プロパティ HasPreviousPage および HasNextPage を使用して、 [Previous] および [Next] ページング ボタンを有効または無効にします。The properties HasPreviousPage and HasNextPage are used to enable or disable Previous and Next paging buttons.

CreateAsync メソッドを使用して PaginatedList<T> を作成します。The CreateAsync method is used to create the PaginatedList<T>. コンストラクターでは PaginatedList<T> オブジェクトを作成できません。コンストラクターでは非同期コードを実行できません。A constructor can't create the PaginatedList<T> object; constructors can't run asynchronous code.

構成へのページ サイズの追加Add page size to configuration

appsettings.json 構成ファイルに PageSize を追加します。Add PageSize to the appsettings.json Configuration file:

{
  "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 へのページングの追加Add paging to IndexModel

Students/Index.cshtml.cs のコードを置き換えて、ページングを追加します。Replace the code in Students/Index.cshtml.cs to add paging.

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

上記のコードでは次の操作が行われます。The preceding code:

  • Students プロパティの型を IList<Student> から PaginatedList<Student> に変更します。Changes the type of the Students property from IList<Student> to PaginatedList<Student>.
  • ページ インデックス、現在の sortOrdercurrentFilterOnGetAsync メソッド シグネチャに追加します。Adds the page index, the current sortOrder, and the currentFilter to the OnGetAsync method signature.
  • 並べ替え順序を CurrentSort プロパティに保存します。Saves the sort order in the CurrentSort property.
  • 新しい検索文字列がある場合に、ページ インデックスを 1 にリセットします。Resets page index to 1 when there's a new search string.
  • PaginatedList クラスを使用して、Student エンティティを取得します。Uses the PaginatedList class to get Student entities.
  • pageSize構成から 3 に設定します。構成に失敗した場合は 4 を設定します。Sets pageSize to 3 from Configuration, 4 if configuration fails.

OnGetAsync で受け取るパラメーターはすべて、次の場合に null になります。All the parameters that OnGetAsync receives are null when:

  • Students リンクからページが呼び出されます。The page is called from the Students link.
  • ユーザーは、ページングまたは並べ替えのリンクをクリックしていません。The user hasn't clicked a paging or sorting link.

ページングのリンクをクリックすると、ページ インデックス変数に表示するページ番号が含まれます。When a paging link is clicked, the page index variable contains the page number to display.

CurrentSort プロパティでは、現在の並べ替え順序を含む Razor ページを提供します。The CurrentSort property provides the Razor Page with the current sort order. ページングの中に並べ替え順序を保持するために、ページングリンクに、現在の並べ替え順序を含まれている必要があります。The current sort order must be included in the paging links to keep the sort order while paging.

CurrentFilter プロパティでは、現在のフィルター文字列を含む Razor ページを提供します。The CurrentFilter property provides the Razor Page with the current filter string. CurrentFilter 値:The CurrentFilter value:

  • ページングの中に、フィルターの設定を維持するために、ページング リンクに含まれている必要があります。Must be included in the paging links in order to maintain the filter settings during paging.
  • ページがリダイレクトされるときに、テキスト ボックスに復元される必要があります。Must be restored to the text box when the page is redisplayed.

ページングの中に検索文字列を変更する場合は、ページが 1 にリセットされます。If the search string is changed while paging, the page is reset to 1. 新しいフィルターのために別のデータが表示されるため、ページを 1 にリセットする必要があります。The page has to be reset to 1 because the new filter can result in different data to display. 検索値が入力され、 [Submit] が選択された場合:When a search value is entered and Submit is selected:

  • 検索文字列が変更されます。The search string is changed.
  • searchString パラメーターは null ではありません。The searchString parameter isn't null.

PaginatedList.CreateAsync メソッドが、ページングをサポートするコレクション型の学生の 1 つのページに学生クエリを変換します。The PaginatedList.CreateAsync method converts the student query to a single page of students in a collection type that supports paging. その 1 つの学生のページが Razor ページに渡されます。That single page of students is passed to the Razor Page.

PaginatedList.CreateAsync 呼び出しの pageIndex の後の 2 つの疑問符は、null 合体演算子を表します。The two question marks after pageIndex in the PaginatedList.CreateAsync call represent the null-coalescing operator. Null 合体演算子は、null 許容型の既定値を定義します。The null-coalescing operator defines a default value for a nullable type. pageIndex ?? 1 からは、pageIndex に値が含まれる場合はそれの値が返され、それ以外の場合は 1 が返されます。The expression pageIndex ?? 1 returns the value of pageIndex if it has a value, otherwise, it returns 1.

Students/Index.cshtml のコードを次のコードに置き換えます。Replace the code in Students/Index.cshtml with the following code. 変更が強調表示されています。The changes are highlighted:

@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 メソッドに渡します。The column header links use the query string to pass the current search string to the OnGetAsync method:

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

タグ ヘルパーによってページング ボタンが表示されます。The paging buttons are displayed by tag helpers:


<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] ページに移動します。Run the app and navigate to the students page.

  • 異なる並べ替え順でページングのリンクをクリックし、ページングが機能することを確認します。To make sure paging works, click the paging links in different sort orders.
  • 検索文字列を入力して、ページングを試し、並べ替えとフィルター処理を使用してもページングが正しく機能することを確認します。To verify that paging works correctly with sorting and filtering, enter a search string and try paging.

ページング リンクを含む Students インデックス ページ

グループ化Grouping

このセクションでは、登録日ごとに何人の学生が登録したかを表示する About ページを作成します。This section creates an About page that displays how many students have enrolled for each enrollment date. 更新では、グループ化を使用し、次の手順が含まれています。The update uses grouping and includes the following steps:

  • About ページで使用されるデータのビュー モデルを作成します。Create a view model for the data used by the About page.
  • ビュー モデルを使用するように About ページを更新します。Update the About page to use the view model.

ビュー モデルを作成するCreate the view model

Models/SchoolViewModels フォルダーを作成します。Create a Models/SchoolViewModels folder.

次のコードを使用して、SchoolViewModels/EnrollmentDateGroup.cs を作成します。Create SchoolViewModels/EnrollmentDateGroup.cs with the following code:

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 ページを作成するCreate the Razor Page

次のコードを使用して、Pages/About.cshtml ファイルを作成します。Create a Pages/About.cshtml file with the following code:

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

ページ モデルの作成Create the page model

次のコードを使用して、Pages/About.cshtml.cs を更新します。Update the Pages/About.cshtml.cs file with the following code:

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 ビュー モデル オブジェクトのコレクションに格納します。The LINQ statement groups the student entities by enrollment date, calculates the number of entities in each group, and stores the results in a collection of EnrollmentDateGroup view model objects.

アプリを実行して [About] ページに移動します。Run the app and navigate to the About page. 登録の日付ごとの学生の数が、テーブルに表示されます。The count of students for each enrollment date is displayed in a table.

About ページ

次の手順Next steps

次のチュートリアルでは、アプリは移行を使用してデータ モデルを更新します。In the next tutorial, the app uses migrations to update the data model.

このチュートリアルでは、並べ替え、フィルター処理、グループ化、ページング、機能が追加されます。In this tutorial, sorting, filtering, grouping, and paging, functionality is added.

次の図は、完成したページを示しています。The following illustration shows a completed page. 列見出しはクリックできるリンクとなっており、クリックすると列が並べ替えられます。The column headings are clickable links to sort the column. 列見出しを繰り返しクリックすると、昇順と降順の並べ替え順序が切り替えられます。Clicking a column heading repeatedly switches between ascending and descending sort order.

Students インデックス ページ

解決できない問題が発生した場合は、完成したアプリをダウンロードしてください。If you run into problems you can't solve, download the completed app.

インデックス ページに並べ替えを追加するAdd sorting to the Index page

Students/Index.cshtml.cs PageModel に文字列を追加し、並べ替えのパラメーターを格納します。Add strings to the Students/Index.cshtml.cs PageModel to contain the sorting parameters:

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.cs OnGetAsync を次のコードで更新します。Update the Students/Index.cshtml.cs OnGetAsync with the following code:

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 パラメーターを受け取ります。The preceding code receives a sortOrder parameter from the query string in the URL. (クエリ文字列を含む) URL がアンカー タグ ヘルパーによって生成されます。The URL (including the query string) is generated by the Anchor Tag Helper

sortOrder パラメーターは "Name" または "Date" です。The sortOrder parameter is either "Name" or "Date." sortOrder パラメーターの後に必要に応じて "_desc" を続け、降順を指定します。The sortOrder parameter is optionally followed by "_desc" to specify descending order. 既定の並べ替え順序は昇順です。The default sort order is ascending.

インデックス ページが、Students リンクから要求された場合、クエリ文字列はありません。When the Index page is requested from the Students link, there's no query string. 学生は、姓の昇順で表示されます。The students are displayed in ascending order by last name. switch ステートメントでは姓の昇順が既定値 (フォールスルー ケース) です。Ascending order by last name is the default (fall-through case) in the switch statement. ユーザーが列見出しリンクをクリックすると、適切な sortOrder 値がクエリ文字列値で提供されます。When the user clicks a column heading link, the appropriate sortOrder value is provided in the query string value.

NameSort および DateSort は、Razor ページで、適切なクエリ文字列値を持つ列見出しのハイパーリンクを構成するために使用されます。NameSort and DateSort are used by the Razor Page to configure the column heading hyperlinks with the appropriate query string values:

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# の条件演算子 ?: を含んでいます。The following code contains the C# conditional ?: operator:

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

最初の行は、sortOrder が null または空の場合に、NameSort を "name_desc" に設定することを指定します。The first line specifies that when sortOrder is null or empty, NameSort is set to "name_desc." sortOrder が null または空 ではない 場合、NameSort は空の文字列に設定されます。If sortOrder is not null or empty, NameSort is set to an empty string.

?: operator は三項演算子とも呼ばれます。The ?: operator is also known as the ternary operator.

これらの 2 つのステートメントを使用して、次のようにページで列見出しのハイパーリンクを設定することができます。These two statements enable the page to set the column heading hyperlinks as follows:

既定の並べ替え順Current sort order 姓のハイパーリンクLast Name Hyperlink 日付のハイパーリンクDate Hyperlink
姓の昇順Last Name ascending descendingdescending ascendingascending
姓の降順Last Name descending ascendingascending ascendingascending
日付の昇順Date ascending ascendingascending descendingdescending
日付の降順Date descending ascendingascending ascendingascending

このメソッドは、並べ替える列を指定するのに LINQ to Entities を使用します。The method uses LINQ to Entities to specify the column to sort by. このコードは、switch ステートメントの前に IQueryable<Student> を初期化し、switch ステートメントでそれを変更します。The code initializes an IQueryable<Student> before the switch statement, and modifies it in the switch statement:

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 が作成または変更されるときには、クエリは、データベースに送信されません。When anIQueryable is created or modified, no query is sent to the database. クエリは、IQueryable オブジェクトがコレクションに変換されるまで実行されません。The query isn't executed until the IQueryable object is converted into a collection. IQueryable は、ToListAsync などのメソッドを呼び出すことで、コレクションに変換されます。IQueryable are converted to a collection by calling a method such as ToListAsync. そのため、IQueryable コードの結果として、次のステートメントまで実行されない 1 つのクエリになります。Therefore, the IQueryable code results in a single query that's not executed until the following statement:

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

並べ替え可能な列が多数ある場合、OnGetAsync は冗長になる可能性があります。OnGetAsync could get verbose with a large number of sortable columns.

Students/Index.cshtml のコードを次の強調表示されたコードに置き換えます。Replace the code in Students/Index.cshtml, with the following highlighted code:

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

上記のコードでは次の操作が行われます。The preceding code:

  • LastNameEnrollmentDate 列見出しにハイパーリンクを追加します。Adds hyperlinks to the LastName and EnrollmentDate column headings.
  • この情報を NameSort および DateSort で使用して、現在の並べ替えの値を含むハイパーリンクを設定します。Uses the information in NameSort and DateSort to set up hyperlinks with the current sort order values.

並べ替えが動作することを確認するにはTo verify that sorting works:

  • アプリを実行し、 [Students] タブを選択します。Run the app and select the Students tab.
  • [Last Name] をクリックします。Click Last Name.
  • [Enrollment Date] をクリックします。Click Enrollment Date.

コードの理解を深めるために、次の手順を実行します。To get a better understanding of the code:

  • Students/Index.cshtml.cs で、switch (sortOrder) にブレークポイントを設定します。In Students/Index.cshtml.cs, set a breakpoint on switch (sortOrder).
  • NameSortDateSort のウォッチを追加します。Add a watch for NameSort and DateSort.
  • Students/Index.cshtml で、@Html.DisplayNameFor(model => model.Student[0].LastName) にブレークポイントを設定します。In Students/Index.cshtml, set a breakpoint on @Html.DisplayNameFor(model => model.Student[0].LastName).

デバッガーの手順を実行します。Step through the debugger.

Students インデックス ページに [検索] ボックスを追加するAdd a Search Box to the Students Index page

Students インデックス ページにフィルターを追加するにはTo add filtering to the Students Index page:

  • テキスト ボックスと [送信] ボタンが、Razor ページに追加されます。A text box and a submit button is added to the Razor Page. テキスト ボックスは、名と姓で検索文字列を指定します。The text box supplies a search string on the first or last name.
  • テキスト ボックスの値を使用するようにページ モデルが更新されます。The page model is updated to use the text box value.

Index メソッドにフィルター機能を追加するAdd filtering functionality to the Index method

Students/Index.cshtml.cs OnGetAsync を次のコードで更新します。Update the Students/Index.cshtml.cs OnGetAsync with the following code:

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

上記のコードでは次の操作が行われます。The preceding code:

  • searchString パラメーターを OnGetAsync メソッドに追加します。Adds the searchString parameter to the OnGetAsync method. 次のセクションで追加されるテキスト ボックスから検索する文字列値を受け取ります。The search string value is received from a text box that's added in the next section.
  • LINQ ステートメントに Where 句を追加します。Added to the LINQ statement a Where clause. Where 句は、名または姓に検索文字列が含まれている学生のみを選択します。The Where clause selects only students whose first name or last name contains the search string. 検索する値がある場合にのみ LINQ ステートメントを実行します。The LINQ statement is executed only if there's a value to search for.

メモ:前のコードは、IQueryable オブジェクトに対して Where メソッドを呼び出し、フィルターがサーバーで処理されます。Note: The preceding code calls the Where method on an IQueryable object, and the filter is processed on the server. 一部のシナリオでは、アプリが Where メソッドをメモリ内コレクションの拡張メソッドとして呼び出す場合があります。In some scenarios, the app might be calling the Where method as an extension method on an in-memory collection. たとえば、_context.Students が EF Core DbSet から IEnumerable コレクションを返すリポジトリ メソッドに変更されるとしますFor example, suppose _context.Students changes from EF Core DbSet to a repository method that returns an IEnumerable collection. 結果は、通常同じになりますが、場合によっては異なる場合があります。The result would normally be the same but in some cases may be different.

たとえば、.NET Framework の Contains の実装では、既定では大文字小文字を区別する比較を実行します。For example, the .NET Framework implementation of Contains performs a case-sensitive comparison by default. SQL Server で、Contains の大文字小文字の区別は、SQL Server インスタンスの照合順序の設定によって決まります。In SQL Server, Contains case-sensitivity is determined by the collation setting of the SQL Server instance. SQL Server は、既定では大文字小文字を区別しません。SQL Server defaults to case-insensitive. ToUpper を呼び出して、テストを明示的に大文字小文字を区別しないようにすることができます。ToUpper could be called to make the test explicitly case-insensitive:

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

上記のコードは、コードが IEnumerable を使用するように変更された場合、結果で大文字小文字が区別されないようにします。The preceding code would ensure that results are case-insensitive if the code changes to use IEnumerable. ContainsIEnumerable コレクションで呼び出されたときには、.NET Core の実装が使用されます。When Contains is called on an IEnumerable collection, the .NET Core implementation is used. ContainsIQueryable オブジェクトで呼び出されたときには、データベースの実装が使用されます。When Contains is called on an IQueryable object, the database implementation is used. リポジトリから IEnumerable を返すと、パフォーマンスが大幅に低下する可能性があります。Returning an IEnumerable from a repository can have a significant performance penalty:

  1. DB サーバーからすべての行が返されます。All the rows are returned from the DB server.
  2. アプリケーションで返されたすべての行にフィルターが適用されます。The filter is applied to all the returned rows in the application.

ToUpper を呼び出すとパフォーマンスが低下します。There's a performance penalty for calling ToUpper. ToUpper コードは、TSQL SELECT ステートメントの WHERE 句に関数を追加します。The ToUpper code adds a function in the WHERE clause of the TSQL SELECT statement. 関数が追加されると、オプティマイザーがインデックスを使用できなくなります。The added function prevents the optimizer from using an index. 大文字小文字を区別しないように SQL がインストールされている場合、不要な場合は ToUpper を呼び出さないようにすることをお勧めします。Given that SQL is installed as case-insensitive, it's best to avoid the ToUpper call when it's not needed.

Students インデックス ページに [検索] ボックスを追加するAdd a Search Box to the Student Index page

Pages/Students/Index.cshtml で、次の強調表示されたコードを追加し、 [検索] ボタンと各種のクロムを追加します。In Pages/Students/Index.cshtml, add the following highlighted code to create a Search button and assorted chrome.

@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> タグ ヘルパーを使用して、検索テキスト ボックスとボタンを追加します。The preceding code uses the <form> tag helper to add the search text box and button. 既定では、<form> タグ ヘルパーが POST を使用してフォーム データを送信します。By default, the <form> tag helper submits form data with a POST. POST を使用すると、URL ではなく HTTP メッセージの本文でパラメーターが渡されます。With POST, the parameters are passed in the HTTP message body and not in the URL. HTTP GET を使用すると、フォームのデータはクエリ文字列として URL で渡されます。When HTTP GET is used, the form data is passed in the URL as query strings. クエリ文字列を使用してデータを渡すことにより、ユーザーが URL にブックマークを設定できます。Passing the data with query strings enables users to bookmark the URL. アクションの結果として更新されない場合、W3C のガイドラインでは、Get の使用が推奨されています。The W3C guidelines recommend that GET should be used when the action doesn't result in an update.

アプリをテストします。Test the app:

  • [Students] タブを選択し、検索文字列を入力します。Select the Students tab and enter a search string.
  • [Search] を選択します。Select Search.

URL に検索文字列が含まれることに注意してください。Notice that the URL contains the search string.

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

ブックマークがブックマークに設定されている場合、ブックマークにページの URL と SearchString クエリ文字列が含まれます。If the page is bookmarked, the bookmark contains the URL to the page and the SearchString query string. form タグ内に method="get" があると、クエリ文字列が生成されます。The method="get" in the form tag is what caused the query string to be generated.

現時点では、列見出しの並べ替えリンクを選択すると、フィルター値が [Search] ボックスから失われます。Currently, when a column heading sort link is selected, the filter value from the Search box is lost. 次のセクションでは、失われたフィルター値は修正されます。The lost filter value is fixed in the next section.

Students インデックス ページにページング機能を追加するAdd paging functionality to the Students Index page

このセクションでは、ページングをサポートする PaginatedList クラスを作成します。In this section, a PaginatedList class is created to support paging. PaginatedList クラスは、SkipTake ステートメントを使用して、テーブルのすべての行を取得する代わりに、サーバー上のデータをフィルター処理します。The PaginatedList class uses Skip and Take statements to filter data on the server instead of retrieving all rows of the table. ページング ボタンを次の図に示します。The following illustration shows the paging buttons.

ページング リンクを含む Students インデックス ページ

プロジェクト フォルダーに、次のコードを使用して PaginatedList.cs を作成します。In the project folder, create PaginatedList.cs with the following code:

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
        {
            get
            {
                return (PageIndex > 1);
            }
        }

        public bool HasNextPage
        {
            get
            {
                return (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 メソッドは、ページ サイズとページ番号を受け取り、適切な Skip および Take ステートメントを IQueryable に適用します。The CreateAsync method in the preceding code takes page size and page number and applies the appropriate Skip and Take statements to the IQueryable. IQueryableToListAsync が呼び出されると、要求されたページのみを含むリストを返します。When ToListAsync is called on the IQueryable, it returns a List containing only the requested page. プロパティ HasPreviousPage および HasNextPage を使用して、 [Previous] および [Next] ページング ボタンを有効または無効にします。The properties HasPreviousPage and HasNextPage are used to enable or disable Previous and Next paging buttons.

CreateAsync メソッドを使用して PaginatedList<T> を作成します。The CreateAsync method is used to create the PaginatedList<T>. コンストラクターは、PaginatedList<T> オブジェクトを作成できません。コンストラクターは、非同期コードを実行できません。A constructor can't create the PaginatedList<T> object, constructors can't run asynchronous code.

Index メソッドにページング機能を追加するAdd paging functionality to the Index method

Students/Index.cshtml.cs で、Student の型を IList<Student> から PaginatedList<Student> に更新します。In Students/Index.cshtml.cs, update the type of Student from IList<Student> to PaginatedList<Student>:

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

Students/Index.cshtml.cs OnGetAsync を次のコードで更新します。Update the Students/Index.cshtml.cs OnGetAsync with the following code:

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

上記のコードは、ページ インデックス、現在の sortOrder、および currentFilter をメソッド シグネチャに追加します。The preceding code adds the page index, the current sortOrder, and the currentFilter to the method signature.

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

すべてのパラメーターは、次のような場合に null になります。All the parameters are null when:

  • Students リンクからページが呼び出されます。The page is called from the Students link.
  • ユーザーは、ページングまたは並べ替えのリンクをクリックしていません。The user hasn't clicked a paging or sorting link.

ページングのリンクをクリックすると、ページ インデックス変数に表示するページ番号が含まれます。When a paging link is clicked, the page index variable contains the page number to display.

CurrentSort は、現在の並べ替え順序を含む Razor ページを提供します。CurrentSort provides the Razor Page with the current sort order. ページングの中に並べ替え順序を保持するために、ページングリンクに、現在の並べ替え順序を含まれている必要があります。The current sort order must be included in the paging links to keep the sort order while paging.

CurrentFilter は、現在のフィルター文字列を含む Razor ページを提供します。CurrentFilter provides the Razor Page with the current filter string. CurrentFilter 値:The CurrentFilter value:

  • ページングの中に、フィルターの設定を維持するために、ページング リンクに含まれている必要があります。Must be included in the paging links in order to maintain the filter settings during paging.
  • ページがリダイレクトされるときに、テキスト ボックスに復元される必要があります。Must be restored to the text box when the page is redisplayed.

ページングの中に検索文字列を変更する場合は、ページが 1 にリセットされます。If the search string is changed while paging, the page is reset to 1. 新しいフィルターのために別のデータが表示されるため、ページを 1 にリセットする必要があります。The page has to be reset to 1 because the new filter can result in different data to display. 検索値が入力され、 [Submit] が選択された場合:When a search value is entered and Submit is selected:

  • 検索文字列が変更されます。The search string is changed.
  • searchString パラメーターは null ではありません。The searchString parameter isn't null.
if (searchString != null)
{
    pageIndex = 1;
}
else
{
    searchString = currentFilter;
}

PaginatedList.CreateAsync メソッドが、ページングをサポートするコレクション型の学生の 1 つのページに学生クエリを変換します。The PaginatedList.CreateAsync method converts the student query to a single page of students in a collection type that supports paging. その 1 つの学生のページが Razor ページに渡されます。That single page of students is passed to the Razor Page.

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

PaginatedList.CreateAsync の 2 つの疑問符は、null 合体演算子を表します。The two question marks in PaginatedList.CreateAsync represent the null-coalescing operator. Null 合体演算子は、null 許容型の既定値を定義します。The null-coalescing operator defines a default value for a nullable type. (pageIndex ?? 1) は、値がある場合に、pageIndex の値を返すことを意味します。The expression (pageIndex ?? 1) means return the value of pageIndex if it has a value. pageIndex に値がない場合は、1 を返します。If pageIndex doesn't have a value, return 1.

Students/Index.cshtml 内のマークアップを更新するUpdate the markup in Students/Index.cshtml. 変更が強調表示されています。The changes are highlighted:

@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 メソッドに渡します。The column header links use the query string to pass the current search string to the OnGetAsync method so that the user can sort within filter results:

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

タグ ヘルパーによってページング ボタンが表示されます。The paging buttons are displayed by tag helpers:


<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] ページに移動します。Run the app and navigate to the students page.

  • 異なる並べ替え順でページングのリンクをクリックし、ページングが機能することを確認します。To make sure paging works, click the paging links in different sort orders.
  • 検索文字列を入力して、ページングを試し、並べ替えとフィルター処理を使用してもページングが正しく機能することを確認します。To verify that paging works correctly with sorting and filtering, enter a search string and try paging.

ページング リンクを含む Students インデックス ページ

コードの理解を深めるために、次の手順を実行します。To get a better understanding of the code:

  • Students/Index.cshtml.cs で、switch (sortOrder) にブレークポイントを設定します。In Students/Index.cshtml.cs, set a breakpoint on switch (sortOrder).
  • NameSortDateSortCurrentSortModel.Student.PageIndex のウォッチを追加します。Add a watch for NameSort, DateSort, CurrentSort, and Model.Student.PageIndex.
  • Students/Index.cshtml で、@Html.DisplayNameFor(model => model.Student[0].LastName) にブレークポイントを設定します。In Students/Index.cshtml, set a breakpoint on @Html.DisplayNameFor(model => model.Student[0].LastName).

デバッガーの手順を実行します。Step through the debugger.

学生の統計情報を表示するように [About] ページを更新します。Update the About page to show student statistics

このステップで、Pages/About.cshtml が更新され、登録日付ごとに登録した学生の数を表示します。In this step, Pages/About.cshtml is updated to display how many students have enrolled for each enrollment date. 更新では、グループ化を使用し、次の手順が含まれています。The update uses grouping and includes the following steps:

  • About ページで使用されるデータのビュー モデルを作成します。Create a view model for the data used by the About Page.
  • ビュー モデルを使用するように About ページを更新します。Update the About page to use the view model.

ビュー モデルを作成するCreate the view model

SchoolViewModels フォルダーを Models フォルダー内に作成します。Create a SchoolViewModels folder in the Models folder.

次のコードを使用して、SchoolViewModels フォルダー内に EnrollmentDateGroup.cs を追加します。In the SchoolViewModels folder, add a EnrollmentDateGroup.cs with the following code:

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

About ページ モデルを更新するUpdate the About page model

ASP.NET Core 2.2 の Web テンプレートには、About ページが含まれていません。The web templates in ASP.NET Core 2.2 do not include the About page. ASP.NET Core 2.2 を使っている場合は、About Razor ページを作成します。If you are using ASP.NET Core 2.2, create the About Razor Page.

次のコードを使用して、Pages/About.cshtml.cs を更新します。Update the Pages/About.cshtml.cs file with the following code:

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 ビュー モデル オブジェクトのコレクションに格納します。The LINQ statement groups the student entities by enrollment date, calculates the number of entities in each group, and stores the results in a collection of EnrollmentDateGroup view model objects.

About Razor ページを変更するModify the About Razor Page

Pages/About.cshtml ファイルのコードを次のコードに置き換えます。Replace the code in the Pages/About.cshtml file with the following code:

@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] ページに移動します。Run the app and navigate to the About page. 登録の日付ごとの学生の数が、テーブルに表示されます。The count of students for each enrollment date is displayed in a table.

解決できない問題が発生した場合は、このステージの完成したアプリをダウンロードしてください。If you run into problems you can't solve, download the completed app for this stage.

About ページ

その他の技術情報Additional resources

次のチュートリアルでは、アプリは移行を使用してデータ モデルを更新します。In the next tutorial, the app uses migrations to update the data model.