WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

ページャー付き一覧画面から編集後同じページに戻る (MVC5)

by WebSurfer 2022年11月7日 14:19

ASP.NET MVC アプリのプロジェクトで、スキャフォールディング機能を使うと、DB の CRUD 操作を行う Controller と View のコードを一式自動生成することができます。

そのレコード一覧の表示のページにページング機能を実装し、例えばページ 5 を表示してから Edit リンクをクリックして編集ページに遷移し、DB の UPDATE 完了で元の一覧ページにリダイレクトされる際、同じページ 5 が表示されるようにする機能を実装してみました。以下に要点を備忘録として残しておきます。

ページャー付き Index

この記事で紹介するのは 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 プロパティで現在のページ番号を取得できます。そのページ番号を、「一覧ページ」⇒「編集ページ」⇒「一覧ページ」と遷移していく際に渡していくことで、最初と最後の「一覧ページ」が同じページになるようにしました。その手順は概略以下の通りです。

  1. 一覧ページから編集ページに遷移するには、上の画像にある[Edit]リンクボタンをクリックして編集ページのアクションメソッドを GET 要求しますが、その際、クエリ文字列で現在のページ番号を渡せるようにします。ページ番号は Model.PageIndex で取得できるので、@Html.ActionLink のパラメータに pageIndex = Model.PageIndex を追加すれば OK です。
  2. 編集ページのアクションメソッドでクエリ文字列からページ番号を取得し、ViewBag を使って編集ページの View にページ番号を渡します。
  3. 編集ページの View の form タグ内に隠しフィールドを追加し、それに ViewBag で受け取ったページ番号を保存します。
  4. 編集ページでユーザーが編集を完了し[Save]ボタンをクリックすると [HttpPost] 属性が付与された方の編集ページのアクションメソッドが呼び出されます。その際、隠しフィールドに保存されたページ番号も一緒に送信されてきます。
  5. 編集ページのアクションメソッドで DB の UPDATE が完了すると一覧ページにリダイレクトされるので、クエリ文字列でページ番号を渡せるように、RedirectToAction メソッドの第 2 引数に new { pageNumber = pageIndex } というようにパラメータを追加します。
  6. 一覧ページがリダイレクトにより GET 要求されますが、クエリ文字列でページ番号が指定されるので、指定されたページを表示します。

以下にこの記事の検証に使ったサンプルコードを載せておきます。

コンテキストクラス、エンティティクラス

Microsoft のサンプルデータベース Northwind から Visual Studio の ADO.NET Entity Data Model ウィザードを使って作成した Entity Data Model に含まれるものを使いました。参考までに自動生成されたダイアグラムの Products, Categories, Supliers テーブル部分の画像を下に貼っておきます。

Entity Data Model

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

以下の画像の赤枠で囲ったコードを追加した以外はスキャフォールディングで生成されるコードと同じです。前者の赤枠部分は隠しフィールドにページ番号を保存するためのもの、後者は編集を途中で止めて一覧ページに戻る際、同じページに戻るためのものです。

編集ページ用の View への追加コード

Tags: , , ,

Paging

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2022年11月  >>
303112345
6789101112
13141516171819
20212223242526
27282930123
45678910

View posts in large calendar