WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

ロールに属するユーザー一覧を表示

by WebSurfer 13. February 2021 11:19

ASP.NET Core MVC アプリでユーザー認証・承認を行うのに ASP.NET Core Identity を利用するケースで、ロールにアサインされたユーザー一覧を表示するサンプルを書きます。

ロールに属するユーザー一覧の表示

先の記事「ASP.NET Identity のロール管理 (CORE)」で、管理者がロールの表示・追加・変更・削除を行うためのサンプルを紹介しましたが、それの Details アクションメソッド / ビューに実装してみます。(先の記事では Details は未実装でした)

(1) ビューモデルの追加

コントローラーからビューに渡すモデルとして UsersInRole クラスを追加します。

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

// 追加
using Microsoft.AspNetCore.Identity;
using MySQLIdentity.Areas.Identity.Data;

namespace MySQLIdentity.Models
{
    // ・・・中略(先の記事のコードと同じ)・・・

    // 追加
    public class UsersInRole
    {
        public IdentityRole Role { set; get; }
        public IList<MySQLIdentityUser> Users { set; get; }
    }
}

(2) Details アクションメソッドを追加

先の記事のコントローラー RoleController にアクションメソッド Details を追加します。

using System.Collections.Generic;
using System.Linq;
// ・・・中略(先の記事のコードと同じ)・・・

namespace MySQLIdentity.Controllers
{
    public class RoleController : Controller
    {
        // ・・・中略(先の記事のコードと同じ)・・・

        // 以下のアクションメソッドを追加
        public async Task<IActionResult> Details(string id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var role = await _roleManager.FindByIdAsync(id);
            if (role == null)
            {
                return NotFound();
            }

            var model = new UsersInRole
            {
                Role = role,
                Users = await _userManager.GetUsersInRoleAsync(role.Name)
            };

            return View(model);
        }

        // ・・・中略(先の記事のコードと同じ)・・・
    }
}

(3) ビュー Details.cshtml を追加

@model MySQLIdentity.Models.UsersInRole

@{
    ViewData["Title"] = "Details";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h1>Details</h1>

<h4>Users in Role "@Model.Role.Name"</h4>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Users.First().UserName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Users.First().HandleName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Users.First().PhoneNumber)
            </th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Users)
        {
            <tr>

                <td>
                    @Html.DisplayFor(modelItem => item.UserName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.HandleName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.PhoneNumber)
                </td>
            </tr>
        }
    </tbody>
</table>

<p>
    @Html.ActionLink("Edit", "Edit", new { id = Model.Role.Id }) |
    @Html.ActionLink("Back to List", "Index")
</p>

一覧表示画面 Index の Details リンクをクリックし、Member ロールに属するユーザー一覧を表示したのが上の画像です。

Tags: , , ,

CORE

ASP.NET Core MVC 用ページャー

by WebSurfer 2. February 2021 14:26

先の記事「ASP.NET MVC でページング (CORE 版)」で実装したページャーは、どうも見栄えが気に入らないので変更しました。以下の画像の下の方にあるのがそれです。

Pager

Visual Studio 2019 のテンプレートを使って ASP.NET Core MVC アプリのプロジェクトを生成すると Bootstrap.css v4.3.1 が wwwroot フォルダに自動的に含まれるようになります。

それには Bootstrap のドキュメント Pagination に書いてあるページャーに使えるスタイルが定義されています。

というわけで、Bootstrap のページング用のスタイルを利用して、先の記事のページャーの外観を上の画像のように書き換えてみました。かなりスマートになったと自画自賛しているのですが、いかがでしょう?

機能的には先の記事のページャーとほとんど同じですが、次の点を変更しています: (1) ページャーの First Prev 1 2 3 ... n Next Last の 1 ~ n のボタンの数 n を任意に設定できるようにした。(2) First Prev Next Last のリンクはそれが無効な時は非表示とした。

以下に View に実装したページャー部分のコードのみアップしておきます。

@{
    // buttonCount はページャーの 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">
                <a asp-action="Pager"
                   asp-route-sortOrder="@ViewData["CurrentSort"]"
                   asp-route-pageNumber="1"
                   asp-route-currentFilter="@ViewData["CurrentFilter"]"
                   class="page-link">
                    First
                </a>
            </li>
        }

        @if (Model.HasPreviousPage)
        {
            <li class="page-item">
                <a asp-action="Pager"
                   asp-route-sortOrder="@ViewData["CurrentSort"]"
                   asp-route-pageNumber="@(Model.PageIndex - 1)"
                   asp-route-currentFilter="@ViewData["CurrentFilter"]"
                   class="page-link">
                    Prev
                </a>
            </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">
                        <a asp-action="Pager"
                           asp-route-sortOrder="@ViewData["CurrentSort"]"
                           asp-route-pageNumber="@i"
                           asp-route-currentFilter="@ViewData["CurrentFilter"]"
                           class="page-link">
                            @i
                        </a>
                    </li>
                }
            }
        }

        @if (Model.HasNextPage)
        {
            <li class="page-item">
                <a asp-action="Pager"
                   asp-route-sortOrder="@ViewData["CurrentSort"]"
                   asp-route-pageNumber="@(Model.PageIndex + 1)"
                   asp-route-currentFilter="@ViewData["CurrentFilter"]"
                   class="page-link">
                    Next
                </a>
            </li>
        }

        @if (Model.HasNextPage)
        {
            <li class="page-item">
                <a asp-action="Pager"
                   asp-route-sortOrder="@ViewData["CurrentSort"]"
                   asp-route-pageNumber="@(Model.TotalPages)"
                   asp-route-currentFilter="@ViewData["CurrentFilter"]"
                   class="page-link">
                    Last
                </a>
            </li>
        }
    </ul>
</nav>

Tags: , ,

Paging

ASP.NET MVC でページング (CORE 版)

by WebSurfer 30. January 2021 14:45

Web アプリでデータベースのレコード一覧を表示する場合、レコード数が多い場合はページングが必要になってくると思いますが、ASP.NET Core MVC でそれをどのように実装するかという話を書きます。

.NET Frammwork 版の MVC5 アプリでのページングの実装方法は Microsoft のドキュメント「チュートリアル: ASP.NET MVC アプリケーションでの Entity Framework を使用した並べ替え、フィルター処理、およびページングの追加」に書かれています。NuGet から PagedList パッケージをインストールしてそれを利用するというものです。

Core 版の MVC アプリではどうすればよいかですが、これも Microsoft のドキュメント「チュートリアル: 並べ替え、フィルター処理、ページングを追加する - ASP.NET MVC と EF Core」に説明がありました。

というわけで、そのチュートリアルを見ればこの記事の話は終わりなのですが、それではブログの記事としては面白くないので、ページャーの機能を拡張し、Microsoft のドキュメントにはページャーとして Previous と Next の機能しかなかったものに、上の画像のように First と Last およびページ番号のリンクを追加しました。

上の画像を表示したサンプルコードは以下の通りです。表示するのを Microsoft のサンプルデータベース Northwind の Products テーブルとした点とページャー機能の拡張以外は Microsoft のチュートリアルと同じです。

サンプルは .NET 5.0 ベースで作りましたが、肝心な部分は .NET Core 3.1 でも同じだと思います。(.NET 5.0 ベースにしたのは EF Core 5.0 で導入されたシンプルなログで SQL Server に渡される SQL 文を見ることができるという理由です)

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

Microsoft のサンプルデータベース Northwind からリバースエンジニアリングで生成したものを利用しました。ここでの話にはほとんど関係ないのですが、参考までに自動生成されたエンティティクラス Product のコードを以下にアップしておきます。

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

#nullable disable

namespace MvcCore5App.Models
{
    [Index(nameof(CategoryId), Name = "CategoriesProducts")]
    [Index(nameof(CategoryId), Name = "CategoryID")]
    [Index(nameof(ProductName), Name = "ProductName")]
    [Index(nameof(SupplierId), Name = "SupplierID")]
    [Index(nameof(SupplierId), Name = "SuppliersProducts")]
    public partial class Product
    {
        [Key]
        [Column("ProductID")]
        public int ProductId { get; set; }
        [Required]
        [StringLength(40)]
        public string ProductName { get; set; }
        [Column("SupplierID")]
        public int? SupplierId { get; set; }
        [Column("CategoryID")]
        public int? CategoryId { get; set; }
        [StringLength(20)]
        public string QuantityPerUnit { get; set; }
        [Column(TypeName = "money")]
        public decimal? UnitPrice { get; set; }
        public short? UnitsInStock { get; set; }
        public short? UnitsOnOrder { get; set; }
        public short? ReorderLevel { get; set; }
        public bool Discontinued { get; set; }

        [ForeignKey(nameof(CategoryId))]
        [InverseProperty("Products")]
        public virtual Category Category { get; set; }
        [ForeignKey(nameof(SupplierId))]
        [InverseProperty("Products")]
        public virtual Supplier Supplier { get; set; }
    }
}

PaginatedList.cs

Microsoft のチュートリアルのコードと全く同じですが、少しコメントを加えて以下にアップしておきます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace MvcCore5App.Services
{
    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);
        }
    }
}

このクラスがページングの中枢を担うもので、コントローラーが並べ替え、フィルター処理を追加した後の IQueryable<Product> オブジェクトを CreateAsync メソッドが受け取って、Skip, Take メソッドでページに表示する部分のみをデータベースから取得し、PaginatedList<Product> オブジェクトを生成して戻り値として返すようにしています。

上の Linq to Entities で使われている Skip, Take メソッドですが、全レコードを取得した後でページに表示するレコードのみを切り取るのかと思っていましたがそうではなく、SQL Server には以下のような SQL 文が渡されてました。

SELECT p.ProductID, ...
FROM Products AS p
ORDER BY p.ProductID ASC
OFFSET {Skip 数} ROWS
FETCH NEXT {Take 数} ROWS ONLY

Web Forms アプリの SqlDataSource + GridView とかですと全レコード取得後に表示する部分を切り取るということになるのですが、そのあたりは進化しているようです。

Comtroller / Action Method

データの取得先を Microsoft のサンプルデータベース Northwind の Products テーブルとした点以外はチュートリアルのコードと同じです。

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using MvcCore5App.DAL;
using MvcCore5App.Models;
using MvcCore5App.Services;

namespace MvcCore5App.Controllers
{
    public class ProductsController : Controller
    {
        private readonly NorthwindContext _context;

        public ProductsController(NorthwindContext context)
        {
            _context = context;
        }

        public async Task<IActionResult> PagedIndex(string sortOrder, 
                                                    string currentFilter, 
                                                    string searchString,
                                                    int? pageNumber)
        {
            ViewData["CurrentSort"] = sortOrder;
            ViewData["NameSortParm"] = 
                String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
            ViewData["PriceSortParm"] = 
                sortOrder == "Price" ? "price_desc" : "Price";

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

            ViewData["CurrentFilter"] = searchString;

            var products = from p in _context.Products.Include(p => p.Category)
                           select p;

            if (!String.IsNullOrEmpty(searchString))
            {
                products = products.Where(p => p.ProductName.Contains(searchString));
            }

            switch (sortOrder)
            {
                case "name_desc":
                    products = products.OrderByDescending(p => p.ProductName);
                    break;
                case "Price":
                    products = products.OrderBy(p => p.UnitPrice);
                    break;
                case "price_desc":
                    products = products.OrderByDescending(p => p.UnitPrice);
                    break;
                default:
                    products = products.OrderBy(p => p.ProductName);
                    break;
            }

            int pageSize = 5;   // 1 ページに表示するレコードの数

            return View(await PaginatedList<Product>.CreateAsync(products.AsNoTracking(), 
                                                                 pageNumber ?? 1, 
                                                                 pageSize));
        }
    }
}

View

ページャーの機能を拡張し、Microsoft のドキュメントにあった Previous と Next に、First と Last およびページ番号のリンクを追加しました。結果は上の画像のようになります。

@model MvcCore5App.Services.PaginatedList<Product>

@{
    ViewData["Title"] = "PagedIndex";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h1>PagedIndex</h1>

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

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

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-action="PagedIndex" 
                   asp-route-sortOrder="@ViewData["NameSortParm"]"
                   asp-route-currentFilter="@ViewData["CurrentFilter"]">
                    Product Name
                </a>
            </th>
            <th>
                Category
            </th>
            <th>
                <a asp-action="PagedIndex" 
                   asp-route-sortOrder="@ViewData["PriceSortParm"]"
                   asp-route-currentFilter="@ViewData["CurrentFilter"]">
                    Unit Price
                </a>
            </th>
            <th>
                Discontinued
            </th>

            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.ProductName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Category.CategoryName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.UnitPrice)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Discontinued)
                </td>

                <td>
                    <a asp-action="Edit" asp-route-id="@item.ProductId">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.ProductId">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.ProductId">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

@{
    var prevDisabled = !Model.HasPreviousPage ? "disabled" : "";
    var nextDisabled = !Model.HasNextPage ? "disabled" : "";
}

<span>Page @Model.PageIndex of @Model.TotalPages</span>
<br />
<a asp-action="PagedIndex"
   asp-route-sortOrder="@ViewData["CurrentSort"]"
   asp-route-pageNumber="1"
   asp-route-currentFilter="@ViewData["CurrentFilter"]"
   class="btn btn-secondary @prevDisabled">
    First
</a>
<a asp-action="PagedIndex"
   asp-route-sortOrder="@ViewData["CurrentSort"]"
   asp-route-pageNumber="@(Model.PageIndex - 1)"
   asp-route-currentFilter="@ViewData["CurrentFilter"]"
   class="btn btn-secondary @prevDisabled">
    Prev
</a>

@{
    // ページ総数が 5 を超える場合は Prev と Next の間に 1 2 3 4 5 と
    // いうようなリンクを表示し、その番号のページに飛べるようにした
    if (Model.TotalPages > 5)
    {
        if (Model.PageIndex <= 3)
        {
            for (int i = 1; i <= 5; i++)
            {
                var disabled = (Model.PageIndex == i) ? "disabled" : "";
                <a asp-action="PagedIndex"
                   asp-route-sortOrder="@ViewData["CurrentSort"]"
                   asp-route-pageNumber="@i"
                   asp-route-currentFilter="@ViewData["CurrentFilter"]"
                   class="btn btn-secondary @disabled">
                    @i
                </a>
            }
        }
        else if (Model.PageIndex < (Model.TotalPages - 2))
        {
            for (int i = Model.PageIndex - 2; i <= Model.PageIndex + 2; i++)
            {
                var disabled = (Model.PageIndex == i) ? "disabled" : "";
                <a asp-action="PagedIndex"
                   asp-route-sortOrder="@ViewData["CurrentSort"]"
                   asp-route-pageNumber="@i"
                   asp-route-currentFilter="@ViewData["CurrentFilter"]"
                   class="btn btn-secondary @disabled">
                    @i
                </a>
            }
        }
        else
        {
            for (int i = Model.TotalPages - 4; i <= Model.TotalPages; i++)
            {
                var disabled = (Model.PageIndex == i) ? "disabled" : "";
                <a asp-action="PagedIndex"
                   asp-route-sortOrder="@ViewData["CurrentSort"]"
                   asp-route-pageNumber="@i"
                   asp-route-currentFilter="@ViewData["CurrentFilter"]"
                   class="btn btn-secondary @disabled">
                    @i
                </a>
            }
        }
    }
}

<a asp-action="PagedIndex"
   asp-route-sortOrder="@ViewData["CurrentSort"]"
   asp-route-pageNumber="@(Model.PageIndex + 1)"
   asp-route-currentFilter="@ViewData["CurrentFilter"]"
   class="btn btn-secondary @nextDisabled">
    Next
</a>
<a asp-action="PagedIndex"
   asp-route-sortOrder="@ViewData["CurrentSort"]"
   asp-route-pageNumber="@(Model.TotalPages)"
   asp-route-currentFilter="@ViewData["CurrentFilter"]"
   class="btn btn-secondary @nextDisabled">
    Last
</a>

まず、最初のページおよび最後のページに飛べるよう First, Last というリンクを追加しました。

さらに、コード内のコメントにも書きましたように、ページ総数が 5 を超える場合は Prev と Next の間に 1 2 3 4 5 というようなページ番号のリンクを表示し、それをクリックすることによりその番号のページに飛べるようにしてみました。

ページング機能は Html ヘルパーにまとめ、全ページ数が 5 を超える場合の 5 という数字は任意の数を外部から設定できるようにした方が良さそうですが、それは今後の検討課題ということで・・・

Tags: , ,

Paging

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  February 2021  >>
MoTuWeThFrSaSu
25262728293031
1234567
891011121314
15161718192021
22232425262728
1234567

View posts in large calendar