WebSurfer's Home

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

カスタムモデルバインダ (CORE)

by WebSurfer 2020年2月15日 14:34

ASP.NET Core 3.1 MVC アプリケーションでカスタムモデルバインダを利用するコードを備忘録として書いておきます。(注:.NET Framework の MVC ではありません)

カスタムモデルバインダ (Core 3.1)

先の記事「カスタムモデルバインダ (MVC5)」で .NET Framework の MVC5 のカスタムモデルバインダのコードを書きましたが、それと同じ機能を ASP.NET Core 3.1 MVC で実装してみます。

モデルバインド機能だけでなく、先の MVC5 版の記事と同様に、ユーザー入力の検証とエラーメッセージの表示ができるようにしました。

カスタムモデルバインダが継承するインターフェイスは Microsoft.AspNetCore.Mvc.ModelBinding 名前空間に属する IModelBinder Interface となります。MVC5 用と名前は同じですが中身が異なることに注意してください。

実装するのは BindModelAsync(ModelBindingContext) という Task を返す非同期メソッドになります。

加えて、ヘルパーメソッドで使っている GetValue(key) メソッドが返す ValueProviderResult は MVC5 と Core で名前は同じながら別物で、Core 用は ValueProviderResult 構造体となります。ユーザーから POST されてきた値は FirstValue プロパティを使って文字列として取得します。

Model, カスタムモデルバインダ、Controller のサンプルコードを以下にアップしておきます。上の画像を表示したものです。View のコードはスキャフォールディング機能を使って自動生成できるので割愛します。

モデルとカスタムモデルバインダ

モデルのコードは MVC5 用と全く同じです。カスタムモデルバインダのコードは上に述べた点が異なりますが、他は MVC5 用と同じです。

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;

namespace MvcCoreApp.Models
{
  // モデル(MVC5 用と同じ)
  public class Person2
  {
    public int PersonId { set; get; }

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

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

    // int? 型にしないと未入力に対応できない
    [Display(Name = "年齢")]
    public int? Age { set; get; }
  }

  // カスタムモデルバインダ
  public class CustomModelBinder : IModelBinder
  {
    public Task BindModelAsync(ModelBindingContext context)
    {
      if (context == null)
      {
        throw new ArgumentNullException("context");
      }

      var model = new Person2();
      model.Name = PostedData(context, "Name");
      model.Mail = PostedData(context, "Mail");
      string age = PostedData(context, "Age");

      if (string.IsNullOrEmpty(age))
      {
        context.ModelState.AddModelError("Age", 
                                         "年齢は必須");
      }
      else
      {
        int intAge = 0;
        if (!int.TryParse(age, out intAge))
        {
          context.ModelState.AddModelError("Age", 
                                         "年齢は整数");
        }
        else
        {
          model.Age = intAge;
          if (intAge < 0 || intAge > 200)
          {
            context.ModelState.AddModelError("Age", 
                           "年齢は 0 ~ 200 の範囲");
          }
        }
      }

      if (string.IsNullOrEmpty(model.Name))
      {
        context.ModelState.AddModelError("Name", 
                                         "名前は必須");
      }
      else if (model.Name.Length < 2 || 
               model.Name.Length > 20)
      {
        context.ModelState.AddModelError("Name", 
                            "名前は 2 ~ 20 文字の範囲");
      }
      else if (model.Name.StartsWith("佐藤") && 
               model.Age < 20)
      {
        context.ModelState.AddModelError("", 
             "佐藤さんは二十歳以上でなければなりません");
      }

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

        if (!isValidEmai)
        {
          context.ModelState.AddModelError("Mail", 
                    "有効な Email 形式ではありません");
        }
      }

      context.Result = ModelBindingResult.Success(model);
      return Task.CompletedTask;
    }

    // ヘルパーメソッド
    // GetValue(key) メソッドが返す ValueProviderResult は
    // MVC5 と Core では別物。前者はクラスで後者は構造体。
    // 値を取得するには FirstValue プロパティを使う
    private static string PostedData(
        ModelBindingContext context, string key)
    {
      var result = context.ValueProvider.GetValue(key);
      context.ModelState.SetModelValue(key, result);
      return result.FirstValue;
    }
  }
}

Controller / Action Method

MVC5 の場合と同様に、モデルバインダをターゲットとなる型に関連付けるため、POST データを受けるアクションメソッドの引数に [ModelBinder(typeof(CustomModelBinder))] を付与します。

using System;
using Microsoft.AspNetCore.Mvc;
using MvcCoreApp.Models;

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

    [HttpPost]
    [ValidateAntiForgeryToken]
    public IActionResult Create4(
      [ModelBinder(typeof(CustomModelBinder))] Person2 model)
    {
      if (!ModelState.IsValid)
      {
        return View(model);
      }

      return RedirectToAction("Index", "Home");
    }
  }
}

Tags: , , ,

Validation

カスタムモデルバインダ (MVC5)

by WebSurfer 2020年2月14日 15:29

.NET Framework の ASP.NET MVC5 アプリケーションでカスタムモデルバインダを利用するコードを備忘録として書いておきます。(注:ASP.NET Core 3.1 MVC の記事は「カスタムモデルバインダ (Core 3.1)」を見てください。下のサンプルコードは ASP.NET Core MVC には使えません)

カスタムモデルバインダ

モデルバインド機能だけでなく、ユーザー入力の検証とエラーメッセージの表示ができるようにしてみました。

上の画像を表示する Model と Controller のサンプルコードを以下にアップしておきます。View のコードはスキャフォールディング機能を使って自動生成できるので省略します。

Model とカスタムモデルバインダ

using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
using System.Globalization;

namespace Mvc5App.Models
{
  // Model
  public class Person2
  {
    public int PersonId { set; get; }

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

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

    // Age は int? にしないと未入力ではカスタムモデルバイ
    // ンダでも動かない。既定のモデルバインダと同様に null
    // が渡されて例外がスローされるようで「年齢 フィールド
    // が必要です。 」というエラーメッセージが表示される
    [Display(Name = "年齢")]
    public int? Age { set; get; }
  }

  // カスタムモデルバインダー
  public class CustomModelBinder : IModelBinder
  {
    public object BindModel(ControllerContext contContext, 
                          ModelBindingContext bindContext)
    {
      if (bindContext == null)
      {
        throw new ArgumentNullException("引数が null");
      }

      var model = new Person2();
      model.Name = PostedData<string>(bindContext, "Name");
      model.Mail = PostedData<string>(bindContext, "Mail");
      // ここでは string 型で取得する
      string age = PostedData<string>(bindContext, "Age");

      if (string.IsNullOrEmpty(age))
      {
        bindContext.ModelState.AddModelError("Age", 
                                           "年齢は必須");
      }
      else
      {
        int intAge;
        if (!int.TryParse(age, out intAge))
        {
          bindContext.ModelState.AddModelError("Age",
                                            "年齢は整数");
        }
        else
        {
          model.Age = intAge;
          if (intAge < 0 || intAge > 200)
          {
            bindContext.ModelState.AddModelError("Age", 
                                "年齢は 0 ~ 200 の範囲");
          }
        }
      }

      if (string.IsNullOrEmpty(model.Name))
      {
        bindContext.ModelState.AddModelError("Name",
                                            "名前は必須");
      }
      else if (model.Name.Length < 2 || 
               model.Name.Length > 20)
      {
        bindContext.ModelState.AddModelError("Name",
                             "名前は 2 ~ 20 文字の範囲");
      }

      else if (model.Name.StartsWith("佐藤") &&
               model.Age < 20)
      {
        bindContext.ModelState.AddModelError("",
              "佐藤さんは二十歳以上でなければなりません");
      }

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

        if (!isValidEmai)
        {
          bindContext.ModelState.AddModelError("Mail", 
                        "有効な Email 形式ではありません");
        }
      }

      return model;
    }

    // ヘルパーメソッド
    // Core では ValueProviderResult.ConvertTo メソッドは使え
    // ませんので注意。
    private static T PostedData<T>(ModelBindingContext context,
                                   string key)
    {
      var result = context.ValueProvider.GetValue(key);
      context.ModelState.SetModelValue(key, result);
      return (T)result.ConvertTo(typeof(T));
    }
  }
}

Controller / Action Method

モデルバインダをターゲットとなる型に関連付けるため、POST データを受けるアクションメソッドの引数に [ModelBinder(typeof(CustomModelBinder))] を付与します。(これはローカルな関連付けで、Global.asax の Application_Start メソッドでグローバルに関連付けを行うこともできるそうです)

using System;
using System.Web.Mvc;
using Mvc5App.Models;

namespace Mvc5App.Controllers
{
  public class ValidationController : Controller
  {
    // カスタムモデルバインダーを使ったサンプル
    public ActionResult Create4()
    {
      return View();
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create4(
      [ModelBinder(typeof(CustomModelBinder))] Person2 model)
    {
      if (!ModelState.IsValid)
      {
        return View(model);
      }

      return RedirectToAction("Index", "Home");
    }
  }
}

Tags: , ,

Validation

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

About this blog

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

Calendar

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

View posts in large calendar