ASP.NET Core Blazor 表单绑定

注意

此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

对于当前版本,请参阅此文的 .NET 8 版本

本文介绍如何在表单中使用 Blazor 绑定。

EditForm/EditContext 模型

EditForm 基于分配的对象创建了 EditContext,用作窗体中其他组件的级联值EditContext 跟踪有关编辑进程的元数据,其中包括已修改的窗体字段和当前的验证消息。 分配给 EditForm.ModelEditForm.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);
    }
}

EditContextModel 分配给 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 的表单。 对于 HelloFormFromLibrary 组件提供的任何表单,FormMappingScope 组件的范围名称为 ParentContext。 尽管本示例中的两个表单都具有表单名称 (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:获取或设置句柄的名称。 该名称用于按窗体名称将参数与窗体匹配,以确定是否需要绑定值。

以下示例按窗体名称单独将两个窗体绑定到其模型。

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> 确保子组件根据模型 (T) 生成正确的窗体字段名称,以下示例中的 TShipDetails

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 和其他内部实现的数据源,以将映射错误显示为验证错误。 在高级自定义方案中,开发人员可以直接以 [CascadingParameter] 的形式访问 FormMappingContext,以编写使用尝试的值和映射错误的自定义代码。

单选按钮

本部分中的示例基于本文窗体示例部分的 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 }
}

使 enums 类可供以下项访问:

  • Starship.cs 中的 Starship 模型(例如,using static ComponentEnums;)。
  • Starfleet Starship Database 窗体 (Starship3.razor)(例如 @using static ComponentEnums)。

结合使用 InputRadio<TValue> 组件和 InputRadioGroup<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 组件)。 添加组件以生成:

  • 用于选择飞船制造商的单选按钮组。
  • 用于引擎和 ship 颜色的嵌套式单选按钮组。

注意

在窗体中,嵌套的单选按钮组并不常用,因为它们可能会导致窗体控件的无序布局,而这会使用户感到困惑。 不过,在某些情况下,这些在 UI 设计中是有意义的,如以下示例所示,为两个用户输入 ship 引擎和 ship 颜色配对建议。 表单验证需要一个引擎和一种颜色。 窗体的布局使用嵌套的 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");
    }
}