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

Blazor フレームワークでは、データ注釈を使用するモデルにバインドされた EditForm コンポーネントを使用して、検証付きの Web フォームがサポートされています。

EditForm コンポーネントでデータ注釈の検証がどのように動作するかを示すために、次の ExampleModel 型を考えてみます。 Name プロパティは、RequiredAttribute で必須とマークされており、StringLengthAttribute で最大文字列長の制限とエラー メッセージが指定されています。

ExampleModel.cs:

using System.ComponentModel.DataAnnotations;

public class ExampleModel
{
    [Required]
    [StringLength(10, ErrorMessage = "Name is too long.")]
    public string Name { get; set; }
}
using System.ComponentModel.DataAnnotations;

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

フォームは、Blazor フレームワークの EditForm コンポーネントを使用して定義されます。 次の Razor コンポーネントでは、EditForm コンポーネントを使用して Web フォームをレンダリングするための一般的な要素、コンポーネント、Razor コードが示されており、これは前の ExampleModel 型にバインドされています。

Pages/FormExample1.razor:

@page "/form-example-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample1> Logger

<EditForm Model="@exampleModel" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <InputText id="name" @bind-Value="exampleModel.Name" />

    <button type="submit">Submit</button>
</EditForm>

@code {
    private ExampleModel exampleModel = new();

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }
}
@page "/form-example-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample1> Logger

<EditForm Model="@exampleModel" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <InputText id="name" @bind-Value="exampleModel.Name" />

    <button type="submit">Submit</button>
</EditForm>

@code {
    private ExampleModel exampleModel = new ExampleModel();

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }
}

前の FormExample1 コンポーネントは次のようなものです。

  • EditForm コンポーネントは、<EditForm> 要素が出現する場所にレンダリングされます。
  • モデルはコンポーネントの @code ブロック内に作成され、プライベート フィールド (exampleModel) に保持されます。 フィールドは、<EditForm> 要素の EditForm.Model の属性 (Model) に割り当てられます。
  • InputText コンポーネント (id="name") は、文字列値を編集するための入力コンポーネントです。 @bind-Value ディレクティブ属性により、exampleModel.Name モデル プロパティは、InputTextコンポーネントの Value プロパティにバインドされます。
  • HandleValidSubmit メソッドは OnValidSubmit に割り当てられます。 フォームが検証に合格すると、ハンドラーが呼び出されます。
  • データ注釈検証コントロール (DataAnnotationsValidator コンポーネント†) は、データ注釈を使用して検証サポートをアタッチします。
    • Submit ボタンが選択されたときに <input> フォーム フィールドが空白のままになっている場合、検証の概要 (ValidationSummary コンポーネント‡) にエラーが表示され ("The Name field is required.")、HandleValidSubmit は呼び出され ません
    • Submit ボタンが選択されたときに <input> フォーム フィールドに 10 文字より多く含まれている場合は、検証の概要にエラーが表示され ("Name is too long.")、HandleValidSubmit は呼び出され ません
    • Submit ボタンが選択されたときに <input> フォーム フィールドに有効な値が含まれている場合、HandleValidSubmit が呼び出されます。

DataAnnotationsValidator コンポーネントについては、「検証コンポーネント」セクションを参照してください。 ‡ ValidationSummary コンポーネントについては、「検証概要コンポーネントと検証メッセージ コンポーネント」を参照してください。 プロパティのバインドの詳細については、ASP.NET Core Blazor データ バインディング を参照してください。

フォームのバインド

割り当てられているモデル インスタンスに基づいて、EditForm により EditContext がフォーム内の他のコンポーネントに対するカスケード値として作成されます。 EditContext により、変更されたフィールドと現在の検証メッセージを含む、編集プロセスに関するメタデータが追跡されます。 EditForm.Model または EditForm.EditContext のいずれかに割り当てると、フォームをデータにバインドできます。

EditForm.Model への割り当て:

<EditForm Model="@exampleModel" ...>

@code {
    private ExampleModel exampleModel = new() { ... };
}

EditForm.EditContext への割り当て:

<EditForm EditContext="@editContext" ...>

@code {
    private ExampleModel exampleModel = new() { ... };
    private EditContext editContext;

    protected override void OnInitialized()
    {
        editContext = new(exampleModel);
    }
}

EditForm.Model への割り当て:

<EditForm Model="@exampleModel" ...>

@code {
    private ExampleModel exampleModel = new ExampleModel() { ... };
}

EditForm.EditContext への割り当て:

<EditForm EditContext="@editContext" ...>

@code {
    private ExampleModel exampleModel = new ExampleModel() { ... };
    private EditContext editContext;

    protected override void OnInitialized()
    {
        editContext = new EditContext(exampleModel);
    }
}

EditContext または ModelいずれかEditForm に割り当てます。 両方とも割り当てることはサポートされておらず、ランタイム エラーが生成されます。

ハンドルされない例外レンダリング コンポーネント: EditForm には、Model パラメーターまたは EditContext パラメーター (両方ではなく) が必要です。

フォームの送信を処理する

EditForm には、フォームの送信を処理するための次のコールバックが用意されています。

  • OnValidSubmit は、有効なフィールドを含むフォームが送信されたときに実行するイベント ハンドラーを割り当てるために使用します。
  • OnInvalidSubmit は、無効なフィールドを含むフォームが送信されたときに実行するイベント ハンドラーを割り当てるために使用します。
  • OnSubmit は、フォーム フィールドの検証状態に関係なく実行するイベント ハンドラーを割り当てるために使用します。 フォームは、イベント ハンドラー メソッドで EditContext.Validate を呼び出すことによって検証されます。 Validate によって true が返された場合、フォームは有効です。

組み込みのフォーム コンポーネント

Blazor フレームワークには、ユーザー入力を受け取って検証するための組み込みのフォーム コンポーネントが用意されています。 入力は、それらが変更されたときとフォームが送信されたときに検証されます。 次の表に、使用できる入力コンポーネントを示しています。

入力コンポーネント … とレンダリング
InputCheckbox <input type="checkbox">
InputDate<TValue> <input type="date">
InputFile <input type="file">
InputNumber<TValue> <input type="number">
InputRadio<TValue> <input type="radio">
InputRadioGroup<TValue> InputRadio<TValue> のグループ
InputSelect<TValue> <select>
InputText <input>
InputTextArea <textarea>

InputFile コンポーネントの詳細については、「ASP.NET Core Blazor ファイルのアップロード」を参照してください。

入力コンポーネント … とレンダリング
InputCheckbox <input type="checkbox">
InputDate<TValue> <input type="date">
InputNumber<TValue> <input type="number">
InputSelect<TValue> <select>
InputText <input>
InputTextArea <textarea>

注意

InputRadio<TValue>InputRadioGroup<TValue> コンポーネントは、ASP.NET Core 5.0 以降で使用できます。 詳細については、この記事のバージョン 5.0 以降を選択してください。

すべての入力コンポーネント (EditForm を含む) で、任意の属性がサポートされています。 コンポーネント パラメーターに一致しない属性は、レンダリングされる HTML 要素に追加されます。

入力コンポーネントにより、フィールド変更時に検証するための既定の動作が提供されます。これには、フィールドの有効または無効の状態を反映するようにフィールドの CSS クラスを更新することが含まれます。 一部のコンポーネントには、便利な解析ロジックが含まれます。 たとえば、InputDate<TValue>InputNumber<TValue> では、解析不能な値を検証エラーとして登録することによって、解析不能な値を適切に処理します。 null 値を受け入れることができる型では、ターゲット フィールドの null 値の許容もサポートされています (たとえば、null 許容整数の場合は int?)。

フォームの例

この記事のいくつかの例で使用されている次の Starship 型では、データ注釈を持つさまざまなプロパティのセットが定義されています。

  • Identifier は、RequiredAttribute で注釈が付けられているため必須です。 Identifier には、StringLengthAttribute を使用して、1 文字以上 16 文字以下の値が要求されています。
  • Description は、RequiredAttribute で注釈が付けられていないため、省略可能です。
  • Classification は必須です。
  • MaximumAccommodation プロパティは、既定値は 0 ですが、RangeAttribute により 1 から 100,000 の値が必要です。
  • IsValidatedDesign により、プロパティの値が true であることが要求されています。これは、プロパティが UI のチェック ボックス (<input type="checkbox">) にバインドされているときは、選択された状態と一致します。
  • ProductionDateDateTime であり、必須です。

Starship.cs:

using System;
using System.ComponentModel.DataAnnotations;

public class Starship
{
    [Required]
    [StringLength(16, ErrorMessage = "Identifier too long (16 character limit).")]
    public string Identifier { get; set; }

    public string Description { get; set; }

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

    [Range(1, 100000, ErrorMessage = "Accommodation invalid (1-100000).")]
    public int MaximumAccommodation { get; set; }

    [Required]
    [Range(typeof(bool), "true", "true", 
        ErrorMessage = "This form disallows unapproved ships.")]
    public bool IsValidatedDesign { get; set; }

    [Required]
    public DateTime ProductionDate { get; set; }
}
using System;
using System.ComponentModel.DataAnnotations;

public class Starship
{
    [Required]
    [StringLength(16, ErrorMessage = "Identifier too long (16 character limit).")]
    public string Identifier { get; set; }

    public string Description { get; set; }

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

    [Range(1, 100000, ErrorMessage = "Accommodation invalid (1-100000).")]
    public int MaximumAccommodation { get; set; }

    [Required]
    [Range(typeof(bool), "true", "true", 
        ErrorMessage = "This form disallows unapproved ships.")]
    public bool IsValidatedDesign { get; set; }

    [Required]
    public DateTime ProductionDate { get; set; }
}

以下のフォームでは、次のものを使用してユーザー入力が受け入れられて検証されます。

Pages/FormExample2.razor:

@page "/form-example-2"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample2> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="@starship" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <p>
        <label>
            Identifier:
            <InputText @bind-Value="starship.Identifier" />
        </label>
    </p>
    <p>
        <label>
            Description (optional):
            <InputTextArea @bind-Value="starship.Description" />
        </label>
    </p>
    <p>
        <label>
            Primary Classification:
            <InputSelect @bind-Value="starship.Classification">
                <option value="">Select classification ...</option>
                <option value="Exploration">Exploration</option>
                <option value="Diplomacy">Diplomacy</option>
                <option value="Defense">Defense</option>
            </InputSelect>
        </label>
    </p>
    <p>
        <label>
            Maximum Accommodation:
            <InputNumber @bind-Value="starship.MaximumAccommodation" />
        </label>
    </p>
    <p>
        <label>
            Engineering Approval:
            <InputCheckbox @bind-Value="starship.IsValidatedDesign" />
        </label>
    </p>
    <p>
        <label>
            Production Date:
            <InputDate @bind-Value="starship.ProductionDate" />
        </label>
    </p>

    <button type="submit">Submit</button>

    <p>
        <a href="http://www.startrek.com/">Star Trek</a>, 
        &copy;1966-2019 CBS Studios, Inc. and 
        <a href="https://www.paramount.com">Paramount Pictures</a>
    </p>
</EditForm>

@code {
    private Starship starship = new() { ProductionDate = DateTime.UtcNow };

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }
}
@page "/form-example-2"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample2> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="@starship" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <p>
        <label>
            Identifier:
            <InputText @bind-Value="starship.Identifier" />
        </label>
    </p>
    <p>
        <label>
            Description (optional):
            <InputTextArea @bind-Value="starship.Description" />
        </label>
    </p>
    <p>
        <label>
            Primary Classification:
            <InputSelect @bind-Value="starship.Classification">
                <option value="">Select classification ...</option>
                <option value="Exploration">Exploration</option>
                <option value="Diplomacy">Diplomacy</option>
                <option value="Defense">Defense</option>
            </InputSelect>
        </label>
    </p>
    <p>
        <label>
            Maximum Accommodation:
            <InputNumber @bind-Value="starship.MaximumAccommodation" />
        </label>
    </p>
    <p>
        <label>
            Engineering Approval:
            <InputCheckbox @bind-Value="starship.IsValidatedDesign" />
        </label>
    </p>
    <p>
        <label>
            Production Date:
            <InputDate @bind-Value="starship.ProductionDate" />
        </label>
    </p>

    <button type="submit">Submit</button>

    <p>
        <a href="http://www.startrek.com/">Star Trek</a>, 
        &copy;1966-2019 CBS Studios, Inc. and 
        <a href="https://www.paramount.com">Paramount Pictures</a>
    </p>
</EditForm>

@code {
    private Starship starship = new Starship() { ProductionDate = DateTime.UtcNow };

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }
}

前の例の EditForm により、割り当てられた Starship インスタンス (EditContext) に基づいて Model="@starship" が作成され、有効なフォームが処理されます。 次の例 (FormExample3 コンポーネント) では、EditContext をフォームに割り当て、フォームが送信されるときに検証する方法を示します。

次に例を示します。

  • 前の Starfleet Starship Database フォーム (FormExample2 コンポーネント) の簡略版が使用され、宇宙船の識別子の値のみが受け入れられます。 Starship の他のプロパティは、Starship 型のインスタンスが作成されるときに、有効な既定値を受け取ります。
  • Submit ボタンが選択されると、HandleSubmit メソッドが実行されます。
  • フォームは、HandleSubmit メソッドで EditContext.Validate を呼び出すことによって検証されます。
  • 検証結果に応じて、ログ記録が実行されます。

注意

FormExample3 コンポーネントの HandleSubmit は、フォーム値の格納で非同期呼び出し (await ...) が使用されることが多いので、非同期メソッドとして示されています。 示されているようにフォームがテスト アプリで使用される場合は、HandleSubmit は単に同期的に実行されます。 テスト目的の場合は、次のビルド警告を無視します。

This async method lacks 'await' operators and will run synchronously. ...

Pages/FormExample3.razor:

@page "/form-example-3"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample3> Logger

<EditForm EditContext="@editContext" OnSubmit="@HandleSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <p>
        <label>
            Identifier:
            <InputText @bind-Value="starship.Identifier" />
        </label>
    </p>

    <button type="submit">Submit</button>

    <p>
        <a href="http://www.startrek.com/">Star Trek</a>,
        &copy;1966-2019 CBS Studios, Inc. and
        <a href="https://www.paramount.com">Paramount Pictures</a>
    </p>
</EditForm>

@code {
    private Starship starship = 
        new()
        {
            Identifier = "NCC-1701",
            Classification = "Exploration",
            MaximumAccommodation = 150,
            IsValidatedDesign = true,
            ProductionDate = new DateTime(2245, 4, 11)
        };
    private EditContext editContext;

    protected override void OnInitialized()
    {
        editContext = new(starship);
    }

    private async Task HandleSubmit()
    {
        if (editContext.Validate())
        {
            Logger.LogInformation("HandleSubmit called: Form is valid");

            // Process the valid form
            // await ...
        }
        else
        {
            Logger.LogInformation("HandleSubmit called: Form is INVALID");
        }
    }
}
@page "/form-example-3"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample3> Logger

<EditForm EditContext="@editContext" OnSubmit="@HandleSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <p>
        <label>
            Identifier:
            <InputText @bind-Value="starship.Identifier" />
        </label>
    </p>

    <button type="submit">Submit</button>

    <p>
        <a href="http://www.startrek.com/">Star Trek</a>,
        &copy;1966-2019 CBS Studios, Inc. and
        <a href="https://www.paramount.com">Paramount Pictures</a>
    </p>
</EditForm>

@code {
    private Starship starship = 
        new Starship()
        {
            Identifier = "NCC-1701",
            Classification = "Exploration",
            MaximumAccommodation = 150,
            IsValidatedDesign = true,
            ProductionDate = new DateTime(2245, 4, 11)
        };
    private EditContext editContext;

    protected override void OnInitialized()
    {
        editContext = new EditContext(starship);
    }

    private async Task HandleSubmit()
    {
        if (editContext.Validate())
        {
            Logger.LogInformation("HandleSubmit called: Form is valid");

            // Process the valid form
            // await ...
        }
        else
        {
            Logger.LogInformation("HandleSubmit called: Form is INVALID");
        }
    }
}

注意

検証メッセージを EditContext から直接クリアする Framework API は存在しません。 このため、通常、フォームの新しい ValidationMessageStore に検証メッセージを追加することはお勧めしません。 検証メッセージを管理するには、この記事で説明するように、ビジネス ロジック検証コード検証コンポーネントを使用します。

さらに、割り当てられた後で EditContext を変更することは、サポートされて いません

表示名のサポート

次の組み込みコンポーネントでは、InputBase<TValue>.DisplayName パラメーターを使用した表示名がサポートされています。

フォームの例」セクションの Starfleet Starship Database フォーム (FormExample2 コンポーネント) で、新しい宇宙船の製造日には表示名が指定されていません。

<label>
    Production Date:
    <InputDate @bind-Value="starship.ProductionDate" />
</label>

フォームの送信時にフィールドに無効な日付が含まれている場合、エラー メッセージにはわかりやすい名前が表示されません。 フィールド名 "ProductionDate" は、検証の概要に表示されるときに、"Production" と "Date" の間に空白がありません。

ProductionDate フィールドは日付である必要があります。

DisplayName プロパティには、"Production" と "Date" の間にスペースがあるフレンドリ名を設定します。

<label>
    Production Date:
    <InputDate @bind-Value="starship.ProductionDate" 
               DisplayName="Production Date" />
</label>

フィールドの値が無効な場合、検証の概要にはフレンドリ名が表示されます。

Production Date フィールドは日付である必要があります。

エラー メッセージ テンプレートのサポート

InputDate<TValue>InputNumber<TValue> では、エラー メッセージ テンプレートがサポートされています。

わかりやすい表示名が割り当てられた「フォームの例」セクションの Starfleet Starship Database フォーム (FormExample2 コンポーネント) の Production Date フィールドでは、次の既定のエラー メッセージ テンプレートを使用してエラー メッセージが生成されます。

The {0} field must be a date.

{0} プレースホルダーの位置は、エラーがユーザーに表示されるときに、DisplayName プロパティの値が表示される場所です。

<label>
    Production Date:
    <InputDate @bind-Value="starship.ProductionDate" 
               DisplayName="Production Date" />
</label>

Production Date フィールドは日付である必要があります。

カスタム メッセージを提供するには、カスタム テンプレートを ParsingErrorMessage に割り当てます。

<label>
    Production Date:
    <InputDate @bind-Value="starship.ProductionDate" 
               DisplayName="Production Date" 
               ParsingErrorMessage="The {0} field has an incorrect date value." />
</label>

Production Date フィールドに無効な日付値が指定されています。

InputDate<TValue>InputNumber<TValue> では、エラー メッセージ テンプレートがサポートされています。

フォームの例」セクションの Starfleet Starship Database フォーム (FormExample2 コンポーネント) では、既定のエラー メッセージ テンプレートが使用されています。

The {0} field must be a date.

{0} プレースホルダーの位置は、エラーがユーザーに表示されるときに、DisplayName プロパティの値が表示される場所です。

<label>
    Production Date:
    <InputDate @bind-Value="starship.ProductionDate" />
</label>

ProductionDate フィールドは日付である必要があります。

カスタム メッセージを提供するには、カスタム テンプレートを ParsingErrorMessage に割り当てます。

<label>
    Production Date:
    <InputDate @bind-Value="starship.ProductionDate" 
               ParsingErrorMessage="The {0} field has an incorrect date value." />
</label>

ProductionDate フィールドに無効な日付値が指定されています。

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

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

注意

ASP.NET Core 参照ソースへのドキュメント リンクを使用すると、リポジトリの main ブランチが読み込まれます。このブランチは、ASP.NET Core の次回リリースに向けて行われている製品単位の現在の開発を表します。 別のリリースのブランチを選択するには、 [Switch branches or tags](ブランチまたはタグの切り替え) ドロップダウン リストを使用して、そのブランチを選択します。 たとえば、ASP.NET Core 5.0 リリースの場合は、release/5.0 ブランチを選択します。

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

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

検証コンポーネント

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

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

注意

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

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

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

CustomValidation.cs (テスト アプリで使用する場合は、名前空間 BlazorSample をアプリの名前空間に一致するように変更します):

using System;
using System.Collections.Generic;
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 == 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)
        {
            foreach (var err in errors)
            {
                messageStore.Add(CurrentEditContext.Field(err.Key), err.Value);
            }
    
            CurrentEditContext.NotifyValidationStateChanged();
        }
    
        public void ClearErrors()
        {
            messageStore.Clear();
            CurrentEditContext.NotifyValidationStateChanged();
        }
    }
}
using System;
using System.Collections.Generic;
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 == 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 ValidationMessageStore(CurrentEditContext);
    
            CurrentEditContext.OnValidationRequested += (s, e) => 
                messageStore.Clear();
            CurrentEditContext.OnFieldChanged += (s, e) => 
                messageStore.Clear(e.FieldIdentifier);
        }
    
        public void DisplayErrors(Dictionary<string, List<string>> errors)
        {
            foreach (var err in errors)
            {
                messageStore.Add(CurrentEditContext.Field(err.Key), err.Value);
            }
    
            CurrentEditContext.NotifyValidationStateChanged();
        }
    
        public void ClearErrors()
        {
            messageStore.Clear();
            CurrentEditContext.NotifyValidationStateChanged();
        }
    }
}

重要

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

" " 文字が含まれるため、タグ ヘルパーでタグ名 "<global namespace>.{CLASS NAME}" をターゲットにすることはできません。

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

注意

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

ビジネス ロジックの検証

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

次に例を示します。

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

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

Pages/FormExample4.razor:

@page "/form-example-4"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample4> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="@starship" OnValidSubmit="@HandleValidSubmit">
    <CustomValidation @ref="customValidation" />
    <ValidationSummary />

    <p>
        <label>
            Primary Classification:
            <InputSelect @bind-Value="starship.Classification">
                <option value="">Select classification ...</option>
                <option value="Exploration">Exploration</option>
                <option value="Diplomacy">Diplomacy</option>
                <option value="Defense">Defense</option>
            </InputSelect>
        </label>
    </p>
    <p>
        <label>
            Description (optional):
            <InputTextArea @bind-Value="starship.Description" />
        </label>
    </p>

    <button type="submit">Submit</button>

    <p>
        <a href="http://www.startrek.com/">Star Trek</a>,
        &copy;1966-2019 CBS Studios, Inc. and
        <a href="https://www.paramount.com">Paramount Pictures</a>
    </p>
</EditForm>

@code {
    private CustomValidation customValidation;
    private Starship starship = new() { ProductionDate = DateTime.UtcNow };

    private void HandleValidSubmit()
    {
        customValidation.ClearErrors();

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

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

        if (errors.Count() > 0)
        {
            customValidation.DisplayErrors(errors);
        }
        else
        {
            Logger.LogInformation("HandleValidSubmit called: Processing the form");

            // Process the valid form
        }
    }
}
@page "/form-example-4"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample4> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="@starship" OnValidSubmit="@HandleValidSubmit">
    <CustomValidation @ref="customValidation" />
    <ValidationSummary />

    <p>
        <label>
            Primary Classification:
            <InputSelect @bind-Value="starship.Classification">
                <option value="">Select classification ...</option>
                <option value="Exploration">Exploration</option>
                <option value="Diplomacy">Diplomacy</option>
                <option value="Defense">Defense</option>
            </InputSelect>
        </label>
    </p>
    <p>
        <label>
            Description (optional):
            <InputTextArea @bind-Value="starship.Description" />
        </label>
    </p>

    <button type="submit">Submit</button>

    <p>
        <a href="http://www.startrek.com/">Star Trek</a>,
        &copy;1966-2019 CBS Studios, Inc. and
        <a href="https://www.paramount.com">Paramount Pictures</a>
    </p>
</EditForm>

@code {
    private CustomValidation customValidation;
    private Starship starship = new Starship() { ProductionDate = DateTime.UtcNow };

    private void HandleValidSubmit()
    {
        customValidation.ClearErrors();

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

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

        if (errors.Count() > 0)
        {
            customValidation.DisplayErrors(errors);
        }
        else
        {
            Logger.LogInformation("HandleValidSubmit called: Processing the form");

            // Process the valid form
        }
    }
}

注意

検証コンポーネントを使用する代わりに、データ注釈検証属性を使用することもできます。 フォームのモデルに適用されるカスタム属性は、DataAnnotationsValidator コンポーネントを使用してアクティブ化されます。 サーバー側の検証で使用する場合、属性はサーバーで実行できる必要があります。 詳細については、「ASP.NET Core MVC でのモデルの検証」を参照してください。

サーバーの検証

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

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

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

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

<ItemGroup>
  <PackageReference Include="System.ComponentModel.Annotations" Version="{VERSION}" />
</ItemGroup>

{VERSION} プレースホルダーに対するパッケージのプレビュー版以外の最新バージョンを確認するには、System.ComponentModel.Annotations の NuGet.org でパッケージの バージョン履歴 を参照してください。

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

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

注意

このセクションの StarshipValidation コントローラーでは、Microsoft Identity 2.0 が使用されます。 Web API は、この API で "API.Access" スコープを持つユーザーのトークンのみが受け入れられます。 API のスコープ名が API.Access と異なる場合は、追加のカスタマイズが必要です。 Microsoft Identity 1.0 およびバージョン 5.0 より前の ASP.NET Core で動作するコントローラーのバージョンについては、この記事の前のバージョンを参照してください。

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

Controllers/StarshipValidation.cs:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Identity.Web.Resource;
using BlazorSample.Shared;

namespace BlazorSample.Server.Controllers
{
    [Authorize]
    [ApiController]
    [Route("[controller]")]
    public class StarshipValidationController : ControllerBase
    {
        private readonly ILogger<StarshipValidationController> logger;

        public StarshipValidationController(
            ILogger<StarshipValidationController> logger)
        {
            this.logger = logger;
        }

        static readonly string[] scopeRequiredByApi = new[] { "API.Access" };

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

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

                    // Process the valid form
                    // async ...

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

            return BadRequest(ModelState);
        }
    }
}

注意

このセクションの StarshipValidation コントローラーは、Microsoft Identity 1.0 での使用に適しています。 Microsoft Identity 2.0 および ASP.NET Core 5.0 以降で使用するには、追加の構成が必要です。 これらのテクノロジの更新バージョン用のコントローラーのバージョンを確認するには、この記事の以降のバージョンを参照してください。

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

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Identity.Web.Resource;
using BlazorSample.Shared;

namespace BlazorSample.Server.Controllers
{
    [Authorize]
    [ApiController]
    [Route("[controller]")]
    public class StarshipValidationController : ControllerBase
    {
        private readonly ILogger<StarshipValidationController> logger;

        public StarshipValidationController(
            ILogger<StarshipValidationController> logger)
        {
            this.logger = logger;
        }

        [HttpPost]
        public async Task<IActionResult> Post(Starship starship)
        {
            try
            {
                if (starship.Classification == "Defense" && 
                    string.IsNullOrEmpty(starship.Description))
                {
                    ModelState.AddModelError(nameof(starship.Description),
                        "For a 'Defense' ship " +
                        "classification, 'Description' is required.");
                }
                else
                {
                    logger.LogInformation("Processing the form asynchronously");

                    // Process the valid form
                    // async ...

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

            return BadRequest(ModelState);
        }
    }
}

ホストされている Blazor WebAssembly アプリで以前のコントローラーを使用している場合は、アプリのコントローラーの名前空間と一致するように名前空間 (BlazorSample.Server.Controllers) を更新します。

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

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

注意

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

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

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

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

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

using Microsoft.AspNetCore.Mvc;

Startup.ConfigureServicesAddControllersWithViews 拡張メソッドを見つけて、次の ConfigureApiBehaviorOptions の呼び出しを追加します。

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));
            }
        };
    });

詳細については、「ASP.NET Core Web API のエラーを処理する」を参照してください。

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

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

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

Pages/FormExample5.razor:

@page "/form-example-5"
@attribute [Authorize]
@using System.Net
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using Microsoft.Extensions.Logging
@using BlazorSample.Shared
@inject HttpClient Http
@inject ILogger<FormExample5> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="@starship" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <CustomValidation @ref="customValidation" />
    <ValidationSummary />

    <p>
        <label>
            Identifier:
            <InputText @bind-Value="starship.Identifier" disabled="@disabled" />
        </label>
    </p>
    <p>
        <label>
            Description (optional):
            <InputTextArea @bind-Value="starship.Description" 
                disabled="@disabled" />
        </label>
    </p>
    <p>
        <label>
            Primary Classification:
            <InputSelect @bind-Value="starship.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>
    </p>
    <p>
        <label>
            Maximum Accommodation:
            <InputNumber @bind-Value="starship.MaximumAccommodation" 
                disabled="@disabled" />
        </label>
    </p>
    <p>
        <label>
            Engineering Approval:
            <InputCheckbox @bind-Value="starship.IsValidatedDesign" 
                disabled="@disabled" />
        </label>
    </p>
    <p>
        <label>
            Production Date:
            <InputDate @bind-Value="starship.ProductionDate" disabled="@disabled" />
        </label>
    </p>

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

    <p style="@messageStyles">
        @message
    </p>

    <p>
        <a href="http://www.startrek.com/">Star Trek</a>,
        &copy;1966-2019 CBS Studios, Inc. and
        <a href="https://www.paramount.com">Paramount Pictures</a>
    </p>
</EditForm>

@code {
    private bool disabled;
    private string message;
    private string messageStyles = "visibility:hidden";
    private CustomValidation customValidation;
    private Starship starship = new() { ProductionDate = DateTime.UtcNow };

    private async Task HandleValidSubmit(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>>>();

            if (response.StatusCode == HttpStatusCode.BadRequest && 
                errors.Count() > 0)
            {
                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.";
        }
    }
}

注意

検証コンポーネントを使用する代わりに、データ注釈検証属性を使用することもできます。 フォームのモデルに適用されるカスタム属性は、DataAnnotationsValidator コンポーネントを使用してアクティブ化されます。 サーバー側の検証で使用する場合、属性はサーバーで実行できる必要があります。 詳細については、「ASP.NET Core MVC でのモデルの検証」を参照してください。

注意

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

入力イベントに基づく InputText

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

次の例では、ExampleModel クラスを使用しています。

ExampleModel.cs:

using System.ComponentModel.DataAnnotations;

public class ExampleModel
{
    [Required]
    [StringLength(10, ErrorMessage = "Name is too long.")]
    public string Name { get; set; }
}
using System.ComponentModel.DataAnnotations;

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

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

Shared/CustomInputText.razor:

@inherits InputText

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

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

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

Pages/FormExample6.razor:

@page "/form-example-6"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample6> Logger

<EditForm Model="@exampleModel" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <CustomInputText @bind-Value="exampleModel.Name" />

    <button type="submit">Submit</button>
</EditForm>

<p>
    CurrentValue: @exampleModel.Name
</p>

@code {
    private ExampleModel exampleModel = new();

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }
}
@page "/form-example-6"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample6> Logger

<EditForm Model="@exampleModel" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <CustomInputText @bind-Value="exampleModel.Name" />

    <button type="submit">Submit</button>
</EditForm>

<p>
    CurrentValue: @exampleModel.Name
</p>

@code {
    private ExampleModel exampleModel = new ExampleModel();

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }
}

ラジオ ボタン

このセクションの例は、この記事の「フォームの例」セクションの Starfleet Starship Database フォームが基になっています。

アプリに以下の enumを追加します。 それらを保持する新しいファイルを作成するか、Starship.cs ファイルに追加します。

public class ComponentEnums
{
    public enum Manufacturer { SpaceX, NASA, ULA, VirginGalactic, Unknown }
    public enum Color { ImperialRed, SpacecruiserGreen, StarshipBlue, VoyagerOrange }
    public enum Engine { Ion, Plasma, Fusion, Warp }
}

以下から enums にアクセスできるようにします。

  • Starship.csStarship モデル (たとえば、enums クラスが ComponentEnums という名前の場合は using static ComponentEnums;)。
  • Starfleet Starship Database フォーム (たとえば、enums クラスが ComponentEnums という名前の場合は @using static ComponentEnums)。

ラジオ ボタン グループを作成するには、InputRadioGroup<TValue> コンポーネントと共に InputRadio<TValue> コンポーネントを使用します。 次の例では、「フォームの例」セクションで説明されている Starship モデルにプロパティが追加されています。

[Required]
[Range(typeof(Manufacturer), nameof(Manufacturer.SpaceX), 
    nameof(Manufacturer.VirginGalactic), ErrorMessage = "Pick a manufacturer.")]
public Manufacturer Manufacturer { get; set; } = Manufacturer.Unknown;

[Required, EnumDataType(typeof(Color))]
public Color? Color { get; set; } = null;

[Required, EnumDataType(typeof(Engine))]
public Engine? Engine { get; set; } = null;

フォームの例」セクションの Starfleet Starship Database フォーム (FormExample2 コンポーネント) を更新します。 生成するコンポーネントを追加します。

  • 船舶製造元のラジオ ボタン グループ。
  • エンジンと船の色に関する入れ子になったラジオ ボタン グループ。

注意

入れ子になったラジオ ボタン グループは、フォーム コントロールのレイアウトが乱れ、ユーザーを混乱させる可能性があるため、フォームではあまり使用されません。 ただし、船のエンジンと船の色の 2 つのユーザー入力に関する推奨事項を組み合わせる次の例のように、UI の設計において意味のある場合があります。 フォームの検証では、1 つのエンジンと 1 つの色が必要です。 フォームのレイアウトでは、入れ子になった InputRadioGroup<TValue> を使用して、エンジンと色の推奨設定が組み合わされています。 ただし、ユーザーは任意のエンジンを任意の色と組み合わせて、フォームを送信できます。

<p>
    <InputRadioGroup @bind-Value="starship.Manufacturer">
        Manufacturer:
        <br>
        @foreach (var manufacturer in (Manufacturer[])Enum
            .GetValues(typeof(Manufacturer)))
        {
            <InputRadio Value="manufacturer" />
            <text>&nbsp;</text>@manufacturer<br>
        }
    </InputRadioGroup>
</p>
<p>
    Select one engine and one color. Recommendations are paired but any 
    combination of engine and color is allowed:<br>
    <InputRadioGroup Name="engine" @bind-Value="starship.Engine">
        <InputRadioGroup Name="color" @bind-Value="starship.Color">
            <InputRadio Name="engine" Value="Engine.Ion" />
            Engine: Ion<br>
            <InputRadio Name="color" Value="Color.ImperialRed" />
            Color: Imperial Red<br><br>
            <InputRadio Name="engine" Value="Engine.Plasma" />
            Engine: Plasma<br>
            <InputRadio Name="color" Value="Color.SpacecruiserGreen" />
            Color: Spacecruiser Green<br><br>
            <InputRadio Name="engine" Value="Engine.Fusion" />
            Engine: Fusion<br>
            <InputRadio Name="color" Value="Color.StarshipBlue" />
            Color: Starship Blue<br><br>
            <InputRadio Name="engine" Value="Engine.Warp" />
            Engine: Warp<br>
            <InputRadio Name="color" Value="Color.VoyagerOrange" />
            Color: Voyager Orange
        </InputRadioGroup>
    </InputRadioGroup>
</p>

注意

Name を省略した場合、InputRadio<TValue> コンポーネントは最新の先祖を基準にグループ化されます。

フォームでオプション ボタンを使用する場合、オプション ボタンはグループとして評価されるため、データ バインディングが他の要素と異なる方法で処理されます。 各オプション ボタンの値は固定ですが、オプション ボタン グループの値は、選択されたオプション ボタンの値です。 以下の例では、次のことを行っています。

  • オプション ボタン グループのデータバインディングを処理する。
  • カスタム InputRadio<TValue> コンポーネントを使用した検証をサポートする。

Shared/InputRadio.razor:

@using System.Globalization
@typeparam TValue
@inherits InputBase<TValue>

<input @attributes="AdditionalAttributes" type="radio" value="@SelectedValue" 
       checked="@(SelectedValue.Equals(Value))" @onchange="OnChange" />

@code {
    [Parameter]
    public TValue SelectedValue { get; set; }

    private void OnChange(ChangeEventArgs args)
    {
        CurrentValueAsString = args.Value.ToString();
    }

    protected override bool TryParseValueFromString(string value, 
        out TValue result, out string errorMessage)
    {
        var success = BindConverter.TryConvertTo<TValue>(
            value, CultureInfo.CurrentCulture, out var parsedValue);
        if (success)
        {
            result = parsedValue;
            errorMessage = null;

            return true;
        }
        else
        {
            result = default;
            errorMessage = $"{FieldIdentifier.FieldName} field isn't valid.";

            return false;
        }
    }
}

次の RadioButtonExample コンポーネントでは、前の InputRadio コンポーネントを使用して、ユーザーから評価を取得して検証しています。

Pages/RadioButtonExample.razor:

@page "/radio-button-example"
@using System.ComponentModel.DataAnnotations
@using Microsoft.Extensions.Logging
@inject ILogger<RadioButtonExample> Logger

<h1>Radio Button Example</h1>

<EditForm Model="@model" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    @for (int i = 1; i <= 5; i++)
    {
        <label>
            <InputRadio name="rate" SelectedValue="@i" @bind-Value="model.Rating" />
            @i
        </label>
    }

    <button type="submit">Submit</button>
</EditForm>

<p>You chose: @model.Rating</p>

@code {
    private Model model = new();

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }

    public class Model
    {
        [Range(1, 5)]
        public int Rating { get; set; }
    }
}

C# オブジェクトの null 値への <select> 要素オプションのバインド

次の理由により、<select> 要素のオプション値を C# オブジェクトの null 値として表すよい方法はありません。

  • HTML 属性には null 値を設定できません。 HTML で null に最も近いのは、<option> 要素から HTML の value 属性を除去することです。
  • value 属性のない <option> を選択すると、ブラウザーではその <option> の要素の "テキスト コンテンツ" として値が扱われます。

Blazor フレームワークでは、次の処理が必要になるため、既定の動作の抑制は試みられません。

  • フレームワークでの特殊なケースの回避策のチェーンの作成。
  • 現在のフレームワークの動作に対する破壊的変更。

HTML で null に最も近いと思われるものは、"空の文字列" の value です。 Blazor フレームワークでは、<select> の値への双方向バインドに対する空の文字列変換として null が処理されます。

Blazor フレームワークでは、<select> の値への双方向バインドを試みるときに、null は空の文字列変換として自動的に処理されません。 詳細については、「null 値への <select> のバインドの修正 (dotnet/aspnetcore #23221)」を参照してください。

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

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

<ValidationSummary />

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

<ValidationSummary Model="@starship" />

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

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

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

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

.validation-message {
    color: red;
}

カスタム検証属性

カスタム検証属性を使用するときに、検証結果がフィールドに正しく関連付けられるようにするには、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.GetServicenullです。 IsValid メソッドでの検証に対する挿入サービスはサポートされていません。

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

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

次の例では、ExampleModel クラスを使用しています。

ExampleModel.cs:

using System.ComponentModel.DataAnnotations;

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

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

wwwroot/css/app.css (Blazor WebAssembly) または wwwroot/css/site.css (Blazor Server):

.validField {
    border-color: lawngreen;
}

.invalidField {
    background-color: tomato;
}

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

CustomFieldClassProvider.cs:

using System.Linq;
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 クラス プロバイダーとして設定します。

Pages/FormExample7.razor:

@page "/form-example-7"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample7> Logger

<EditForm EditContext="@editContext" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <InputText id="name" @bind-Value="exampleModel.Name" />

    <button type="submit">Submit</button>
</EditForm>

@code {
    private ExampleModel exampleModel = new();
    private EditContext editContext;

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

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }
}

前の例では、すべてのフォーム フィールドの有効性をチェックし、各フィールドにスタイルを適用しています。 フォームを使用してフィールドのサブセットにのみカスタム スタイルを適用する場合は、スタイルが CustomFieldClassProvider によって条件的に適用されるようにします。 次の CustomFieldClassProvider2 の例では、スタイルが Name フィールドに適用されるだけです。 名前が Name と一致しないフィールドの場合は、string.Empty が返され、スタイルは適用されません。

CustomFieldClassProvider2.cs:

using System.Linq;
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;
    }
}

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

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

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

<InputText id="description" @bind-Value="exampleModel.Description" />

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

editContext.SetFieldCssClassProvider(new CustomFieldClassProvider2());

CSS 検証クラスは Description フィールド (id="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 System.Linq;
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 検証スタイルが使用されます。

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

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

注意

Microsoft.AspNetCore.Components.DataAnnotations.Validation パッケージには、Nuget.org に "リリース候補" の最新バージョンが含まれています。現時点では、"試験的" リリース候補パッケージを継続して使用します。 パッケージのアセンブリは、将来のリリースでフレームワークまたはランタイムのいずれかに移動される可能性があります。 更新の詳細については、Announcements GitHub リポジトリdotnet/aspnetcore GitHub リポジトリ、またはこのトピック セクションを参照してください。

[CompareProperty] 属性

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

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

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

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

<EditForm Model="@model" OnValidSubmit="@HandleValidSubmit">
    <ObjectGraphDataAnnotationsValidator />
    ...
</EditForm>

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

Starship.cs:

using System;
using System.ComponentModel.DataAnnotations;

public class Starship
{
    ...

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

    ...
}
using System;
using System.ComponentModel.DataAnnotations;

public class Starship
{
    ...

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

    ...
}

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 フォーム (FormExample2 コンポーネント) の簡略版を使用します。 Starship の他のプロパティは、Starship 型のインスタンスが作成されるときに、有効な既定値を受け取ります。
  • コンポーネントを初期化するときに、フォームの EditContext を使用してモデルを割り当てます。
  • コンテキストの OnFieldChanged コールバックでフォームを検証して、送信ボタンを有効または無効にします。
  • IDisposable を実装し、Dispose メソッドでイベント ハンドラーの登録を解除します。 詳細については、「ASP.NET Core Razor コンポーネントのライフサイクル」を参照してください。

注意

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

Pages/FormExample8.razor:

@page "/form-example-8"
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<FormExample8> Logger

<EditForm EditContext="@editContext" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <p>
        <label>
            Identifier:
            <InputText @bind-Value="starship.Identifier" />
        </label>
    </p>

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

@code {
    private Starship starship = 
        new()
        {
            Identifier = "NCC-1701",
            Classification = "Exploration",
            MaximumAccommodation = 150,
            IsValidatedDesign = true,
            ProductionDate = new DateTime(2245, 4, 11)
        };
    private bool formInvalid = false;
    private EditContext editContext;

    protected override void OnInitialized()
    {
        editContext = new(starship);
        editContext.OnFieldChanged += HandleFieldChanged;
    }

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

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }

    public void Dispose()
    {
        editContext.OnFieldChanged -= HandleFieldChanged;
    }
}
@page "/form-example-8"
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<FormExample8> Logger

<EditForm EditContext="@editContext" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <p>
        <label>
            Identifier:
            <InputText @bind-Value="starship.Identifier" />
        </label>
    </p>

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

@code {
    private Starship starship = 
        new Starship()
        {
            Identifier = "NCC-1701",
            Classification = "Exploration",
            MaximumAccommodation = 150,
            IsValidatedDesign = true,
            ProductionDate = new DateTime(2245, 4, 11)
        };
    private bool formInvalid = false;
    private EditContext editContext;

    protected override void OnInitialized()
    {
        editContext = new EditContext(starship);
        editContext.OnFieldChanged += HandleFieldChanged;
    }

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

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }

    public void Dispose()
    {
        editContext.OnFieldChanged -= HandleFieldChanged;
    }
}

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

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

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

    ...

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

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

    ...

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

トラブルシューティング

InvalidOperationException:EditForm には、Model パラメーターまたは EditContext パラメーター (両方ではなく) が必要です。

EditForm によって Model または EditContext が割り当てられることを確認します。 同じフォームに両方を使用しないでください。

Model に割り当てるときは、次の例に示すように、そのモデルの種類がインスタンス化されていることを確認します。

private ExampleModel exampleModel = new();
private ExampleModel exampleModel = new ExampleModel();

その他のリソース