BlazorでSPAするぞ!(8) - Validation -正式版対応済

Blazor連載ブログの続きになります。

という事で、↓↓↓の続きです。

ryuichi111std.hatenablog.com

本記事では、Blazorでの入力Validation(入力検証)についてまとめたいと思います。
(本記事は、Blazor WebAssemblyを軸としています)

1. DataAnnotation(データ注釈)ベースでの入力検証

BlazorのValidation機能(入力検証機能)は、ASP.NETでのWeb開発者にはお馴染みの「データ注釈(Data Annotation)」方式で実装されています。
データ注釈による方式とは、[Required] や [StringLength(10)] のような属性設定をモデルクラスのプロパティに指定することで、検証設定を宣言的に定義する方法です。

とりあえず、ValidationExamAppというプロジェクトを作成して話を進めたいと思います。

dotnet new blazorwasm -o ValidationExamApp

1.1. モデルクラス定義

では早速、モデル定義を行います。

Modelsフォルダを作成し、Account.csファイルを追加します。
何らかのサービスアプリにおけるユーザアカウントを想定します。

# Models/Account.cs #

using System.ComponentModel.DataAnnotations;

namespace ValidationExamApp.Models
{
  public class Account
  {
    [Required(ErrorMessage = "アカウント名を入力してください。")]
    public string AccountName { get; set; }

    [Required(ErrorMessage = "年齢を入力してください。")]
    [Range(0, 200, ErrorMessage = "年齢は0~200で設定してください。")]
    public int Age { get; set; }

    [Required(ErrorMessage = "メールアドレスを入力してください。")]
    [RegularExpression(@"[\w!#$%&'*+/=?^_@{}\\|~-]+(\.[\w!#$%&'*+/=?^_{}\\|~-]+)*@([\w][\w-]*\.)+[\w][\w-]*",
     ErrorMessage = "メールアドレスが正しくありません。")]
    public string EMailAddress { get; set; }
  }
}

上記のように、POCOなC#クラス定義のプロパティに、Validation属性が付与されているモデルクラス実装になります。

【Require属性】

AccountName / Age / EMailAddress の各プロパティに「Required」属性を付与しました。
つまり、入力必須であることを表しています。
ErrorMessage値は、検証エラーが発生した場合のエラーメッセージです。
また、Required属性は「System.ComponentModel.DataAnnotations名前空間」の「RequiredAttributeクラス」です。クラス定義の先頭で using 定義を行っており、またxxAttributeクラスは [xx] として属性定義できることはC#の言語仕様になります。

【Range】

Ageプロパティに対してはRange属性を指定しています。
想像できる通り「値の妥当範囲」を指定しています。
0歳から200歳までが妥当な入力範囲としています。

【RegularExpression】

EMailAddressプロパティに対してRegularExpression属性を指定しています。
正規表現により「入力値がメールアドレスであること」をチェックしています。

1.2. EditForm

さて、検証内容が定義されたAccountモデルクラスが用意できました。
Blazorでは、このモデルクラスに対してValidationを有効にして入力項目として表示するコントロールを用意しています。
「EditFormコンポーネントMicrosoft.AspNetCore.Components.Forms名前空間)」がこれに該当します。
Pages/Index.razorにEditFormを定義したサンプルが以下です。

# Pages/Index.razor #

@page "/"
@using ValidationExamApp.Models

<h1>ユーザ登録フォーム</h1>

<EditForm Model="@EntryAccount">
  <DataAnnotationsValidator />
  <ValidationSummary />

  アカウント名:<br />
  <InputText id="accountName" @bind-Value="EntryAccount.AccountName" />
  <br />

  年齢:<br />
  <InputNumber id="age" @bind-Value="EntryAccount.Age" />
  <br />

  メールアドレス:<br />
  <InputText id="eMail" @bind-Value="EntryAccount.EMailAddress" />
  <br />

  <button type="submit">送信</button>
</EditForm>   

@code
{
  private Account EntryAccount {get; set; } = new Account();

}

【EditForm】

EditFormはComponentBaseを継承したコンポーネントです。
このコンポーネントは以下にValidation処理対象の入力項目を定義します。
(本質な実装概念は異なりますが)Webの<form>を思い浮かべれば良いでしょうか。

【DataAnnotationValidator】

DataAnnotationsValidatorクラスは、モデルクラスで定義した「データ注釈」を解釈して検証処理をサポートします。
ひとまず、最初はおまじない的に配置しておけばよいと思います。

【ValidationSummary】

ValidationSummaryは、検証結果(主に検証エラーメッセージ)をまとめて表示してくれるコンポーネントです。

【@bind-value

InputText や InputNumberのような各入力コンポーネントに対してモデルクラスのプロパティ値をデータバインディングしています。

【submitボタン】

type属性値が "submit" のボタンです。
このボタンがクリックされると、データValidationが行われ、「エラーメッセージの表示」および「EditFormのValidation関連イベントが発生」します。

ここまでの実装で、画面を表示して動作を確認することができます。
dotnet run を実行し、http://localhost:5000 をブラウザで表示した画面キャプチャが以下です。

f:id:daigo-knowlbo:20200526033940p:plain

f:id:daigo-knowlbo:20200526033956p:plain

1.3. サブミットイベント(OnSubmit / OnValidSubmit / OnInvalidSubmit)

Validation属性を付与したモデルクラスの用意・EditFormの定義、により入力検証が動作することまで確認できました。
実際の処理では、妥当性をクリアした入力に対して、何らかの処理を行う必要があります。
例えば、「Validationチェックにクリアした入力内容のアカウント情報をサーバに登録する」等ですね。
EditFormは、サブミットボタンがクリックされた際に、イベントを発行します。

OnSubmit

サブミットボタンがクリックされた際にイベントが発生します。
EditForm.OnSubmitイベントが発生しますが、これは「EventCallback」型のイベントになります。
以下のように実装することができます。
イベント引数 EditContext を利用し入力値の検証を行っています。

@page "/"
@using ValidationExamApp.Models

<h1>ユーザ登録フォーム</h1>

<EditForm Model="@EntryAccount" OnSubmit="@OnSubmit">
  <DataAnnotationsValidator />
  <ValidationSummary />

  アカウント名:<br />
  <InputText id="accountName" @bind-Value="EntryAccount.AccountName" />
  <br />

  年齢:<br />
  <InputNumber id="age" @bind-Value="EntryAccount.Age" />
  <br />

  メールアドレス:<br />
  <InputText id="eMail" @bind-Value="EntryAccount.EMailAddress" />
  <br />

  <button type="submit">送信</button>
</EditForm>   

@code
{
  private Account EntryAccount {get; set; } = new Account();

  private void OnSubmit(EditContext context)
  {
    Console.WriteLine($"OnSubmit() => {context.Validate().ToString()}");
  }
}

OnValidSubmit / OnInvalidSubmit

サブミットボタンがクリックされた際に、入力項目が妥当であれば「OnValidSubmitイベント」が発生します。
また、入力項目が不正であった場合「OnInvalidSubmitイベント」が発生します。
EditForm.OnValidSubmitイベント および EditForm.OnInvalidSubmitイベントは、共に「EventCallback」型のイベントになります。
注意点として「OnSubmitイベントとOnValidSubmit / OnInvalidSubmitイベントは併用不可」となっています。同時に定義すると実行時エラーが発生します。
以下のように実装することができます。

@page "/"
@using ValidationExamApp.Models

<h1>ユーザ登録フォーム</h1>

<EditForm Model="@EntryAccount"  OnValidSubmit="@OnValidSubmit" OnInvalidSubmit="@OnInvalidSubmit">
  <DataAnnotationsValidator />
  <ValidationSummary />

  アカウント名:<br />
  <InputText id="accountName" @bind-Value="EntryAccount.AccountName" />
  <br />

  年齢:<br />
  <InputNumber id="age" @bind-Value="EntryAccount.Age" />
  <br />

  メールアドレス:<br />
  <InputText id="eMail" @bind-Value="EntryAccount.EMailAddress" />
  <br />

  <button type="submit">送信</button>
</EditForm>   

@code
{
  private Account EntryAccount {get; set; } = new Account();

  private void OnValidSubmit(EditContext context) 
  {
    Console.WriteLine($"OnValidSubmit()");
  }

  private void OnInvalidSubmit(EditContext context) 
  {
    Console.WriteLine($"OnInvalidSubmit()");
  }
}

1.4. EditForm.Model と EditForm.EditContext

前述の例では、EditFormを利用する際に Modelプロパティ にValidation対象のモデルクラスインスタンスを設定していました。
もう1つのEditFormの利用方法として、「EditContextプロパティを設定する」というものがあります。
以下がその例です。

@page "/"
@using ValidationExamApp.Models

<h1>ユーザ登録フォーム</h1>

<EditForm EditContext="@EntryAccountEditContext" OnValidSubmit="@OnValidSubmit" OnInvalidSubmit="@OnInvalidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    アカウント名:<br />
    <InputText id="accountName" @bind-Value="EntryAccount.AccountName" />
    <br />

    年齢:<br />
    <InputNumber id="age" @bind-Value="EntryAccount.Age" />
    <br />

    メールアドレス:<br />
    <InputText id="eMail" @bind-Value="EntryAccount.EMailAddress" />
    <br />

    <button type="submit">送信</button>
</EditForm>   

@code
{
    private Account EntryAccount = new Account();
    private EditContext EntryAccountEditContext;

    protected override void OnInitialized()
    {
        this.EntryAccountEditContext = new EditContext(this.EntryAccount);
    }

    private void OnValidSubmit(EditContext context) 
    {
        Console.WriteLine($"OnValidSubmit()");
    }

    private void OnInvalidSubmit(EditContext context) 
    {
        Console.WriteLine($"OnInvalidSubmit()");
    }
}

EditForm定義において Modelプロパティ の設定を削除し、EditContextプロパティ の設定定義を追加しました。
EditContextプロパティにバインドするオブジェクト値は、コンポーネントコード(@code{})定義にて private 変数定義(EntryAccountEditContext)しています。
また、このEntryAccountEditContext値は、コンポーネントの初期化イベントである OnInitialized() においてnew初期化を行っています。

EditForm利用においては、「Modelプロパティ」もしくは「EditContextプロパティ」を必ず設定する必要があります。
これらの違いですが、内部的にはどちらのプロパティを設定するのも同意となります。
以下のEditForm自体の実装が、その解になります。

aspnetcore/EditForm.cs at c79002bf38f2a08f496c645ee04e0a9084601f31 · dotnet/aspnetcore · GitHub

public class EditForm : ComponentBase
{
  ...省略
  
  protected override void OnParametersSet()
  {
    if (_fixedEditContext == null || EditContext != null || Model != _fixedEditContext.Model)
    {
     _fixedEditContext = EditContext ?? new EditContext(Model);
    }
  }
}

EditForm.OnParametersSet()において、「EditContext != nullであれば その値 を」「EditContext == null であれば new EditContext(model) を」、EditFormの内部プロパティ_fixedEditContextに設定しています。

2. カスタム検証

入力必須の Required、値範囲の Range、正規表現チェックの RegularExpression等々、.NET標準のValidationクラスが用意されていますが、独自のValidation実装を追加することができます。
パスワードポリシーとして「8文字以上、#文字が入っていることをチェック」するカスタムValidationクラスを実装してみます。
以下がカスタムValidation 実装である、PasswordPolicyValidatorになります。

# CustomValidators/PasswordPolicyValidator.cs #

using System;
using System.ComponentModel.DataAnnotations;

namespace ValidationExamApp.CustomValidators
{
  public class PasswordPolicyValidator : ValidationAttribute
  {
    protected override ValidationResult IsValid(object value, 
      ValidationContext validationContext)
    {
      string password = value?.ToString();
      if( !String.IsNullOrEmpty(password)  && password.Length > 8 && password.Contains('#') ) 
      {
        return ValidationResult.Success;
      }

      return new ValidationResult("8文字以上で、# 文字を入れてください");
    }
  }
}
# Models/Account.cs #
...
namespace ValidationExamApp.Models
{
  public class Account
  {
    ...

    [PasswordPolicyValidator]
    public string Password { get; set; }
  }
}
# /Pages/Index.razor #

<EditForm EditContext="@EntryAccountEditContext" OnValidSubmit="@OnValidSubmit" OnInvalidSubmit="@OnInvalidSubmit">
  <DataAnnotationsValidator />
  <ValidationSummary />

  ...
  
  パスワード:<br />
  <InputText id="password" @bind-Value="EntryAccount.Password" />
  <br />

  <button type="submit">送信</button>
</EditForm>   

f:id:daigo-knowlbo:20200526034034p:plain

3. 入れ子(ネスト)モデルのValidationとObjectGraphDataAnnotationsValidator

Validation対象が入れ子構造になっている場合、少し工夫をしないとそのままではValidationが正しく動作しません。

このケースでは現行Blazorでは若干の問題があります。
1つの解決策としては「Microsoft.AspNetCore.Components.DataAnnotations.Validation」パッケージを利用します。
ただし、このパッケージは Experimental なパッケージであるため、プロダクション環境での利用には注意が必要です。

https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation

# パッケージの追加

dotnet add package Microsoft.AspNetCore.Components.DataAnnotations.Validation --version 3.2.0-rc1.20223.4

Microsoft.AspNetCore.Components.DataAnnotations.Validationを適用した入れ子(ネスト)構造のモデルとは、具体的には以下のようなモデルです。

using System.ComponentModel.DataAnnotations;
using ValidationExamApp.CustomValidators;

namespace ValidationExamApp.Models
{
  public class Account
  {
    [Required(ErrorMessage = "アカウント名を入力してください。")]
    public string AccountName { get; set; }

    [ValidateComplexType]
    public Address Address { get; set; } // ← 入れ子構造

    public Account() 
    {
      this.Address = new Address();
    }
  }

  public class Address 
  {
    [Required]
    [RegularExpression(@"^[0-9]{3}-[0-9]{4}$", ErrorMessage = "郵便番号の書式はnnn-nnnnです。")]
    public string PostalCode { get; set; }

    [Required]
    public string Address1 { get; set; }

    public string Address2 { get; set; }
  }
}

【Address】

前述のAccountモデルにAddressプロパティを追加しました。
Addressクラスは PostalCode / Address1 / Address2 の3つのプロパティを持つクラスです。
このようにAccountクラスのプロパティが、カスタムモデルクラスAddressになっており、そのプロパティに対してValidation属性が付与されているようなケースです。
ちなみに、Address.PostalCodeプロパティには正規化表現で「nnn-nnnn」という郵便番号書式が求められています。

【ValidateComplexType】

入れ子対象となるプロパティには [ValidateComplexType] 属性を付与します。

f:id:daigo-knowlbo:20200526034056p:plain

4. まとめ

BlazorのValidation機能は、ASP.NETによるWeb開発者に取り掛かりやすい、基本概念を共にするアーキテクチャで構成されていました。
比較的、感覚的に着手することができるのではないかと思います。
と同時に、もうひとクオリティの改善が行われると、安心してプロダクション環境で取り入れられるような部分もあるかと思います。
総括としては、基本部分の実装は備えているので、サポート機能範囲を把握したうえで十分にプロダクション環境でも使えるのではないかと思いました。