Blazorにおけるフォームバリデーション手法のまとめ

# 概要
Blazorにおけるフォームバリデーションの手法に関して紹介します。
下記のようなログインフォームを例にして紹介します。

<img width="400" alt="ファイル名" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/c8027502-b493-72f4-3ca7-d199e6761a26.gif">

[本記事のデモ(メニューのFormを選択)](https://nobu17.github.io/MatBlazorSample)
[ソースコード](https://github.com/nobu17/MatBlazorSample)


## 前提
.NET Core SDK 3.1.100-preview3-014645
Microsoft.AspNetCore.Blazor 3.1.0-preview2.19528.8
Visual Studio 2019

WebAssembly版(Client版)を使用しています。

また、サンプルではUI要素としてMatBlazorを使用しています。
詳細は下記を参照してください。
https://qiita.com/nobu17/items/ecf2121f7bbb6bc5294b

MatBlazorを使わない場合、一般的なForm要素に置き換えてください。
MatTextField　→　InputTextもしくはinput
MatButton 　→　button (type="submit")

# 基本編
一番多く使う基本的なパターンの実装と説明を行います。

## 入力モデルの作成
まずは、入力画面にバインドするクラスを定義します。
その上で、各項目に対するバリデーション定義を追加します。
バリデーションを属性で表現するのは、ASP.NET MVC等でもおなじみな方法なので.NET開発者であれば見慣れたものかと思います。

```csharp:LoginData.cs

    public class LoginData
    {
        [Required(ErrorMessage = "ユーザIDを入力してください。")]
        [StringLength(16, ErrorMessage = "ユーザIDが長すぎます。")]
        public string UserID { get; set; }

        [Required(ErrorMessage = "パスワードを入力してください。")]
        [StringLength(32, ErrorMessage = "パスワードが長すぎます。")]
        public string Password { get; set; }
    }

```

## コードビハインドの作成
次にViewにバインドする、先ほどのLoginDataをメンバとして保持するクラスを作成します。
今回はコードビハインドでrazorコンポーネントとC#コードを分離して記載します。
コードビハインドに関しての詳細は、下記を参照してください。
https://qiita.com/nobu17/items/b7dc78db7beb1d833dc8

```csharp:Form1ViewModel.cs

    public class Form1ViewModel : ComponentBase
    {
        public LoginData LoginData { get; set; } = new LoginData();

        public void Submit()
        {
            // do something
        }
    }
```

## ビューの定義

コードビハインドとバインドする画面を作成します。

```html:Form1.razor
@inherits MatTest.Models.Form.Form1ViewModel
<EditForm Model="@LoginData" OnValidSubmit="@Submit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <MatTextField FullWidth="true" Label="UserID" @bind-Value="@LoginData.UserID"></MatTextField> 
    <ValidationMessage For="@(() => LoginData.UserID)" />

    <MatTextField FullWidth="true" Label="Password" @bind-Value="@LoginData.Password" Type="password"></MatTextField>
    <ValidationMessage For="@(() => LoginData.Password)" />

    <MatButton Label="Login" Outlined="true" Type="submit">
</EditForm>

```

### EditForm 
フォームで入力する要素をこのタグで囲みます。

- Modelにフォームに入力するプロパティをバインドします。
- OnValidSubmitに入力が正常な場合の確定処理のメソッドをバインドします。

また、不正な入力で確定ボタン押下を検知したい場合には、OnInvalidSubmitイベントをバインドすることで検知できます。

### DataAnnotationsValidator
先ほど入力データクラスに付与した属性(Requiredなど)のバリデーションを実施する場合に記載します。

### ValidationSummary
バリデーションで発生したエラー内容を表示します。

![vali_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/41fecb85-561f-64f2-f670-3503e66170c5.png)


### ValidationMessage
ValidationSummaryはすべてのエラー内容が表示されるため、
各入力項目に対して個別のバリデーションを表示したい場合に使用します。
For内にラムダでプロパティを指定します。

![vali_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/77dcf8d2-b5d0-ecb3-00a5-f24133295a91.png)

#### 2019/12/22 追記
### OnSubmit
検証結果によってOnValidSubmitとOnInvalidSubmitのいずれかが実行されますが、OnSubmitを使うことで、Submit時に常に実行される処理が登録できます。
引数に渡されたEditContextを使用してバリデーションを実施したり、独自のバリデーション処理が実装できます。

```html:Form1.razor
@inherits MatTest.Models.Form.Form1ViewModel
<EditForm Model="@LoginData" OnSubmit="@Submit">
 //略
</EditForm>
```
```csharp:Form1ViewModel.cs

    public class Form1ViewModel : ComponentBase
    {
        public void Submit(EditContext editContext)
        {
            // 検証を実施して結果を取得
            bool isValid = editContext.Validate();
        }
    }

```


## カスタムの検証属性
独自のバリデーション属性を作成したい場合は、ValidationAttributeクラスを継承します。
これはBlazor固有というよりも.NETでは一般的に使われている手法です。

```csharp:CustomeValidationAttribute.cs
    public class CustomeValidationAttribute : ValidationAttribute
    {
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            var str = value as string;
            if (str != null && string.IsNullOrWhiteSpace(str))
            {
                return new ValidationResult("空白は無効です。", new[] { validationContext.MemberName });
            }
            return ValidationResult.Success;
        }
    }

```

IsValidをオーバーライドして対象のオブジェクトを検証します。

- 正常の場合はValidationResult.Success戻す
- 入力エラーの場合は、ValidationResult内にエラーメッセージとvalidationContext.MemberNameを入れて戻す

入力エラー時には下記のようにエラーメッセージが表示されます。

![cus.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149198/2d80598a-36aa-87a3-d8b3-fb4e12cfda75.png)



# 応用編
基本をベースに、色々な場合を紹介します。

## ネストしたクラスに対するバリデーション
DataAnnotationsValidatorはネストしたオブジェクトに対しては、機能しません。
現在はまだプレビュー版となりますが、下記の手順で可能となります。

### モジュール追加
Nugetから下記モジュールを追加します。

[Microsoft.AspNetCore.Blazor.DataAnnotations.Validation](https://www.nuget.org/packages/Microsoft.AspNetCore.Blazor.DataAnnotations.Validation)

### 属性追加
ネストしたクラスのプロパティに対して、ValidateComplexType属性を付与します。

```csharp:NestedData.cs
    public class NestedData
    {
        [Required]
        [ValidateComplexType]
        public LoginData LoginData { get; set; } = new LoginData();
    }
```

### バリデータの変更
DataAnnotationsValidatorの代わりにObjectGraphDataAnnotationsValidatorを使用します。

```html:Form2.razor
@inherits MatTest.Models.Form.Form2ViewModel
<EditForm Model="@NestedData" OnValidSubmit="@Submit">
    <ObjectGraphDataAnnotationsValidator />
    <ValidationSummary />

    <MatTextField FullWidth="true" Label="UserID" @bind-Value="@NestedData.LoginData.UserID"></MatTextField> 
    <MatTextField FullWidth="true" Label="Password" @bind-Value="@NestedData.LoginData.Password" Type="password"></MatTextField>
    <MatButton Label="Login" Outlined="true" Type="submit">
</EditForm>

```

## OSSモジュール(FluentValidation)を使ったカスタムバリデーション
OSSで提供されいてる機能で属性検証以外のバリデーションを作成できます。
FluentValidationといった.NET向けのバリデーションライブラリをBlazor対応させる方法が紹介されているのでそちらを参考に実装します。

https://chrissainty.com/using-fluentvalidation-for-forms-validation-in-razor-components/

### パッケージの導入
NugetからFluentValidationをインストールします。

### バリデータの作成

FluentValidationの作法に沿ったバリデーションクラスを作成します。

```csharp:LoginDataValidator.cs

    public class LoginDataValidator : AbstractValidator<LoginData>
    {
        public LoginDataValidator()
        {
            RuleFor(p => p.UserID).NotEmpty().WithMessage("ログインIDを入力してください。");
            RuleFor(p => p.UserID).MaximumLength(10).WithMessage("ログインIDは10文字まで入力してください。");
            RuleFor(p => p.Password).NotEmpty().WithMessage("パスワードを入力してください。");
            RuleFor(p => p.Password).MaximumLength(10).WithMessage("パスワードは10文字まで入力してください。");
        }
    }

```

AbstractValidator<T>を継承したクラスを作成します。
(ジェネリクスにはバリデーション対象のクラスを指定)
コンストラクタ内でRuleForラムダで各メンバーのバリデーションを実装します。

### コンポーネントの作成

作成したバリデータだけではBlazorではそのまま使えないため、Blazor側のバリデーションに対応させるためのコンポーネントを作成します。
BlazorにはバリデーションのためのEditContextといった仕組みが提供されており、その仕組み内でFluentValidationのバリデーションを行います。
EditContextの詳細に関しては割愛しますが、下記等が参考になります。
https://gunnarpeipman.com/blazor-form-validation/


[掲載元](https://chrissainty.com/using-fluentvalidation-for-forms-validation-in-razor-components/)(掲載時より、一部APIの仕様が変わっているのでその対応を行っています。AddRangeをAddに変更。[参考](https://github.com/aspnet/AspNetCore/issues/12549))

```csharp:FluentValidationValidator.cs
    public class FluentValidationValidator : ComponentBase
    {
        [CascadingParameter] EditContext CurrentEditContext { get; set; }

        protected override void OnInitialized()
        {
            if (CurrentEditContext == null)
            {
                throw new InvalidOperationException($"{nameof(FluentValidationValidator)} requires a cascading " +
                    $"parameter of type {nameof(EditContext)}. For example, you can use {nameof(FluentValidationValidator)} " +
                    $"inside an {nameof(EditForm)}.");
            }

            CurrentEditContext.AddFluentValidation();
        }
    }

    public static class EditContextFluentValidationExtensions
    {
        public static EditContext AddFluentValidation(this EditContext editContext)
        {
            if (editContext == null)
            {
                throw new ArgumentNullException(nameof(editContext));
            }

            var messages = new ValidationMessageStore(editContext);

            editContext.OnValidationRequested +=
                (sender, eventArgs) => ValidateModel((EditContext)sender, messages);

            editContext.OnFieldChanged +=
                (sender, eventArgs) => ValidateField(editContext, messages, eventArgs.FieldIdentifier);

            return editContext;
        }

        private static void ValidateModel(EditContext editContext, ValidationMessageStore messages)
        {
            var validator = GetValidatorForModel(editContext.Model);
            var validationResults = validator.Validate(editContext.Model);

            messages.Clear();
            foreach (var validationResult in validationResults.Errors)
            {
                messages.Add(editContext.Field(validationResult.PropertyName), validationResult.ErrorMessage);
            }

            editContext.NotifyValidationStateChanged();
        }

        private static void ValidateField(EditContext editContext, ValidationMessageStore messages, in FieldIdentifier fieldIdentifier)
        {
            var properties = new[] { fieldIdentifier.FieldName };
            var context = new ValidationContext(fieldIdentifier.Model, new PropertyChain(), new MemberNameValidatorSelector(properties));

            var validator = GetValidatorForModel(fieldIdentifier.Model);
            var validationResults = validator.Validate(context);

            messages.Clear(fieldIdentifier);
            // APIの仕様変更のため、Addに変更
            //messages.AddRange(fieldIdentifier, validationResults.Errors.Select(error => error.ErrorMessage));
            messages.Add(fieldIdentifier, validationResults.Errors.Select(error => error.ErrorMessage));

            editContext.NotifyValidationStateChanged();
        }

        private static IValidator GetValidatorForModel(object model)
        {
            var abstractValidatorType = typeof(AbstractValidator<>).MakeGenericType(model.GetType());
            var modelValidatorType = Assembly.GetExecutingAssembly().GetTypes().FirstOrDefault(t => t.IsSubclassOf(abstractValidatorType));
            var modelValidatorInstance = (IValidator)Activator.CreateInstance(modelValidatorType);

            return modelValidatorInstance;
        }
    }

```

### 実装

作成したバリデータコンポーネント(FluentValidationValidator)を配置します。

```html:Form3.razor

<EditForm Model="@LoginData" OnValidSubmit="@HandleValidSubmit" class="mat-layout-grid-cell mat-layout-grid-cell-span-12">
    <FluentValidationValidator />
    <ValidationSummary />
    <MatTextField FullWidth="true" Label="UserID" @bind-Value="@LoginData.UserID"></MatTextField>
    <MatTextField FullWidth="true" Label="Password" @bind-Value="@LoginData.Password" Type="password"></MatTextField>
    <MatButton Label="Login" Outlined="true" Type="submit"></MatButton>
</EditForm>
```

# まとめ
Blazorにおけるフォームバリデーション手法に関してまとめました。
バリデーション手法は従来の.NETの手法を踏襲しているため、親しみがある人も多いのではないでしょうか。

# Blazorのその他の投稿記事

何点かBlazorに関して記事を書いていますので、良ければ見てみてください。

- [Blazor向けのUIフレームワークのMatBlazorを使ってみる]
(https://qiita.com/nobu17/items/ecf2121f7bbb6bc5294b)
- [Blazorの初期読み込み画面(Loading)を変更する]
(https://qiita.com/nobu17/items/adce29d366c1fc1e86a9)
- [Blazorで未ログイン時にログインページにリダイレクトする]
(https://qiita.com/nobu17/items/d43b18b8d42e7d0b4535)
- [Blazorでコードビハインドでロジックとビューを分離して記述する]
(https://qiita.com/nobu17/items/b7dc78db7beb1d833dc8)
- [Blazor向けのUIフレームワークのRadzen.Blazorを使ってみる]
(https://qiita.com/nobu17/items/b6fb2e35f4943a1d325a)
- [Blazorで作成したウェブサイトをGitHub Pagesで公開する]
(https://qiita.com/nobu17/items/116a0d1c949885e21d70)
- [Blazorで作成したウェブサイトをFirebaseで公開する]
(https://qiita.com/nobu17/items/67c11cf622f2e4721635)


# 参考資料
https://docs.microsoft.com/ja-jp/aspnet/core/blazor/forms-validation?view=aspnetcore-3.0
https://gunnarpeipman.com/blazor-form-validation/
https://chrissainty.com/using-fluentvalidation-for-forms-validation-in-razor-components/
https://blazor-university.com/forms/writing-custom-validation/
http://blazorhelpwebsite.com/Blog/tabid/61/EntryId/4337/Blazor-Forms-and-Validation.aspx
https://remibou.github.io/Client-side-validation-with-Blazor-and-Data-Annotations/
https://dzone.com/articles/blazor-form-validation
https://itnext.io/blazor-forms-and-validation-418173350435