WebSurfer's Home

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

ASP.NET Core MVC 用ページャー

by WebSurfer 2021年2月2日 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 2021年1月30日 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 とかですと全レコード取得後に表示する部分を切り取るということになるのですが、そのあたりは進化しているようです。

Controller / 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 2021年1月26日 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: , , , ,

Validation

About this blog

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

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar