WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

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

ファイルアップロード時の検証 (CORE 版)

by WebSurfer 26. January 2021 22:32

ASP.NET Core. 3.1 MVC アプリでファイルをアップロードする際に、カスタム検証属性を利用してファイルのサイズとタイプをクライアント側とサーバー側の両方で検証し、検証結果 NG の場合はエラーメッセージを表示する方法について書きます。

ファイルアップロード時の検証

基本的には先の記事「ASP.NET Core MVC 検証属性の自作」の応用です。先の記事ではパラメータが 1 つだけでしたが、この記事ではファイルのサイズとタイプという 2 つを検証するところが大きな違いです。

クライアント側での検証は、Web Forms アプリの記事「FileUpload と CustomValidator」と同様に、ブラウザの HTML5 File API を利用して JavaScript でファイルのサイズとタイプ情報を取得し、外部から入力されるパラメータ値と比較して検証します。

そのパラメータ値はカスタム検証属性(下のコード例では FileValidationAttribute)をモデルのプロパティに設定する際に引数(下のコード例では 50000 と "image/jpeg")として渡すようにしています。

サーバー側では引数に渡されたパラメータ値(ファイルサイズ / タイプ)を容易に取得でき、検証属性クラスの IsValid メソッドに引数として渡される IFromFile オブジェクトから取得できるファイルサイズとタイプを比較して検証できます。問題はクライアント側での検証用 JavaScript / jQuery にそのパラメータ値をどのように渡すかです。

パラメータが 1 つだけなら先の記事「ASP.NET Core MVC 検証属性の自作」で書いた View の中のスクリプトのように addSingleVal メソッドを利用できます。しかし、2 つあると addSingleVal メソッドでは何ともならず、add メソッドを利用せざるを得ないようです。

add メソッドの使い方は、先の記事を書くときに調べた Unobtrusive Client Validation in ASP.NET MVC 3 という記事に記述がありましたが、具体的にどういうコードを書けばいいのかが分かりません。

やむを得ず検証用の控えめな JavaScript のソースコード jquery.validate.unobtrusive.js を調べると、addBool, addSingleVal, addMinMax メソッドは add メソッドをラップしたものだということが分かりました。その中の addMinMax メソッドが min, max というパラメータを 2 つ渡していますので、それを見よう見まねで書いてみたのが下の View の中のコードです。

Edge, Chrome のデバッガでスクリプトをステップ実行するなどして調べましたが一応は期待通り動いていることを確認しました。具体的にどのような仕組みで動いているかまでは分かっていませんが。(汗)

いかにサンプルコードをアップしておきます。上の画像を表示したものです。要点はコメントに書いたつもりですのでそれを見てください。(手抜きでスミマセン)

Model とカスタム検証属性

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.Globalization;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace MvcCoreApp.Models
{
    // Model
    public class FileUpload
    {
        [Required(ErrorMessage = "{0} is required.")]
        public string Name { get; set; }

        [Display(Name = "Upload file")]
        [Required(ErrorMessage = "{0} is required.")]
        [FileValidation(50000, "image/jpeg")]
        public IFormFile PostedFile { get; set; }
    }

    // カスタム検証属性
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class FileValidationAttribute : ValidationAttribute, 
                                           IClientModelValidator
    {
        private readonly long size;
        private readonly string type;

        public FileValidationAttribute(long size, string type)
        {
            this.size = size;
            this.type = type;
            this.ErrorMessage = "{0} must be less than {1} byte, {2}";
        }

        public override string FormatErrorMessage(string displayName)
        {
            return String.Format(CultureInfo.CurrentCulture, 
                                 ErrorMessageString, displayName, 
                                 this.size, this.type);
        }

        public override bool IsValid(object value)
        {
            var postedFile = value as IFormFile;

            if (postedFile == null || postedFile.Length == 0)
            {
                return false;
            }

            if (postedFile.Length <= this.size && 
                postedFile.ContentType == this.type)
            {
                return true;
            }

            return false;
        }
        // IClientModelValidator が実装するメソッド。検証対象の input
        // 要素に控えめな JavaScript (jquery.validate.unobtrusive.js)
        // による検証のための属性と値を追加
        public void AddValidation(ClientModelValidationContext context)
        {
            MergeAttribute(context.Attributes, "data-val", "true");
            var errorMessage = FormatErrorMessage(
                context.ModelMetadata.GetDisplayName());
            MergeAttribute(context.Attributes, 
                "data-val-filevalidation", errorMessage);
            MergeAttribute(context.Attributes, 
                "data-val-filevalidation-size", this.size.ToString());
            MergeAttribute(context.Attributes, 
                "data-val-filevalidation-type", this.type);
        }

        // 上の AddValidation メソッドで使うヘルパーメソッド
        private bool MergeAttribute(IDictionary<string, string> attributes, 
                                    string key, string value)
        {
            if (attributes.ContainsKey(key))
            {
                return false;
            }
            attributes.Add(key, value);
            return true;
        }
    }
}

View

@model MvcCoreApp.Models.FileUpload

@{
    ViewData["Title"] = "Upload";
}

<h1>Upload</h1>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Upload" enctype="multipart/form-data">
            <div asp-validation-summary="ModelOnly" class="text-danger">
            </div>
            <div class="form-group">
                <label asp-for="Name" class="control-label"></label>
                <input asp-for="Name" class="form-control" />
                <span asp-validation-for="Name" class="text-danger"></span>
            </div>

            <div class="form-group">
                <label asp-for="PostedFile" class="control-label"></label>
                <input type="file" asp-for="PostedFile" />
                <br />
                <span asp-validation-for="PostedFile" class="text-danger">
                </span>
            </div>

            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}


<script type="text/javascript">
    //<![CDATA[
    $.validator.addMethod("filevalidation",
        function (value, element, parameters){
            // HTML5 File API の File, FileList がサポートされている
            // 場合のみ検証する。サポートされてないとスクリプトエラ
            // ーになるので下の if 文は必要
            if (window.File && window.FileList) {

                // ファイルが選択されてないと fileUpload.files[0] は
                // undefined になる。element.files[0] == null は念のため
                if (element.files[0] == undefined ||
                    element.files[0] == null) {
                    // ファイル未選択は RequiredAttribute で検証する
                    // こととし、ここでは true を返すことにした(検証
                    // とエラーメッセージがダブらないように)
                    return true;
                }

                if (element.files[0].type != parameters.type) {                    
                    return false;
                }

                if (element.files[0].size > Number(parameters.size)) {
                    return false;
                }

                return ture;
            } else {
                // HTML5 File API の File, FileList がサポートされて
                // いない場合はクライアント側では検証しない
                return ture;
            }
        });

    // パラメータを 2 つ渡す方法が問題。add メソッドを使わざるを得ない
    $.validator.unobtrusive.adapters.
        add("filevalidation", ["type", "size"], function (options) {

            // 上の検証スクリプトで parameters.type, parameters.size と
            // という形でパラメータを取得するには以下のように JavaScript
            // オブジェクトとして setValidationValues の第 3 引数に渡す。
            // 配列 [options.params.size,options.params.type] としても渡
            // せるが、取得するとき parameters[0] のようにする必要がある
            var value = {
                size: options.params.size,
                type: options.params.type
            };

            setValidationValues(options, "filevalidation", value);
        });

    // jquery.validate.unobtrusive.js からコピーしたヘルパーメソッド
    function setValidationValues(options, ruleName, value) {
        options.rules[ruleName] = value;
        if (options.message) {
            options.messages[ruleName] = options.message;
        }
    }
    //]]>
</script>
}

Comtroller / Action Method

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using MvcCoreApp.Models;
using Microsoft.AspNetCore.Http;
using System.IO;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace MvcCoreApp.Controllers
{
    public class ValidationController : Controller
    {
        public IActionResult Upload()
        {
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Upload(FileUpload model)
        {
            string result = "";
            IFormFile postedFile = model.PostedFile;

            if (ModelState.IsValid)            
            {
                string filename = Path.GetFileName(postedFile.FileName);
                
                result = filename + " (" + postedFile.ContentType + ") - " +
                        postedFile.Length + " bytes アップロード完了";
            }
            else
            {
                result = "ファイルアップロードに失敗しました";
            }

            ViewBag.Result = result;
            return View();
        }
    }
}

Tags: , , , ,

Upload Download

DropDownList への SelectList の渡し方 (CORE)

by WebSurfer 22. January 2021 18:10

.NET Framework 版 MVC の話は先の記事「DropDownList への SelectList の渡し方」に書きましたが、その Core 版の話を聞きます。

DropDownList で複数項目を選択

Core 版 MVC アプリでは従前から提供されている Html ヘルパーの DropDownList, DropDownListFor メソッドに加えて、タグヘルパーというものが使えますのでそれを使った場合の例も書いておきます。

タグヘルパーに関する詳細は以下の Microsoft のドキュメントが参考になると思いますので、そちらを見てください(手抜きでスミマセン)。

SelectList のコンストラクタの第 4 引数に設定した selectedValue の通りに html にレンダリングされた時の option 要素に selected 属性を設定するにはどうするかという点がポイントでしたが、Html ヘルパーの DropDownList, DropDownListFor メソッドについてはそのあたりは .NET Framework 版と同様でした。

例えば ViewBag.CategoryID で SelectList を DropDownList メソッドに渡す場合は、第 1 引数は "CategoryID" という文字列、第 2 引数は null にします。詳しい説明は先の記事「DropDownList への SelectList の渡し方」を見てください。

select タグヘルパーを使う場合ですが、SelectList を ViewBag (または ViewData) 経由で渡す場合でかつ Model を使用しない場合は asp-for は使えませんので、select タグヘルパーに id, name 属性を設定して asp-items 属性に ViewBag (または ViewData) を渡します。

さらに、上に紹介した記事「ASP.NET Core のフォームのタグ ヘルパー」の「選択タグ ヘルパー」のセクションに Model で渡すサンプルコードが書いてあって、そのようにしても option 要素に selected 属性の設定が可能です。

その記事によると "We don't recommend using ViewBag or ViewData with the Select Tag Helper. A view model is more robust at providing MVC metadata and generally less problematic." とのことで、可能ならば SelectList を渡すには Model を使った方が良さそうです。(Model を使えないケースも多々ありそうですが)

以下に、(1) 1 項目だけ選択できるドロップダウンリスト、(2) 複数項目が選択可能なドロップダウンリスト、(3) 初期表示の際複数項目を指定して選択状態にするドロップダウンリストのサンプルコードを書いておきます。

(1) 1 項目だけ選択可

先の記事「DropDownList への SelectList の渡し方」と同じ条件です。

Model

select タグヘルパーに Model 経由で SelectList と初期状態の選択項目を渡すために使います。上に紹介した 3 番目の記事によると "asp-for 属性に指定されているプロパティが IEnumerable の場合、選択タグ ヘルパーは multiple = "multiple" 属性を自動的に生成します" とのことですので注意してください。

public class CategoryViewModel
{
    public int Category { get; set; }

    public SelectList Categories { get; set; }
}

Controller / Action Method

public async Task<IActionResult> Edit()
{
    int selected = 3;
    var categoryList = await _context.Categories.ToListAsync();

    ViewBag.CategoryId = new SelectList(categoryList, 
                                        "CategoryId", 
                                        "CategoryName", 
                                        selected);

    var model = new CategoryViewModel
    {
        Category = selected,
        Categories = new SelectList(categoryList,
                                    "CategoryId",
                                    "CategoryName")
    };

    return View(model);
}

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit(int categoryid, 
                          int categoryid2, 
                          int category)
{
    if (ModelState.IsValid)
    {
        return RedirectToAction("Index");
    }

    return View();
}

View

@model MvcCore5App.Controllers.CategoryViewModel

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

<h1>Edit</h1>

<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div class="form-group">
                <label class="control-label" for="CategoryId">
                    @@Html.DropDownList 利用
                </label>
                @Html.DropDownList("CategoryId", null,
                    htmlAttributes: new { @class = "form-control" })
            </div>
            <div class="form-group">
                <label class="control-label" for="CategoryId2">
                    タグヘルパー &lt;select&gt; + ViewBag 利用
                </label>
                <select id="categoryid2" name="categoryid2"
                        asp-items="@ViewBag.CategoryId"
                        class="form-control">
                </select>
            </div>
            <div class="form-group">
                <label class="control-label" for="CategoryId2">
                    タグヘルパー &lt;select&gt; + Model 利用
                </label>
                <select asp-for="Category"
                        asp-items="Model.Categories"
                        class="form-control">
                </select>
            </div>
            <div class="form-group">
                <input type="submit" value="Create"
                       class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

(2) 複数項目の選択可

Model

上記 (1) の Model と同じ。

Controller / Action Method

[HttpPost] 側のアクションメソッドの引数が int[] 型になっている点に注目してください。int[] 型にすることにより、ユーザーが複数項目を選択した結果を受け取れます。

public async Task<IActionResult> Edit2()
{
    int selected = 3;
    var categoryList = await _context.Categories.ToListAsync();

    ViewBag.CategoryId = new SelectList(categoryList,
                                        "CategoryId",
                                        "CategoryName",
                                        selected);

    var model = new CategoryViewModel
    {
        Category = selected,
        Categories = new SelectList(categoryList,
                                    "CategoryId",
                                    "CategoryName")
    };

    return View(model);
}

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit2(int[] categoryid, 
                           int[] categoryid2, 
                           int[] category)
{
    if (ModelState.IsValid)
    {
        return RedirectToAction("Index");
    }

    return View();
}

View

ユーザーが複数項目を選択できるよう multiple = "multiple" を付与しているところに注目してください。上にも書きましたが、asp-for 属性に指定されるプロパティが IEnumerable の場合、選択タグ ヘルパーは multiple = "multiple" 属性を自動的に生成しますとのことです。

@model MvcCore5App.Controllers.CategoryViewModel

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

<h1>Edit2</h1>

<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit2">
            <div class="form-group">
                <label class="control-label" for="CategoryId">
                    @@Html.DropDownList 利用
                </label>
                @Html.DropDownList("CategoryId", null,
                    htmlAttributes: new
                    {
                        @class = "form-control",
                        multiple = "multiple"
                    })
            </div>
            <div class="form-group">
                <label class="control-label" for="CategoryId2">
                    タグヘルパー &lt;select&gt; + ViewBag 利用
                </label>
                <select id="categoryid2" name="categoryid2"
                        asp-items="@ViewBag.CategoryId"
                        multiple="multiple"
                        class="form-control">
                </select>
            </div>
            <div class="form-group">
                <label class="control-label" for="CategoryId2">
                    タグヘルパー &lt;select&gt; + Model 利用
                </label>
                <select asp-for="Category"
                        asp-items="Model.Categories"
                        multiple="multiple"
                        class="form-control">
                </select>
            </div>
            <div class="form-group">
                <input type="submit" value="Create"
                       class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

(3) 初期表示の際複数項目を選択状態にする

上の画像を表示したのがこれです。

Model

SelectList に代えて MultiSelectList を使っているところに注目してください。

public class MultiCategoryViewModel
{
    public int[] Category { get; set; }

    public MultiSelectList Categories { get; set; }
}

Controller / Action Method

初期表示の時点で複数項目が選択されるようにするため、int selected を int[] selected に代えて、それを MultiSelectList の第 4 引数に渡している点に注目してください。View に渡す Model も CategoryViewModel から MultiCategoryViewModel に変えています。

public async Task<IActionResult> Edit3()
{
    int[] selected = new int[] { 1, 3, 5 };
    var categoryList = await _context.Categories.ToListAsync();

    ViewBag.CategoryId = new MultiSelectList(categoryList,
                                             "CategoryId",
                                             "CategoryName",
                                             selected);

    var model = new MultiCategoryViewModel
    {
        Category = selected,
        Categories = new MultiSelectList(categoryList,
                                         "CategoryId",
                                         "CategoryName")
    };

    return View(model);
}

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit3(int[] categoryid, 
                           int[] categoryid2, 
                           int[] category)
{
    if (ModelState.IsValid)
    {
        return RedirectToAction("Index");
    }

    return View();
}

View

Model を CategoryViewModel から MultiCategoryViewModel に変えたこと、下のタグヘルパー <select> + Model 利用のコードには multiple = "multiple" を設定してないこと以外は上記 (2) の View と同じです。上の Model のコードの通り Category プロパティを int[] 型にしてそれを asp-for 属性に指定していますので、html には multiple = "multiple" 属性が自動的に生成されます。

@model MvcCore5App.Controllers.MultiCategoryViewModel

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

<h1>Edit3</h1>

<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit3">
            <div class="form-group">
                <label class="control-label" for="CategoryId">
                    @@Html.DropDownList 利用
                </label>
                @Html.DropDownList("CategoryId", null,
                    htmlAttributes: new
                    {
                        @class = "form-control",
                        multiple = "multiple"
                    })
            </div>
            <div class="form-group">
                <label class="control-label" for="CategoryId2">
                    タグヘルパー &lt;select&gt; + ViewBag 利用
                </label>
                <select id="categoryid2" name="categoryid2"
                        asp-items="@ViewBag.CategoryId"
                        multiple="multiple"
                        class="form-control">
                </select>
            </div>
            <div class="form-group">
                <label class="control-label" for="CategoryId2">
                    タグヘルパー &lt;select&gt; + Model 利用
                </label>
                <select asp-for="Category"
                        asp-items="Model.Categories"
                        class="form-control">
                </select>
            </div>
            <div class="form-group">
                <input type="submit" value="Create"
                       class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

Tags: , , ,

CORE

About this blog

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

Calendar

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

View posts in large calendar