パート 3、ASP.NET Core の Razor ページと EF Core - 並べ替え、フィルター、ページング
作成者: Tom Dykstra、Jeremy Likness、Jon P Smith
Contoso 大学 Web アプリでは、EF Core と Visual Studio を使用して Razor Pages Web アプリを作成する方法を示します。 チュートリアル シリーズについては、最初のチュートリアルを参照してください。
解決できない問題が発生した場合は、完成したアプリをダウンロードし、チュートリアルに従って作成した内容とコードを比較します。
このチュートリアルでは、Students ページに並べ替え、フィルター、ページング機能を追加します。
次の図は、完成したページを示しています。 列見出しはクリックできるリンクとなっており、クリックすると列が並べ替えられます。 列見出しを繰り返しクリックすると、昇順と降順の並べ替え順序が切り替えられます。
並べ替えの追加
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
が置かれます。 既定の並べ替え順序は昇順です。
インデックス ページが、Students リンクから要求された場合、クエリ文字列はありません。 学生は、姓の昇順で表示されます。 switch
ステートメントでは、姓の昇順が default
です。 ユーザーが列見出しリンクをクリックすると、適切な sortOrder
値がクエリ文字列値で提供されます。
NameSort
および DateSort
は、Razor ページで、適切なクエリ文字列値を持つ列見出しのハイパーリンクを構成するために使用されます。
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
このコードによって、C# の条件演算子 ?: が使用されています。 ?:
演算子は三項演算子であり、3 つのオペランドを取ります。 最初の行により、sortOrder
が null または空の場合に、NameSort
を name_desc
に設定することが指定されます。 sortOrder
が null でも空でも "ない" 場合、NameSort
は空の文字列に設定されます。
これらの 2 つのステートメントを使用して、次のようにページで列見出しのハイパーリンクを設定することができます。
既定の並べ替え順 | 姓のハイパーリンク | 日付のハイパーリンク |
---|---|---|
姓の昇順 | descending | ascending |
姓の降順 | ascending | ascending |
日付の昇順 | ascending | descending |
日付の降順 | ascending | ascending |
このメソッドは、並べ替える列を指定するのに LINQ to Entities を使用します。 このコードは、switch ステートメントの前に IQueryable<Student>
を初期化し、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
オブジェクトがコレクションに変換されるまで実行されません。 IQueryable
は、ToListAsync
などのメソッドを呼び出すことで、コレクションに変換されます。 そのため、IQueryable
コードの結果として、次のステートメントまで実行されない 1 つのクエリになります。
Students = await studentsIQ.AsNoTracking().ToListAsync();
並べ替え可能な列が多数ある場合、OnGetAsync
は冗長になる可能性があります。 この機能をコーディングする別の方法については、このチュートリアル シリーズの MVC バージョンの「動的な LINQ を使ってコードを簡略化する」を参照してください。
列見出しハイパーリンクを Student インデックス ページに追加する
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>
上記のコードでは次の操作が行われます。
LastName
とEnrollmentDate
列見出しにハイパーリンクを追加します。- この情報を
NameSort
およびDateSort
で使用して、現在の並べ替えの値を含むハイパーリンクを設定します。 - ページ見出しを Index から Students に変更します。
Model.Student
をModel.Students
に変更します。
並べ替えが動作することを確認するには
- アプリを実行し、 [Students] タブを選択します。
- 列見出しをクリックします。
フィルターの追加
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
プロパティのパラメーター値を保存します。 次のセクションで追加されるテキスト ボックスから検索する文字列値を受け取ります。- LINQ ステートメントに
Where
句を追加します。Where
句は、名または姓に検索文字列が含まれている学生のみを選択します。 検索する値がある場合にのみ LINQ ステートメントを実行します。
IQueryable/IEnumerable
このコードでは、IQueryable
オブジェクトに対して Where メソッドを呼び出し、フィルターがサーバーで処理されます。 一部のシナリオでは、アプリが Where
メソッドをメモリ内コレクションの拡張メソッドとして呼び出す場合があります。 たとえば、_context.Students
が EF CoreDbSet
から IEnumerable
コレクションを返すリポジトリ メソッドに変更されるとします。 結果は、通常同じになりますが、場合によっては異なる場合があります。
たとえば、.NET Framework の Contains
の実装では、既定では大文字小文字を区別する比較を実行します。 SQL Server で、Contains
の大文字小文字の区別は、SQL Server インスタンスの照合順序の設定によって決まります。 SQL Server は、既定では大文字小文字を区別しません。 SQLite では、既定で大文字と小文字が区別されます。 ToUpper
を呼び出して、テストを明示的に大文字小文字を区別しないようにすることができます。
Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())`
上記のコードでは、Where
メソッドが IEnumerable
で呼び出された場合でも、SQLite で実行された場合でも、フィルターの大文字と小文字が確実に区別されないようにします。
Contains
が IEnumerable
コレクションで呼び出されたときには、.NET Core の実装が使用されます。 Contains
が IQueryable
オブジェクトで呼び出されたときには、データベースの実装が使用されます。
通常、パフォーマンス上の理由から、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 の使用が推奨されています。
アプリをテストします。
[Students] タブを選択し、検索文字列を入力します。 SQLite を使用している場合、前に示したオプションの
ToUpper
コードを実装した場合にのみ、フィルターの大文字と小文字が区別されません。[Search] を選択します。
URL に検索文字列が含まれることに注意してください。 次に例を示します。
https://localhost:5001/Students?SearchString=an
ブックマークがブックマークに設定されている場合、ブックマークにページの URL と SearchString
クエリ文字列が含まれます。 form
タグ内に method="get"
があると、クエリ文字列が生成されます。
現時点では、列見出しの並べ替えリンクを選択すると、フィルター値が [Search] ボックスから失われます。 次のセクションでは、失われたフィルター値は修正されます。
ページングの追加
このセクションでは、ページングをサポートする PaginatedList
クラスを作成します。 PaginatedList
クラスは、Skip
と Take
ステートメントを使用して、テーブルのすべての行を取得する代わりに、サーバー上のデータをフィルター処理します。 ページング ボタンを次の図に示します。
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
メソッドは、ページ サイズとページ番号を受け取り、適切な Skip
および Take
ステートメントを IQueryable
に適用します。 IQueryable
で ToListAsync
が呼び出されると、要求されたページのみを含むリストを返します。 プロパティ HasPreviousPage
および HasNextPage
を使用して、 [Previous] および [Next] ページング ボタンを有効または無効にします。
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>
に変更します。- ページ インデックス、現在の
sortOrder
、currentFilter
をOnGetAsync
メソッド シグネチャに追加します。 - 並べ替え順序を
CurrentSort
プロパティに保存します。 - 新しい検索文字列がある場合に、ページ インデックスを 1 にリセットします。
PaginatedList
クラスを使用して、Student エンティティを取得します。pageSize
は構成から 3 に設定します。構成に失敗した場合は 4 を設定します。
OnGetAsync
で受け取るパラメーターはすべて、次の場合に null になります。
- Students リンクからページが呼び出されます。
- ユーザーは、ページングまたは並べ替えのリンクをクリックしていません。
ページングのリンクをクリックすると、ページ インデックス変数に表示するページ番号が含まれます。
CurrentSort
プロパティでは、現在の並べ替え順序を含む Razor ページを提供します。 ページングの中に並べ替え順序を保持するために、ページングリンクに、現在の並べ替え順序を含まれている必要があります。
CurrentFilter
プロパティでは、現在のフィルター文字列を含む Razor ページを提供します。 CurrentFilter
値:
- ページングの中に、フィルターの設定を維持するために、ページング リンクに含まれている必要があります。
- ページがリダイレクトされるときに、テキスト ボックスに復元される必要があります。
ページングの中に検索文字列を変更する場合は、ページが 1 にリセットされます。 新しいフィルターのために別のデータが表示されるため、ページを 1 にリセットする必要があります。 検索値が入力され、 [Submit] が選択された場合:
- 検索文字列が変更されます。
searchString
パラメーターは null ではありません。
PaginatedList.CreateAsync
メソッドが、ページングをサポートするコレクション型の学生の 1 つのページに学生クエリを変換します。 その 1 つの学生のページが Razor ページに渡されます。
PaginatedList.CreateAsync
呼び出しの pageIndex
の後の 2 つの疑問符は、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] ページに移動します。
- 異なる並べ替え順でページングのリンクをクリックし、ページングが機能することを確認します。
- 検索文字列を入力して、ページングを試し、並べ替えとフィルター処理を使用してもページングが正しく機能することを確認します。
グループ化
このセクションでは、登録日ごとに何人の学生が登録したかを表示する 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 ページを作成する
次のコードを使用して、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] ページに移動します。 登録の日付ごとの学生の数が、テーブルに表示されます。
次の手順
次のチュートリアルでは、アプリは移行を使用してデータ モデルを更新します。
このチュートリアルでは、並べ替え、フィルター処理、グループ化、ページング、機能が追加されます。
次の図は、完成したページを示しています。 列見出しはクリックできるリンクとなっており、クリックすると列が並べ替えられます。 列見出しを繰り返しクリックすると、昇順と降順の並べ替え順序が切り替えられます。
解決できない問題が発生した場合は、完成したアプリをダウンロードしてください。
インデックス ページに並べ替えを追加する
文字列を Students/Index.cshtml.cs
の PageModel
に追加して、並べ替えのパラメーターを含めます。
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
を更新します。
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" を続け、降順を指定します。 既定の並べ替え順序は昇順です。
インデックス ページが、Students リンクから要求された場合、クエリ文字列はありません。 学生は、姓の昇順で表示されます。 switch
ステートメントでは姓の昇順が既定値 (フォールスルー ケース) です。 ユーザーが列見出しリンクをクリックすると、適切な sortOrder
値がクエリ文字列値で提供されます。
NameSort
および DateSort
は、Razor ページで、適切なクエリ文字列値を持つ列見出しのハイパーリンクを構成するために使用されます。
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
は三項演算子とも呼ばれます。
これらの 2 つのステートメントを使用して、次のようにページで列見出しのハイパーリンクを設定することができます。
既定の並べ替え順 | 姓のハイパーリンク | 日付のハイパーリンク |
---|---|---|
姓の昇順 | descending | ascending |
姓の降順 | ascending | ascending |
日付の昇順 | ascending | descending |
日付の降順 | ascending | ascending |
このメソッドは、並べ替える列を指定するのに LINQ to Entities を使用します。 このコードは、switch ステートメントの前に IQueryable<Student>
を初期化し、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
オブジェクトがコレクションに変換されるまで実行されません。 IQueryable
は、ToListAsync
などのメソッドを呼び出すことで、コレクションに変換されます。 そのため、IQueryable
コードの結果として、次のステートメントまで実行されない 1 つのクエリになります。
Student = await studentIQ.AsNoTracking().ToListAsync();
並べ替え可能な列が多数ある場合、OnGetAsync
は冗長になる可能性があります。
列見出しハイパーリンクを Student インデックス ページに追加する
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>
上記のコードでは次の操作が行われます。
LastName
とEnrollmentDate
列見出しにハイパーリンクを追加します。- この情報を
NameSort
およびDateSort
で使用して、現在の並べ替えの値を含むハイパーリンクを設定します。
並べ替えが動作することを確認するには
- アプリを実行し、 [Students] タブを選択します。
- [Last Name] をクリックします。
- [Enrollment Date] をクリックします。
コードの理解を深めるために、次の手順を実行します。
Students/Index.cshtml.cs
で、switch (sortOrder)
にブレークポイントを設定します。NameSort
とDateSort
のウォッチを追加します。Students/Index.cshtml
で、@Html.DisplayNameFor(model => model.Student[0].LastName)
にブレークポイントを設定します。
デバッガーの手順を実行します。
Students インデックス ページに [検索] ボックスを追加する
Students インデックス ページにフィルターを追加するには
- テキスト ボックスと [送信] ボタンが、Razor ページに追加されます。 テキスト ボックスは、名と姓で検索文字列を指定します。
- テキスト ボックスの値を使用するようにページ モデルが更新されます。
Index メソッドにフィルター機能を追加する
次のコードで Students/Index.cshtml.cs
の OnGetAsync
を更新します。
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
コレクションを返すリポジトリ メソッドに変更されるとします。 結果は、通常同じになりますが、場合によっては異なる場合があります。
たとえば、.NET Framework の Contains
の実装では、既定では大文字小文字を区別する比較を実行します。 SQL Server で、Contains
の大文字小文字の区別は、SQL Server インスタンスの照合順序の設定によって決まります。 SQL Server は、既定では大文字小文字を区別しません。 ToUpper
を呼び出して、テストを明示的に大文字小文字を区別しないようにすることができます。
Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
上記のコードは、コードが IEnumerable
を使用するように変更された場合、結果で大文字小文字が区別されないようにします。 Contains
が IEnumerable
コレクションで呼び出されたときには、.NET Core の実装が使用されます。 Contains
が IQueryable
オブジェクトで呼び出されたときには、データベースの実装が使用されます。 リポジトリから IEnumerable
を返すと、パフォーマンスが大幅に低下する可能性があります。
- DB サーバーからすべての行が返されます。
- アプリケーションで返されたすべての行にフィルターが適用されます。
ToUpper
を呼び出すとパフォーマンスが低下します。 ToUpper
コードは、TSQL SELECT ステートメントの WHERE 句に関数を追加します。 関数が追加されると、オプティマイザーがインデックスを使用できなくなります。 大文字小文字を区別しないように SQL がインストールされている場合、不要な場合は ToUpper
を呼び出さないようにすることをお勧めします。
Students インデックス ページに [検索] ボックスを追加する
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 の使用が推奨されています。
アプリをテストします。
- [Students] タブを選択し、検索文字列を入力します。
- [Search] を選択します。
URL に検索文字列が含まれることに注意してください。
http://localhost:5000/Students?SearchString=an
ブックマークがブックマークに設定されている場合、ブックマークにページの URL と SearchString
クエリ文字列が含まれます。 form
タグ内に method="get"
があると、クエリ文字列が生成されます。
現時点では、列見出しの並べ替えリンクを選択すると、フィルター値が [Search] ボックスから失われます。 次のセクションでは、失われたフィルター値は修正されます。
Students インデックス ページにページング機能を追加する
このセクションでは、ページングをサポートする PaginatedList
クラスを作成します。 PaginatedList
クラスは、Skip
と Take
ステートメントを使用して、テーブルのすべての行を取得する代わりに、サーバー上のデータをフィルター処理します。 ページング ボタンを次の図に示します。
プロジェクト フォルダーに、次のコードを使用して 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
メソッドは、ページ サイズとページ番号を受け取り、適切な Skip
および Take
ステートメントを IQueryable
に適用します。 IQueryable
で ToListAsync
が呼び出されると、要求されたページのみを含むリストを返します。 プロパティ HasPreviousPage
および HasNextPage
を使用して、 [Previous] および [Next] ページング ボタンを有効または無効にします。
CreateAsync
メソッドを使用して PaginatedList<T>
を作成します。 コンストラクターは、PaginatedList<T>
オブジェクトを作成できません。コンストラクターは、非同期コードを実行できません。
Index メソッドにページング機能を追加する
Students/Index.cshtml.cs
で、Student
の型を IList<Student>
から PaginatedList<Student>
に更新します。
public PaginatedList<Student> Student { get; set; }
次のコードで Students/Index.cshtml.cs
の OnGetAsync
を更新します。
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
をメソッド シグネチャに追加します。
public async Task OnGetAsync(string sortOrder,
string currentFilter, string searchString, int? pageIndex)
すべてのパラメーターは、次のような場合に null になります。
- Students リンクからページが呼び出されます。
- ユーザーは、ページングまたは並べ替えのリンクをクリックしていません。
ページングのリンクをクリックすると、ページ インデックス変数に表示するページ番号が含まれます。
CurrentSort
は、現在の並べ替え順序を含む Razor ページを提供します。 ページングの中に並べ替え順序を保持するために、ページングリンクに、現在の並べ替え順序を含まれている必要があります。
CurrentFilter
は、現在のフィルター文字列を含む Razor ページを提供します。 CurrentFilter
値:
- ページングの中に、フィルターの設定を維持するために、ページング リンクに含まれている必要があります。
- ページがリダイレクトされるときに、テキスト ボックスに復元される必要があります。
ページングの中に検索文字列を変更する場合は、ページが 1 にリセットされます。 新しいフィルターのために別のデータが表示されるため、ページを 1 にリセットする必要があります。 検索値が入力され、 [Submit] が選択された場合:
- 検索文字列が変更されます。
searchString
パラメーターは null ではありません。
if (searchString != null)
{
pageIndex = 1;
}
else
{
searchString = currentFilter;
}
PaginatedList.CreateAsync
メソッドが、ページングをサポートするコレクション型の学生の 1 つのページに学生クエリを変換します。 その 1 つの学生のページが Razor ページに渡されます。
Student = await PaginatedList<Student>.CreateAsync(
studentIQ.AsNoTracking(), pageIndex ?? 1, pageSize);
PaginatedList.CreateAsync
の 2 つの疑問符は、null 合体演算子を表します。 Null 合体演算子は、null 許容型の既定値を定義します。 式 (pageIndex ?? 1)
は、値がある場合に、pageIndex
の値を返すことを意味します。 pageIndex
に値がない場合は、1 を返します。
学生の Razor ページにページングのリンクを追加する
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>
<label>Find by name: <input type="text" name="SearchString" value="@Model.CurrentFilter" /></label>
<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] ページに移動します。
- 異なる並べ替え順でページングのリンクをクリックし、ページングが機能することを確認します。
- 検索文字列を入力して、ページングを試し、並べ替えとフィルター処理を使用してもページングが正しく機能することを確認します。
コードの理解を深めるために、次の手順を実行します。
Students/Index.cshtml.cs
で、switch (sortOrder)
にブレークポイントを設定します。NameSort
、DateSort
、CurrentSort
、Model.Student.PageIndex
のウォッチを追加します。Students/Index.cshtml
で、@Html.DisplayNameFor(model => model.Student[0].LastName)
にブレークポイントを設定します。
デバッガーの手順を実行します。
学生の統計情報を表示するように [About] ページを更新します。
このステップで、Pages/About.cshtml
が更新され、登録日付ごとに登録した学生の数を表示します。 更新では、グループ化を使用し、次の手順が含まれています。
- About ページで使用されるデータのビュー モデルを作成します。
- ビュー モデルを使用するように About ページを更新します。
ビュー モデルを作成する
SchoolViewModels フォルダーを 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; }
}
}
About ページ モデルを更新する
ASP.NET Core 2.2 の Web テンプレートには、About ページが含まれていません。 ASP.NET Core 2.2 を使っている場合は、About 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
ビュー モデル オブジェクトのコレクションに格納します。
About 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] ページに移動します。 登録の日付ごとの学生の数が、テーブルに表示されます。
解決できない問題が発生した場合は、このステージの完成したアプリをダウンロードしてください。
その他のリソース
次のチュートリアルでは、アプリは移行を使用してデータ モデルを更新します。
ASP.NET Core