次の方法で共有


ASP.NET Core Blazor フォームの検証

Note

これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

警告

このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、「.NET および .NET Core サポート ポリシー」を参照してください。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

重要

この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。

現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

この記事では、Blazor フォームで検証を使う方法について説明します。

フォーム検証

基本的なフォーム検証シナリオでは、EditForm インスタンスは宣言された EditContextValidationMessageStore インスタンスを使用してフォーム フィールドを検証できます。 EditContextOnValidationRequested イベントのハンドラーにより、カスタム検証ロジックが実行されます。 ハンドラーの結果によって ValidationMessageStore インスタンスが更新されます。

基本的なフォーム検証は、フォームをホストするコンポーネント内でフォームのモデルが定義されている場合に便利です。これは、コンポーネントのメンバーとして直接、またはサブクラスで行います。 複数のコンポーネントで独立したモデル クラスを使用する場合は、検証コンポーネントの使用をお勧めします。

Blazor Web App において、クライアント側の検証にはアクティブな BlazorSignalR 回線が必要です。 静的サーバー側レンダリング (静的 SSR) を採用したコンポーネントのフォームでは、クライアント側の検証を利用できません。 静的 SSR を採用するフォームは、フォームの送信後にサーバー上で検証されます。

次のコンポーネントの場合、HandleValidationRequested ハンドラー メソッドでは、フォームを検証する前に ValidationMessageStore.Clear を呼び出すことによって、既存の検証メッセージをクリアします。

Starship8.razor:

@page "/starship-8"
@implements IDisposable
@inject ILogger<Starship8> Logger

<h2>Holodeck Configuration</h2>

<EditForm EditContext="editContext" OnValidSubmit="Submit" FormName="Starship8">
    <div>
        <label>
            <InputCheckbox @bind-Value="Model!.Subsystem1" />
            Safety Subsystem
        </label>
    </div>
    <div>
        <label>
            <InputCheckbox @bind-Value="Model!.Subsystem2" />
            Emergency Shutdown Subsystem
        </label>
    </div>
    <div>
        <ValidationMessage For="() => Model!.Options" />
    </div>
    <div>
        <button type="submit">Update</button>
    </div>
</EditForm>

@code {
    private EditContext? editContext;

    [SupplyParameterFromForm]
    private Holodeck? Model { get; set; }

    private ValidationMessageStore? messageStore;

    protected override void OnInitialized()
    {
        Model ??= new();
        editContext = new(Model);
        editContext.OnValidationRequested += HandleValidationRequested;
        messageStore = new(editContext);
    }

    private void HandleValidationRequested(object? sender,
        ValidationRequestedEventArgs args)
    {
        messageStore?.Clear();

        // Custom validation logic
        if (!Model!.Options)
        {
            messageStore?.Add(() => Model.Options, "Select at least one.");
        }
    }

    private void Submit() => Logger.LogInformation("Submit: Processing form");

    public class Holodeck
    {
        public bool Subsystem1 { get; set; }
        public bool Subsystem2 { get; set; }
        public bool Options => Subsystem1 || Subsystem2;
    }

    public void Dispose()
    {
        if (editContext is not null)
        {
            editContext.OnValidationRequested -= HandleValidationRequested;
        }
    }
}
@page "/starship-8"
@implements IDisposable
@inject ILogger<Starship8> Logger

<h2>Holodeck Configuration</h2>

<EditForm EditContext="editContext" OnValidSubmit="Submit" FormName="Starship8">
    <div>
        <label>
            <InputCheckbox @bind-Value="Model!.Subsystem1" />
            Safety Subsystem
        </label>
    </div>
    <div>
        <label>
            <InputCheckbox @bind-Value="Model!.Subsystem2" />
            Emergency Shutdown Subsystem
        </label>
    </div>
    <div>
        <ValidationMessage For="() => Model!.Options" />
    </div>
    <div>
        <button type="submit">Update</button>
    </div>
</EditForm>

@code {
    private EditContext? editContext;

    [SupplyParameterFromForm]
    private Holodeck? Model { get; set; }

    private ValidationMessageStore? messageStore;

    protected override void OnInitialized()
    {
        Model ??= new();
        editContext = new(Model);
        editContext.OnValidationRequested += HandleValidationRequested;
        messageStore = new(editContext);
    }

    private void HandleValidationRequested(object? sender,
        ValidationRequestedEventArgs args)
    {
        messageStore?.Clear();

        // Custom validation logic
        if (!Model!.Options)
        {
            messageStore?.Add(() => Model.Options, "Select at least one.");
        }
    }

    private void Submit() => Logger.LogInformation("Submit: Processing form");

    public class Holodeck
    {
        public bool Subsystem1 { get; set; }
        public bool Subsystem2 { get; set; }
        public bool Options => Subsystem1 || Subsystem2;
    }

    public void Dispose()
    {
        if (editContext is not null)
        {
            editContext.OnValidationRequested -= HandleValidationRequested;
        }
    }
}
@page "/starship-8"
@implements IDisposable
@inject ILogger<Starship8> Logger

<h2>Holodeck Configuration</h2>

<EditForm EditContext="editContext" OnValidSubmit="Submit">
    <div>
        <label>
            <InputCheckbox @bind-Value="Model!.Subsystem1" />
            Safety Subsystem
        </label>
    </div>
    <div>
        <label>
            <InputCheckbox @bind-Value="Model!.Subsystem2" />
            Emergency Shutdown Subsystem
        </label>
    </div>
    <div>
        <ValidationMessage For="() => Model!.Options" />
    </div>
    <div>
        <button type="submit">Update</button>
    </div>
</EditForm>

@code {
    private EditContext? editContext;

    public Holodeck? Model { get; set; }

    private ValidationMessageStore? messageStore;

    protected override void OnInitialized()
    {
        Model ??= new();
        editContext = new(Model);
        editContext.OnValidationRequested += HandleValidationRequested;
        messageStore = new(editContext);
    }

    private void HandleValidationRequested(object? sender,
        ValidationRequestedEventArgs args)
    {
        messageStore?.Clear();

        // Custom validation logic
        if (!Model!.Options)
        {
            messageStore?.Add(() => Model.Options, "Select at least one.");
        }
    }

    private void Submit()
    {
        Logger.LogInformation("Submit called: Processing the form");
    }

    public class Holodeck
    {
        public bool Subsystem1 { get; set; }
        public bool Subsystem2 { get; set; }
        public bool Options => Subsystem1 || Subsystem2;
    }

    public void Dispose()
    {
        if (editContext is not null)
        {
            editContext.OnValidationRequested -= HandleValidationRequested;
        }
    }
}

データ注釈検証コンポーネントとカスタム検証

DataAnnotationsValidator コンポーネントにより、データ注釈検証がカスケードされる EditContext にアタッチされます。 データ注釈の検証を有効にするには、DataAnnotationsValidator コンポーネントが必要です。 データ注釈と異なる検証システムを使用するには、DataAnnotationsValidator コンポーネントの代わりにカスタム実装を使用します。 参照ソースでの検査には、DataAnnotationsValidator のフレームワークの実装を使用できます。

注意

通常、.NET 参照ソースへのドキュメント リンクを使用すると、リポジトリの既定のブランチが読み込まれます。このブランチは、.NET の次回リリースに向けて行われている現在の開発を表します。 特定のリリースのタグを選択するには、[Switch branches or tags](ブランチまたはタグの切り替え) ドロップダウン リストを使います。 詳細については、「ASP.NET Core ソース コードのバージョン タグを選択する方法」 (dotnet/AspNetCore.Docs #26205) を参照してください。

Blazor は 2 種類の検証を実行します。

  • フィールド検証 は、ユーザーがタブでフィールドを離れたときに実行されます。 フィールドの検証時に、DataAnnotationsValidator コンポーネントによって、報告されたすべての検証結果がフィールドに関連付けられます。
  • モデル検証は、ユーザーがフォームを送信したときに実行されます。 モデルの検証時に、DataAnnotationsValidator コンポーネントは、検証結果で報告されたメンバー名に基づいてフィールドを判断しようとします。 個々のメンバーに関連付けられていない検証結果は、フィールドではなくモデルに関連付けられます。

検証コンポーネント

検証コンポーネントでは、フォームの EditContextValidationMessageStore を管理することで、フォームの検証をサポートします。

Blazor フレームワークでは、検証属性 (データ注釈)に基づいて検証サポートをフォームにアタッチする DataAnnotationsValidator コンポーネントを提供します。 カスタム検証コンポーネントを作成して、同じページの異なるフォームの検証メッセージを処理するか、フォーム処理の異なるステップ (たとえば、クライアント検証の後のサーバー検証) で同じフォームの検証メッセージを処理できます。 このセクションで示す検証コンポーネントの例 (CustomValidation) は、この記事の次のセクションで使用します。

データ注釈の組み込み検証コントロールのうち、Blazor では [Remote] 検証属性のみがサポートされていません。

Note

多くの場合、カスタムのデータ注釈検証属性をカスタム検証コンポーネントの代わりに使用できます。 フォームのモデルに適用されるカスタム属性は、DataAnnotationsValidator コンポーネントを使用してアクティブ化されます。 サーバー検証で使用する場合、モデルに適用されるカスタム属性はすべてサーバー上で実行可能である必要があります。 詳しくは、「カスタム検証属性」セクションをご覧ください。

ComponentBaseから検証コンポーネントを作成します。

  • フォームの EditContext は、コンポーネントのカスケード パラメーターです
  • 検証コンポーネントが初期化されると、フォーム エラーの現在の一覧を保持するために新しい ValidationMessageStore が作成されます。
  • フォームのコンポーネント内の開発者コードで DisplayErrors メソッドが呼び出されると、メッセージ ストアでエラーが受け取られます。 そのエラーは、Dictionary<string, List<string>>内の DisplayErrors メソッドに渡されます。 ディクショナリでは、キーは、1 つ以上のエラーがあるフォーム フィールドの名前です。 値は、エラー一覧です。
  • 次のいずれかが発生すると、メッセージはクリアされます。
    • EditContext で検証が要求され、OnValidationRequested イベントが発生した場合。 すべてのエラーがクリアされます。
    • フォームのフィールドが変更され、OnFieldChanged イベントが発生した場合。 そのフィールドのエラーのみがクリアされます。
    • 開発者コードによって ClearErrors メソッドが呼び出された場合。 すべてのエラーがクリアされます。

アプリの名前空間と一致するよう、次のクラスの名前空間を更新します。

CustomValidation.cs:

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;

namespace BlazorSample;

public class CustomValidation : ComponentBase
{
    private ValidationMessageStore? messageStore;

    [CascadingParameter]
    private EditContext? CurrentEditContext { get; set; }

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

        messageStore = new(CurrentEditContext);

        CurrentEditContext.OnValidationRequested += (s, e) => 
            messageStore?.Clear();
        CurrentEditContext.OnFieldChanged += (s, e) => 
            messageStore?.Clear(e.FieldIdentifier);
    }

    public void DisplayErrors(Dictionary<string, List<string>> errors)
    {
        if (CurrentEditContext is not null)
        {
            foreach (var err in errors)
            {
                messageStore?.Add(CurrentEditContext.Field(err.Key), err.Value);
            }

            CurrentEditContext.NotifyValidationStateChanged();
        }
    }

    public void ClearErrors()
    {
        messageStore?.Clear();
        CurrentEditContext?.NotifyValidationStateChanged();
    }
}

重要

ComponentBase から派生する場合は、名前空間を指定する必要があります。 名前空間を指定しないと、ビルド エラーが発生します。

Tag helpers cannot target tag name '<global namespace>.{CLASS NAME}' because it contains a ' ' character.

{CLASS NAME} プレースホルダーは、コンポーネント クラスの名前です。 このセクションのカスタム検証の例では、例の名前空間 BlazorSample を指定します。

Note

匿名のラムダ式は、OnValidationRequested と前の例の OnFieldChanged に対して登録されているイベント ハンドラーです。 このシナリオでは、IDisposable を実装したり、イベント デリゲートの登録を解除したりする必要はありません。 詳しくは、「ASP.NET Core Razor コンポーネントのライフサイクル」をご覧ください。

検証コンポーネントを使用したビジネス ロジック検証

一般的なビジネス ロジックの検証では、ディクショナリ内のフォーム エラーを受け取る検証コンポーネントを使用します。

基本検証は、フォームをホストするコンポーネント内でフォームのモデルが定義されている場合に便利です。これは、コンポーネントのメンバーとして直接、またはサブクラスで行います。 複数のコンポーネントで独立したモデル クラスを使用する場合は、検証コンポーネントの使用をお勧めします。

次に例を示します。

  • 入力コンポーネントに関する記事の「フォームの例」セクションの Starfleet Starship Database フォーム (Starship3 コンポーネント) の簡略版が使用されており、宇宙船の分類と説明のみを受け取ります。 DataAnnotationsValidator コンポーネントがフォームに含まれていないため、フォームの送信時にデータ注釈の検証はトリガーされません。
  • この記事の「検証コンポーネント」セクションのCustomValidation コンポーネントが使用されています。
  • 検証では、ユーザーが宇宙船の分類 (Classification) で Defense を選択した場合、宇宙船の説明 (Description) の値が要求されます。

検証メッセージをコンポーネントで設定すると、検証の ValidationMessageStore に追加され、EditForm の検証の概要に表示されます。

Starship9.razor:

@page "/starship-9"
@inject ILogger<Starship9> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship9">
    <CustomValidation @ref="customValidation" />
    <ValidationSummary />
    <div>
        <label>
            Primary Classification:
            <InputSelect @bind-Value="Model!.Classification">
                <option value="">
                    Select classification ...
                </option>
                <option checked="@(Model!.Classification == "Exploration")" 
                    value="Exploration">
                    Exploration
                </option>
                <option checked="@(Model!.Classification == "Diplomacy")" 
                    value="Diplomacy">
                    Diplomacy
                </option>
                <option checked="@(Model!.Classification == "Defense")" 
                    value="Defense">
                    Defense
                </option>
            </InputSelect>
        </label>
    </div>
    <div>
        <label>
            Description (optional):
            <InputTextArea @bind-Value="Model!.Description" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    private CustomValidation? customValidation;

    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() =>
        Model ??= new() { ProductionDate = DateTime.UtcNow };

    private void Submit()
    {
        customValidation?.ClearErrors();

        var errors = new Dictionary<string, List<string>>();

        if (Model!.Classification == "Defense" &&
                string.IsNullOrEmpty(Model.Description))
        {
            errors.Add(nameof(Model.Description),
                new() { "For a 'Defense' ship classification, " +
                "'Description' is required." });
        }

        if (errors.Any())
        {
            customValidation?.DisplayErrors(errors);
        }
        else
        {
            Logger.LogInformation("Submit called: Processing the form");
        }
    }
}
@page "/starship-9"
@inject ILogger<Starship9> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship9">
    <CustomValidation @ref="customValidation" />
    <ValidationSummary />
    <div>
        <label>
            Primary Classification:
            <InputSelect @bind-Value="Model!.Classification">
                <option value="">
                    Select classification ...
                </option>
                <option checked="@(Model!.Classification == "Exploration")" 
                    value="Exploration">
                    Exploration
                </option>
                <option checked="@(Model!.Classification == "Diplomacy")" 
                    value="Diplomacy">
                    Diplomacy
                </option>
                <option checked="@(Model!.Classification == "Defense")" 
                    value="Defense">
                    Defense
                </option>
            </InputSelect>
        </label>
    </div>
    <div>
        <label>
            Description (optional):
            <InputTextArea @bind-Value="Model!.Description" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    private CustomValidation? customValidation;

    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() =>
        Model ??= new() { ProductionDate = DateTime.UtcNow };

    private void Submit()
    {
        customValidation?.ClearErrors();

        var errors = new Dictionary<string, List<string>>();

        if (Model!.Classification == "Defense" &&
                string.IsNullOrEmpty(Model.Description))
        {
            errors.Add(nameof(Model.Description),
                new() { "For a 'Defense' ship classification, " +
                "'Description' is required." });
        }

        if (errors.Any())
        {
            customValidation?.DisplayErrors(errors);
        }
        else
        {
            Logger.LogInformation("Submit called: Processing the form");
        }
    }
}
@page "/starship-9"
@inject ILogger<Starship9> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="Model" OnValidSubmit="Submit">
    <CustomValidation @ref="customValidation" />
    <ValidationSummary />
    <div>
        <label>
            Primary Classification:
            <InputSelect @bind-Value="Model!.Classification">
                <option value="">Select classification ...</option>
                <option value="Exploration">Exploration</option>
                <option value="Diplomacy">Diplomacy</option>
                <option value="Defense">Defense</option>
            </InputSelect>
        </label>
    </div>
    <div>
        <label>
            Description (optional):
            <InputTextArea @bind-Value="Model!.Description" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    private CustomValidation? customValidation;

    public Starship? Model { get; set; }

    protected override void OnInitialized() =>
        Model ??= new() { ProductionDate = DateTime.UtcNow };

    private void Submit()
    {
        customValidation?.ClearErrors();

        var errors = new Dictionary<string, List<string>>();

        if (Model!.Classification == "Defense" &&
                string.IsNullOrEmpty(Model.Description))
        {
            errors.Add(nameof(Model.Description),
                new() { "For a 'Defense' ship classification, " +
                "'Description' is required." });
        }

        if (errors.Any())
        {
            customValidation?.DisplayErrors(errors);
        }
        else
        {
            Logger.LogInformation("Submit called: Processing the form");
        }
    }
}

注意

検証コンポーネントを使用する代わりに、データ注釈検証属性を使用することもできます。 フォームのモデルに適用されるカスタム属性は、DataAnnotationsValidator コンポーネントを使用してアクティブ化されます。 サーバー検証で使用する場合、属性はサーバー上で実行可能である必要があります。 詳しくは、「カスタム検証属性」セクションをご覧ください。

検証コンポーネントを使用したサーバー検証

このセクションでは、Blazor Web App シナリオに焦点を当てていますが、Web API でサーバー検証を使用するあらゆる種類のアプリのアプローチには、同じ一般的なアプローチが採用されています。

このセクションでは、ホストされている Blazor WebAssembly シナリオに焦点を当てていますが、Web API でサーバー検証を使用するあらゆる種類のアプリのアプローチには、同じ一般的なアプローチが採用されています。

クライアント検証に加えて、サーバー検証がサポートされています。

  • DataAnnotationsValidator コンポーネントを使用して、フォーム内のクライアント検証を処理します。
  • フォームによってクライアント検証が渡されると (OnValidSubmit が呼び出されると)、フォーム処理のために、EditContext.Model がバックエンド サーバー API に送信されます。
  • サーバーでモデルの検証を処理します。
  • サーバー API には、組み込みのフレームワーク データ注釈検証と開発者によって提供されるカスタムの検証ロジックの両方が含まれています。 サーバーで検証が成功すると、フォームが処理され、成功の状態コード (200 - OK) が返されます。 検証が失敗すると、失敗の状態コード (400 - Bad Request) とフィールド検証エラーが返されます。
  • 成功時にフォームを無効にするか、エラーを表示します。

基本検証は、フォームをホストするコンポーネント内でフォームのモデルが定義されている場合に便利です。これは、コンポーネントのメンバーとして直接、またはサブクラスで行います。 複数のコンポーネントで独立したモデル クラスを使用する場合は、検証コンポーネントの使用をお勧めします。

次の例は、以下のものに基づいています。

Starship モデル (Starship.cs) を共有クラス ライブラリ プロジェクトに配置して、クライアントとサーバーの両方のアプリでモデルを使用できるようにします。 共有アプリの名前空間と一致するように、名前空間を追加または更新します (たとえば、namespace BlazorSample.Shared)。 モデルにはデータ注釈が必要であるため、共有クラス ライブラリでは共有フレームワークを使用するか、System.ComponentModel.Annotations パッケージを共有プロジェクトに追加します。

Note

.NET アプリへのパッケージの追加に関するガイダンスについては、「パッケージ利用のワークフロー」 (NuGet ドキュメント) の "パッケージのインストールと管理" に関する記事を参照してください。 NuGet.org で正しいパッケージ バージョンを確認します。

Blazor Web App のメイン プロジェクトで、宇宙船の検証要求を処理し、失敗した検証のメッセージを返すコントローラーを追加します。 共有クラス ライブラリ プロジェクトの最後の using ステートメントの名前空間と、コントローラー クラスの namespace を更新します。 クライアントとサーバーのデータ注釈の検証に加えて、コントローラーでは、ユーザーが宇宙船の分類 (Classification) で Defense を選択した場合に、宇宙船の説明 (Description) に値が指定されているかどうかが検証されます。

Starship モデル (Starship.cs) をソリューションの Shared プロジェクトに配置して、クライアントとサーバーの両方のアプリでモデルを使用できるようにします。 共有アプリの名前空間と一致するように、名前空間を追加または更新します (たとえば、namespace BlazorSample.Shared)。 モデルにはデータ注釈が必要であるため、System.ComponentModel.Annotations パッケージを Shared プロジェクトに追加します。

Note

.NET アプリへのパッケージの追加に関するガイダンスについては、「パッケージ利用のワークフロー」 (NuGet ドキュメント) の "パッケージのインストールと管理" に関する記事を参照してください。 NuGet.org で正しいパッケージ バージョンを確認します。

Server プロジェクトで、宇宙船の検証要求を処理し、失敗した検証のメッセージを返すコントローラーを追加します。 Shared プロジェクトの最後の using ステートメントの名前空間と、コントローラー クラスの namespace を更新します。 クライアントとサーバーのデータ注釈の検証に加えて、コントローラーでは、ユーザーが宇宙船の分類 (Classification) で Defense を選択した場合に、宇宙船の説明 (Description) に値が指定されているかどうかが検証されます。

宇宙船の分類 Defense の検証は、コントローラーのサーバーでのみ行われます。これは、フォームがサーバーに送信されるときに、今後のフォームではクライアント側で同じ検証が実行されないためです。 クライアント検証を伴わないサーバー検証は、サーバーでのユーザー入力のプライベート ビジネス ロジックの検証を必要とするアプリで一般的です。 たとえば、ユーザー用に保存されたデータからの個人情報が、ユーザー入力の検証に必要になる場合があります。 クライアント検証のために、プライベート データをクライアントに送信することはできません。

Note

このセクションの StarshipValidation コントローラーでは、Microsoft Identity 2.0 が使用されます。 Web API は、この API で "API.Access" スコープを持つユーザーのトークンのみが受け入れられます。 API のスコープ名が API.Access と異なる場合は、追加のカスタマイズが必要です。

セキュリティの詳細については、以下を参照してください。

Controllers/StarshipValidation.cs:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using BlazorSample.Shared;

namespace BlazorSample.Server.Controllers;

[Authorize]
[ApiController]
[Route("[controller]")]
public class StarshipValidationController(
    ILogger<StarshipValidationController> logger) 
    : ControllerBase
{
    static readonly string[] scopeRequiredByApi = new[] { "API.Access" };

    [HttpPost]
    public async Task<IActionResult> Post(Starship model)
    {
        HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);

        try
        {
            if (model.Classification == "Defense" && 
                string.IsNullOrEmpty(model.Description))
            {
                ModelState.AddModelError(nameof(model.Description),
                    "For a 'Defense' ship " +
                    "classification, 'Description' is required.");
            }
            else
            {
                logger.LogInformation("Processing the form asynchronously");

                // async ...

                return Ok(ModelState);
            }
        }
        catch (Exception ex)
        {
            logger.LogError("Validation Error: {Message}", ex.Message);
        }

        return BadRequest(ModelState);
    }
}

アプリのコントローラーの名前と一致するように、上記のコントローラー (BlazorSample.Server.Controllers) の名前空間を確認または更新します。

サーバーでモデル バインド検証エラーが発生した場合、通常、ApiController (ApiControllerAttribute) により、既定の無効な要求応答ValidationProblemDetails が返されます。 Starfleet Starship Database フォームのすべてのフィールドが送信されず、フォームの検証が失敗した場合、次の例に示すように、応答には、検証エラー以外のデータも含まれます。

{
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Id": ["The Id field is required."],
    "Classification": ["The Classification field is required."],
    "IsValidatedDesign": ["This form disallows unapproved ships."],
    "MaximumAccommodation": ["Accommodation invalid (1-100000)."]
  }
}

Note

上記の JSON 応答を実際に確認するには、フォームのクライアント検証を無効にして空のフィールド フォームの送信を許可するか、Firefox Browser Developer などのツールを使ってサーバー API に要求を直接送信する必要があります。

サーバー API によって、前述の既定の JSON 応答が返された場合、クライアントでは、開発者コードで応答を解析して、フォーム検証エラーの処理のために errors ノードの子を取得することができます。 ファイルを解析するための開発者コードを記述するのは不便です。 JSON を手動で解析するには、ReadFromJsonAsync を呼び出した後でエラーの Dictionary<string, List<string>> を生成する必要があります。 次の例に示すように、サーバー API で検証エラーのみを返すのが理想的です。

{
  "Id": ["The Id field is required."],
  "Classification": ["The Classification field is required."],
  "IsValidatedDesign": ["This form disallows unapproved ships."],
  "MaximumAccommodation": ["Accommodation invalid (1-100000)."]
}

サーバー API の応答を変更して、検証エラーのみが返されるようにするには、Program ファイルの ApiControllerAttribute で注釈が付けられたアクションで呼び出されるデリゲートを変更します。 API エンドポイント (/StarshipValidation) の場合、BadRequestObjectResultModelStateDictionary が返されます。 他の API エンドポイントの場合、オブジェクトの結果と新しい ValidationProblemDetailsが返され、既定の動作が保持されます。

Blazor Web App のメイン プロジェクトで Program ファイルの先頭に Microsoft.AspNetCore.Mvc 名前空間を追加します。

using Microsoft.AspNetCore.Mvc;

Program ファイルで次の AddControllersWithViews 拡張メソッドを追加または更新して、次の ConfigureApiBehaviorOptions の呼び出しを追加します。

builder.Services.AddControllersWithViews()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
        {
            if (context.HttpContext.Request.Path == "/StarshipValidation")
            {
                return new BadRequestObjectResult(context.ModelState);
            }
            else
            {
                return new BadRequestObjectResult(
                    new ValidationProblemDetails(context.ModelState));
            }
        };
    });

Blazor Web App のメイン プロジェクトに初めてコントローラーを追加する場合は、コントローラーのサービスを追加する上記のコードを配置するときにコントローラー エンドポイントをマップします。 次の例では、既定のコントローラー ルートを使用します。

app.MapDefaultControllerRoute();

Note

前の例では、AddControllersWithViews を呼び出して、自動的にクロスサイト リクエスト フォージェリ (XSRF/CSRF) 攻撃を軽減することで、コントローラー サービスを明示的に登録します。 単に AddControllers を使用する場合、偽造防止は自動的には有効になりません。

コントローラー ルーティングと検証失敗エラーの応答の詳細については、次のリソースを参照してください。

.Client プロジェクトで、「検証コンポーネント」セクションに示されている CustomValidation コンポーネントを追加します。 アプリと一致するように名前空間を更新します (たとえば、namespace BlazorSample.Client)。

.Client プロジェクトで、Starfleet Starship Database フォームが、CustomValidation コンポーネントを使用してサーバー検証エラーを示すように更新されます。 サーバー API によって検証メッセージが返されると、それらは、CustomValidation コンポーネントの ValidationMessageStoreに追加されます。 エラーは、フォームの検証の概要によって表示するため、フォームの EditContext で使用できます。

次のコンポーネントで、共有プロジェクトの名前空間 (@using BlazorSample.Shared) を、共有プロジェクトの名前空間に更新します。 フォームには承認が必要なため、ユーザーは、フォームに移動するには、アプリにサインインする必要があることに注意してください。

Server アプリの Program ファイルの先頭に Microsoft.AspNetCore.Mvc 名前空間を追加します。

using Microsoft.AspNetCore.Mvc;

Program ファイルで AddControllersWithViews 拡張メソッドを見つけて、次の ConfigureApiBehaviorOptions の呼び出しを追加します。

builder.Services.AddControllersWithViews()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
        {
            if (context.HttpContext.Request.Path == "/StarshipValidation")
            {
                return new BadRequestObjectResult(context.ModelState);
            }
            else
            {
                return new BadRequestObjectResult(
                    new ValidationProblemDetails(context.ModelState));
            }
        };
    });

Note

前の例では、AddControllersWithViews を呼び出して、自動的にクロスサイト リクエスト フォージェリ (XSRF/CSRF) 攻撃を軽減することで、コントローラー サービスを明示的に登録します。 単に AddControllers を使用する場合、偽造防止は自動的には有効になりません。

Client プロジェクトで、「検証コンポーネント」セクションに示されている CustomValidation コンポーネントを追加します。 アプリと一致するように名前空間を更新します (たとえば、namespace BlazorSample.Client)。

Client プロジェクトで、Starfleet Starship Database フォームが、CustomValidation コンポーネントを使用してサーバー検証エラーを示すように更新されます。 サーバー API によって検証メッセージが返されると、それらは、CustomValidation コンポーネントの ValidationMessageStoreに追加されます。 エラーは、フォームの検証の概要によって表示するため、フォームの EditContext で使用できます。

次のコンポーネントで、@using BlazorSample.Shared プロジェクトの名前空間 (Shared) を共有プロジェクトの名前空間に更新します。 フォームには承認が必要なため、ユーザーは、フォームに移動するには、アプリにサインインする必要があることに注意してください。

Starship10.razor:

Note

既定では、EditForm に基づくフォームでは、偽造防止サポートが自動的に有効になります。 コントローラーでは、コントローラー サービスを登録し、Web API の偽造防止サポートを自動的に有効にするために、AddControllersWithViews を使用する必要があります。

@page "/starship-10"
@rendermode InteractiveWebAssembly
@using System.Net
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using BlazorSample.Shared
@attribute [Authorize]
@inject HttpClient Http
@inject ILogger<Starship10> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm FormName="Starship10" Model="Model" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <CustomValidation @ref="customValidation" />
    <ValidationSummary />
    <div>
        <label>
            Identifier: 
            <InputText @bind-Value="Model!.Id" disabled="@disabled" />
        </label>
    </div>
    <div>
        <label>
            Description (optional):
            <InputTextArea @bind-Value="Model!.Description" 
                disabled="@disabled" />
        </label>
    </div>
    <div>
        <label>
            Primary Classification:
            <InputSelect @bind-Value="Model!.Classification" disabled="@disabled">
                <option value="">Select classification ...</option>
                <option value="Exploration">Exploration</option>
                <option value="Diplomacy">Diplomacy</option>
                <option value="Defense">Defense</option>
            </InputSelect>
        </label>
    </div>
    <div>
        <label>
            Maximum Accommodation:
            <InputNumber @bind-Value="Model!.MaximumAccommodation" 
                disabled="@disabled" />
        </label>
    </div>
    <div>
        <label>
            Engineering Approval:
            <InputCheckbox @bind-Value="Model!.IsValidatedDesign" 
                disabled="@disabled" />
        </label>
    </div>
    <div>
        <label>
            Production Date:
            <InputDate @bind-Value="Model!.ProductionDate" disabled="@disabled" />
        </label>
    </div>
    <div>
        <button type="submit" disabled="@disabled">Submit</button>
    </div>
    <div style="@messageStyles">
        @message
    </div>
</EditForm>

@code {
    private CustomValidation? customValidation;
    private bool disabled;
    private string? message;
    private string messageStyles = "visibility:hidden";

    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() => 
        Model ??= new() { ProductionDate = DateTime.UtcNow };

    private async Task Submit(EditContext editContext)
    {
        customValidation?.ClearErrors();

        try
        {
            var response = await Http.PostAsJsonAsync<Starship>(
                "StarshipValidation", (Starship)editContext.Model);

            var errors = await response.Content
                .ReadFromJsonAsync<Dictionary<string, List<string>>>() ?? 
                new Dictionary<string, List<string>>();

            if (response.StatusCode == HttpStatusCode.BadRequest && 
                errors.Any())
            {
                customValidation?.DisplayErrors(errors);
            }
            else if (!response.IsSuccessStatusCode)
            {
                throw new HttpRequestException(
                    $"Validation failed. Status Code: {response.StatusCode}");
            }
            else
            {
                disabled = true;
                messageStyles = "color:green";
                message = "The form has been processed.";
            }
        }
        catch (AccessTokenNotAvailableException ex)
        {
            ex.Redirect();
        }
        catch (Exception ex)
        {
            Logger.LogError("Form processing error: {Message}", ex.Message);
            disabled = true;
            messageStyles = "color:red";
            message = "There was an error processing the form.";
        }
    }
}

Blazor Web App の .Client プロジェクトでは、バックエンド Web API コントローラーに対する HTTP POST 要求の HttpClient も登録する必要があります。 .Client プロジェクトの Program ファイルで次の行を確認するか、追加します。

builder.Services.AddScoped(sp => 
    new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

上記の例では、ベース アドレスの設定に builder.HostEnvironment.BaseAddress (IWebAssemblyHostEnvironment.BaseAddress) を使っています。これを使って、アプリのベース アドレス (通常は、ホスト ページの <base> タグの href 値に由来します) を取得できます。

@page "/starship-10"
@using System.Net
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using BlazorSample.Shared
@attribute [Authorize]
@inject HttpClient Http
@inject ILogger<Starship10> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="Model" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <CustomValidation @ref="customValidation" />
    <ValidationSummary />
    <div>
        <label>
            Identifier: 
            <InputText @bind-Value="Model!.Id" disabled="@disabled" />
        </label>
    </div>
    <div>
        <label>
            Description (optional):
            <InputTextArea @bind-Value="Model!.Description" 
                disabled="@disabled" />
        </label>
    </div>
    <div>
        <label>
            Primary Classification:
            <InputSelect @bind-Value="Model!.Classification" disabled="@disabled">
                <option value="">Select classification ...</option>
                <option value="Exploration">Exploration</option>
                <option value="Diplomacy">Diplomacy</option>
                <option value="Defense">Defense</option>
            </InputSelect>
        </label>
    </div>
    <div>
        <label>
            Maximum Accommodation:
            <InputNumber @bind-Value="Model!.MaximumAccommodation" 
                disabled="@disabled" />
        </label>
    </div>
    <div>
        <label>
            Engineering Approval:
            <InputCheckbox @bind-Value="Model!.IsValidatedDesign" 
                disabled="@disabled" />
        </label>
    </div>
    <div>
        <label>
            Production Date:
            <InputDate @bind-Value="Model!.ProductionDate" disabled="@disabled" />
        </label>
    </div>
    <div>
        <button type="submit" disabled="@disabled">Submit</button>
    </div>
    <div style="@messageStyles">
        @message
    </div>
</EditForm>

@code {
    private CustomValidation? customValidation;
    private bool disabled;
    private string? message;
    private string messageStyles = "visibility:hidden";

    public Starship? Model { get; set; }

    protected override void OnInitialized() => 
        Model ??= new() { ProductionDate = DateTime.UtcNow };

    private async Task Submit(EditContext editContext)
    {
        customValidation?.ClearErrors();

        try
        {
            var response = await Http.PostAsJsonAsync<Starship>(
                "StarshipValidation", (Starship)editContext.Model);

            var errors = await response.Content
                .ReadFromJsonAsync<Dictionary<string, List<string>>>() ?? 
                new Dictionary<string, List<string>>();

            if (response.StatusCode == HttpStatusCode.BadRequest && 
                errors.Any())
            {
                customValidation?.DisplayErrors(errors);
            }
            else if (!response.IsSuccessStatusCode)
            {
                throw new HttpRequestException(
                    $"Validation failed. Status Code: {response.StatusCode}");
            }
            else
            {
                disabled = true;
                messageStyles = "color:green";
                message = "The form has been processed.";
            }
        }
        catch (AccessTokenNotAvailableException ex)
        {
            ex.Redirect();
        }
        catch (Exception ex)
        {
            Logger.LogError("Form processing error: {Message}", ex.Message);
            disabled = true;
            messageStyles = "color:red";
            message = "There was an error processing the form.";
        }
    }
}

Note

検証コンポーネントを使用する代わりに、データ注釈検証属性を使用することもできます。 フォームのモデルに適用されるカスタム属性は、DataAnnotationsValidator コンポーネントを使用してアクティブ化されます。 サーバー検証で使用する場合、属性はサーバー上で実行可能である必要があります。 詳しくは、「カスタム検証属性」セクションをご覧ください。

Note

このセクションで説明したサーバー検証の手法は、このドキュメント セットにある、Blazor WebAssembly でホストされるどのソリューション例にも適しています。

入力イベントに基づく InputText

onchange イベント (change) ではなく、oninput イベント (input) を使用するカスタム コンポーネントを作成するには、InputText コンポーネントを使用します。 各キー入力で、input イベント トリガーのフィールド検証を使用します。

次の CustomInputText コンポーネントによってフレームワークの InputText コンポーネントが継承され、イベント バインディングが oninput イベント (input) に設定されます。

CustomInputText.razor:

@inherits InputText

<input @attributes="AdditionalAttributes" 
       class="@CssClass" 
       @bind="CurrentValueAsString" 
       @bind:event="oninput" />

CustomInputText コンポーネントは、InputText が使用される場所であればどこでも使用できます。 次のコンポーネントでは、共有 CustomInputText コンポーネントが使用されています。

Starship11.razor:

@page "/starship-11"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship11> Logger

<EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship11">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Identifier: 
            <CustomInputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

<div>
    CurrentValue: @Model?.Id
</div>

@code {
    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit() => Logger.LogInformation("Submit: Processing form");

    public class Starship
    {
        [Required]
        [StringLength(10, ErrorMessage = "Id is too long.")]
        public string? Id { get; set; }
    }
}
@page "/starship-11"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship11> Logger

<EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship11">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Identifier: 
            <CustomInputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

<div>
    CurrentValue: @Model?.Id
</div>

@code {
    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit() => Logger.LogInformation("Submit: Processing form");

    public class Starship
    {
        [Required]
        [StringLength(10, ErrorMessage = "Id is too long.")]
        public string? Id { get; set; }
    }
}
@page "/starship-11"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship11> Logger

<EditForm Model="Model" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <CustomInputText @bind-Value="Model!.Id" />
    <button type="submit">Submit</button>
</EditForm>

<div>
    CurrentValue: @Model?.Id
</div>

@code {
    public Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit()
    {
        Logger.LogInformation("Submit called: Processing the form");
    }

    public class Starship
    {
        [Required]
        [StringLength(10, ErrorMessage = "Id is too long.")]
        public string? Id { get; set; }
    }
}

検証概要コンポーネントと検証メッセージ コンポーネント

ValidationSummary コンポーネントは、すべての検証メッセージを要約します。これは検証概要タグヘルパーと似ています。

<ValidationSummary />

Model パラメーターを使用して、特定のモデルの検証メッセージを出力します。

<ValidationSummary Model="Model" />

ValidationMessage<TValue> コンポーネントは、特定のフィールドの検証メッセージを表示します。これは、検証メッセージ タグ ヘルパーに似ています。 For 属性と、モデル プロパティに名前を付けるラムダ式で、検証するフィールドを指定します。

<ValidationMessage For="@(() => Model!.MaximumAccommodation)" />

ValidationMessage<TValue> コンポーネントと ValidationSummary コンポーネントでは、任意の属性をサポートしています。 コンポーネント パラメーターに一致しない属性は、生成された <div> 要素または <ul> 要素に追加されます。

アプリのスタイルシート (wwwroot/css/app.css または wwwroot/css/site.css) での検証メッセージのスタイルを制御します。 既定の validation-message クラスでは、検証メッセージのテキストの色が赤に設定されます。

.validation-message {
    color: red;
}

フォーム フィールドが有効かどうかを判断する

検証メッセージを取得せずにフィールドが有効かどうかを判断するには、EditContext.IsValid を使用します。

サポートされていますが、推奨されません:

var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();

推奨:

var isValid = editContext.IsValid(fieldIdentifier);

カスタム検証属性

カスタム検証属性を使用するときに、検証結果がフィールドに正しく関連付けられるようにするには、ValidationResult の作成時に検証コンテキストの MemberName を渡します。

CustomValidator.cs:

using System;
using System.ComponentModel.DataAnnotations;

public class CustomValidator : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, 
        ValidationContext validationContext)
    {
        ...

        return new ValidationResult("Validation message to user.",
            new[] { validationContext.MemberName });
    }
}

ValidationContext を使用してカスタム検証属性にサービスを挿入します。 次の例は、依存関係の挿入 (DI) を使用してユーザー入力を検証する、サラダ シェフ用のフォームを示しています。

SaladChef クラスは、Ten Forward サラダに承認されている宇宙船の材料リストを示しています。

SaladChef.cs:

namespace BlazorSample;

public class SaladChef
{
    public string[] SaladToppers = { "Horva", "Kanda Root", "Krintar", "Plomeek",
        "Syto Bean" };
}

Program ファイルでアプリの DI コンテナーに SaladChef を登録します。

builder.Services.AddTransient<SaladChef>();

次の SaladChefValidatorAttribute クラスの IsValid メソッドでは、DI から SaladChef サービスを取得して、ユーザーの入力をチェックします。

SaladChefValidatorAttribute.cs:

using System.ComponentModel.DataAnnotations;

namespace BlazorSample;

public class SaladChefValidatorAttribute : ValidationAttribute
{
    protected override ValidationResult? IsValid(object? value,
        ValidationContext validationContext)
    {
        var saladChef = validationContext.GetRequiredService<SaladChef>();

        if (saladChef.SaladToppers.Contains(value?.ToString()))
        {
            return ValidationResult.Success;
        }

        return new ValidationResult("Is that a Vulcan salad topper?! " +
            "The following toppers are available for a Ten Forward salad: " +
            string.Join(", ", saladChef.SaladToppers));
    }
}

次のコンポーネントでは、SaladChefValidatorAttribute ([SaladChefValidator]) をサラダの材料の文字列 (SaladIngredient) に適用し、ユーザー入力を検証します。

Starship12.razor:

@page "/starship-12"
@inject SaladChef SaladChef

<EditForm Model="this" autocomplete="off" FormName="Starship12">
    <DataAnnotationsValidator />
    <div>
        <label>
            Salad topper (@saladToppers):
            <input @bind="SaladIngredient" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
    <ul>
        @foreach (var message in context.GetValidationMessages())
        {
            <li class="validation-message">@message</li>
        }
    </ul>
</EditForm>

@code {
    private string? saladToppers;

    [SaladChefValidator]
    public string? SaladIngredient { get; set; }

    protected override void OnInitialized() =>
        saladToppers ??= string.Join(", ", SaladChef.SaladToppers);
}
@page "/starship-12"
@inject SaladChef SaladChef

<EditForm Model="this" autocomplete="off" FormName="Starship12">
    <DataAnnotationsValidator />
    <div>
        <label>
            Salad topper (@saladToppers):
            <input @bind="SaladIngredient" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
    <ul>
        @foreach (var message in context.GetValidationMessages())
        {
            <li class="validation-message">@message</li>
        }
    </ul>
</EditForm>

@code {
    private string? saladToppers;

    [SaladChefValidator]
    public string? SaladIngredient { get; set; }

    protected override void OnInitialized() =>
        saladToppers ??= string.Join(", ", SaladChef.SaladToppers);
}
@page "/starship-12"
@inject SaladChef SaladChef

<EditForm Model="this" autocomplete="off">
    <DataAnnotationsValidator />
    <p>
        <label>
            Salad topper (@saladToppers):
            <input @bind="SaladIngredient" />
        </label>
    </p>
    <button type="submit">Submit</button>
    <ul>
        @foreach (var message in context.GetValidationMessages())
        {
            <li class="validation-message">@message</li>
        }
    </ul>
</EditForm>

@code {
    private string? saladToppers;

    [SaladChefValidator]
    public string? SaladIngredient { get; set; }

    protected override void OnInitialized() => 
        saladToppers ??= string.Join(", ", SaladChef.SaladToppers);
}

カスタム検証 CSS クラスの属性

カスタム検証 CSS クラスの属性は、Bootstrap などの CSS フレームワークと統合する場合に便利です。

カスタム検証 CSS クラス属性を指定するには、まず、カスタム検証用の CSS スタイルを提供します。 次の例では、有効なスタイル (validField) と無効なスタイル (invalidField) が指定されています。

アプリのスタイルシートに次の CSS クラスを追加します。

.validField {
    border-color: lawngreen;
}

.invalidField {
    background-color: tomato;
}

フィールド検証メッセージをチェックし、有効なスタイルと無効なスタイルを適切に適用する FieldCssClassProvider から派生したクラスを作成します。

CustomFieldClassProvider.cs:

using Microsoft.AspNetCore.Components.Forms;

public class CustomFieldClassProvider : FieldCssClassProvider
{
    public override string GetFieldCssClass(EditContext editContext, 
        in FieldIdentifier fieldIdentifier)
    {
        var isValid = editContext.IsValid(fieldIdentifier);

        return isValid ? "validField" : "invalidField";
    }
}
using Microsoft.AspNetCore.Components.Forms;

public class CustomFieldClassProvider : FieldCssClassProvider
{
    public override string GetFieldCssClass(EditContext editContext, 
        in FieldIdentifier fieldIdentifier)
    {
        var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();

        return isValid ? "validField" : "invalidField";
    }
}

SetFieldCssClassProvider を使用して、CustomFieldClassProvider クラスをフォームの EditContext インスタンスのフィールド CSS クラス プロバイダーとして設定します。

Starship13.razor:

@page "/starship-13"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship13> Logger

<EditForm EditContext="editContext" OnValidSubmit="Submit" FormName="Starship13">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Identifier: 
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    private EditContext? editContext;

    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized()
    {
        Model ??= new();
        editContext = new(Model);
        editContext.SetFieldCssClassProvider(new CustomFieldClassProvider());
    }

    private void Submit() => Logger.LogInformation("Submit: Processing form");

    public class Starship
    {
        [Required]
        [StringLength(10, ErrorMessage = "Id is too long.")]
        public string? Id { get; set; }
    }
}
@page "/starship-13"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship13> Logger

<EditForm EditContext="editContext" OnValidSubmit="Submit" FormName="Starship13">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Identifier: 
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    private EditContext? editContext;

    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized()
    {
        Model ??= new();
        editContext = new(Model);
        editContext.SetFieldCssClassProvider(new CustomFieldClassProvider());
    }

    private void Submit() => Logger.LogInformation("Submit: Processing form");

    public class Starship
    {
        [Required]
        [StringLength(10, ErrorMessage = "Id is too long.")]
        public string? Id { get; set; }
    }
}
@page "/starship-13"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship13> Logger

<EditForm EditContext="editContext" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <InputText @bind-Value="Model!.Id" />
    <button type="submit">Submit</button>
</EditForm>

@code {
    private EditContext? editContext;

    public Starship? Model { get; set; }

    protected override void OnInitialized()
    {
        Model ??= new();
        editContext = new(Model);
        editContext.SetFieldCssClassProvider(new CustomFieldClassProvider());
    }

    private void Submit()
    {
        Logger.LogInformation("Submit called: Processing the form");
    }

    public class Starship
    {
        [Required]
        [StringLength(10, ErrorMessage = "Id is too long.")]
        public string? Id { get; set; }
    }
}

前の例では、すべてのフォーム フィールドの有効性をチェックし、各フィールドにスタイルを適用しています。 フォームを使用してフィールドのサブセットにのみカスタム スタイルを適用する場合は、スタイルが CustomFieldClassProvider によって条件的に適用されるようにします。 次の CustomFieldClassProvider2 の例では、スタイルが Name フィールドに適用されるだけです。 名前が Name と一致しないフィールドの場合は、string.Empty が返され、スタイルは適用されません。 リフレクションを使用すると、フィールドが、HTML エンティティに割り当てられたid ではなく、モデル メンバーのプロパティまたはフィールド名と照合されます。

CustomFieldClassProvider2.cs:

using Microsoft.AspNetCore.Components.Forms;

public class CustomFieldClassProvider2 : FieldCssClassProvider
{
    public override string GetFieldCssClass(EditContext editContext,
        in FieldIdentifier fieldIdentifier)
    {
        if (fieldIdentifier.FieldName == "Name")
        {
            var isValid = editContext.IsValid(fieldIdentifier);

            return isValid ? "validField" : "invalidField";
        }

        return string.Empty;
    }
}
using Microsoft.AspNetCore.Components.Forms;

public class CustomFieldClassProvider2 : FieldCssClassProvider
{
    public override string GetFieldCssClass(EditContext editContext,
        in FieldIdentifier fieldIdentifier)
    {
        if (fieldIdentifier.FieldName == "Name")
        {
            var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();

            return isValid ? "validField" : "invalidField";
        }

        return string.Empty;
    }
}

メモ

前の例のフィールド名の照合では大文字と小文字が区別されるため、モデル プロパティ メンバーで指定された "Name" が "Name" の条件付きのチェックと一致する必要があります。

  • 正しく一致します: fieldId.FieldName == "Name"
  • 一致しません: fieldId.FieldName == "name"
  • 一致しません: fieldId.FieldName == "NAME"
  • 一致しません: fieldId.FieldName == "nAmE"

追加のプロパティを Model に追加します。次に例を示します。

[StringLength(10, ErrorMessage = "Description is too long.")]
public string? Description { get; set; } 

CustomValidationForm コンポーネントのフォームに Description を追加します。

<InputText @bind-Value="Model!.Description" />

新しいフィールド CSS クラス プロバイダーを使用するように、コンポーネントの OnInitialized メソッドで EditContext インスタンスを更新します。

editContext?.SetFieldCssClassProvider(new CustomFieldClassProvider2());

CSS 検証クラスは Description フィールドに適用されないため、スタイルは設定されません。 ただし、フィールドの検証は普通に実行されます。 入力が 10 文字を超えた場合、検証の概要でエラーが示されます。

Description is too long.

次に例を示します。

  • カスタム CSS スタイルが Name フィールドに適用されます。

  • その他のフィールドでは Blazor の既定のロジックに似たロジックが適用され、modifiedvalid または invalid にして Blazor の既定のフィールド CSS 検証スタイルが使用されます。 既定のスタイルの場合、アプリが Blazor プロジェクト テンプレートに基づいている場合は、アプリのスタイルシートに追加する必要がないことに注意してください。 Blazor プロジェクト テンプレートに基づいていないアプリの場合は、既定のスタイルをアプリのスタイルシートに追加できます。

    .valid.modified:not([type=checkbox]) {
        outline: 1px solid #26b050;
    }
    
    .invalid {
        outline: 1px solid red;
    }
    

CustomFieldClassProvider3.cs:

using Microsoft.AspNetCore.Components.Forms;

public class CustomFieldClassProvider3 : FieldCssClassProvider
{
    public override string GetFieldCssClass(EditContext editContext,
        in FieldIdentifier fieldIdentifier)
    {
        var isValid = editContext.IsValid(fieldIdentifier);

        if (fieldIdentifier.FieldName == "Name")
        {
            return isValid ? "validField" : "invalidField";
        }
        else
        {
            if (editContext.IsModified(fieldIdentifier))
            {
                return isValid ? "modified valid" : "modified invalid";
            }
            else
            {
                return isValid ? "valid" : "invalid";
            }
        }
    }
}
using Microsoft.AspNetCore.Components.Forms;

public class CustomFieldClassProvider3 : FieldCssClassProvider
{
    public override string GetFieldCssClass(EditContext editContext,
        in FieldIdentifier fieldIdentifier)
    {
        var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();

        if (fieldIdentifier.FieldName == "Name")
        {
            return isValid ? "validField" : "invalidField";
        }
        else
        {
            if (editContext.IsModified(fieldIdentifier))
            {
                return isValid ? "modified valid" : "modified invalid";
            }
            else
            {
                return isValid ? "valid" : "invalid";
            }
        }
    }
}

前記のフィールド CSS クラス プロバイダーを使用するように、コンポーネントの OnInitialized メソッドで EditContext インスタンスを更新します。

editContext.SetFieldCssClassProvider(new CustomFieldClassProvider3());

CustomFieldClassProvider3の使用

  • Name フィールドには、アプリのカスタム検証 CSS スタイルが使用されます。
  • Description フィールドには、Blazor のロジックに似たロジックと、Blazor の既定のフィールド CSS 検証スタイルが使用されます。

IValidatableObject でのクラスレベルの検証

IValidatableObject を使用したクラスレベルの検証 (API ドキュメント) は、Blazor フォーム モデルでサポートされています。 IValidatableObject 検証は、フォームが送信されたときにのみ実行され、他のすべての検証が成功した場合にのみ実行されます。

Blazor データ注釈検証パッケージ

Microsoft.AspNetCore.Components.DataAnnotations.Validation は、DataAnnotationsValidator コンポーネントを使用して検証エクスペリエンスのギャップを埋めるパッケージです。 パッケージは現在、試験段階です。

警告

Microsoft.AspNetCore.Components.DataAnnotations.Validation パッケージには、NuGet.org に "リリース候補" の最新バージョンが含まれています。現時点では、"試験段階" リリース候補パッケージを引き続きお使いください。 試験段階の機能は、機能の有効性を調べる目的で提供されており、安定バージョンには含まれていない場合があります。 更新の詳細については、Announcements GitHub リポジトリdotnet/aspnetcoreGitHub リポジトリ、またはこのトピック セクションを参照してください。

[CompareProperty] 属性

CompareAttribute は、DataAnnotationsValidator が検証結果を特定のメンバーに関連付けないため、DataAnnotationsValidator コンポーネントで正しく機能しません。 これにより、フィールドレベルの検証と、送信時のモデル全体が検証されたときの動作に一貫性がなくなることがあります。 Microsoft.AspNetCore.Components.DataAnnotations.Validation "試験的" パッケージでは、これらの制限を回避する追加の検証属性 ComparePropertyAttribute が導入されています。 Blazor アプリでは、[CompareProperty][Compare] 属性の直接の代わりとなるものです。

入れ子になったモデル、コレクション型、および複合型

Blazor では、組み込みの DataAnnotationsValidator によるデータ注釈を使用したフォーム入力の検証をサポートしています。 ただし、DataAnnotationsValidatorで は、コレクション型または複合型のプロパティではないフォームにバインドされているモデルの最上位レベルのプロパティのみが検証されます。

コレクション型と複合型のプロパティを含む、バインドされたモデルのオブジェクト グラフ全体を検証するには、"試験的" Microsoft.AspNetCore.Components.DataAnnotations.Validation パッケージによって提供される ObjectGraphDataAnnotationsValidator を使用します。

<EditForm ...>
    <ObjectGraphDataAnnotationsValidator />
    ...
</EditForm>

[ValidateComplexType] でモデルのプロパティに注釈を付けます。 次のモデル クラスでは、ShipDescription クラスに、モデルがフォームにバインドされたときに検証する追加のデータ注釈が含まれています。

Starship.cs:

using System;
using System.ComponentModel.DataAnnotations;

public class Starship
{
    ...

    [ValidateComplexType]
    public ShipDescription ShipDescription { get; set; } = new();

    ...
}

ShipDescription.cs:

using System;
using System.ComponentModel.DataAnnotations;

public class ShipDescription
{
    [Required]
    [StringLength(40, ErrorMessage = "Description too long (40 char).")]
    public string? ShortDescription { get; set; }

    [Required]
    [StringLength(240, ErrorMessage = "Description too long (240 char).")]
    public string? LongDescription { get; set; }
}

フォームの検証に基づいて送信ボタンを有効にする

フォームの検証に基づいて送信ボタンを有効または無効にするため、以下の例では次のことが行われています。

  • 入力コンポーネントに関する記事の「フォームの例」セクションの前の Starfleet Starship Database フォーム (Starship3 コンポーネント) の簡略版が使用され、宇宙船の ID の値のみが受け取られます。Starship の他のプロパティは、Starship 型のインスタンスが作成されるときに、有効な既定値を受け取ります。
  • コンポーネントを初期化するときに、フォームの EditContext を使用してモデルを割り当てます。
  • コンテキストの OnFieldChanged コールバックでフォームを検証して、送信ボタンを有効または無効にします。
  • IDisposable を実装し、Dispose メソッドでイベント ハンドラーの登録を解除します。 詳しくは、「ASP.NET Core Razor コンポーネントのライフサイクル」をご覧ください。

Note

EditForm.EditContext に割り当てるとき、EditForm には EditForm.Model を割り当てないでください。

Starship14.razor:

@page "/starship-14"
@implements IDisposable
@inject ILogger<Starship14> Logger

<EditForm EditContext="editContext" OnValidSubmit="Submit" FormName="Starship14">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Identifier:
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit" disabled="@formInvalid">Submit</button>
    </div>
</EditForm>

@code {
    private bool formInvalid = false;
    private EditContext? editContext;

    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized()
    {
        Model ??=
            new()
                {
                    Id = "NCC-1701",
                    Classification = "Exploration",
                    MaximumAccommodation = 150,
                    IsValidatedDesign = true,
                    ProductionDate = new DateTime(2245, 4, 11)
                };
        editContext = new(Model);
        editContext.OnFieldChanged += HandleFieldChanged;
    }

    private void HandleFieldChanged(object? sender, FieldChangedEventArgs e)
    {
        if (editContext is not null)
        {
            formInvalid = !editContext.Validate();
            StateHasChanged();
        }
    }

    private void Submit() => Logger.LogInformation("Submit: Processing form");

    public void Dispose()
    {
        if (editContext is not null)
        {
            editContext.OnFieldChanged -= HandleFieldChanged;
        }
    }
}
@page "/starship-14"
@implements IDisposable
@inject ILogger<Starship14> Logger

<EditForm EditContext="editContext" OnValidSubmit="Submit" FormName="Starship14">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Identifier:
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit" disabled="@formInvalid">Submit</button>
    </div>
</EditForm>

@code {
    private bool formInvalid = false;
    private EditContext? editContext;

    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized()
    {
        Model ??=
            new()
                {
                    Id = "NCC-1701",
                    Classification = "Exploration",
                    MaximumAccommodation = 150,
                    IsValidatedDesign = true,
                    ProductionDate = new DateTime(2245, 4, 11)
                };
        editContext = new(Model);
        editContext.OnFieldChanged += HandleFieldChanged;
    }

    private void HandleFieldChanged(object? sender, FieldChangedEventArgs e)
    {
        if (editContext is not null)
        {
            formInvalid = !editContext.Validate();
            StateHasChanged();
        }
    }

    private void Submit() => Logger.LogInformation("Submit: Processing form");

    public void Dispose()
    {
        if (editContext is not null)
        {
            editContext.OnFieldChanged -= HandleFieldChanged;
        }
    }
}
@page "/starship-14"
@implements IDisposable
@inject ILogger<Starship14> Logger

<EditForm EditContext="editContext" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Identifier: 
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit" disabled="@formInvalid">Submit</button>
    </div>
</EditForm>

@code {
    private bool formInvalid = false;
    private EditContext? editContext;

    private Starship? Model { get; set; }

    protected override void OnInitialized()
    {
        Model ??=
            new()
            {
                Id = "NCC-1701",
                Classification = "Exploration",
                MaximumAccommodation = 150,
                IsValidatedDesign = true,
                ProductionDate = new DateTime(2245, 4, 11)
            };
        editContext = new(Model);
        editContext.OnFieldChanged += HandleFieldChanged;
    }

    private void HandleFieldChanged(object? sender, FieldChangedEventArgs e)
    {
        if (editContext is not null)
        {
            formInvalid = !editContext.Validate();
            StateHasChanged();
        }
    }

    private void Submit()
    {
        Logger.LogInformation("Submit called: Processing the form");
    }

    public void Dispose()
    {
        if (editContext is not null)
        {
            editContext.OnFieldChanged -= HandleFieldChanged;
        }
    }
}

フォームに有効な値が事前に読み込まれておらず、フォームの読み込みで Submit ボタンを無効にする場合は、formInvalidtrue に設定します。

上記の方法の副作用として、ユーザーがいずれかのフィールドを操作した後、検証の概要 (ValidationSummary コンポーネント) に無効なフィールドが設定されます。 このシナリオには、次のいずれかの方法で対処します。

  • フォームでは ValidationSummary コンポーネントを使用しないでください。
  • 送信ボタンが選択された (たとえば、Submit メソッドで) ときに ValidationSummary コンポーネントを表示します。
<EditForm ... EditContext="editContext" OnValidSubmit="Submit" ...>
    <DataAnnotationsValidator />
    <ValidationSummary style="@displaySummary" />

    ...

    <button type="submit" disabled="@formInvalid">Submit</button>
</EditForm>

@code {
    private string displaySummary = "display:none";

    ...

    private void Submit()
    {
        displaySummary = "display:block";
    }
}