WebSurfer's Home

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

ASP.NET Core MVC 検証属性の自作

by WebSurfer 2020年2月3日 14:52

@IT の連載に「自作の検証属性を定義する(クライアントサイド編)」という .NET Framework MVC アプリにクライアント側で JavaScript による検証を含めて検証属性を自作する記事があります。ASP.NET Core 3.1 MVC ではそれと同等の検証属性をどのように実装するかという話です。

Custom Validator

(注:プロパティにまたがる検証を行うための CustomValidationAttribute の話ではなく、一つのフィールドの内容を独自の条件で検証するためのものです)

.NET Framework MVC との違いは IClientValidatable の代わりに IClientModelValidator インターフェイスを継承し AddValidation メソッドを実装するところです。

Model、検証属性の定義、クライアント側での検証用 JavaScript のコードを以下にアップしておきます。注意点はそれらに書きましたので 見てください。

Model

Model は .NET Framework MVC のものと全く同じです。自作の検証属性 InArrayAttribute は Publish プロパティに付与されています。

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

namespace MvcCoreApp.Models
{
  public class Book
  {
    [Key]
    [Display(Name = "ISBNコード")]
    [Required(ErrorMessage = "{0}は必須です。")]
    [RegularExpression(
      "[0-9]{3}-[0-9]{1}-[0-9]{3,5}-[0-9]{3,5}-[0-9A-Z]{1}",
      ErrorMessage = "{0}はISBNの形式で入力してください。")]
    public string Isbn { get; set; }

    [Display(Name = "書名")]
    [Required(ErrorMessage = "{0}は必須です。")]
    [StringLength(100, 
      ErrorMessage = "{0}は{1}文字以内で入力してください。")]
    public string Title { get; set; }

    [Display(Name = "価格")]
    [Range(100, 10000, 
      ErrorMessage = "{0}は{1}~{2}の間で入力してください。")]
    public int? Price { get; set; }

    [Display(Name = "出版社")]
    [StringLength(30, 
      ErrorMessage = "{0}は{1}文字以内で入力してください。")]
    [InArray("翔泳社,技術評論社,秀和システム," + 
      "毎日コミュニケーションズ,日経BP社,インプレスジャパン")]
    public string Publish { get; set; }

    [Display(Name = "刊行日")]
    [Required(ErrorMessage = "{0}は必須です。")]
    public DateTime Published { get; set; }
  }
}

自作の検証属性

出版社のリストとの比較検証を行う InArrayAttribute を定義します。IClientModelValidator を継承しているところと、AddValidation メソッドの実装に注目してください。(ちなみに .NET Framework MVC では IClientValidatable インターフェイスの GetClientValidationRules メソッドを使います)

using System;
using System.Collections.Generic;
using System.Linq;
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;

namespace MvcCoreApp.Models
{
  [AttributeUsage(AttributeTargets.Property, 
                  AllowMultiple = false)]
  public class InArrayAttribute : 
               ValidationAttribute, IClientModelValidator
  {
    // 値リストを表すプライベート変数
    private string _opts;

    // コンストラクタ(値リストとエラーメッセージを設定)
    public InArrayAttribute(string opts)
    {
      this._opts = opts;
      this.ErrorMessage = 
            "{0} は「{1}」のいずれかで指定してください。";
    }

    // プロパティの表示名と値リストでエラーメッセージ作成
    public override string FormatErrorMessage(string name)
    {
      return String.Format(CultureInfo.CurrentCulture, 
                           ErrorMessageString, name, _opts);
    }

    // サーバー側での検証
    // 値リストに入力値が含まれているかをチェック
    public override bool IsValid(object value)
    {
      // 入力値が空の場合は検証をスキップ 
      if (value == null) { return true; }

      // カンマ区切りテキストを分解し、入力値valueと比較 
      if (Array.IndexOf(_opts.Split(','), value) == -1)
      {
        return false;
      }
      return true;
    }

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

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

検証用 JavaScript(View に組み込み)

View にインラインで書いていますがコード自体は @IT の記事のものと同じです。検証用 JavaScript ライブラリに自作の検証スクリプトをどのように追加し、連携を取って動くようにするかという点がキモです。後者については、Unobtrusive Client Validation in ASP.NET MVC 3 という記事の中の Single value validators というセクションに記述がありました。正直読んでもよく分からなかったですが。(汗)

@model MvcCoreApp.Models.Book

・・・中略・・・

<div class="form-group">
  <label asp-for="Publish" class="control-label"></label>
  <input asp-for="Publish" class="form-control" />
  <span asp-validation-for="Publish" class="text-danger">
  </span>
</div>

・・・中略・・・

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

  <script type="text/javascript">
    $.validator.addMethod("inarray",
      function (value, element, parameters) {
        // 入力値が空の場合は検証をスキップ
        value = $.trim(value);
        if (value === '') {
          return true;
        }

        // カンマ区切りテキストを分解し、入力値valueと比較
        if ($.inArray(value, parameters.split(',')) === -1) {
          return false;
        }
        return true;
      });

    $.validator.unobtrusive.adapters.
                addSingleVal('inarray', 'opts');
  </script>
}

Tags: , , ,

Validation

コンマ区切りスクリプトと検証の整合

by WebSurfer 2019年11月28日 15:41

数字を 3 桁でコンマ区切りする JavaScript と ASP.NET MVC5 のクライアント側での検証の話です。

コンマ区切りスクリプトと検証の整合

元の話は Teratail のスレッド「3桁コンマ区切り数字をコンマ無しでFrom送信したい」です。

��ンマ区切り用 JavaScript のコードの動作は、初期画面では数字を 3 桁でコンマ区切りし、ユーザーが編集するときはコンマを除去し、編集完了後は再び 3 桁でコンマ区切りするというものです。コードは Teratail のスレッドの私 SurferOnWww の回答にありますので見てください。

そのコンマ区切り用 JavaScript の動作と、以下のようにモデルにアノテーション属性を付与するとデフォルトで有効になる控えめな JavaScript による検証が、かなり微妙ながら基本的なところでは整合を取って動きましたので、備忘録として書いておくことにしました。(実は、バッティングして動かないと思い込んでました(汗))

public class PurchaseRecord
{
  // ・・・中略・・・

  [Display(Name = "価格")]
  [Required(ErrorMessage = "{0} は必須")]
  [RegularExpression(@"^\d{1,6}$", 
    ErrorMessage = "数字 1 ~ 6 文字")]
  [Range(100, 10000, 
    ErrorMessage = "{0}は{1}~{2}の間で入力してください。")]
  [DisplayFormat(DataFormatString = "{0:N0}", 
      ApplyFormatInEditMode = true)]
  public decimal Price { get; set; }
}

どのような動きになるかと言うと以下の通りです:

  1. テキストボックスの初期表示は 1,234
  2. ユーザーが編集動作に入る時 focus イベントが発生しスクリプトで 1234 に書き換わる
  3. ユーザーが例えば 3210 というように編集
  4. 次の作業に移るためフォーカスを外す
  5. change イベントが発生し 3210 に対し検証がかかる
  6. blur イベントが発生しスクリプトで 3,210 に書き換える
  7. ユーザーが送信ボタンをクリック
  8. submit イベントが発生しスクリプトで 3,210 を 3210 に書き換える
  9. サーバーで 3210 を受信、サーバー側での検証は OK となる。

・・・という順序になってうまくいきます。

以上、基本的な動きは OK ではあるものの、かなり微妙なところで動いていますので、実際に運用に使う場合は十分な検証が必要だと思います。思いつくのは:

(1) change ⇒ blur の順序でイベントが発生しなければならないが、全てのブラウザでそうかは不明。(メジャーなブラウザは大丈夫のようですが、昔の Forefox は反対だったという話があります)

(2) 上のステップ 3 でユーザーが数字だけ入力してくれると期待するのは無理がある。(上のサンプルでは RegularExpression 属性を追加してチェックするようにしてますが、それで十分か?)

(3) ユーザーがブラウザの JavaScript を無効にした場合はサーバー側だけで検証することになる。

・・・などです。

特に、不特定多数のユーザーが不特定多種のブラウザでアクセスしてくるインターネットに公開するような場合は別の方法(サーバー側だけで検証するとか、String 型にするとか)を考えた方が良いかもしれません。

今のところ気が付いた問題点は以下の通りです:

問題 1: ステップ 3 でユーザーが 3,210 とカンマを入れて入力すると、ステップ 5 の時点ではカンマ入りなので正規表現での検証で引っかかるという問題があります。

問題 2: コンマ区切り用 JavaScript のコードには全角 ⇒ 半角変換の機能が実装されていますが、タイミングの問題で検証に引っかかってしまいます。どういうことかと言うと、全角数字を入力するとステップ 6 の時点で半角に変換しますが、検証がかかるステップ 5 の時点ではまだ全角なので正規表現による検証で NG となります。

その後、ステップ 6 の時点で半角に変換されるので、見かけは正しく半角なのにエラーが出て混乱を招くと思います。なので、全角 ⇒ 半角変換のコードは削除した方がよさそうです。

最後にオマケを二つ書いておきます。

その 1: 上のステップ 2 で編集操作に入った時、キャレットが末尾にあるのが自然と思いますが、そうしたい場合は以下のように 2 行追加してください。

elm.addEventListener('focus',
  function () {
    this.value = delFigure(this.value);

    // キャレットを文字列の末尾に持ってくる
    // ため以下の 2 行を追加
    var len = this.value.length;
    this.setSelectionRange(len, len);
  }, false);

その 2: クライアント側での検証を無効にすると「価格」として有効でない文字列、例えば 123x とかでもサーバーに送信されてしまいます。その場合、モデルバインディングできないのでアノテーション属性に設定した検証がかかる以前にエラーとなります。

そのエラーメッセージが気に入らないので自分で設定したいという場合は Controller にコードを追加して書き換えることができます。詳しくは別の記事「int 型プロパティの検証、エラーメッセージ」を見てください。

Tags: , , ,

Validation

int 型プロパティの検証、エラーメッセージ (MVC5)

by WebSurfer 2019年3月24日 16:06

ASP.NET MVC で、モデルのプロパティが int 型の場合の検証とエラーメッセージに関する注意点を書きます。

NULL 許容参照型がデフォルトで有効になっている ASP.NET Core 6.0+ の場合については別の記事「整数型プロパティの検証、エラーメッセージ (CORE)」を見てください。

検証��エラーメッセージ

モデルのプロパティが int 型の場合、クライアント側の検証を有効にしておけば、RequiredAttribute, RegularExpressionAttribute は付与しなくても input 要素には data-val-required="xxx フィールドが必要です。", data-val-number="フィールド xxx には数字を指定してください。" という属性が付与され、入力に応じてそれらのエラーメッセージが表示されます。

プロパティに RequiredAttribute が付与され ErrorMessage が設定されている場合は、data-val-required 属性に設定される文字列が ErrorMessage に置き換わります。(ちなみに、プロパティが int? 型の場合は data-val-required 属性そのものが付与されません)

上記は TextBoxFor, EditorFor いずれを使っても同じです。

ただし、EditorFor を使うと input 要素の type 属性が "number" となるので、それによりブラウザ依存の動きが出るのに要注意です。(TextBoxFor を使った場合は type 属性は "text" となります)

input 要素の type 属性が "number" となると、例えば、Chrome は数字以外の入力は受け付けなくなりますが、IE11 は最初の文字が数字であれば後に続く文字は何でも入力できてしまうという違いが出ます。

さらに、プロパティに RegularExpressionAttribute を追加して数字か否かをチェックするようにしても、input 要素の type 属性が "number" となっていると無視されます。

その場合動きはブラウザ依存になり、"1x" というような入力を受け付ける IE11 では data-val-number 属性に設定されたメッセージが、Firefox では data-val-required 属性に設定されたメッセージが表示されます。Chrome は "1x" というような文字は入力できませんが、"1..." という文字列は受け付けるので、その場合は Firefox と同様に data-val-required 属性に設定されたメッセージが表示されます。

EditorFor ではなく TextBoxFor を使えば input 要素の type 属性は "text" となって、RegularExpressionAttribute による検証が行われ、検証 NG の場合は ErrorMessage に設定したメッセージが表示されます。

input 要素の type 属性が "number" となることによりブラウザ依存の動きとなって期待と異なるエラーメッセージが出るのを避けるためには以下の対応が必要です:

  1. TextBoxFor を使って input 要素の type 属性が "text" となるようにし、さらに
  2. RegularExpressionAttribute で数字か否かの検証を行う。  

以上はクライアント側での検証の話です。サーバー側での検証によるエラーメッセージは上記とは異なります。上の画像の「価格2 (int)」のエラーメッセージを見てください。

クライアント側での検証を無効にして "2000x" という文字列を送信していますが「値 '2000x' は 価格2 (int) に対して無効です。」というエラーメッセージが出ています。

それは EditorFor (type="number") でも TextBoxFor (type="text") でも同じで、数字として不正な文字が混ざって POST されると、モデルバインディングの際 int 型にパースできないということで、RegularExpressionAttribute による検証が行われる前に検証 NG となって、そのエラーメッセージが出るようです。

RegularExpressionAttribute の ErrorMessage に設定したメッセージが表示されて欲しいのですが、int 型にパースできない文字列が POST されては何ともならないようです。ただし、このエラーメッセージを書き換える方法はあります。

マイクロソフト公式解説書「プログラミング ASP.NET MVC」の p186「エラーメッセージを制御する」に書いてあったことですが、ModelStateDictionary に含まれる ModelState は同じ Key でマージした方に上書きされます。具定例は以下のコードの通りです。上の画像の「ID (int)」がこのコードによる書き換え結果です。

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult WankumaEdit(Keiyaku model)
{
  if (ModelState.IsValid)
  {
    // DB の編集処理
    return RedirectToAction("Index");
  }

  // デバッグ用
  ModelStateDictionary dictionary = ModelState;

  // ValidationSummary(true) に表示するために追加
  var newDictionary = new ModelStateDictionary();
  newDictionary.AddModelError("",
    "ValidationSummary に表示するために追加。");
  ModelState.Merge(newDictionary);

  // エラーメッセージを書き換えることはできる。
  // 「プログラミング ASP.NET MVC」の p186「エラーメッセージ
  // を制御する」参照。同じ Key でマージした方に上書きされる
  ModelState state = dictionary["KeiyakuID"];

  if (state.Errors.Count > 0)
  {
    string msg = state.Errors[0].ErrorMessage;
    if (msg.StartsWith("値"))
    {
      // マージすると Value が null になるので書き戻すために
      // 取得しておく
      ValueProviderResult value = state.Value;
      var newDictionary2 = new ModelStateDictionary();
      newDictionary2.AddModelError("KeiyakuID",
        "入力不正(デフォルトの「値 'xx' は ID に対して" +
        "無効です。」を書き換え)");
      ModelState.Merge(newDictionary2);

      // Value を書き戻す。そうしないと再描画されたとき元の
      // ユーザー入力が表示されず 0 になってしまう
      ModelState["KeiyakuID"].Value = value;
    }
  }
  return View(model);
}

Tags: , ,

Validation

About this blog

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

Calendar

<<  2024年3月  >>
252627282912
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar