对于表单功能,Blazor框架不仅支持HTML原生的表单,还提供了内置组件以及相应的内置输入组件。
表单的使用
一、<form>
-
示例-StarshipPlainForm.razor
@page "/starship-plain-form" @inject ILogger<StarshipPlainForm> Logger<form method="post" @onsubmit="Submit" @formname="starship-plain-form"><AntiforgeryToken /><div><label>Identifier: <InputText @bind-Value="Model!.Id" /></label></div><div><button type="submit">Submit</button></div> </form>@code {[SupplyParameterFromForm]public Starship? Model { get; set; }protected override void OnInitialized() => Model ??= new();private void Submit(){Logger.LogInformation("Id = {Id}", Model?.Id);}public class Starship{public string? Id { get; set; }} }
上例中,有如下几个要点:
@formname
:对表单进行了命名,名称需要具有唯一性,这会将该表单在 Blazor 框架中进行唯一标识。注意,使用表单时,应该始终使用@formname
属性指令对表单进行唯一命名[SupplyParameterFromForm]
:指定在组件中专门为表单提供数据的模型<AntiforgeryToken>
:防伪标识组件,将防伪标识隐藏在表单中,防止跨站请求伪造篡改攻击,也就是防止 CSRF 攻击
在Blazor中,HTML表单是支持流式渲染的。
二、EditForm组件
在Blazor框架中,通常会使用内置表单组件EditForm
来定义表单,而不是在Blazor应用中使用原生表单。
EditForm
组件同样支持流渲染。- 默认为Post请求
- 默认携带防伪标识,不需要再设置
<AntiforgeryToken>
组件
常用组件属性
Model
:指定表单所关联的模型对象。
OnValidSubmit
:当表单提交时, 如果表单验证成功,触发事件,需要配合组件使用。
FormName
:指定表单的唯一名称。
-
示例-Starship1.razor
@page "/starship-1" @inject ILogger<Starship1> Logger<EditForm Model="Model" OnSubmit="Submit" FormName="Starship1"><div><label>Identifier:<InputText @bind-Value="Model!.Id" /></label></div><div><button type="submit">Submit</button></div> </EditForm>@code {[SupplyParameterFromForm]public Starship? Model { get; set; }protected override void OnInitialized() => Model ??= new();private void Submit(){Logger.LogInformation("Id = {Id}", Model?.Id);}public class Starship{public string? Id { get; set; }} }
表单的校验
在使用表单时,可以对表单进行校验,基本的校验处理手法如下:
-
在组件内开启数据特性验证程序(
<DataAnnotationsValidator>
组件),根据提交数据的特性信息,进行数据验证 -
使用
OnValidSubmit
事件,当表单校验通过,才会调用事件处理程序 -
在组件内使用
ValidationSummary
组件,如果在提交表单时表单验证失败,则会显示验证消息 -
示例-Starship2.razor
@page "/starship-2" @using System.ComponentModel.DataAnnotations @inject ILogger<Starship2> Logger<EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship2"><DataAnnotationsValidator /><ValidationSummary /><label>Identifier:<InputText @bind-Value="Model!.Id" /></label><button type="submit">Submit</button> </EditForm>@code {[SupplyParameterFromForm]public Starship? Model { get; set; }protected override void OnInitialized() => Model ??= new();private void Submit(){Logger.LogInformation("Id = {Id}", Model?.Id);}public class Starship{[Required][StringLength(10, ErrorMessage = "Id is too long.")]public string? Id { get; set; }} }
表单的提交事件
EditForm
组件中,对于表单的提交有三种回调事件。
-
OnValidSubmit
:当表单提交时, 如果表单验证成功,触发事件,需要配合<DataAnnotationsValidator/>
组件使用 -
OnInvalidSubmit
:当表单提交时,如果表单数据验证失败,触发事件 -
OnSubmit
:不考虑表单数据验证状态,只要提交就触发事件。回调函数中可以通过调用EditContext.Validate
来验证表单。 如果Validate
返回true
,则表单有效 -
示例-Starship3.razor
@page "/starship-3" @using System.ComponentModel.DataAnnotations @inject ILogger<Starship3> Logger<EditForm EditContext="_editContext" OnSubmit="Submit" FormName="Starship3"><DataAnnotationsValidator /><label>Identifier:<InputText @bind-Value="Model!.Id" /></label><button type="submit">Submit</button> </EditForm>@code {private EditContext? _editContext;[SupplyParameterFromForm]public Starship? Model { get; set; }protected override void OnInitialized(){Model ??= new();_editContext = new EditContext(Model);}private void Submit(){if (!_editContext!.Validate()){Logger.LogInformation($"检验失败");}else{Logger.LogInformation("检验成功,Id = {Id}", Model?.Id);}}public class Starship{[Required][StringLength(10, ErrorMessage = "Id is too long.")]public string? Id { get; set; }} }
三、清除表单数据
要清除表单中的数据,只需要将表单所关联的模型清除回默认状态就可以了。方式有很多种
-
示例
<button @onclick="ClearForm">Clear form</button>...private void ClearForm() => Model = new();
需要注意的是,如果是像例子中那样,通过事件处理函数来进行清除的,完成清楚后不需要调用StateHasChanged
,因为在调用事件处理程序后,Blazor框架会自动调用StateHasChanged
来重新渲染组件。
四、表单名
对于form
元素,可以使用@formname
属性指令对表单进行命名。对于EditForm
组件,可以使用FormName
组件参数设置表单的名称。
在Blazor中,对表单设置名称是很有必要的:
- 在静态SSR模式下,所有表单都是必需命名,与
[SupplyParameterFromForm]
配合使用 - 对于交互式渲染组件提交的表单则不是必需的,但建议为每个表单提供唯一的表单名称,以防止表单交互性下降时出现运行时表单发布错误。
当表单作为来自静态渲染的服务器端组件的传统HTTP POST请求发布到终结点时,才会检查表单名称。框架不会在渲染窗体时引发异常,仅在HTTP POST到达且未指定表单名称时才会引发异常。
表单名的作用域
默认情况下,表单的命名在组件中具有唯一性,如果出现了重复的表单,那么在进行表单提交时就会引发异常。特别是当我们使用组件库的表单组件时,遇到重复的表单名称是较为常见的。这个时候可以在组件中通过FormMappingScope
组件来限制表单名的范围。
-
HelloFormFromLibrary.razor(假定这个组件为自定义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.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; }
上例中,通过 FormMappingScope
组件,将 HelloFormFromLibrary 组件提供的任何表单的名称进行了范围限制,因此即使NamedFormsWithScope 组件中有两个相同的表单名,但并不会冲突,且可以正常提交。
五、表单的嵌套
有时候会遇到表单模型存在嵌套的情况(类中某个属性为另一个类型对象,后续成为导航属性),这种情况下往往会导致表单的内容过多,难以维护,此时不妨根据导航属性,将表单中的对应部分内容抽离出来,做成子表单组件,然后再在主表单中使用。
-
要使用子表单组件,先要让组件继承
Editor<T>
,其作用是提供一些内置的功能,如自动绑定表单数据到数据模型、验证表单数据、显示验证错误消息从而简化组件的开发过程,提升开发效率 -
在子表单组件中,使用
Value
来表示表单模型对象 -
ShipDetails.cs
public class ShipDetails {public string? Description { get; set; }public int? Length { get; set; } }
-
Ship.cs
public class Ship {public string? Id { get; set; }public ShipDetails Details { get; set; } = new(); }
-
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>
-
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);} }
表单模型
一、模型绑定
EditContext模型
在Blazor中,使用表单就离不开编辑上下文对象EditContext
,其主要有如下几点作用:
- 跟踪表单字段的修改状态:
EditContext
会跟踪表单字段的修改状态,以便在需要时触发表单数据的更新或验证。 - 管理表单字段的验证状态:
EditContext
会根据表单字段的验证规则,跟踪表单字段的验证状态,并在需要时显示相应的验证消息。 - 提供表单字段值的绑定:
EditContext
可以将表单字段的值与Blazor组件中的属性进行绑定,实现表单数据的双向绑定。
在Blazor中,使用EditForm
组件时可以通过两种方式来指定EditContext
,即模型绑定和直接绑定上下文对象
- 不论哪种方式,如果在静态SSR模式中,要进行绑定的数据模型对象,在定义时,都需要使用
[SupplyParameterFromForm]
特性声明(交互式渲染模式下则不需要) - 对于
EditForm
组件,只能设置Model
或EditContext
其中一个,不能同时都设置(其实也没必要同时设置)。 - 不管使用哪种方式,都可以在
OnSubmit
、OnInvalidSubmit
和OnValidSubmit
事件挂载的事件处理方法中,选择性的接受EditContext
对象作为方法的参数。
模型绑定
-
设置
Model
组件参数:通过设置Model
参数,可以将一个模型对象传递给EditForm
,EditForm
会自动为该模型对象创建一个EditContext
,这种方式简单方便,适用于简单的表单场景 -
示例
@page "/edit-context" @rendermode InteractiveServer @using System.ComponentModel.DataAnnotations <h3>EditorTest</h3><EditForm Model="StudentModel" OnSubmit="SubmitHandler" FormName="EditContextForm"><DataAnnotationsValidator/><ValidationSummary/><InputText @bind-Value="StudentModel!.Name" /><button type="submit">提交</button> </EditForm>@code {[SupplyParameterFromForm]public Student StudentModel { get; set; }protected override void OnInitialized(){StudentModel ??= new() { Name = "Schuyler" };}private void SubmitHandler(){}public class Student{[Required]public required string Name { get; set; }}; }
上下文绑定
除了设置Model
参数外,还可以显式地创建一个EditContext
对象,并将其传递给EditForm
的EditContext
组件参数。这种方式允许开发人员更灵活地控制EditContext
的创建和使用,适用于复杂的表单场景或需要自定义EditContext
行为的情况。
-
示例
@page "/edit-context" @implements IDisposable<PageTitle>EditContext 的使用</PageTitle><EditForm EditContext="_editContext" OnSubmit="Submit"><div class="mb-3"><label class="form-label">姓名:</label><InputText @bind-Value="Model!.FullName" /></div><div class="mb-3"><button class="btn btn-primary" type="submit">提交</button></div> </EditForm>@code {private EditContext? _editContext;[SupplyParameterFromForm]public Student? Model { get; set; }//初始化protected override void OnInitialized(){Model ??= new();_editContext = new(Model);_editContext.OnFieldChanged += FieldChangeHanlder;}private void FieldChangeHanlder(object? sender, FieldChangedEventArgs e){Console.WriteLine($"值发生变化的字段:{e.FieldIdentifier.FieldName}");}void Submit(EditContext editContext){//提交前获取上下文对象,进行业务处理Console.WriteLine(editContext.Field("FullName").FieldName);}//释放资源public void Dispose(){_editContext!.OnFieldChanged -= FieldChangeHanlder;}public class Student{public string? FullName { get; set; }} }
SupplyParameterFromForm特性
[SupplyParameterFromForm]
:用于表单模型上,表示该模型对象应该从表单数据中获取关联属性的值。
-
Name
:获取或设置属性在表单中所使用的名称,默认为模型属性的名称。 -
FormName
:获取或设置该表单模型所对应的表单的名称。当组件中存在多个表单时,可以通过此属性设置对应的表单名称。 -
在静态SSR模式中,表单上要绑定模型对象,必须使用
[SupplyParameterFromForm]
特性声明,交互式模式下则不需要 -
示例
@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; }} }
二、数据绑定
支持的类型
在Blazor中,支持进行绑定的数据类型如下:
- 基元类型
- 集合
- 复杂类型
- 递归类型
- 具有构造函数的类型
- 枚举
[IgnoreDataMember]
在定义绑定模型时,可以使用 [IgnoreDataMember]
特性给属性进行声明,进行忽略属性的设置。
-
示例
public class Student {[Required]public required string Name { get; set; }[DataMember]public int Age {get; set;}[IgnoreDataMember]public int Score { get; set; } };
三、绑定选项的设置
如果需要对表单的绑定进行一些设置,可以在Program.cs
文件中,添加Razor组件时,进行配置,主要有如下几个常用的模型绑定选项:
-
MaxFormMappingCollectionSize
:设置表单集合中允许的最大元素数。 -
MaxFormMappingRecursionDepth
:递归映射表单数据时允许的最大深度。 -
MaxFormMappingErrorCount
:映射表单数据时允许的最大错误数。 -
MaxFormMappingKeySize
:用于读取表单数据键的缓冲区的最大大小。 -
示例-
Program.cs
...... builder.Services.AddRazorComponents(options => {options.FormMappingUseCurrentCulture = true;options.MaxFormMappingCollectionSize = 1024;options.MaxFormMappingErrorCount = 200;options.MaxFormMappingKeySize = 1024 * 2;options.MaxFormMappingRecursionDepth = 64; }).AddInteractiveServerComponents(); ......
防伪支持
防伪的原因
跨网站请求伪造是一种网络安全威胁,攻击者通过伪装成合法用户向目标网站发送恶意请求,以执行未经授权的操作或获取敏感信息。终结点防伪保护是一种机制,用于验证请求的来源是否合法,以防止这种类型的攻击。Blazor使用这种机制来保护应用程序免受CSRF/XSRF攻击的影响
CSRF (Cross-Site Request Forgery)和XSRF (Cross-Site Request Forgery)攻击是指攻击者利用用户已经登录的身份,在用户不知情的情况下向目标网站发送恶意请求,以执行未经授权的操作。攻击者通常会通过在第三方网站上植入恶意代码或链接,诱使用户点击,从而触发对目标网站的请求。由于用户已经在目标网站上保持登录状态,因此目标网站会认为这些请求是合法的,从而执行恶意操作,例如更改密码、转账、删除数据等
防伪中间件的调用顺序
默认情况下,Blazor项目在Program.cs
中调用AddRazorComponents
时,会自动将防伪服务添加到 Blazor 应用中。
应用通过在 Program.cs
文件中的处理管道请求中调用 UseAntiforgery
来使用防伪中间件。
对于UseAntiforgery
的调用位置是有讲究的,需要注意以下几点:
UseAntiforgery
需要在调用UseRouting
之后调用。 因此如果有对UseRouting
和UseEndpoints
的调用,则对UseAntiforgery
的调用必须介于两者之间。- 如果项目中使用了鉴权和授权,那么对
UseAntiforgery
的调用必须在对UseAuthentication
和UseAuthorization
的调用后发出。
防伪特性
默认情况下,组件中的防伪特性是处于开启状态的,如果防伪检查失败,则会则会引发 400 - Bad Request
响应,并且不会处理表单。
如果希望将防伪状态关闭,可以在组件中使用[RequireAntiforgeryToken]
并将required
属性设置为false
。
- 不建议针对公共应用禁用防伪
@using Microsoft.AspNetCore.Antiforgery
@attribute [RequireAntiforgeryToken(required: false)]
防伪组件
AntiforgeryToken
组件可以将携带防伪信息的标签渲染为隐藏字段。如果应用中开启了防伪保护,那么就需要在表单中使用此组件,以携带正确的防伪信息,才能正常提交。
对于内置表单组件EditForm
,默认情况已经自动使用了AntiforgeryToken
组件,不需要我们再显式使用。HTML的原生<form>
表单,则需要我们自己显式使用。
-
示例
<form method="post" @onsubmit="Submit" @formname="starshipForm"><AntiforgeryToken /><input id="send" type="submit" value="Send" /> </form>@if (submitted) {<p>Form submitted!</p> }@code{private bool submitted = false;private void Submit() => submitted = true; }
当使用\<form>
标签以application/x-www-form-urlencoded、multipart/form-data 或 text/plain 编码提交时,必须使用防伪令牌(<AntiforgeryToken>
)。
HTTP的防伪信息获取
如果想要在HTTP请求中添加防伪信息,可以通过在组件中注入 AntiforgeryStateProvider
服务,并通过其 GetAntiforgeryToken()
方法获取当前的 AntiforgeryRequestToken
。
-
示例
@inject AntiforgeryStateProvider Antiforgery......@code{private async Task OnSubmit(){var antiforgery = Antiforgery.GetAntiforgeryToken();var request = new HttpRequestMessage(HttpMethod.Post, "action");request.Headers.Add("RequestVerificationToken", antiforgery.RequestToken);var response = await client.SendAsync(request);...} }
增强型表单处理
启用增强型表单
EditForm
组件的Enhance
组件参数或 HTML 的<form>
表单的data-enhance
属性可以设置对表单的 POST 请求采用增强型导航。
-
增强型表单的使用仅适用于 Blazor 终结点,将增强型表单在非 Blazor 终结点中使用会导致错误。
-
示例
<EditForm ... Enhance ...>... </EditForm><form ... data-enhance ...>... </form>
禁用增强型表单
- 对于
EditForm
组件,禁用增强型表单,只需要将Enhance
组件参数移除或将其设置为Enhance="false"
。 - 对于 HTML
<form>
表单,禁用增强型表单,只需要将data-enhance
属性移除或将其设置为data-enhance="false"
。
注意
Enhance
属性只在静态SSR模式下有效,在交互式渲染下会被完全忽视。
在静态SSR下开启增强型表单与传统表单的区别如下:
- 提交方式:增强型表单提交时使用AJAX提交;传统表单则会通过POST请求
- 用户体验:增强型表达实现SPA式局部刷新;传统表单则整页刷新
- 模型绑定:增强型表单可以通过
[SupplyParameterFromForm]
特性绑定数据模型;传统表单则需要通过服务器端来匹配