概要
Blazorにおけるフォームバリデーションの手法に関して紹介します。
下記のようなログインフォームを例にして紹介します。
前提
.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開発者であれば見慣れたものかと思います。
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
public class Form1ViewModel : ComponentBase
{
public LoginData LoginData { get; set; } = new LoginData();
public void Submit()
{
// do something
}
}
ビューの定義
コードビハインドとバインドする画面を作成します。
@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
バリデーションで発生したエラー内容を表示します。
ValidationMessage
ValidationSummaryはすべてのエラー内容が表示されるため、
各入力項目に対して個別のバリデーションを表示したい場合に使用します。
For内にラムダでプロパティを指定します。
2019/12/22 追記
OnSubmit
検証結果によってOnValidSubmitとOnInvalidSubmitのいずれかが実行されますが、OnSubmitを使うことで、Submit時に常に実行される処理が登録できます。
引数に渡されたEditContextを使用してバリデーションを実施したり、独自のバリデーション処理が実装できます。
@inherits MatTest.Models.Form.Form1ViewModel
<EditForm Model="@LoginData" OnSubmit="@Submit">
//略
</EditForm>
public class Form1ViewModel : ComponentBase
{
public void Submit(EditContext editContext)
{
// 検証を実施して結果を取得
bool isValid = editContext.Validate();
}
}
カスタムの検証属性
独自のバリデーション属性を作成したい場合は、ValidationAttributeクラスを継承します。
これはBlazor固有というよりも.NETでは一般的に使われている手法です。
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を入れて戻す
入力エラー時には下記のようにエラーメッセージが表示されます。
応用編
基本をベースに、色々な場合を紹介します。
ネストしたクラスに対するバリデーション
DataAnnotationsValidatorはネストしたオブジェクトに対しては、機能しません。
現在はまだプレビュー版となりますが、下記の手順で可能となります。
モジュール追加
Nugetから下記モジュールを追加します。
Microsoft.AspNetCore.Blazor.DataAnnotations.Validation
属性追加
ネストしたクラスのプロパティに対して、ValidateComplexType属性を付与します。
public class NestedData
{
[Required]
[ValidateComplexType]
public LoginData LoginData { get; set; } = new LoginData();
}
バリデータの変更
DataAnnotationsValidatorの代わりにObjectGraphDataAnnotationsValidatorを使用します。
@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の作法に沿ったバリデーションクラスを作成します。
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に変更。参考)
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)を配置します。
<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