ASP.NET Core Blazor フォームのバインド

Note

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

重要

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

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

この記事では、Blazor フォームでバインディングを使う方法について説明します。

EditForm/EditContext モデル

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

モデル バインド

EditForm.Model への割り当て:

<EditForm ... Model="Model" ...>
    ...
</EditForm>

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

    protected override void OnInitialized() => Model ??= new();
}
<EditForm ... Model="Model" ...>
    ...
</EditForm>

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

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

メモ

この記事のフォーム モデルの例のほとんどでフォームを C# プロパティにバインドしますが、C# フィールド バインドもサポートされています。

コンテキスト バインド

EditForm.EditContext への割り当て:

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

@code {
    private EditContext? editContext;

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

    protected override void OnInitialized()
    {
        Model ??= new();
        editContext = new(Model);
    }
}
<EditForm ... EditContext="editContext" ...>
    ...
</EditForm>

@code {
    private EditContext? editContext;

    public Starship? Model { get; set; }

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

EditContextまたはModelいずれかEditForm に割り当てます。 両方が割り当てられている場合、ランタイム エラーがスローされます。

サポートされている型

バインドでサポートされるもの:

  • プリミティブ型
  • コレクション
  • 複合型
  • 再帰型
  • コンストラクターのある型
  • 列挙体

[DataMember] 属性と [IgnoreDataMember] 属性を使用してモデル バインドをカスタマイズすることもできます。 これらの属性を使用してプロパティの名前を変更したり、プロパティを無視したり、必要に応じてプロパティをマークしたりします。

その他のバインド オプション

AddRazorComponents を呼び出すとき、RazorComponentsServiceOptions から追加のモデル バインド オプションを利用できます。

フレームワークによって割り当てられる既定値を次に示します。

builder.Services.AddRazorComponents(options =>
{
    options.FormMappingUseCurrentCulture = true;
    options.MaxFormMappingCollectionSize = 1024;
    options.MaxFormMappingErrorCount = 200;
    options.MaxFormMappingKeySize = 1024 * 2;
    options.MaxFormMappingRecursionDepth = 64;
}).AddInteractiveServerComponents();

フォーム名

フォーム名を割り当てるには、FormName パラメーターを使用します。 モデル データをバインドするには、フォーム名を一意にする必要があります。 次の形式には RomulanAle という名前が付いています。

<EditForm ... FormName="RomulanAle" ...>
    ...
</EditForm>

フォーム名の指定:

  • 静的にレンダリングされたサーバー側コンポーネントによって送信されるすべてのフォームに必要です。
  • Blazor WebAssembly アプリのフォームや、対話型レンダリング モードのコンポーネントが含まれる、対話形式でレンダリングされたコンポーネントによって送信されるフォームには必要ありません。 ただし、フォームのインタラクティビティが削除された場合に実行時のフォームの投稿エラーを防ぐために、すべてのフォームに一意のフォーム名を指定することをお勧めします。

フォーム名は、静的にレンダリングされたサーバー側コンポーネントからの従来の HTTP POST 要求としてフォームがエンドポイントにポストされた場合にのみチェックされます。 フレームワークはフォームのレンダリング時点では例外をスローせず、HTTP POST が到着し、フォーム名が指定されない時点でのみスローされます。

既定では、アプリのルート コンポーネントの上に名前のない (空の文字列) フォーム スコープがあります。これは、アプリにフォーム名の競合がない場合に十分です。 ライブラリからのフォームを含め、ライブラリの開発者が使用するフォーム名を制御できない場合など、フォーム名の競合が発生する可能性がある場合は、Blazor Web アプリのメイン プロジェクトの FormMappingScope コンポーネントにフォーム名スコープを指定します。

次の例では、HelloFormFromLibrary コンポーネントには Hello という名前のフォームがあり、ライブラリに含まれています。

HelloFormFromLibrary.razor:

<EditForm FormName="Hello" Model="this" OnSubmit="Submit">
    <InputText @bind-Value="Name" />
    <button type="submit">Submit</button>
</EditForm>

@if (submitted)
{
    <p>Hello @Name from the library's form!</p>
}

@code {
    bool submitted = false;

    [SupplyParameterFromForm]
    public string? Name { get; set; }

    private void Submit() => submitted = true;
}

次の NamedFormsWithScope コンポーネントは、ライブラリの HelloFormFromLibrary コンポーネントを使用し、同じく Hello という名前のフォームを持っています。 FormMappingScope コンポーネントのスコープ名は ParentContext で、HelloFormFromLibrary コンポーネントによって提供されるすべてのフォーム用です。 この例の両方のフォームにはフォーム名 (Hello) がありますが、フォーム名は競合せず、イベントはフォーム POST イベントの正しいフォームにルーティングされます。

NamedFormsWithScope.razor:

@page "/named-forms-with-scope"

<div>Hello form from a library</div>

<FormMappingScope Name="ParentContext">
    <HelloFormFromLibrary />
</FormMappingScope>

<div>Hello form using the same form name</div>

<EditForm FormName="Hello" Model="this" OnSubmit="Submit">
    <InputText @bind-Value="Name" />
    <button type="submit">Submit</button>
</EditForm>

@if (submitted)
{
    <p>Hello @Name from the app form!</p>
}

@code {
    bool submitted = false;

    [SupplyParameterFromForm]
    public string? Name { get; set; }

    private void Submit() => submitted = true;
}

フォームからパラメーターを提供します ([SupplyParameterFromForm])

[SupplyParameterFromForm] 属性は、関連付けられているプロパティの値をフォームのフォーム データから指定する必要があることを示します。 プロパティの名前と一致する要求内のデータは、プロパティにバインドされます。 InputBase<TValue> に基づく入力により、Blazor でモデル バインドに使用される名前に一致するフォーム値名が生成されます。

[SupplyParameterFromForm] 属性には、次のフォーム バインド パラメーターを指定できます。

  • Name: パラメーターの名前を取得または設定します。 この名前は、フォーム データとの照合に使用するプレフィックスを決定し、値をバインドする必要があるかどうか判断するために使用されます。
  • FormName: ハンドラーの名前を取得または設定します。 この名前は、フォーム名でパラメーターとフォームを照合し、値をバインドする必要があるかどうか判断するために使用されます。

次の例では、2 つのフォームを他に依存せずフォーム名でモデルにバインドします。

Starship6.razor:

@page "/starship-6"
@inject ILogger<Starship6> Logger

<EditForm Model="Model1" OnSubmit="Submit1" FormName="Holodeck1">
    <div>
        <label>
            Holodeck 1 Identifier: 
            <InputText @bind-Value="Model1!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

<EditForm Model="Model2" OnSubmit="Submit2" FormName="Holodeck2">
    <div>
        <label>
            Holodeck 2 Identifier: 
            <InputText @bind-Value="Model2!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    [SupplyParameterFromForm(FormName = "Holodeck1")]
    public Holodeck? Model1 { get; set; }

    [SupplyParameterFromForm(FormName = "Holodeck2")]
    public Holodeck? Model2 { get; set; }

    protected override void OnInitialized()
    {
        Model1 ??= new();
        Model2 ??= new();
    }

    private void Submit1()
    {
        Logger.LogInformation("Submit1: Id = {Id}", Model1?.Id);
    }

    private void Submit2()
    {
        Logger.LogInformation("Submit2: Id = {Id}", Model2?.Id);
    }

    public class Holodeck
    {
        public string? Id { get; set; }
    }
}

フォームの入れ子とバインド

次のガイダンスでは、子フォームを入れ子にしてバインドする方法を示します。

次の船の詳細クラス (ShipDetails) には、サブフォームの説明と長さが保持されています。

ShipDetails.cs:

namespace BlazorSample;

public class ShipDetails
{
    public string? Description { get; set; }
    public int? Length { get; set; }
}

次の Ship クラスによって識別子に名前が付けられます (Id)。また、船の詳細が含まれます。

Ship.cs:

namespace BlazorSample
{
    public class Ship
    {
        public string? Id { get; set; }
        public ShipDetails Details { get; set; } = new();
    }
}

次のサブフォームは、ShipDetails 型の値を編集するために使用されます。 これは、コンポーネントの上部にある Editor<T> を継承することで実装されます。 Editor<T> により確実に、次の例のTShipDetails であるモデル (T) に基づいて子コンポーネントで正しいフォーム フィールド名が生成されます。

StarshipSubform.razor:

@inherits Editor<ShipDetails>

<div>
    <label>
        Description: 
        <InputText @bind-Value="Value!.Description" />
    </label>
</div>
<div>
    <label>
        Length: 
        <InputNumber @bind-Value="Value!.Length" />
    </label>
</div>

メイン フォームは Ship クラスにバインドされます。 StarshipSubform コンポーネントは、Model!.Details としてバインドされた船の詳細を編集するために使用されます。

Starship7.razor:

@page "/starship-7"
@inject ILogger<Starship7> Logger

<EditForm Model="Model" OnSubmit="Submit" FormName="Starship7">
    <div>
        <label>
            Identifier: 
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <StarshipSubform @bind-Value="Model!.Details" />
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    [SupplyParameterFromForm]
    public Ship? Model { get; set; }

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

    private void Submit()
    {
        Logger.LogInformation("Id = {Id} Desc = {Description} Length = {Length}",
            Model?.Id, Model?.Details?.Description, Model?.Details?.Length);
    }
}

高度なフォーム マッピングのエラー シナリオ

フレームワークによりフォームの FormMappingContext がインスタンス化され、データが入力されます。これは、特定のフォームのマッピング操作に関連付けられているコンテキストです。 各マッピング スコープ (FormMappingScope コンポーネントによって定義) により FormMappingContext がインスタンス化されます。 [SupplyParameterFromForm] でコンテキストに値が要求されるたびに、試行された値と、マッピング エラーがあればそれがフレームワークによって FormMappingContext に入力されます。

FormMappingContext は主に、検証エラーとしてマッピング エラーを示すための、InputBase<TValue>EditContext、その他の内部実行のデータソースであるため、開発者がこれを直接操作することは想定されていません。 高度なカスタム シナリオでは、試行された値とマッピング エラーを使用するカスタム コードを記述するため、開発者は FormMappingContext に直接、[CascadingParameter] としてアクセスできます。

ラジオ ボタン

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

アプリに以下の 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 }
}

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

  • Starship.csStarship モデル (たとえば、using static ComponentEnums;)。
  • Starfleet Starship Database フォーム (Starship3.razor) (たとえば、@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 フォーム (Starship3 コンポーネント) を更新します。 生成するコンポーネントを追加します。

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

Note

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

メモ

次の例では、必ずコンポーネントで ComponentEnums クラスを使用できるようにします。

@using static ComponentEnums
<fieldset>
    <legend>Manufacturer</legend>
    <InputRadioGroup @bind-Value="Model!.Manufacturer">
        @foreach (var manufacturer in Enum.GetValues<Manufacturer>())
        {
            <div>
                <label>
                    <InputRadio Value="manufacturer" />
                    @manufacturer
                </label>
            </div>
        }
    </InputRadioGroup>
</fieldset>

<fieldset>
    <legend>Engine and Color</legend>
    <p>
        Engine and color pairs are recommended, but any
        combination of engine and color is allowed.
    </p>
    <InputRadioGroup Name="engine" @bind-Value="Model!.Engine">
        <InputRadioGroup Name="color" @bind-Value="Model!.Color">
            <div style="margin-bottom:5px">
                <div>
                    <label>
                        <InputRadio Name="engine" Value="Engine.Ion" />
                        Ion
                    </label>
                </div>
                <div>
                    <label>
                        <InputRadio Name="color" Value="Color.ImperialRed" />
                        Imperial Red
                    </label>
                </div>
            </div>
            <div style="margin-bottom:5px">
                <div>
                    <label>
                        <InputRadio Name="engine" Value="Engine.Plasma" />
                        Plasma
                    </label>
                </div>
                <div>
                    <label>
                        <InputRadio Name="color" Value="Color.SpacecruiserGreen" />
                        Spacecruiser Green
                    </label>
                </div>
            </div>
            <div style="margin-bottom:5px">
                <div>
                    <label>
                        <InputRadio Name="engine" Value="Engine.Fusion" />
                        Fusion
                    </label>
                </div>
                <div>
                    <label>
                        <InputRadio Name="color" Value="Color.StarshipBlue" />
                        Starship Blue
                    </label>
                </div>
            </div>
            <div style="margin-bottom:5px">
                <div>
                    <label>
                        <InputRadio Name="engine" Value="Engine.Warp" />
                        Warp
                    </label>
                </div>
                <div>
                    <label>
                        <InputRadio Name="color" Value="Color.VoyagerOrange" />
                        Voyager Orange
                    </label>
                </div>
            </div>
        </InputRadioGroup>
    </InputRadioGroup>
</fieldset>

メモ

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

入力コンポーネント」記事の「フォームの例」セクションの Starship3 コンポーネントに上記の Razor マークアップを実装した場合は、Submit メソッドのログ記録を更新します:

Logger.LogInformation("Id = {Id} Description = {Description} " +
    "Classification = {Classification} MaximumAccommodation = " +
    "{MaximumAccommodation} IsValidatedDesign = " +
    "{IsValidatedDesign} ProductionDate = {ProductionDate} " +
    "Manufacturer = {Manufacturer}, Engine = {Engine}, " +
    "Color = {Color}",
    Model?.Id, Model?.Description, Model?.Classification,
    Model?.MaximumAccommodation, Model?.IsValidatedDesign,
    Model?.ProductionDate, Model?.Manufacturer, Model?.Engine, 
    Model?.Color);

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

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

InputRadio.razor:

@using System.Globalization
@inherits InputBase<TValue>
@typeparam 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 = "The field isn't valid.";

            return false;
        }
    }
}

ジェネリック型パラメーター (@typeparam) の詳細については、次の記事を参照してください。

次のモデル例を使用します。

StarshipModel.cs:

using System.ComponentModel.DataAnnotations;

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

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

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++)
    {
        <div>
            <label>
                <InputRadio name="rate" SelectedValue="i" 
                    @bind-Value="Model.Rating" />
                @i
            </label>
        </div>
    }

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

<div>@Model.Rating</div>

@code {
    public StarshipModel Model { get; set; }

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

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