ASP.NET MVC アプリのプロジェクトで、スキャフォールディング機能を使うと、DB の CRUD 操作を行う Controller と View のコードを一式自動生成することができます。
そのレコード一覧の表示のページにページング機能を実装し、例えばページ 5 を表示してから Edit リンクをクリックして編集ページに遷移し、DB の UPDATE 完了で元の一覧ページにリダイレクトされる際、同じページ 5 が表示されるようにする機能を実装してみました。以下に要点を備忘録として残しておきます。
この記事で紹介するのは Visual Studio 2022 のテンプレートで、ターゲットフレームワーク .NET Framework 4.8 として作成した ASP.NET MVC5 アプリです。ASP.NET Core MVC アプリでも同様なことは可能です。
まず、ページング機能ですが、Microsoft のドキュメント「チュートリアル: 並べ替え、フィルター処理、ページングを追加する - ASP.NET MVC と EF Core」で紹介されている PaginatedList.cs を利用して実装します。そのコードはこの記事にも載せておきます。
一覧ページにページングを実装する場合、アクションメソッドで当該ページ部分のレコードを含む PaginatedList<T> のオブジェクトを作成し、それを View に Model として渡すようにします。
PaginatedList<T> オブジェクトからは PageIndex プロパティで現在のページ番号を取得できます。そのページ番号を、「一覧ページ」⇒「編集ページ」⇒「一覧ページ」と遷移していく際に渡していくことで、最初と最後の「一覧ページ」が同じページになるようにしました。その手順は概略以下の通りです。
-
一覧ページから編集ページに遷移するには、上の画像にある[Edit]リンクボタンをクリックして編集ページのアクションメソッドを GET 要求しますが、その際、クエリ文字列で現在のページ番号を渡せるようにします。ページ番号は Model.PageIndex で取得できるので、@Html.ActionLink のパラメータに pageIndex = Model.PageIndex を追加すれば OK です。
-
編集ページのアクションメソッドでクエリ文字列からページ番号を取得し、ViewBag を使って編集ページの View にページ番号を渡します。
-
編集ページの View の form タグ内に隠しフィールドを追加し、それに ViewBag で受け取ったページ番号を保存します。
-
編集ページでユーザーが編集を完了し[Save]ボタンをクリックすると [HttpPost] 属性が付与された方の編集ページのアクションメソッドが呼び出されます。その際、隠しフィールドに保存されたページ番号も一緒に送信されてきます。
-
編集ページのアクションメソッドで DB の UPDATE が完了すると一覧ページにリダイレクトされるので、クエリ文字列でページ番号を渡せるように、RedirectToAction メソッドの第 2 引数に new { pageNumber = pageIndex } というようにパラメータを追加します。
-
一覧ページがリダイレクトにより GET 要求されますが、クエリ文字列でページ番号が指定されるので、指定されたページを表示します。
以下にこの記事の検証に使ったサンプルコードを載せておきます。
コンテキストクラス、エンティティクラス
Microsoft のサンプルデータベース Northwind から Visual Studio の ADO.NET Entity Data Model ウィザードを使って作成した Entity Data Model に含まれるものを使いました。参考までに自動生成されたダイアグラムの Products, Categories, Supliers テーブル部分の画像を下に貼っておきます。
PaginatedList.cs
上に紹介した Microsoft のチュートリアルのコードと同じですが、少しコメントを加えて以下にアップしておきます。
このクラスがページングの中核を担うもので、コントローラーが生成した IQueryable<Product> オブジェクトを CreateAsync メソッドで受け取って、Skip, Take メソッドでページに表示する部分のみをデータベースから取得し、PaginatedList<Product> オブジェクトを生成して戻り値として返すようにしています。
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
namespace Mvc5App2.Models
{
public class PaginatedList<T> : List<T>
{
// 表示するページのページ番号
public int PageIndex { get; private set; }
// 全ページ数
public int TotalPages { get; private set; }
// コンストラクタ。下の CreateAsync メソッドから呼ばれる
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);
}
}
// 下の静的メソッドがコントローラーから呼ばれて戻り値がモデルとして
// ビューに渡される。引数の pageSize は 1 ページに表示するレコード
// 数でコントローラーから渡される
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);
}
}
}
Controller / Action Method
以下のコードのアクションメソッドは一覧ページ用の Pager と編集ページ用の EditPaging のみで他は省略していますので注意してください。
using System;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;
using System.Net;
using System.Web.Mvc;
using Mvc5App2.Models;
namespace Mvc5App2.Controllers
{
public class ProductsController : Controller
{
private NORTHWINDEntities db = new NORTHWINDEntities();
// 引数の pageNumber が表示するページ
public async Task<ActionResult> Pager(int? pageNumber)
{
// IDENTITY で主キーの ProductID 順に並べる
var products = db.Products
.Include(p => p.Categories)
.OrderBy(p => p.ProductID);
// 1 ページに表示するレコード数を指定
int pageSize = 5;
// CreateAsync メソッドで pageNumber に指定されるページの
// レコードのリストを取得
return View(await PaginatedList<Products>
.CreateAsync(products.AsNoTracking(),
pageNumber ?? 1,
pageSize));
}
// クエリ文字列で渡されるページ番号を取得するため引数に
// pageIndex を追加
public async Task<ActionResult> EditPaging(int? id, int? pageIndex)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Products products = await db.Products.FindAsync(id);
if (products == null)
{
return HttpNotFound();
}
// View に現在のページ番号を渡す
ViewBag.PageIndex = pageIndex ?? 1;
ViewBag.CategoryID = new SelectList(db.Categories,
"CategoryID",
"CategoryName",
products.CategoryID);
ViewBag.SupplierID = new SelectList(db.Suppliers,
"SupplierID",
"CompanyName",
products.SupplierID);
return View(products);
}
// クエリ文字列で渡されるページ番号を取得するため引数に
// pageIndex を追加
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> EditPaging(
[Bind(Include = "ProductID,ProductName,SupplierID,CategoryID," +
"QuantityPerUnit,UnitPrice,UnitsInStock,UnitsOnOrder," +
"ReorderLevel,Discontinued")] Products products,
int pageIndex)
{
if (ModelState.IsValid)
{
db.Entry(products).State = EntityState.Modified;
await db.SaveChangesAsync();
// リダイレクトの際クエリ文字列でページ番号を渡せるよう第 2 引数
// に new { pageNumber = pageIndex } を追加
return RedirectToAction("Pager",
new { pageNumber = pageIndex });
}
ViewBag.CategoryID = new SelectList(db.Categories,
"CategoryID",
"CategoryName",
products.CategoryID);
ViewBag.SupplierID = new SelectList(db.Suppliers,
"SupplierID",
"CompanyName",
products.SupplierID);
return View(products);
}
// ・・・中略・・・
protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}
}
}
一覧ページ用の View
上に書いたように、[Edit]リンクボタン用の @Html.ActionLink のパラメータに pageIndex = Model.PageIndex を追加しているところに注目してください。table 要素の下にページャーも実装しています。
@model Mvc5App2.Models.PaginatedList<Products>
@{
ViewBag.Title = "Pager";
}
<h2>Pager</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
Id
</th>
<th>
Product Name
</th>
<th>
Category
</th>
<th>
Unit Price
</th>
<th>
Discontinued
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.ProductID)
</td>
<td>
@Html.DisplayFor(modelItem => item.ProductName)
</td>
<td>
@Html.DisplayFor(modelItem => item.Categories.CategoryName)
</td>
<td>
@Html.DisplayFor(modelItem => item.UnitPrice)
</td>
<td>
@Html.DisplayFor(modelItem => item.Discontinued)
</td>
<td>
@Html.ActionLink("Edit", "EditPaging",
new { id = item.ProductID,
pageIndex = Model.PageIndex }) |
@Html.ActionLink("Details", "Details",
new { id = item.ProductID }) |
@Html.ActionLink("Delete", "Delete",
new { id = item.ProductID })
</td>
</tr>
}
</tbody>
</table>
@*Pagination
https://getbootstrap.jp/docs/4.2/components/pagination/*@
@{
// ページャーの First Prev 1 2 3 ... n Next Last の 1 ~ n のボタン数
// n は奇数にしてください
int buttonCount = 7;
}
<span>Page @Model.PageIndex of @Model.TotalPages</span>
<br />
<nav aria-label="Page navigation">
<ul class="pagination">
@if (Model.HasPreviousPage)
{
<li class="page-item">
@Html.ActionLink("First", "Pager",
new { pageNumber = 1 },
new { @class = "page-link" })
</li>
}
@if (Model.HasPreviousPage)
{
<li class="page-item">
@Html.ActionLink("Prev", "Pager",
new { pageNumber = (Model.PageIndex - 1) },
new { @class = "page-link" })
</li>
}
@{
int startPage;
int stopPage;
if (Model.TotalPages > buttonCount)
{
if (Model.PageIndex <= buttonCount / 2 + 1)
{
startPage = 1;
stopPage = buttonCount;
}
else if (Model.PageIndex < (Model.TotalPages - buttonCount / 2))
{
startPage = Model.PageIndex - buttonCount / 2;
stopPage = Model.PageIndex + buttonCount / 2;
}
else
{
startPage = Model.TotalPages - buttonCount + 1;
stopPage = Model.TotalPages;
}
}
else
{
startPage = 1;
stopPage = Model.TotalPages;
}
for (int i = startPage; i <= stopPage; i++)
{
if (Model.PageIndex == i)
{
<li class="page-item active">
<span class="page-link">@i</span>
</li>
}
else
{
<li class="page-item">
@Html.ActionLink(i.ToString(), "Pager",
new { pageNumber = i },
new { @class = "page-link" })
</li>
}
}
}
@if (Model.HasNextPage)
{
<li class="page-item">
@Html.ActionLink("Next", "Pager",
new { pageNumber = (Model.PageIndex + 1) },
new { @class = "page-link" })
</li>
}
@if (Model.HasNextPage)
{
<li class="page-item">
@Html.ActionLink("Last", "Pager",
new { pageNumber = (Model.TotalPages) },
new { @class = "page-link" })
</li>
}
</ul>
</nav>
編集ページ用の View
以下の画像の赤枠で囲ったコードを追加した以外はスキャフォールディングで生成されるコードと同じです。前者の赤枠部分は隠しフィールドにページ番号を保存するためのもの、後者は編集を途中で止めて一覧ページに戻る際、同じページに戻るためのものです。