LoginSignup
34
25

More than 1 year has passed since last update.

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

Last updated at Posted at 2019-12-04

概要

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

ファイル名

本記事のデモ(メニューのFormを選択)
ソースコード

前提

.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開発者であれば見慣れたものかと思います。

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

Form1ViewModel.cs

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

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

ビューの定義

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

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

ValidationMessage

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

vali_2.png

2019/12/22 追記

OnSubmit

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

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

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

カスタムの検証属性

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

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

応用編

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

ネストしたクラスに対するバリデーション

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

モジュール追加

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

Microsoft.AspNetCore.Blazor.DataAnnotations.Validation

属性追加

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

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

バリデータの変更

DataAnnotationsValidatorの代わりにObjectGraphDataAnnotationsValidatorを使用します。

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対応させる方法が紹介されているのでそちらを参考に実装します。

パッケージの導入

NugetからFluentValidationをインストールします。

バリデータの作成

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

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

コンポーネントの作成

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

掲載元(掲載時より、一部APIの仕様が変わっているのでその対応を行っています。AddRangeをAddに変更。参考)

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)を配置します。

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に関して記事を書いていますので、良ければ見てみてください。

参考資料

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

34
25
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
34
25