第四章 表单(2)- 输入组件
在 Blazor 框架中,微软对 HTML 输入元素进行了封装,称为输入组件。使用输入组件相较于原生 HTML 输入元素,在编码和安全性方面具有显著优势。
输入组件简介
Blazor框架提供了用于接收和验证用户输入的内置输入组件,这些组件可以在EditForm
表单组件中使用,也可以在表单之外使用,当更改输入和提交表单时,会验证输入(如果开启了验证)。
输入组件 | 渲染为如下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> | InputRadio<TValue> 子组 | 单选按钮组 |
InputSelect<TValue> | <select> | 下拉菜单/列表选择框 |
InputText | <input> | 单行文本框 |
InputTextArea | <textarea> | 多行文本框 |
InputBase<TValue>
是表单输入组件的抽象基类,在上述输入组件列表中,只有InputFile
和InputRadio<TValue>
不是它的子类,其他内置输入组件都派生自InputBase<TValue>
。
一、通用属性
InputBase<TValue>
中有几个较为常用的公共属性,所有子类输入组件皆可使用,分别为:AdditionalAttributes
、DisplayName
、Value
、ValueChanged
和ValueExpression
。
1、AdditionalAttributes属性
AdditionalAttributes
:IReadOnlyDictionary<string, object>
类型,用于指定在输入组件上附加的多个属性和值。
-
所有输入组件(包括
EditForm
)都支持设置任意属性,如果属性与提供的组件参数不匹配,会将属性直接添加到渲染的Html元素上。 -
示例
@page "/form-params" <h3>FormParam</h3> <EditForm FormName="testForm" Model="Model" OnSubmit="Submit"> <div> <label> 输入:<InputText @bind-Value="Model!.Name" AdditionalAttributes="paramDic" /> </label> </div> <div> <button type="submit">Submit</button> </div> </EditForm> @code { [SupplyParameterFromForm] public Student? Model { get; set; } protected override void OnInitialized() { Model = new(); } //字典扩展的附加属性 Dictionary<string, object> paramDic = new Dictionary<string, object>() { {"style","color:blue" }, {"id","formId" } }; private void Submit() { //提交、存储、导航啥的 } public class Student { public string Name { get; set; } = null!; } }
2、DisplayName属性
DisplayName
:当输入内容校验不通过时,使用DisplayName
所设置的内容来代替所绑定的属性名来进行异常信息的展示。
-
DisplayName
属性并不是所有的输入组件都生效,只是 HTML5 新增的部分输入元素有用,例如InputNumber
、InputDate
等组件,而InputText
和InputTextArea
等组件是不支持的,感觉很鸡肋 -
示例
这里面的
<DataAnnotationsValidator/>
和<ValidationSummary/>
组件的作用,可以查阅表单验证章节@page "/display-name" @using System.ComponentModel.DataAnnotations @rendermode InteractiveServer <label> <EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship3"> <DataAnnotationsValidator/> <ValidationSummary/> Product Date: <InputDate @bind-Value="Model!.Date" DisplayName="Product Date" /> Product Name: <InputText @bind-Value="Model!.Name" DisplayName="Product Name" /> <button type="submit">Submit</button> </EditForm> </label> @code { [SupplyParameterFromForm] private Starship? Model { get; set; } protected override void OnInitialized() { Model = new Starship(); } public class Starship { [Required] public DateTime Date { get; set; } [Required] public string Name { get; set; } } public void Submit() { } }
错误信息
InputDate
和 InputNumber
组件支持通过组件参数ParsingErrorMessage
来设置验证失败时的显示信息
-
ParsingErrorMessage
可以通过占位符{0}
配合DisplayName
使用 -
示例
@page "/display-name" @using System.ComponentModel.DataAnnotations @rendermode InteractiveServer <label> <EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship3"> <DataAnnotationsValidator/> <ValidationSummary/> Product Date: <InputDate @bind-Value="Model!.Date" DisplayName="Product Date" ParsingErrorMessage="{0} 必须正确填写"/> </EditForm> </label> @code { [SupplyParameterFromForm] private Starship? Model { get; set; } protected override void OnInitialized() { Model = new Starship(); } public class Starship { [Required] public DateTime Date { get; set; } } public void Submit() { } }
注意,较为常规的用法是在表单所绑定的模型的属性上通过使用特性声明来设置展示名称了异常消息。
3、Value属性
默认情况下,在使用输入组件时,使用@bind-Value
属性指令就可以实现输入组件的双向数据绑定。
实际上,@bind-Value
属性指令是对 Value
、ValueChanged
和 ValueExpression
的一种语法糖,它会自动处理Value
、ValueChanged
和ValueExpression
的设置
Value
:输入控件的输入值,可以通过Razor表达式绑定到指定的属性上,实现模型到视图的数据绑定ValueChanged
:当绑定的值发生变化时,会触发该事件。这通常用于通知组件或父组件,绑定的值已经改变,并且可以在这里执行必要的逻辑,从而实现视图到模型的数据绑定ValueExpression
:指定要进行校验或元数据管理的数据,直接返回要进行处理的数据即可
一般情况下直接使用@bind-Value
属性指令进行数据绑定就可以了,如果需要对过程进行拆分处理,可以组合使用Value
、ValueChanged
和ValueExpression
,需要注意的是这 3 个属性不可单独使用,要3个组合使用。
-
示例
@page "/value-test" <PageTitle>Value属性测试</PageTitle> <EditForm Model="Model" OnSubmit="Submit" FormName="ValueTest"> <div class="mb-3"> <InputText Value="@Model.FullName" ValueChanged="ValueChanged" ValueExpression="()=>Model.FullName"/> </div> <div class="mb-3"> <button class="btn btn-primary">提交</button> </div> </EditForm> @code { public Student Model = new(); void ValueChanged(string value) { //当输入值发生改变时,修改Model.FullName的值,实现双向绑定 Model.FullName = value; } void Submit() { Console.WriteLine(Model.FullName); } public class Student { public string? FullName { get; set; } } }
二、验证说明
输入组件会自动检查用户输入的字段是否已更改:
- 对于带有
EditContext
的输入组件,默认会根据验证结果更新CSS,以通过基础的HTML元素来展示不同的验证结果- 即使在没有使用
<ValidationSummary>
组件的情况下,也会展示验证结果,不过默认是没有展示校验的错误信息的
- 即使在没有使用
- 对于没有 EditContext 的输入组件,也是支持校验的,可以可通过代码调用验证逻辑(如 Validator.Validate),但不会为基础的HTML元素提供校验结果的样式
当使用<EditForm>
组件时,会自动生成一个EditContext
,这里所说的带有EditContext
的输入组件指的是被EditForm
所包括的输入组件,反之则是没有EditContext
的输入组件。
基本示例
-
Starship.cs
public class Starship { [Required] [StringLength(16, ErrorMessage = "Identifier too long (16 character limit).")] public string? Id { 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 = "Approval required.")] public bool IsValidatedDesign { get; set; } [Required] public DateTime ProductionDate { get; set; } }
-
Starship3.razor
@page "/starship-3" @using BlazorServerStudy.Models @inject ILogger<Starship3> Logger <h1>Starfleet Starship Database</h1> <h2>New Ship Entry Form</h2> <EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship3"> <DataAnnotationsValidator /> <ValidationSummary /> <div> <label> Identifier: <InputText @bind-Value="Model!.Id" /> </label> </div> <div> <label> Description (optional): <InputTextArea @bind-Value="Model!.Description"/> </label> </div> <div> <label> Primary Classification: <InputSelect @bind-Value="Model!.Classification"> <option value=""> Select classification ... </option> <option checked="@(Model!.Classification == "Exploration")" value="Exploration"> Exploration </option> <option checked="@(Model!.Classification == "Diplomacy")" value="Diplomacy"> Diplomacy </option> <option checked="@(Model!.Classification == "Defense")" value="Defense"> Defense </option> </InputSelect> </label> </div> <div> <label> Maximum Accommodation: <InputNumber @bind-Value="Model!.MaximumAccommodation" /> </label> </div> <div> <label> Engineering Approval: <InputCheckbox @bind-Value="Model!.IsValidatedDesign" /> </label> </div> <div> <label> Production Date: <InputDate @bind-Value="Model!.ProductionDate" /> </label> </div> <div> <button type="submit">Submit</button> </div> </EditForm> @code { [SupplyParameterFromForm] private Starship? Model { get; set; } protected override void OnInitialized() => Model ??= new() { ProductionDate = DateTime.UtcNow }; private void Submit() { Logger.LogInformation("Id = {Id} Description = {Description} " + "Classification = {Classification} MaximumAccommodation = " + "{MaximumAccommodation} IsValidatedDesign = " + "{IsValidatedDesign} ProductionDate = {ProductionDate}", Model?.Id, Model?.Description, Model?.Classification, Model?.MaximumAccommodation, Model?.IsValidatedDesign, Model?.ProductionDate); } }
异步+EditContext示例
注意,在赋值 EditContext
对象之后不能改变
-
Starship4.razor
@page "/starship-4" @using BlazorServerStudy.Models @inject ILogger<Starship4> Logger <EditForm EditContext="editContext" OnSubmit="Submit" FormName="Starship4"> <DataAnnotationsValidator /> <div> <label> Identifier: <InputText @bind-Value="Model!.Id" /> </label> </div> <div> <button type="submit">Submit</button> </div> </EditForm> @code { private EditContext? editContext; [SupplyParameterFromForm] private Starship? Model { get; set; } protected override void OnInitialized() { Model ??= new Starship(){ Id = "NCC-1701", Classification = "Exploration", MaximumAccommodation = 150, IsValidatedDesign = true, ProductionDate = new DateTime(2245, 4, 11) }; editContext = new(Model); } private async Task Submit() { if (editContext != null && editContext.Validate()) { Logger.LogInformation("Submit called: Form is valid"); await Task.Delay(1000); } else { Logger.LogInformation("Submit called: Form is INVALID"); } } }
输入组件详解
Blazor中提供了一系列输入组件,方便进行表单处理,下面逐一进行学习。
一、输入框
1、单行文本框
InputText
组件,是对 HTML 中的<input type="text">
元素的封装,为单行文本框,用于处理string
类型的文本值。
-
InputText
组件在纯 HTML 的<form>
元素和<EditForm>
组件中都可以使用。 -
示例
@page "/form-test" @inject ILogger<FormTest> Logger <h3>FormTest</h3> <EditForm FormName="testForm" Model="Model" OnSubmit="Submit"> <div> <label> 姓名: <InputText @bind-Value="Model!.Name"/> </label> </div> <div> <button type="submit">Submit</button> </div> <div> @Model!.Name </div> </EditForm> @code { public Student? Model { get; set; } protected override void OnInitialized() => Model ??= new(); private void Submit() { Logger.LogInformation(Model?.Name); } public class Student { public string? Name { get; set; } = "Schuyler"; } }
2、多行文本框
InputTextArea
组件是对 HTML 标记中的<textarea>
元素进行了封装,用于表示多行文本框。
常用属性
rows
:设置文本框的展示行数,也是高度
cols
:设置文本框的展示列数,也是宽度
-
示例
@page "/form-test" @inject ILogger<FormTest> Logger <h3>FormTest</h3> <EditForm FormName="testForm" Model="Model" OnSubmit="Submit"> <div> <label> <InputTextArea @bind-Value="Model!.Name" rows="10" cols="50"/> </label> </div> <div> <button type="submit">Submit</button> </div> </EditForm> @code { public Student? Model { get; set; } protected override void OnInitialized() => Model ??= new(); private void Submit() { Logger.LogInformation(Model?.Name); } public class Student { public string? Name { get; set; } = "Schuyler"; } }
二、多选框
InputSelect
组件是对 HTML 中<select>
元素的封装,用于展示下拉菜单或选择列表框,其子选项使用<option>
元素表示。
单选(下拉菜单)
当InputSelect
组件所绑定的数据源为非数组属性时,所展示的就是单选下拉菜单
-
示例
@page "/form-test" @inject ILogger<FormTest> Logger <h3>FormTest</h3> <EditForm FormName="testForm" Model="Model" OnSubmit="Submit"> <div> <InputSelect @bind-Value="Model.City" class="form-select"> <option value="@City.BJ">北京</option> <option value="@City.SH">上海</option> <option value="@City.GZ">广州</option> <option value="@City.SZ">深圳</option> </InputSelect> </div> <div> <button type="submit">Submit</button> </div> </EditForm> @code { public Student Model = new(); private void Submit() { //进行提交处理 Console.WriteLine(Model.City); } public class Student { public City? City { get; set; } } public enum City { SH, BJ, GZ, SZ } }
多选(选择列表框)
InputSelect
组件支持多选,其绑定值必须绑定到数组类型的属性。
-
此外,
InputSelect
支持@onchange
事件,该事件通过参数ChangeEventArgs
提供了一个选中值的数组 -
示例
@page "/form-test" @inject ILogger<FormTest> Logger <h3>FormTest</h3> <EditForm FormName="testForm" Model="Model" OnSubmit="Submit"> <div> <InputSelect @bind-Value="Model.Citys" class="form-select"> <option value="@City.BJ">北京</option> <option value="@City.SH">上海</option> <option value="@City.GZ">广州</option> <option value="@City.SZ">深圳</option> </InputSelect> </div> <div> <button type="submit">Submit</button> </div> </EditForm> @code { public Student Model = new(); private void Submit() { //进行提交处理 foreach (var city in Model.Citys!) { Console.WriteLine(city); } } public class Student { public City[]? Citys { get; set; } = new[] { City.NONE }; } public enum City { NONE, SH, BJ, GZ, SZ } }
三、单选按钮
在Blazor中,可以结合使用InputRadio<TValue>
组件和InputRadioGroup<TValue>
组件来使用单选按钮组。
单选按钮常常与枚举类型结合使用,有如下要点:
-
[EnumDataType(typeof(enumName))]
:在表单模型中定义枚举属性时,使用[EnumDataType]
特性进行声明,可以在表单进行验证时以确保属性值在指定的枚举范围内,如果属性值不在指定的枚举范围内,则模型验证将失败并返回相应的错误消息 -
枚举属性也可以使用
[Range]
特性进行范围的限制以及错误信息的设置,如[Range(typeof(Manufacturer), nameof(Manufacturer.SpaceX),nameof(Manufacturer.VirginGalactic), ErrorMessage = "Pick a manufacturer.")]
,分别限制该属性为Manufacturer
枚举类型、限制最小值为Manufacturer.SpaceX
、最大值为Manufacturer.VirginGalactic
-
ComponentEnums.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 } }
-
RadioTestModel.cs
public class RadioTestModel { [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; }
-
RadioTest.razor
@page "/radio-test" @inject ILogger<RadioTest> Logger @using static ComponentEnums <EditForm Model="Model" OnSubmit="Submit" FormName="RadioTest"> <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> </EditForm> @code { [SupplyParameterFromForm] public RadioTestModel? Model { get; set; } protected override void OnInitialized() { Model ??= new RadioTestModel(); } private void Submit() { } }
例子中的<fieldset>
元素主要用于在表单中对相关的控件(如输入框、选择框等)进行分组,以便于组织和渲染信息。<fieldset>
通常与<legend>
元素一起使用,<legend>
用于为分组提供一个标题或描述。
四、复选框
在Blazor中,复选框为InputCheckbox
组件,本质是对<input type="checkbox">
元素的封装,用于通过勾选来确认bool
类型的值。
-
InputCheckbox
组件在纯 HTML 的<form>
元素和<EditForm>
组件中都可以使用 -
示例
@page "/form-test" @inject ILogger<FormTest> Logger <h3>FormTest</h3> <EditForm FormName="testForm" Model="Model" OnSubmit="Submit"> <div> <label> <InputCheckbox @bind-Value="Model!.Sex" class="form-check-input" /> 男 </label> </div> <div> <button type="submit">Submit</button> </div> </EditForm> @code { public Student? Model { get; set; } protected override void OnInitialized() => Model ??= new() { Sex=true}; private void Submit() { //随便干点啥 } public class Student { public string? Name { get; set; } = "Schuyler"; public bool Sex { get; set; } } }
五、数字框
InputNumber
组件是对<input type=”number”/>
元素的封装,这是在 HTML5 新增的元素,用于表示具有上下箭头的数字框,只能用于输入数字。
在InputNumber
组件中,支持DisplayName
属性和ParsingErrorMessage
属性:
-
DisplayName
:当输入内容校验不通过时,使用DisplayName
所设置的内容来代替所绑定的属性名来进行异常信息的展示 -
ParsingErrorMessage
:用于指定错误消息的模板,模板中的占位符{0}
指的就是DisplayName
的值 -
示例
@page "/form-test" @inject ILogger<FormTest> Logger <h3>FormTest</h3> <EditForm FormName="testForm" Model="Model" OnSubmit="Submit"> <DataAnnotationsValidator /> <ValidationSummary /> <div class="mb-3 w-50"> <InputNumber @bind-Value="Model.Count" DisplayName="测试数量" ParsingErrorMessage="{0}数量太多"/> </div> <div> <button type="submit">Submit</button> </div> </EditForm> @code { public Student Model = new(); private void Submit() { //进行提交处理 Console.WriteLine($"数量:{Model.Count}"); } public class Student { public int Count { get; set; } } }
六、日期选择器
InputDate
组件是对<input type="date"/>
元素的封装,<input type="date"/>
元素是在 HTML 5 中新增的,用于渲染一个可以让用户选择日期值的选择器。
-
绑定到
<InputDate>
组件上的属性或字段的类型必须是DateTime
或DateOnly
-
InputDate
组件也支持DisplayName
属性和ParsingErrorMessage
属性 -
示例
@page "/form-test" @inject ILogger<FormTest> Logger <h3>FormTest</h3> <EditForm FormName="testForm" Model="Model" OnSubmit="Submit"> <DataAnnotationsValidator /> <ValidationSummary /> <div class="mb-3 w-50"> <label> 日期: <InputDate @bind-Value="Model.CreateDateTime" DisplayName="日期" ParsingErrorMessage="输入的{0}字段值不正确" /> </label> </div> <div> <button type="submit">Submit</button> </div> </EditForm> @code { public Student Model = new(); private void Submit() { //进行提交处理 Console.WriteLine($"日期:{Model.CreateDateTime}"); } public class Student { public DateTime CreateDateTime { get; set; } = DateTime.Now; } }
提交按钮的禁用
在表单中,表单的提交按钮可以使用<button type="submit">
元素来实现。这个没啥特别的,比较需要注意的是提交按钮的禁用。一般情况下,当表单中的某个字段没有通过验证的时候,应该将提交按钮禁用的,以确保在前端就拦截提交。
想要禁用提交按钮,可以通过设置<button>
元素的disabled
属性来实现
-
示例
@page "/starship-14" @rendermode InteractiveServer @implements IDisposable @inject ILogger<Starship14> Logger <EditForm EditContext="editContext" OnValidSubmit="Submit" FormName="Starship14"> <DataAnnotationsValidator /> <ValidationSummary /> <div> <label> Identifier: <InputText @bind-Value="Model!.Id" /> </label> </div> <div> <button type="submit" disabled="@formInvalid">Submit</button> </div> </EditForm> @code { private bool formInvalid = false; private EditContext? editContext; [SupplyParameterFromForm] private Starship? Model { get; set; } protected override void OnInitialized() { Model ??= new() { Id = "NCC-1701", Classification = "Exploration", MaximumAccommodation = 150, IsValidatedDesign = true, ProductionDate = new DateTime(2245, 4, 11) }; editContext = new(Model); editContext.OnFieldChanged += HandleFieldChanged; } private void HandleFieldChanged(object? sender, FieldChangedEventArgs e) { if (editContext is not null) { formInvalid = !editContext.Validate(); StateHasChanged(); } } private void Submit() { Logger.LogInformation("Submit called: Processing the form"); } public void Dispose() { if (editContext is not null) { editContext.OnFieldChanged -= HandleFieldChanged; } } }
上面的例子中,存在一个问题,就是只要在任何一个输入组件中失焦后,<ValidationSummary>
组件都会填充所有字段验证失败的信息,想要解决这个问题可以使用如下个方法:
-
不使用
<ValidationSummary>
组件 -
先将
<ValidationSummary>
组件隐藏,在点击提交按钮时,再将其展示 -
示例
<EditForm ... EditContext="editContext" OnValidSubmit="Submit" ......> <DataAnnotationsValidator /> <ValidationSummary style="@displaySummary"/> ...... <button type="submit" disabled="@formInvalid">Submit</button> </EditForm> @code { private string displaySummary = "display:none"; ...... private void Submit() { displaySummary = "display:block"; } }