ASP.NET Core 3.1 MVC アプリケーションでカスタムモデルバインダを利用するコードを備忘録として書いておきます。(注:.NET Framework の MVC ではありません)
先の記事「カスタムモデルバインダ (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");
}
}
}