用户使用表单输入数据。 在经典 Web 应用中,可以使用 <form> 元素创建表单,并允许用户使用 <input> 元素提供数据。 可在用户提交表单时验证输入。 如果验证成功,则可以执行相应的操作,例如使用提供的信息向数据库添加新条目或更新记录。

<form> 和 <input> 元素提供的功能非常简单,但相对基础。 Blazor 扩展了表单功能及其 <EditForm> 组件。 此外,Blazor 提供了一系列专用输入元素,可用于设置用户所输入数据的格式并进行验证。

在本单元中,你将了解如何使用 <EditForm> 元素和输入元素来生成功能性表单。 你还将了解如何使用表单进行数据绑定。

什么是 EditForm?

EditForm 是一个 Blazor 组件,它在 Blazor 页面上履行 HTML 表单这一角色。 EditForm 和 HTML 表单之间的主要区别是:

  • 数据绑定:可将对象与 EditForm 关联。 EditForm 的作用类似于用于数据输入和显示的对象视图。
  • 验证EditForm 提供了广泛且可扩展的验证功能。 可以向指定验证规则的 EditForm 中的元素添加属性。 EditForm 将自动应用这些规则。 将在本模块的后续单元中介绍此功能。
  • 表单提交:HTML 表单将在提交后向表单处理程序发送一个发布请求。 该表单处理程序应会执行提交过程,然后显示任何结果。 EditForm 遵循 Blazor 事件模型;请指定捕获 OnSubmit 事件的 C# 事件处理程序。 事件处理程序执行提交逻辑。
  • 输入元素:HTML 表单使用 <input> 控件收集用户输入,并使用 submit 按钮发布表单以供处理。 EditForm 可以使用这些相同的元素,但 Blazor 提供了具有其他功能(例如内置验证和数据绑定)的输入组件库。

创建具有数据绑定的 EditForm

<EditForm> 元素支持使用 Model 参数进行数据绑定。 指定一个对象作为此形参的实参。 EditForm 中的输入元素可使用 @bind-Value 参数绑定到由模型公开的属性和字段。 下面的示例基于由默认 Blazor Server 应用模板创建的 WeatherForecast 类。 该类如下所示:

public class WeatherForecast
{
    public DateTime Date { get; set; }

    public int TemperatureC { get; set; }

    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

    public string Summary { get; set; }
}

EditForm 的模型是存储在 @currentForecast 变量中的 WeatherForecast 类的实例,并且输入元素绑定到该类中的字段:

@page "/fetchdata"

@using WebApplication.Data
@inject WeatherForecastService ForecastService

<h1>Weather forecast</h1>

<input type="number" width="2" min="0" max="@upperIndex" @onchange="ChangeForecast" value="@index"/>

<EditForm Model=@currentForecast>
    <InputDate @bind-Value=currentForecast.Date></InputDate>
    <InputNumber @bind-Value=currentForecast.TemperatureC></InputNumber>
    <InputText @bind-Value=currentForecast.Summary></InputText>
</EditForm>

@code {
    private WeatherForecast[] forecasts;
    private WeatherForecast currentForecast;
    private int index = 0;
    private int upperIndex = 0;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
        currentForecast = forecasts[index];
        upperIndex = forecasts.Count() - 1;
    }

    private async Task ChangeForecast(ChangeEventArgs e)
    {
        index = int.Parse(e.Value as string);
        if (index <= upperIndex && index >= 0)
        {
            currentForecast = forecasts[index];
        }
    }
}

在此示例中,OnInitialized 事件使用外部服务填充 WeatherForecast 对象数组。 currentForecast 变量设置为数组中的第一个项;这是由 EditForm 显示的对象。 用户可以使用页面上 EditForm 上方的数值输入域在数组中循环。 将此字段的值用作数组的索引,并使用 ChangeForecast 方法将 currentForecast 变量设置为在该索引处找到的对象。

下图显示了运行以下项的示例:

Screenshot of the EditForm containing controls bound to a WeatherForecast object.

 重要

EditForm 组件实现双向数据绑定。 表单会显示从模型中检索到的值,但用户可以在表单中更新这些值,并将其推送回该模型。

了解 Blazor 输入控件

HTML <form> 元素支持 <input> 元素,以便用户能够输入数据。 <input> 有一个 type 属性,用于指定输入的类型及其显示方式(作为数字、文本框、单选按钮、复选框、按钮等)。

Blazor 拥有自己的一组组件,旨在专用于 <EditForm> 元素并支持其他功能中的数据绑定。 下表列出了这些组件。 当 Blazor 呈现包含这些组件的页面时,它们将转换为表中列出的相应 HTML <input> 元素。 一些 Blazor 组件是通用的;类型参数由 Blazor 运行时根据绑定到元素的数据类型确定:

输入组件 呈现为 (HTML)
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> 一组子单选按钮
InputSelect<TValue> <select>
InputText <input>
InputTextArea <textarea>

这些元素中的每一个都具有由 Blazor 识别的属性,例如 DisplayName(用于将输入元素与标签关联)和 @ref(可用于保存对 C# 变量中字段的引用)。 任何无法识别的非 Blazor 属性都将按原样传递给 HTML 呈现器。 这意味着可以利用 HTML 输入元素属性。 例如,可以将 minmax 和 step 属性添加到 InputNumber 组件,它们将作为所呈现的 <input type="number"> 元素一部分正常运行。 在前面的示例中,可以将 TemperatureC 输入域指定为:

<EditForm Model=@currentForecast>
    <InputNumber @bind-Value=currentForecast.TemperatureC width="5" min="-100" step="5"></InputNumber>
</EditForm>

下一个示例展示了如何使用 InputRadioGroup<TValue> 和 InputRadio<TValue> 组件。 通常使用单选按钮组来呈现一系列单选按钮。 用户可以通过这些按钮从给定的集合中选择一个值。 一个 EditForm 可以包含多个 RadioButtonGroup<TValue> 组件,并且每个组都可以绑定到模型中的 EditForm 字段。 以下示例展示了来自服装店应用的详细信息。 该表单显示了 T 恤的数据。 Shirt 模型类如下所示:

public enum ShirtColor
{
    Red, Blue, Yellow, Green, Black, White
};

public enum ShirtSize
{
    Small, Medium, Large, ExtraLarge
};

public class Shirt
{
    public ShirtColor Color { get; set; }
    public ShirtSize Size { get; set; }
    public decimal Price;
}

请注意,T 恤的颜色和尺寸被指定为枚举项。 在以下 Razor 页中,代码将创建一个 Shirt 对象来充当测试数据。 <EditForm> 元素绑定到此对象。 该表单显示了 T 恤的尺寸、颜色和价格。 第一个 <InputRadioGroup> 元素附加到 Size 属性。 foreach 循环遍历枚举中的可能值并为每个值创建一个 <InputRadio> 元素。 <InputRadio> 元素的 Name 属性必须与 <InputRadioGroup> 元素的该属性匹配;HTML 呈现器使用此属性将组和单选按钮绑定在一起。 将第二个 <InputRadioGroup> 元素附加到 Color 属性,并使用相同的方法生成每个尺寸的单选按钮。 最后一个元素使用 <InputNumber> 元素显示价格。 此元素应用 HTML <input> 元素可用的 maxmin 和 step 属性。 此示例使用 <label> 元素显示与每个组件关联的值名称。

<EditForm Model="@shirt">
    <label>
        <h3>Size</h3>
        <InputRadioGroup Name="size" @bind-Value=shirt.Size>
            @foreach(var shirtSize in Enum.GetValues(typeof(ShirtSize)))
            {
                <label>@shirtSize:
                    <InputRadio Name="size" Value="@shirtSize"></InputRadio>
                </label>
                <br />
            }
        </InputRadioGroup>
    </label>
    <p></p>
    <label>
        <h3>Color</h3>
        <InputRadioGroup Name="color" @bind-Value=shirt.Color>
            @foreach(var shirtColor in Enum.GetValues(typeof(ShirtColor)))
            {
                <label>@shirtColor:
                    <InputRadio Name="color" Value="@shirtColor"></InputRadio>
                </label>
                <br />
            }
        </InputRadioGroup>
    </label>
    <p></p>
    <label>
        <h3>Price</h3>
        <InputNumber @bind-Value=shirt.Price min="0" max="100" step="0.01"></InputNumber>
    </label>
</EditForm>

@code {
    private Shirt shirt = new Shirt
    {
        Size = ShirtSize.Large,
        Color = ShirtColor.Blue,
        Price = 9.99M
    };
}

运行表单时,它如下所示:

Screenshot of the EditForm showing the radio button groups for T-Shirt size and color.

处理窗体提交

你已经知道可以使用 EditForm 来修改基础模型中的数据。 更改完成后,可以提交表单以验证服务器上的数据并保存所做的更改。 Blazor 支持两种类型的验证:声明性和编程性。 声明性验证规则在客户端上和浏览器中运行。 对于在数据传输到服务器之前执行基本的客户端验证,它们会非常有用。 对于处理声明性验证无法实现的复杂场景,服务器端验证非常有用,例如针对来自其他源的数据交叉检查字段中的数据。 实际的应用程序应结合使用客户端验证和服务器端验证;客户端验证可捕获基本的用户输入错误,并防止将无效数据发送到服务器以进行处理等许多情况。 服务器端验证可确保用于保存数据的用户请求不会试图绕过数据验证并存储不完整或损坏的数据。

 备注

也可以捕获 onchange 和 oninput 等 JavaScript 事件,以及 EditForm 中许多控件的 Blazor 等效 @onchange 和 @oninput 事件。 在用户提交表单之前,你可使用这些事件以编程方式逐个字段地检查和验证数据。 但是,不建议使用这种方法。 当用户在字段之间输入每个按键或按 Tab 键时出现验证消息,这可能会让他们感到非常苦恼。 保存用户完成其输入时的验证。

EditForm 具有三个在提交后运行的事件:

  • OnValidSubmit:如果输入域成功通过其验证属性定义的验证规则,则会触发此事件。
  • OnInvalidSubmit:如果表单上的任何输入域都未能通过其验证属性定义的验证,则会触发此事件。
  • OnSubmit:无论所有输入域是否有效,提交 EditForm 时都会发生此事件。

对于在单个输入域级别实现基本验证的 EditForm,OnValidSubmit 和 OnInvalidSubmit 事件很有用。 如果验证要求更复杂,例如将一个输入域与另一个输入域进行交叉检查以确保值的有效组合,请考虑使用 OnSubmit 事件。 EditForm 可以处理 OnValidSubmit 和 OnInvalidSubmit 事件对,也可以处理 OnSubmit 事件,但不能同时处理全部三个事件。 通过向 EditForm 添加一个 Submit 按钮来触发提交。 当用户选择此按钮时,将触发由 EditForm 指定的提交事件。

 备注

生成和部署过程不会检查提交事件组合是否无效,但非法选择将在运行时产生错误。 例如,如果尝试将 OnValidSubmit 与 OnSubmit 结合使用,应用程序将生成以下运行时异常:

Error: System.InvalidOperationException: When supplying an OnSubmit parameter to EditForm, do not also supply OnValidSubmit or OnInvalidSubmit.

 

 

EditForm 使用 EditContext 对象跟踪作为模型的当前对象的状态,包括哪些字段已更改及其当前值。 提交事件作为参数传递此 EditContext 对象。 事件处理程序可以使用此对象中的 Model 字段来检索用户的输入。

下面的示例显示了上一个示例中带有“提交”按钮的 EditForm。 EditForm 捕获 OnSubmit 事件以验证对 T 恤对象所做的更改。 在此示例中,仅允许某些值组合:

  • 红色 T 恤不提供特大号
  • 蓝色 T 恤不提供小号或中号
  • 白色 T 恤的最高价格为 50 美元。

如果检测到非法组合,表单上的 Message 字段会显示验证失败的原因。 如果字段有效,则处理数据并保存数据(未显示此过程的逻辑)。

<EditForm Model="@shirt" OnSubmit="ValidateData">
    <!-- Omitted for brevity -->
    <input type="submit" class="btn btn-primary" value="Save"/>
    <p></p>
    <div>@Message</div>
</EditForm>

@code {
    private string Message = String.Empty;

    // Omitted for brevity

    private async Task ValidateData(EditContext editContext)
    {
        if (editContext.Model is not Shirt shirt)
        {
            Message = "T-Shirt object is invalid";
            return;
        }

        if (shirt is { Color: ShirtColor.Red, Size: ShirtSize.ExtraLarge })
        {
            Message = "Red T-Shirts not available in Extra Large size";
            return;
        }

        if (shirt is { Color: ShirtColor.Blue, Size: <= ShirtSize.Medium)
        {
            Message = "Blue T-Shirts not available in Small or Medium sizes";
            return;
        }

        if (shirt is { Color: ShirtColor.White, Price: > 50 })
        {
            Message = "White T-Shirts must be priced at 50 or lower";
            return;
        }

        // Data is valid
        // Save the data
        Message = "Changes saved";
    }
}

下图显示了用户尝试提供无效数据时的结果:

Screenshot of the T-shirt form showing a validation error after it has been submitted.

验证 Blazor 表单中的用户输入

当你收集来自网站用户的信息时,必须检查其是否有意义且格式是否正确:

  • 出于业务原因:客户信息(例如电话号码或订单详细信息)必须正确无误才能为用户提供优质服务。 例如,如果网页可以在用户输入电话号码时立即发现该号码格式错误,则可以防止以后出现代价高昂的延迟。
  • 出于技术原因:如果代码使用表单输入进行计算或其他处理,则不正确的输入可能会导致错误和异常。
  • 出于安全原因:恶意用户可能会尝试通过利用未经检查的输入字段来注入代码。

网站用户熟悉用于检查其输入的详细信息是否存在以及格式是否正确的验证规则。 必填字段通常标有星号或“必填”标签。 如果他们省略了某个值或输入了格式错误的值,将看到一条指示他们如何解决问题的验证消息。 当用户按 Tab 键离开某个字段或单击“提交”按钮时,可能会显示验证消息。

下面是一个示例表单,其中用户提交了无效数据。 在这种情况下,表单底部有验证消息并且无效字段用红色突出显示。 你将在下一练习中生成此表单:

Screenshot of an example form displaying feedback for the user about invalid data.

最好使验证消息尽可能有帮助。 不要假定用户了解所有信息:例如,并非每个人都知道有效电子邮件地址的格式。

在 Blazor 中使用 EditForm 组件时,有多种验证选项可供选择,而无需编写复杂的代码:

  • 在模型中,可以对每个属性使用数据注释来告诉 Blazor 何时需要值以及它们应该采用什么格式。
  • 在 EditForm 组件中,添加 DataAnnotationsValidator 组件,它将根据用户输入的值检查模型注释。
  • 如果希望在提交的表单中显示所有验证消息的摘要,请使用 ValidationSummary 组件。
  • 如果要显示特定模型属性的验证消息,请使用 ValidationMessage 组件。

准备用于验证的模型

首先告诉 DataAnnotationsValidator 组件有效数据的外观。 可以通过在数据模型中使用注释属性来声明验证限制。 请看以下示例:

using  System.ComponentModel.DataAnnotations;

public class Pizza
{
    public int Id { get; set; }
    
    [Required]
    public string Name { get; set; }
    
    public string Description { get; set; }
    
    [EmailAddress]
    public string ChefEmail { get; set;}
    
    [Required]
    [Range(10.00, 25.00)]
    public decimal Price { get; set; }
}

我们将在表单中使用此模型,以使 Blazing Pizza 员工能够将新的比萨添加到菜单中。 它包含 [Required] 属性以确保 Name 和 Price 值始终完整。 它还使用 [Range] 属性来检查输入的比萨价格是否在合理范围内。 最后,它使用 [EmailAddress] 属性来检查输入的 ChefEmail 值是否是有效的电子邮件地址。

可以在模型中使用的其他注释包括:

  • [ValidationNever]:如果要确保该字段从不包含在验证中,请使用此注释。
  • [CreditCard]:如果要记录用户的有效信用卡号,请使用此注释。
  • [Compare]:如果要确保模型中的两个属性匹配,请使用此注释。
  • [Phone]:如果要记录用户的有效电话号码,请使用此注释。
  • [RegularExpression]:如果通过将值与正则表达式进行比较来检查值的格式,请使用此注释。
  • [StringLength]:如果要检查字符串值的长度是否不超过最大长度,请使用此注释。
  • [Url]:如果要记录用户的有效 URL,请使用此注释。

 备注

正则表达式广泛用于将字符串与模式进行比较,也用于修改字符串。 可以使用它们来定义表单值必须符合的自定义格式。 要详细了解 .NET 中的正则表达式,请参阅《.NET 正则表达式》。

向表单添加验证组件

要将表单配置为使用数据注释验证,请首先确保已将输入控件绑定到模型属性。 然后,在 EditForm 组件内的某个位置添加 DataAnnotationsValidator 组件。 若要显示验证生成的消息,请使用 ValidationSummary 组件,该组件显示表单中所有控件的所有验证消息。 如果想要在每个控件旁边显示验证消息,请使用多个 ValidationMessage 组件。 请记住,使用 For 属性将每个 ValidationMessage 控件与模型的特定属性相关联:

@page "/admin/createpizza"

<h1>Add a new pizza</h1>

<EditForm Model="@pizza">
    <DataAnnotationsValidator />
    <ValidationSummary />
    
    <InputText id="name" @bind-Value="pizza.Name" />
    <ValidationMessage For="@(() => pizza.Name)" />
    
    <InputText id="description" @bind-Value="pizza.Description" />
    
    <InputText id="chefemail" @bind-Value="pizza.ChefEmail" />
    <ValidationMessage For="@(() => pizza.ChefEmail)" />
    
    <InputNumber id="price" @bind-Value="pizza.Price" />
    <ValidationMessage For="@(() => pizza.Price)" />
</EditForm>

@code {
    private Pizza pizza = new();
}

控制应用的表单验证

Blazor 在两个不同的时间执行验证:

  • 当用户按 Tab 键离开某个字段时,将执行字段验证。 字段验证可确保用户及早了解验证问题。
  • 当用户提交表单时,将执行模型验证。 模型验证可确保不会存储无效数据。

如果表单验证失败,消息将显示在 ValidationSummary 和 ValidationMessage 组件中。 若要自定义这些消息,可以为模型中每个字段的数据注释添加一个 ErrorMessage 属性:

public class Pizza
{
    public int Id { get; set; }
    
    [Required(ErrorMessage = "You must set a name for your pizza.")]
    public string Name { get; set; }
    
    public string Description { get; set; }
    
    [EmailAddress(ErrorMessage = "You must set a valid email address for the chef responsible for the pizza recipe.")]
    public string ChefEmail { get; set;}
    
    [Required]
    [Range(10.00, 25.00, ErrorMessage = "You must set a price between $10 and $25.")]
    public decimal Price { get; set; }
}

内置验证属性用途广泛,你可以使用正则表达式来检查多种文本模式。 但如果具有特定或不寻常的验证要求,则使用内置属性可能无法完全满足这些要求。 在这些情况下,可以创建自定义验证属性。 首先创建一个从 ValidationAttribute 类继承的类并替代 IsValid 方法:

public class PizzaBase : ValidationAttribute
{
    public string GetErrorMessage() => $"Sorry, that's not a valid pizza base.";

    protected override ValidationResult IsValid(
        object value, ValidationContext validationContext)
    {
        if (value != "Tomato" || value != "Pesto")
        {
            return new ValidationResult(GetErrorMessage());
        }

        return ValidationResult.Success;
    }
}

现在,可以在模型类中使用内置属性时,使用自定义验证属性:

public class Pizza
{
    public int Id { get; set; }
    
    [Required(ErrorMessage = "You must set a name for your pizza.")]
    public string Name { get; set; }
    
    public string Description { get; set; }
    
    [EmailAddress(
        ErrorMessage = "You must set a valid email address for the chef responsible for the pizza recipe.")]
    public string ChefEmail { get; set;}
    
    [Required]
    [Range(10.00, 25.00, ErrorMessage = "You must set a price between $10 and $25.")]
    public decimal Price { get; set; }
    
    [PizzaBase]
    public string Base { get; set; }
}

在表单提交时,在服务器端处理表单验证

使用 EditForm 组件时,有三个事件可用于响应表单提交:

  • OnSubmit:无论验证结果如何,只要用户提交表单,就会触发此事件。
  • OnValidSubmit:当用户提交表单并且他们的输入验证通过时,将触发此事件。
  • OnInvalidSubmit:当用户提交表单并且他们的输入验证失败时,将触发此事件。

如果使用 OnSubmit,则不会触发其他两个事件。 可改用 EditContext 参数来检查是否处理输入数据。 如果要编写自己的逻辑来处理表单提交,请使用此事件:

@page "/admin/createpizza"

<h1>Add a new pizza</a>

<EditForm Model="@pizza" OnSubmit=@HandleSubmission>
    <DataAnnotationsValidator />
    <ValidationSummary />
    
    <InputText id="name" @bind-Value="pizza.Name" />
    <ValidationMessage For="@(() => pizza.Name)" />
    
    <InputText id="description" @bind-Value="pizza.Description" />
    
    <InputText id="chefemail" @bind-Value="pizza.ChefEmail" />
    <ValidationMessage For="@(() => pizza.ChefEMail)" />
    
    <InputNumber id="price" @bind-Value="pizza.Price" />
    <ValidationMessage For="@(() => pizza.Price" />
</EditForm>

@code {
    private Pizza pizza = new();
    
    void HandleSubmission(EditContext context)
    {
        bool dataIsValid = context.Validate();
        if (dataIsValid)
        {
            // Store valid data here
        }
    }
}

如果改用 OnValidSubmit 和 OnInvalidSubmit,则不必在每个事件处理程序中检查验证状态:

@page "/admin/createpizza"

<h1>Add a new pizza</a>

<EditForm Model="@pizza" OnValidSubmit=@ProcessInputData OnInvalidSubmit=@ShowFeedback>
    <DataAnnotationsValidator />
    <ValidationSummary />
    
    <InputText id="name" @bind-Value="pizza.Name" />
    <ValidationMessage For="@(() => pizza.Name)" />
    
    <InputText id="description" @bind-Value="pizza.Description" />
    
    <InputText id="chefemail" @bind-Value="pizza.ChefEmail" />
    <ValidationMessage For="@(() => pizza.ChefEMail)" />
    
    <InputNumber id="price" @bind-Value="pizza.Price" />
    <ValidationMessage For="@(() => pizza.Price" />
</EditForm>

@code {
    private Pizza pizza = new();
    
    void ProcessInputData(EditContext context)
    {
        // Store valid data here
    }
    
    void ShowFeedback(EditContext context)
    {
        // Take action here to help the user correct the issues
    }
}