WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

CheckBox と DropDownList の検証 (CORE)

by WebSurfer 7. March 2021 14:44

CheckBox には、例えばユーザーが条件を許諾したという意思表示のため、チェックを入れるのが必須というケースがあると思います。その検証をクライアント側とサーバー側の両方で行うためのカスタム検証属性を考えてみました。

CheckBox の検証

CheckBox の場合 RequiredAttribute ではクライアント側でもサーバー側でも検証はかかりません。RangeAttribute を使って [Range(typeof(bool), "true", "true")] というように設定するという手段がありますが、クライアント側での検証がかかりません。

というわけで、先の記事「ASP.NET Core MVC 検証属性の自作」とか「ファイルアップロード時の検証 (CORE)」で書いたようなカスタム検証属性を作って利用するのが良さそうです。

DropDownList にもそのようなカスタム検証属性を作ってみようと思いましたが、未選択で検証 NG とする場合は一番最初の option 要素が value="" となっていれば RequiredAttribute でクライアント/サーバー両方で検証がかかること、特定の項目を選ばなければならないというのは他の条件も絡むであろうから先の記事「CustomValidation 属性」で書いたようにすべきで、意味がなさそうなので止めました。

以下にそのコードをアップしておきます。上の画像を表示したものです。DropDownList 関係のコードもせっかく書いたので一緒に載せておきます。

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
{
    // ビューモデル
    public class Partner
    {
        [Required(ErrorMessage = "{0} is required.")]
        public string Name { get; set; }

        // Required 属性では検証はかからない
        [Required(ErrorMessage = "{0} is required.")]
        [CheckBoxValidate(true)]
        public bool PartnerAccepted { get; set; }

        [Required(ErrorMessage = "{0} is required.")]
        public string Area { get; set; }

        public SelectList AreaList { get; set; }
    }

    // DropDownList に渡す SelectList 生成用のクラス
    // AreaId は int? または string 型にしておけば、SelectList を
    // 生成する際、下の Controller のコード例のように null を設定
    // できる。結果 option value="" となり未選択で検証 NG とできる
    public class Area
    {
        public int? AreaId { set; get; }
        public string AreaName { set; get; }
    }

    // CheckBox 用の検証属性
    // true/false を引数で指定(要チェックなら true とする)
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class CheckBoxValidateAttribute : ValidationAttribute, 
                                             IClientModelValidator
    {
        private readonly bool option;

        public CheckBoxValidateAttribute(bool option)
        {
            this.option = option;
            this.ErrorMessage = "{0} must be set to {1}.";
        }

        public override string FormatErrorMessage(string name)
        {
            return String.Format(CultureInfo.CurrentCulture, 
                                 ErrorMessageString, 
                                 name, 
                                 option);
        }

        public override bool IsValid(object value)
        {
            // テキストボックスに入力された文字列が検証対象の場合は
            // ここで value が空白か否かをチェックして空白の場合は
            // true を返すようにしていた(空白の検証は Required だけで
            // 行い、その他の検証属性は空白の場合は true を返すのが
            // 原則なので)。チェックボックスの場合は ASP.NET MVC では
            // 必ず true/false のいずれかが返されるようになっている。
            // なので null が帰ってきた場合は何か良からぬことが起こって
            // いるかもしれないということで例外をスローすることにした
            if (value == null) 
            {
                throw new ArgumentNullException("CheckBoxValidate");
            }

            if ((bool)value == option)
            {
                return true;
            }
            return false;
        }

        public void AddValidation(ClientModelValidationContext context)
        {
            MergeAttribute(context.Attributes, "data-val", "true");
            var errorMessage = 
                FormatErrorMessage(context.ModelMetadata.GetDisplayName());
            MergeAttribute(context.Attributes, 
                           "data-val-checkboxvalidate", 
                           errorMessage);
            MergeAttribute(context.Attributes, 
                           "data-val-checkboxvalidate-option", 
                           this.option.ToString());
        }

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

Controller / 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 CheckBoxDropDown()
        {
            // DropDwonList に渡す SelectList の生成
            // AreaId を null に設定すると結果 option value="" となり
            // 未選択では RequiredAttribute による検証は NG となる
            var list = new List<Area>
            { 
                new Area { AreaId = null, AreaName = "--- Select One ---"},
                new Area { AreaId = 1, AreaName = "North" },
                new Area { AreaId = 2, AreaName = "South"},
                new Area { AreaId = 3, AreaName = "Eastt" },
                new Area { AreaId = 4, AreaName = "West" }
            };

            var partner = new Partner 
            { 
                AreaList = new SelectList(list, "AreaId", "AreaName")
            };

            return View(partner);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult CheckBoxDropDown(Partner partner)
        {
            if (!ModelState.IsValid)
            {
                // 検証 NG で再表示する場合は SelectList も再生成要
                var list = new List<Area>
                {
                    new Area { AreaId = null, AreaName = "--- Select One ---"},
                    new Area { AreaId = 1, AreaName = "North" },
                    new Area { AreaId = 2, AreaName = "South"},
                    new Area { AreaId = 3, AreaName = "Eastt" },
                    new Area { AreaId = 4, AreaName = "West" }
                };

                partner.AreaList = new SelectList(list, "AreaId", "AreaName");

                return View(partner);
            }

            return RedirectToAction("Create", "Validation");
        }
    }
}

View

@model MvcCoreApp.Models.Partner

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

<h1>CheckBoxDropDown</h1>

<h4>Pertner</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="CheckBoxDropDown">
            <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 form-check">
                <label class="form-check-label">
                    <input class="form-check-input" asp-for="PartnerAccepted" />
                    @Html.DisplayNameFor(model => model.PartnerAccepted)
                </label>
                <br />
                <span asp-validation-for="PartnerAccepted" class="text-danger">
                </span>
            </div>
            <div class="form-group">
                <label asp-for="Area" class="control-label"></label>
                <select asp-for="Area" asp-items="Model.AreaList" 
                        class="form-control">
                </select>
                <span asp-validation-for="Area" 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("checkboxvalidate",
        function (value, element, parameters) {
            // チェックされてないと value は undefined となり
            // value では判定できないので注意
            var result = element.checked;
            var isTrue = (parameters.toUpperCase() == 'TRUE');
            if (result == isTrue) {
                return true;
            }
            return false;
        });

    $.validator.unobtrusive.adapters.
        addSingleVal('checkboxvalidate', 'option');

    //]]>
</script>
}

Tags: , , , ,

Validation

ファイルアップロード時の検証 (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

IValidatableObject を継承したカスタムモデル

by WebSurfer 28. February 2020 15:31

IValidatableObject インターフェイスを継承したカスタムモデルを使って検証を行う方法を備忘録として書いておきます。

IValidatableObject を継承したクラスで検証

先の記事「CustomValidation 属性」で書いたモデルレベルの検証と同等な機能を実装してみます。

マイクロソフト公式解説書「プログラミング Microsoft ASP.NET MVC」によると "IValidatableObject インターフェイスを実装すると、クラスレベルで CustomValidation 属性を使用するのと機能的には同じになります"、"クラスレベルの検証を実装するにあたって、クラスレベルで IValidatableObject インターフェイスと CustomValidation 属性のどちらを利用するかは、完全にあなた次第です" とのことです。

ただし、"モデルにデータアノテーションも追加されている場合、有効な状態でないプロパティが存在すると、Validate メソッドが呼び出されなくなります。IValidatableObject インターフェイスを利用する場合は、問題が起きないよう、データアノテーションを完全に削除することをお勧めします" との注記があります。

ということは、データアノテーションによるクライアント側での検証ができなくなるということで、あまり使い道はなさそうな気がしますが。

コードは以下の通りです。.NET Framework MVC5 と ASP.NET Core 3.1 MVC で同じコードが使えます。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using System.Globalization;

namespace MvcCoreApp.Models
{
  // IValidatableObject インターフェイス利用
  public class CustomValidationModel : IValidatableObject
  {
    public int PersonId { set; get; }

    [Display(Name = "名前")]
    public string Name { set; get; }

    [Display(Name = "メールアドレス")]
    public string Mail { set; get; }

    // int? にしないと未入力では Validate メソッドが動かない。
    // int, int? どちらも "1.0" とか入力された場合は Validate
    // メソッドが動かない。モデルバインドされる際に例外がスロ
    // ーされ Validate メソッドに制御が飛ばないのだと思われる
    [Display(Name = "年齢")]
    public int? Age { set; get; }

    // 検証を行う Validate メソッド
    public IEnumerable<ValidationResult> Validate(
                                ValidationContext context)
    {
      var model = 
        context.ObjectInstance as CustomValidationModel;

      if (model == null)
      {
        throw new NullReferenceException();
      }

      if (string.IsNullOrEmpty(model.Name))
      {
        yield return new ValidationResult(
            "名前は必須", new string[] { "Name" });
      }
      else if (model.Name.Length < 2 ||
               model.Name.Length > 20)
      {
        yield return new ValidationResult(
            "名前は 2 ~ 20 文字の範囲",
            new string[] { "Name" });
      }

      if (string.IsNullOrEmpty(model.Mail))
      {
        yield return new ValidationResult(
            "メールアドレスは必須",
            new string[] { "Mail" });
      }
      else
      {
        bool isValidEmai = Regex.IsMatch(model.Mail,
            @"・・・正規表現(省略)・・・",
            RegexOptions.IgnoreCase,
            TimeSpan.FromMilliseconds(250));

        if (!isValidEmai)
        {
          yield return new ValidationResult(
              "有効な Email 形式ではありません",
              new string[] { "Mail" });
        }
      }

      if (model.Age == null)
      {
        yield return new ValidationResult(
            "年齢は必須", new string[] { "Age" });
      }
      else if (model.Age < 0 || model.Age > 200)
      {
        yield return new ValidationResult(
            "年齢は 0 ~ 200 の範囲",
            new string[] { "Age" });
      }
      else if (!string.IsNullOrEmpty(model.Name) && 
               model.Name.StartsWith("佐藤") && 
               model.Age < 20)
      {
        yield return new ValidationResult(
            "佐藤さんは二十歳以上でなければなりません",
            new string[] { "" });
      }
    }
  }
}

.NET Framework MVC5 と ASP.NET Core 3.1 MVC でコードは同じものが使えますが、動作は若干異なります。主な違いは以下の通りです。

  1. Age プロパティを int 型にすると、MVC5 の場合 input 要素に data-val="true" data-val-number="フィールド 年齢 には数字を指定してください。 " data-val-required="年齢 フィールドが必要です。" という検証属性が付与される。Core には data-val-number 属性の設定はない。
  2. Age プロパティを int? 型 (null 許容) にすると、MVC5 の場合 input 要素に検証属性 data-val="true" data-val-number="フィールド 年齢 には数字を指定してください。" が付与される。Core の場合は検証属性は何も付与されない。
  3. さらに、type 属性には "number" と設定されるが、IE, Chrome, Firefox いずれも "1.0" というような整数としては不正な文字列を入力できる。 その場合、クライアント側の検証はパスしてしまう。 POST すると、MVC5 の場合は Validate メソッドに制御が飛ぶが、Core の場合はモデルバインドしようとする際に例外がスローされるようで Validate メソッドには制御が飛ばない。

    ただし、Validate メソッドに制御が飛んでも、Validate メソッドで設定したエラーメッセージはどこかで上書きされ "値 '1.0' は 年齢 に対して無効です。" となる。理由不明。

Tags: , ,

CORE

About this blog

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

Calendar

<<  October 2021  >>
MoTuWeThFrSaSu
27282930123
45678910
11121314151617
18192021222324
25262728293031
1234567

View posts in large calendar