ASP.NET Core Blazor 窗体验证
注意
此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本。
警告
此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本。
本文介绍如何在 Blazor 窗体中使用验证。
窗体验证
在基本窗体验证场景中,EditForm 实例可以使用声明的 EditContext 和 ValidationMessageStore 实例来验证表单域。 EditContext 的 OnValidationRequested 事件处理程序执行自定义验证逻辑。 处理程序的结果会更新 ValidationMessageStore 实例。
如果窗体模型是在托管窗体的组件中定义的,无论是直接定义为组件上的成员,还是在子类中定义,基本窗体验证都是有用的。 在多个组件中使用独立模型类时,建议使用验证程序组件。
在 Blazor Web App 中,客户端验证需要活动 BlazorSignalR 线路。 组件中采用静态服务器端呈现(静态 SSR)的表单无法使用客户端验证。 采用静态 SSR 的表单会在表单提交后在服务器上进行验证。
在下面的组件中,HandleValidationRequested
处理程序方法通过在验证窗体之前调用 ValidationMessageStore.Clear 来清除任何现有的验证消息。
Starship8.razor
:
@page "/starship-8"
@implements IDisposable
@inject ILogger<Starship8> Logger
<h2>Holodeck Configuration</h2>
<EditForm EditContext="editContext" OnValidSubmit="Submit" FormName="Starship8">
<div>
<label>
<InputCheckbox @bind-Value="Model!.Subsystem1" />
Safety Subsystem
</label>
</div>
<div>
<label>
<InputCheckbox @bind-Value="Model!.Subsystem2" />
Emergency Shutdown Subsystem
</label>
</div>
<div>
<ValidationMessage For="() => Model!.Options" />
</div>
<div>
<button type="submit">Update</button>
</div>
</EditForm>
@code {
private EditContext? editContext;
[SupplyParameterFromForm]
private Holodeck? Model { get; set; }
private ValidationMessageStore? messageStore;
protected override void OnInitialized()
{
Model ??= new();
editContext = new(Model);
editContext.OnValidationRequested += HandleValidationRequested;
messageStore = new(editContext);
}
private void HandleValidationRequested(object? sender,
ValidationRequestedEventArgs args)
{
messageStore?.Clear();
// Custom validation logic
if (!Model!.Options)
{
messageStore?.Add(() => Model.Options, "Select at least one.");
}
}
private void Submit() => Logger.LogInformation("Submit: Processing form");
public class Holodeck
{
public bool Subsystem1 { get; set; }
public bool Subsystem2 { get; set; }
public bool Options => Subsystem1 || Subsystem2;
}
public void Dispose()
{
if (editContext is not null)
{
editContext.OnValidationRequested -= HandleValidationRequested;
}
}
}
@page "/starship-8"
@implements IDisposable
@inject ILogger<Starship8> Logger
<h2>Holodeck Configuration</h2>
<EditForm EditContext="editContext" OnValidSubmit="Submit" FormName="Starship8">
<div>
<label>
<InputCheckbox @bind-Value="Model!.Subsystem1" />
Safety Subsystem
</label>
</div>
<div>
<label>
<InputCheckbox @bind-Value="Model!.Subsystem2" />
Emergency Shutdown Subsystem
</label>
</div>
<div>
<ValidationMessage For="() => Model!.Options" />
</div>
<div>
<button type="submit">Update</button>
</div>
</EditForm>
@code {
private EditContext? editContext;
[SupplyParameterFromForm]
private Holodeck? Model { get; set; }
private ValidationMessageStore? messageStore;
protected override void OnInitialized()
{
Model ??= new();
editContext = new(Model);
editContext.OnValidationRequested += HandleValidationRequested;
messageStore = new(editContext);
}
private void HandleValidationRequested(object? sender,
ValidationRequestedEventArgs args)
{
messageStore?.Clear();
// Custom validation logic
if (!Model!.Options)
{
messageStore?.Add(() => Model.Options, "Select at least one.");
}
}
private void Submit() => Logger.LogInformation("Submit: Processing form");
public class Holodeck
{
public bool Subsystem1 { get; set; }
public bool Subsystem2 { get; set; }
public bool Options => Subsystem1 || Subsystem2;
}
public void Dispose()
{
if (editContext is not null)
{
editContext.OnValidationRequested -= HandleValidationRequested;
}
}
}
@page "/starship-8"
@implements IDisposable
@inject ILogger<Starship8> Logger
<h2>Holodeck Configuration</h2>
<EditForm EditContext="editContext" OnValidSubmit="Submit">
<div>
<label>
<InputCheckbox @bind-Value="Model!.Subsystem1" />
Safety Subsystem
</label>
</div>
<div>
<label>
<InputCheckbox @bind-Value="Model!.Subsystem2" />
Emergency Shutdown Subsystem
</label>
</div>
<div>
<ValidationMessage For="() => Model!.Options" />
</div>
<div>
<button type="submit">Update</button>
</div>
</EditForm>
@code {
private EditContext? editContext;
public Holodeck? Model { get; set; }
private ValidationMessageStore? messageStore;
protected override void OnInitialized()
{
Model ??= new();
editContext = new(Model);
editContext.OnValidationRequested += HandleValidationRequested;
messageStore = new(editContext);
}
private void HandleValidationRequested(object? sender,
ValidationRequestedEventArgs args)
{
messageStore?.Clear();
// Custom validation logic
if (!Model!.Options)
{
messageStore?.Add(() => Model.Options, "Select at least one.");
}
}
private void Submit()
{
Logger.LogInformation("Submit called: Processing the form");
}
public class Holodeck
{
public bool Subsystem1 { get; set; }
public bool Subsystem2 { get; set; }
public bool Options => Subsystem1 || Subsystem2;
}
public void Dispose()
{
if (editContext is not null)
{
editContext.OnValidationRequested -= HandleValidationRequested;
}
}
}
数据注释验证程序组件和自定义验证
DataAnnotationsValidator 组件将数据注释验证附加到级联 EditContext。 启用数据注释验证需要 DataAnnotationsValidator 组件。 若要使用不同于数据注释的验证系统,请用自定义实现替换 DataAnnotationsValidator 组件。 可在以下参考源中检查 DataAnnotationsValidator 框架的实现:
注意
指向 .NET 参考源的文档链接通常会加载存储库的默认分支,该分支表示针对下一个 .NET 版本的当前开发。 若要为特定版本选择标记,请使用“切换分支或标记”下拉列表。 有关详细信息,请参阅如何选择 ASP.NET Core 源代码的版本标记 (dotnet/AspNetCore.Docs #26205)。
Blazor 执行两种类型的验证:
- 当用户从某个字段中跳出时,将执行字段验证。 在字段验证期间,DataAnnotationsValidator 组件将报告的所有验证结果与该字段相关联。
- 当用户提交窗体时,将执行模型验证。 在模型验证期间,DataAnnotationsValidator 组件尝试根据验证结果报告的成员名称来确定字段。 与单个成员无关的验证结果将与模型而不是字段相关联。
验证器组件
验证器组件通过管理窗体的 EditContext 的 ValidationMessageStore 来支持窗体验证。
Blazor 框架提供了 DataAnnotationsValidator 组件,以将验证支持附加到基于验证属性(数据批注)的窗体。 可以创建自定义验证器组件,以处理同一页上不同窗体或同一窗体上不同处理步骤的验证消息,例如先进行客户端验证,再进行服务器端验证。 本文的以下部分将使用本部分 CustomValidation
中所示的验证器组件示例:
在数据注释内置验证程序中,仅 Blazor 中不支持 [Remote]
验证属性。
注意
在许多情况下,可使用自定义数据注释验证属性来代替自定义验证器组件。 应用于窗体模型的自定义属性使用 DataAnnotationsValidator 组件激活。 当与服务器验证一起使用时,应用于模型的所有自定义属性都必须可在服务器上执行。 有关详细信息,请参阅自定义验证属性部分。
从 ComponentBase 创建验证器组件:
- 窗体的 EditContext 是组件的级联参数。
- 初始化验证器组件时,将创建一个新的 ValidationMessageStore 来维护当前的窗体错误列表。
- 当窗体组件中的开发人员代码调用
DisplayErrors
方法时,消息存储接收错误。 这些错误会传递到Dictionary<string, List<string>>
中的DisplayErrors
方法。 在字典中,键是具有一个或多个错误的窗体字段的名称。 值为错误列表。 - 发生以下任一情况时,将清除消息:
- 引发 EditContext 事件时,会在 OnValidationRequested 上请求验证。 所有错误都将被清除。
- 引发 OnFieldChanged 事件时,窗体中的字段会更改。 仅清除字段的错误。
ClearErrors
方法由开发人员代码调用。 所有错误都将被清除。
更新以下类中的命名空间以匹配应用的命名空间。
CustomValidation.cs
:
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
namespace BlazorSample;
public class CustomValidation : ComponentBase
{
private ValidationMessageStore? messageStore;
[CascadingParameter]
private EditContext? CurrentEditContext { get; set; }
protected override void OnInitialized()
{
if (CurrentEditContext is null)
{
throw new InvalidOperationException(
$"{nameof(CustomValidation)} requires a cascading " +
$"parameter of type {nameof(EditContext)}. " +
$"For example, you can use {nameof(CustomValidation)} " +
$"inside an {nameof(EditForm)}.");
}
messageStore = new(CurrentEditContext);
CurrentEditContext.OnValidationRequested += (s, e) =>
messageStore?.Clear();
CurrentEditContext.OnFieldChanged += (s, e) =>
messageStore?.Clear(e.FieldIdentifier);
}
public void DisplayErrors(Dictionary<string, List<string>> errors)
{
if (CurrentEditContext is not null)
{
foreach (var err in errors)
{
messageStore?.Add(CurrentEditContext.Field(err.Key), err.Value);
}
CurrentEditContext.NotifyValidationStateChanged();
}
}
public void ClearErrors()
{
messageStore?.Clear();
CurrentEditContext?.NotifyValidationStateChanged();
}
}
重要
从 ComponentBase 派生时需要指定命名空间。 未能指定命名空间会导致生成错误:
Tag helpers cannot target tag name '<global namespace>.{CLASS NAME}' because it contains a ' ' character.
{CLASS NAME}
占位符是组件类的名称。 本部分中的自定义验证程序示例指定了示例命名空间 BlazorSample
。
注意
匿名 Lambda 表达式是前面的示例中 OnValidationRequested 和 OnFieldChanged 的已注册的事件处理程序。 在此方案中,无需实现 IDisposable 和取消订阅事件委托。 有关详细信息,请参阅 ASP.NET Core Razor 组件生命周期。
使用验证程序组件的业务逻辑验证
对于一般的业务逻辑验证,可以使用接收字典中的窗体错误的验证程序组件。
如果窗体模型是在托管窗体的组件中定义的,无论是直接定义为组件上的成员,还是在子类中定义,基本验证都是有用的。 在多个组件中使用独立模型类时,建议使用验证程序组件。
如下示例中:
- 使用“输入组件”一文的窗体示例部分的
Starfleet Starship Database
窗体(Starship3
组件)的缩写版本,它仅接受 Starship 的分类和说明。 由于窗体中未包含 DataAnnotationsValidator 组件,因此不会在窗体提交时触发数据注释验证。 - 使用本文的验证器组件部分的
CustomValidation
组件。 - 如果用户选择
Defense
ship 分类 (Classification
),则需要 ship 说明 (Description
) 的值才能验证。
在组件中设置验证消息时,这些消息将被添加到验证器的 ValidationMessageStore,并在 EditForm 验证摘要中显示。
Starship9.razor
:
@page "/starship-9"
@inject ILogger<Starship9> Logger
<h1>Starfleet Starship Database</h1>
<h2>New Ship Entry Form</h2>
<EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship9">
<CustomValidation @ref="customValidation" />
<ValidationSummary />
<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>
Description (optional):
<InputTextArea @bind-Value="Model!.Description" />
</label>
</div>
<div>
<button type="submit">Submit</button>
</div>
</EditForm>
@code {
private CustomValidation? customValidation;
[SupplyParameterFromForm]
private Starship? Model { get; set; }
protected override void OnInitialized() =>
Model ??= new() { ProductionDate = DateTime.UtcNow };
private void Submit()
{
customValidation?.ClearErrors();
var errors = new Dictionary<string, List<string>>();
if (Model!.Classification == "Defense" &&
string.IsNullOrEmpty(Model.Description))
{
errors.Add(nameof(Model.Description),
new() { "For a 'Defense' ship classification, " +
"'Description' is required." });
}
if (errors.Any())
{
customValidation?.DisplayErrors(errors);
}
else
{
Logger.LogInformation("Submit called: Processing the form");
}
}
}
@page "/starship-9"
@inject ILogger<Starship9> Logger
<h1>Starfleet Starship Database</h1>
<h2>New Ship Entry Form</h2>
<EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship9">
<CustomValidation @ref="customValidation" />
<ValidationSummary />
<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>
Description (optional):
<InputTextArea @bind-Value="Model!.Description" />
</label>
</div>
<div>
<button type="submit">Submit</button>
</div>
</EditForm>
@code {
private CustomValidation? customValidation;
[SupplyParameterFromForm]
private Starship? Model { get; set; }
protected override void OnInitialized() =>
Model ??= new() { ProductionDate = DateTime.UtcNow };
private void Submit()
{
customValidation?.ClearErrors();
var errors = new Dictionary<string, List<string>>();
if (Model!.Classification == "Defense" &&
string.IsNullOrEmpty(Model.Description))
{
errors.Add(nameof(Model.Description),
new() { "For a 'Defense' ship classification, " +
"'Description' is required." });
}
if (errors.Any())
{
customValidation?.DisplayErrors(errors);
}
else
{
Logger.LogInformation("Submit called: Processing the form");
}
}
}
@page "/starship-9"
@inject ILogger<Starship9> Logger
<h1>Starfleet Starship Database</h1>
<h2>New Ship Entry Form</h2>
<EditForm Model="Model" OnValidSubmit="Submit">
<CustomValidation @ref="customValidation" />
<ValidationSummary />
<div>
<label>
Primary Classification:
<InputSelect @bind-Value="Model!.Classification">
<option value="">Select classification ...</option>
<option value="Exploration">Exploration</option>
<option value="Diplomacy">Diplomacy</option>
<option value="Defense">Defense</option>
</InputSelect>
</label>
</div>
<div>
<label>
Description (optional):
<InputTextArea @bind-Value="Model!.Description" />
</label>
</div>
<div>
<button type="submit">Submit</button>
</div>
</EditForm>
@code {
private CustomValidation? customValidation;
public Starship? Model { get; set; }
protected override void OnInitialized() =>
Model ??= new() { ProductionDate = DateTime.UtcNow };
private void Submit()
{
customValidation?.ClearErrors();
var errors = new Dictionary<string, List<string>>();
if (Model!.Classification == "Defense" &&
string.IsNullOrEmpty(Model.Description))
{
errors.Add(nameof(Model.Description),
new() { "For a 'Defense' ship classification, " +
"'Description' is required." });
}
if (errors.Any())
{
customValidation?.DisplayErrors(errors);
}
else
{
Logger.LogInformation("Submit called: Processing the form");
}
}
}
注意
除了使用验证组件,还可使用数据注释验证属性。 应用于窗体模型的自定义属性使用 DataAnnotationsValidator 组件激活。 与服务器端验证一起使用时,属性都必须可在服务器上执行。 有关详细信息,请参阅自定义验证属性部分。
使用验证程序组件的服务器验证
本部分侧重介绍 Blazor Web App 方案,但对于使用 Web API 进行服务器验证的任何类型应用,都采用相同的常规方法。
本部分侧重介绍托管的 Blazor WebAssembly 方案,但对将服务器验证与 Web API 配合使用的任何类型应用的方法采用相同的常规方法。
除客户端验证外,还支持服务器验证:
- 使用 DataAnnotationsValidator 组件处理窗体中的客户端验证。
- 当窗体传递客户端验证(调用 OnValidSubmit)时,将 EditContext.Model 发送到后端服务器 API 进行窗体处理。
- 处理服务器上的模型验证。
- 服务器 API 包括开发人员提供的内置框架数据注释验证和自定义验证逻辑。 如果验证在服务器上传递,则处理窗格并发送回成功状态代码 (
200 - OK
)。 如果验证失败,则返回失败状态代码 (400 - Bad Request
) 和字段验证错误。 - 成功时禁用窗体,否则显示错误。
如果窗体模型是在托管窗体的组件中定义的,无论是直接定义为组件上的成员,还是在子类中定义,基本验证都是有用的。 在多个组件中使用独立模型类时,建议使用验证程序组件。
下面的示例基于:
- 包含从 Blazor Web App 项目模板创建的交互式 WebAssembly 组件的 Blazor Web App。
- “输入组件”一文的窗体示例部分的
Starship
模型 (Starship.cs
)。 - 验证器组件部分中显示的
CustomValidation
组件。
将 Starship
模型 (Starship.cs
) 放入共享类库项目中,以便客户端和服务器项目都可使用该模型。 添加或更新命名空间,使之与共享应用程序的命名空间相匹配,例如 namespace BlazorSample.Shared
。 由于模型需要数据注释,请确认共享类库使用共享框架或将 System.ComponentModel.Annotations
包添加到共享项目。
备注
有关将包添加到 .NET 应用的指南,请参阅包使用工作流(NuGet 文档)中“安装和管理包”下的文章。 在 NuGet.org 中确认正确的包版本。
在 Blazor Web App 的主项目中,添加控制器来处理 Starship 验证请求并返回失败的验证消息。 更新共享类库项目最后一条 using
语句中的命名空间和控制器类中的 namespace
。 如果用户选择 Defense
ship 分类 (Classification
),除了客户端和服务器的数据批注验证,控制器还验证是否为 ship 说明 (Description
) 提供了值。
- 从 Blazor WebAssembly 项目模板创建的托管 Blazor WebAssembly解决方案。 托管的 Blazor WebAssembly 安全文档中所述的任何安全托管 Blazor 解决方案都支持此方法。
- “输入组件”一文的窗体示例部分的
Starship
模型 (Starship.cs
)。 - 验证器组件部分中显示的
CustomValidation
组件。
将 Starship
模型 (Starship.cs
) 放入解决方案的 Shared
项目中,以便客户端和服务器应用程序都可使用该模型。 添加或更新命名空间,使之与共享应用程序的命名空间相匹配,例如 namespace BlazorSample.Shared
。 模型需要数据注释,因此请将 System.ComponentModel.Annotations
包添加到 Shared
项目。
备注
有关将包添加到 .NET 应用的指南,请参阅包使用工作流(NuGet 文档)中“安装和管理包”下的文章。 在 NuGet.org 中确认正确的包版本。
在 Server 项目中,添加控制器来处理 Starship 验证请求并返回失败的验证消息。 更新 Shared
项目最后一条 using
语句中的命名空间和控制器类中的 namespace
。 如果用户选择 Defense
ship 分类 (Classification
),除了客户端和服务器的数据批注验证,控制器还验证是否为 ship 说明 (Description
) 提供了值。
Defense
ship 分类的验证仅在控制器的服务器上进行,因为在窗体提交到服务器时,即将发布的窗体不会在客户端执行相同的验证。 在需要对服务器上的用户输入进行专用业务逻辑验证的应用中,无需客户端验证的服务器验证很常见。 例如,可能需要使用为用户存储的专用数据来验证用户输入。 专用数据显然无法发送到客户端进行客户端验证。
备注
本部分中的 StarshipValidation
控制器使用 Microsoft Identity 2.0。 只有用户具有此 API 的 API.Access
作用域,Web API 才会接受对应的令牌。 如果 API 的作用域名称不同于 API.Access
,则需要进行其他自定义。
有关代理安全性的详细信息,请参阅:
- ASP.NET Core Blazor 身份验证和授权(以及 Blazor 安全性和 Identity 节点中的其他文章)
- Microsoft identity 平台文档
Controllers/StarshipValidation.cs
:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using BlazorSample.Shared;
namespace BlazorSample.Server.Controllers;
[Authorize]
[ApiController]
[Route("[controller]")]
public class StarshipValidationController(
ILogger<StarshipValidationController> logger)
: ControllerBase
{
static readonly string[] scopeRequiredByApi = new[] { "API.Access" };
[HttpPost]
public async Task<IActionResult> Post(Starship model)
{
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
try
{
if (model.Classification == "Defense" &&
string.IsNullOrEmpty(model.Description))
{
ModelState.AddModelError(nameof(model.Description),
"For a 'Defense' ship " +
"classification, 'Description' is required.");
}
else
{
logger.LogInformation("Processing the form asynchronously");
// async ...
return Ok(ModelState);
}
}
catch (Exception ex)
{
logger.LogError("Validation Error: {Message}", ex.Message);
}
return BadRequest(ModelState);
}
}
确认或更新上述控制器 (BlazorSample.Server.Controllers
) 的命名空间,以匹配应用的控制器命名空间。
当服务器上发生模型绑定验证错误时,ApiController
(ApiControllerAttribute) 通常通过 ValidationProblemDetails 返回默认错误请求响应。 如以下示例所示,当 Starfleet Starship Database
窗格的所有字段未提交且窗格未通过验证时,响应包含的数据不仅仅是验证错误:
{
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Id": ["The Id field is required."],
"Classification": ["The Classification field is required."],
"IsValidatedDesign": ["This form disallows unapproved ships."],
"MaximumAccommodation": ["Accommodation invalid (1-100000)."]
}
}
备注
若要演示上述的 JSON 响应,必须禁用窗体的客户端验证以允许提交空的字段窗体,或使用工具直接将请求发送到服务器 API,如 Firefox 浏览器(开发者版)。
如果服务器 API 返回上述的默认 JSON 响应,则客户端可分析开发人员代码中的响应,为窗体验证错误处理进程获取 errors
节点的子节点。 你无法轻松通过编写开发人员代码来分析文件。 手动分析 JSON 需要在调用 ReadFromJsonAsync 后生成错误 Dictionary<string, List<string>>
。 理想情况下,服务器 API 应只返回验证错误,如以下示例所示:
{
"Id": ["The Id field is required."],
"Classification": ["The Classification field is required."],
"IsValidatedDesign": ["This form disallows unapproved ships."],
"MaximumAccommodation": ["Accommodation invalid (1-100000)."]
}
若要修改服务器 API 的响应,使其仅返回验证错误,请更改在 Program
文件中注释了 ApiControllerAttribute 的操作上调用的委托。 对于 API 终结点 (/StarshipValidation
),返回具有 ModelStateDictionary 的 BadRequestObjectResult。 对于任何其他 API 终结点,通过使用新的 ValidationProblemDetails 返回对象结果来保留默认行为。
将 Microsoft.AspNetCore.Mvc 命名空间添加到 Blazor Web App 的主项目中的 Program
文件顶部:
using Microsoft.AspNetCore.Mvc;
在 Program
文件中,添加或更新以下 AddControllersWithViews 扩展方法并将以下调用添加到 ConfigureApiBehaviorOptions:
builder.Services.AddControllersWithViews()
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
if (context.HttpContext.Request.Path == "/StarshipValidation")
{
return new BadRequestObjectResult(context.ModelState);
}
else
{
return new BadRequestObjectResult(
new ValidationProblemDetails(context.ModelState));
}
};
});
如果第一次将控制器添加到 Blazor Web App 的主项目,请在放置上述用于注册控制器服务的代码时,映射控制器终结点。 以下示例使用默认控制器路由:
app.MapDefaultControllerRoute();
备注
前面的示例通过调用 AddControllersWithViews 显式注册控制器服务,以自动缓解跨网站请求伪造 (XSRF/CSRF) 攻击。 如果仅使用 AddControllers,则不会自动启用防伪造。
有关控制器路由和验证失败错误响应的详细信息,请参阅以下资源:
在 .Client
项目中,添加验证器组件部分中显示的 CustomValidation
组件。 更新命名空间以匹配应用程序,例如 namespace BlazorSample.Client
。
在 .Client
项目中,借助 CustomValidation
组件的支持,系统更新 Starfleet Starship Database
窗体,以显示服务器验证错误和。 当服务器 API 返回验证消息时,这些消息将添加到 CustomValidation
组件的 ValidationMessageStore。 此错误会按窗体的验证摘要显示在窗体的 EditContext 中。
在以下组件中,将共享项目 (@using BlazorSample.Shared
) 的命名空间更新为共享项目的命名空间。 请注意,窗体需要授权,因此用户必须登录到应用程序以导航到窗体。
将 Microsoft.AspNetCore.Mvc 命名空间添加到 Server 应用程序的 Program
文件上方:
using Microsoft.AspNetCore.Mvc;
在 Program
文件中,找到 AddControllersWithViews 扩展方法并将以下调用添加到 ConfigureApiBehaviorOptions:
builder.Services.AddControllersWithViews()
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
if (context.HttpContext.Request.Path == "/StarshipValidation")
{
return new BadRequestObjectResult(context.ModelState);
}
else
{
return new BadRequestObjectResult(
new ValidationProblemDetails(context.ModelState));
}
};
});
注意
前面的示例通过调用 AddControllersWithViews 显式注册控制器服务,以自动缓解跨网站请求伪造 (XSRF/CSRF) 攻击。 如果仅使用 AddControllers,则不会自动启用防伪造。
在 Client 项目中,添加验证器组件部分中显示的 CustomValidation
组件。 更新命名空间以匹配应用程序,例如 namespace BlazorSample.Client
。
在 Client 项目中,借助 CustomValidation
组件的支持,系统更新 Starfleet Starship Database
窗体,以显示服务器验证错误和。 当服务器 API 返回验证消息时,这些消息将添加到 CustomValidation
组件的 ValidationMessageStore。 此错误会按窗体的验证摘要显示在窗体的 EditContext 中。
在以下组件中,将 Shared
项目 (@using BlazorSample.Shared
) 的命名空间更新为共享项目的命名空间。 请注意,窗体需要授权,因此用户必须登录到应用程序以导航到窗体。
Starship10.razor
:
注意
基于 EditForm 的窗体会自动启用防伪支持。 控制器应使用 AddControllersWithViews 注册控制器服务,并自动为 Web API 启用防伪支持。
@page "/starship-10"
@rendermode InteractiveWebAssembly
@using System.Net
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using BlazorSample.Shared
@attribute [Authorize]
@inject HttpClient Http
@inject ILogger<Starship10> Logger
<h1>Starfleet Starship Database</h1>
<h2>New Ship Entry Form</h2>
<EditForm FormName="Starship10" Model="Model" OnValidSubmit="Submit">
<DataAnnotationsValidator />
<CustomValidation @ref="customValidation" />
<ValidationSummary />
<div>
<label>
Identifier:
<InputText @bind-Value="Model!.Id" disabled="@disabled" />
</label>
</div>
<div>
<label>
Description (optional):
<InputTextArea @bind-Value="Model!.Description"
disabled="@disabled" />
</label>
</div>
<div>
<label>
Primary Classification:
<InputSelect @bind-Value="Model!.Classification" disabled="@disabled">
<option value="">Select classification ...</option>
<option value="Exploration">Exploration</option>
<option value="Diplomacy">Diplomacy</option>
<option value="Defense">Defense</option>
</InputSelect>
</label>
</div>
<div>
<label>
Maximum Accommodation:
<InputNumber @bind-Value="Model!.MaximumAccommodation"
disabled="@disabled" />
</label>
</div>
<div>
<label>
Engineering Approval:
<InputCheckbox @bind-Value="Model!.IsValidatedDesign"
disabled="@disabled" />
</label>
</div>
<div>
<label>
Production Date:
<InputDate @bind-Value="Model!.ProductionDate" disabled="@disabled" />
</label>
</div>
<div>
<button type="submit" disabled="@disabled">Submit</button>
</div>
<div style="@messageStyles">
@message
</div>
</EditForm>
@code {
private CustomValidation? customValidation;
private bool disabled;
private string? message;
private string messageStyles = "visibility:hidden";
[SupplyParameterFromForm]
private Starship? Model { get; set; }
protected override void OnInitialized() =>
Model ??= new() { ProductionDate = DateTime.UtcNow };
private async Task Submit(EditContext editContext)
{
customValidation?.ClearErrors();
try
{
var response = await Http.PostAsJsonAsync<Starship>(
"StarshipValidation", (Starship)editContext.Model);
var errors = await response.Content
.ReadFromJsonAsync<Dictionary<string, List<string>>>() ??
new Dictionary<string, List<string>>();
if (response.StatusCode == HttpStatusCode.BadRequest &&
errors.Any())
{
customValidation?.DisplayErrors(errors);
}
else if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException(
$"Validation failed. Status Code: {response.StatusCode}");
}
else
{
disabled = true;
messageStyles = "color:green";
message = "The form has been processed.";
}
}
catch (AccessTokenNotAvailableException ex)
{
ex.Redirect();
}
catch (Exception ex)
{
Logger.LogError("Form processing error: {Message}", ex.Message);
disabled = true;
messageStyles = "color:red";
message = "There was an error processing the form.";
}
}
}
Blazor Web App 的 .Client
项目还必须为针对后端 Web API 控制器的 HTTP POST 请求注册 HttpClient。 确认 .Client
项目的 Program
文件中有以下内容或将其添加到该文件中:
builder.Services.AddScoped(sp =>
new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
前面的示例使用 builder.HostEnvironment.BaseAddress
(IWebAssemblyHostEnvironment.BaseAddress) 设置基址,该属性会获取应用的基址,并且通常派生自主机页中 <base>
标记的 href
值。
@page "/starship-10"
@using System.Net
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using BlazorSample.Shared
@attribute [Authorize]
@inject HttpClient Http
@inject ILogger<Starship10> Logger
<h1>Starfleet Starship Database</h1>
<h2>New Ship Entry Form</h2>
<EditForm Model="Model" OnValidSubmit="Submit">
<DataAnnotationsValidator />
<CustomValidation @ref="customValidation" />
<ValidationSummary />
<div>
<label>
Identifier:
<InputText @bind-Value="Model!.Id" disabled="@disabled" />
</label>
</div>
<div>
<label>
Description (optional):
<InputTextArea @bind-Value="Model!.Description"
disabled="@disabled" />
</label>
</div>
<div>
<label>
Primary Classification:
<InputSelect @bind-Value="Model!.Classification" disabled="@disabled">
<option value="">Select classification ...</option>
<option value="Exploration">Exploration</option>
<option value="Diplomacy">Diplomacy</option>
<option value="Defense">Defense</option>
</InputSelect>
</label>
</div>
<div>
<label>
Maximum Accommodation:
<InputNumber @bind-Value="Model!.MaximumAccommodation"
disabled="@disabled" />
</label>
</div>
<div>
<label>
Engineering Approval:
<InputCheckbox @bind-Value="Model!.IsValidatedDesign"
disabled="@disabled" />
</label>
</div>
<div>
<label>
Production Date:
<InputDate @bind-Value="Model!.ProductionDate" disabled="@disabled" />
</label>
</div>
<div>
<button type="submit" disabled="@disabled">Submit</button>
</div>
<div style="@messageStyles">
@message
</div>
</EditForm>
@code {
private CustomValidation? customValidation;
private bool disabled;
private string? message;
private string messageStyles = "visibility:hidden";
public Starship? Model { get; set; }
protected override void OnInitialized() =>
Model ??= new() { ProductionDate = DateTime.UtcNow };
private async Task Submit(EditContext editContext)
{
customValidation?.ClearErrors();
try
{
var response = await Http.PostAsJsonAsync<Starship>(
"StarshipValidation", (Starship)editContext.Model);
var errors = await response.Content
.ReadFromJsonAsync<Dictionary<string, List<string>>>() ??
new Dictionary<string, List<string>>();
if (response.StatusCode == HttpStatusCode.BadRequest &&
errors.Any())
{
customValidation?.DisplayErrors(errors);
}
else if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException(
$"Validation failed. Status Code: {response.StatusCode}");
}
else
{
disabled = true;
messageStyles = "color:green";
message = "The form has been processed.";
}
}
catch (AccessTokenNotAvailableException ex)
{
ex.Redirect();
}
catch (Exception ex)
{
Logger.LogError("Form processing error: {Message}", ex.Message);
disabled = true;
messageStyles = "color:red";
message = "There was an error processing the form.";
}
}
}
注意
除了使用验证组件外,还可使用数据注释验证属性。 应用于窗体模型的自定义属性使用 DataAnnotationsValidator 组件激活。 与服务器端验证一起使用时,属性都必须可在服务器上执行。 有关详细信息,请参阅自定义验证属性部分。
注意
本部分中的服务器验证方法适用于本文档集中的所有 Blazor WebAssembly 托管解决方案示例:
基于输入事件的 InputText
使用 InputText 组件创建一个使用 oninput
事件 (input
) 而不是 onchange
事件 (change
) 的自定义组件。 对每个击键使用 input
事件触发器字段验证。
以下 CustomInputText
组件继承框架的 InputText
组件,并将事件绑定设置为 oninput
事件 (input
)。
CustomInputText.razor
:
@inherits InputText
<input @attributes="AdditionalAttributes"
class="@CssClass"
@bind="CurrentValueAsString"
@bind:event="oninput" />
CustomInputText
组件可在任何使用 InputText 的位置使用。 以下组件使用共享 CustomInputText
组件。
Starship11.razor
:
@page "/starship-11"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship11> Logger
<EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship11">
<DataAnnotationsValidator />
<ValidationSummary />
<div>
<label>
Identifier:
<CustomInputText @bind-Value="Model!.Id" />
</label>
</div>
<div>
<button type="submit">Submit</button>
</div>
</EditForm>
<div>
CurrentValue: @Model?.Id
</div>
@code {
[SupplyParameterFromForm]
private Starship? Model { get; set; }
protected override void OnInitialized() => Model ??= new();
private void Submit() => Logger.LogInformation("Submit: Processing form");
public class Starship
{
[Required]
[StringLength(10, ErrorMessage = "Id is too long.")]
public string? Id { get; set; }
}
}
@page "/starship-11"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship11> Logger
<EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship11">
<DataAnnotationsValidator />
<ValidationSummary />
<div>
<label>
Identifier:
<CustomInputText @bind-Value="Model!.Id" />
</label>
</div>
<div>
<button type="submit">Submit</button>
</div>
</EditForm>
<div>
CurrentValue: @Model?.Id
</div>
@code {
[SupplyParameterFromForm]
private Starship? Model { get; set; }
protected override void OnInitialized() => Model ??= new();
private void Submit() => Logger.LogInformation("Submit: Processing form");
public class Starship
{
[Required]
[StringLength(10, ErrorMessage = "Id is too long.")]
public string? Id { get; set; }
}
}
@page "/starship-11"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship11> Logger
<EditForm Model="Model" OnValidSubmit="Submit">
<DataAnnotationsValidator />
<ValidationSummary />
<CustomInputText @bind-Value="Model!.Id" />
<button type="submit">Submit</button>
</EditForm>
<div>
CurrentValue: @Model?.Id
</div>
@code {
public Starship? Model { get; set; }
protected override void OnInitialized() => Model ??= new();
private void Submit()
{
Logger.LogInformation("Submit called: Processing the form");
}
public class Starship
{
[Required]
[StringLength(10, ErrorMessage = "Id is too long.")]
public string? Id { get; set; }
}
}
验证摘要和验证消息组件
ValidationSummary 组件用于汇总所有验证消息,这与验证摘要标记帮助程序类似:
<ValidationSummary />
使用 Model
参数输出特定模型的验证消息:
<ValidationSummary Model="Model" />
ValidationMessage<TValue> 组件用于显示特定字段的验证消息,这与验证消息标记帮助程序类似。 使用 For 属性和一个为模型属性命名的 Lambda 表达式来指定要验证的字段:
<ValidationMessage For="@(() => Model!.MaximumAccommodation)" />
ValidationMessage<TValue> 和 ValidationSummary 组件支持任意属性。 与某个组件参数不匹配的所有属性都将添加到生成的 <div>
或 <ul>
元素中。
在应用的样式表(wwwroot/css/app.css
或 wwwroot/css/site.css
)中控制验证消息的样式。 默认 validation-message
类将验证消息的文本颜色设置为红色:
.validation-message {
color: red;
}
确定表单字段是否有效
使用 EditContext.IsValid 在不获取验证消息的情况下确定字段是否有效。
支持
,但不建议使用:var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();
推荐
:var isValid = editContext.IsValid(fieldIdentifier);
自定义验证属性
当使用自定义验证属性时,为确保验证结果与字段正确关联,请在创建 ValidationResult 时传递验证上下文的 MemberName。
CustomValidator.cs
:
using System;
using System.ComponentModel.DataAnnotations;
public class CustomValidator : ValidationAttribute
{
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
...
return new ValidationResult("Validation message to user.",
new[] { validationContext.MemberName });
}
}
通过 ValidationContext 将服务注入到自定义验证属性中。 以下示例演示沙拉厨师表单,该表单使用依赖项注入 (DI) 验证用户输入。
SaladChef
类指示 Ten Forward 沙拉的批准的星际飞船成分列表。
SaladChef.cs
:
namespace BlazorSample;
public class SaladChef
{
public string[] SaladToppers = { "Horva", "Kanda Root", "Krintar", "Plomeek",
"Syto Bean" };
}
在 Program
文件的应用 DI 容器中注册 SaladChef
:
builder.Services.AddTransient<SaladChef>();
以下 SaladChefValidatorAttribute
类的 IsValid
方法从 DI 获取 SaladChef
服务以检查用户输入。
SaladChefValidatorAttribute.cs
:
using System.ComponentModel.DataAnnotations;
namespace BlazorSample;
public class SaladChefValidatorAttribute : ValidationAttribute
{
protected override ValidationResult? IsValid(object? value,
ValidationContext validationContext)
{
var saladChef = validationContext.GetRequiredService<SaladChef>();
if (saladChef.SaladToppers.Contains(value?.ToString()))
{
return ValidationResult.Success;
}
return new ValidationResult("Is that a Vulcan salad topper?! " +
"The following toppers are available for a Ten Forward salad: " +
string.Join(", ", saladChef.SaladToppers));
}
}
以下组件通过将 SaladChefValidatorAttribute
([SaladChefValidator]
) 应用到沙拉成分字符串 (SaladIngredient
) 来验证用户输入。
Starship12.razor
:
@page "/starship-12"
@inject SaladChef SaladChef
<EditForm Model="this" autocomplete="off" FormName="Starship12">
<DataAnnotationsValidator />
<div>
<label>
Salad topper (@saladToppers):
<input @bind="SaladIngredient" />
</label>
</div>
<div>
<button type="submit">Submit</button>
</div>
<ul>
@foreach (var message in context.GetValidationMessages())
{
<li class="validation-message">@message</li>
}
</ul>
</EditForm>
@code {
private string? saladToppers;
[SaladChefValidator]
public string? SaladIngredient { get; set; }
protected override void OnInitialized() =>
saladToppers ??= string.Join(", ", SaladChef.SaladToppers);
}
@page "/starship-12"
@inject SaladChef SaladChef
<EditForm Model="this" autocomplete="off" FormName="Starship12">
<DataAnnotationsValidator />
<div>
<label>
Salad topper (@saladToppers):
<input @bind="SaladIngredient" />
</label>
</div>
<div>
<button type="submit">Submit</button>
</div>
<ul>
@foreach (var message in context.GetValidationMessages())
{
<li class="validation-message">@message</li>
}
</ul>
</EditForm>
@code {
private string? saladToppers;
[SaladChefValidator]
public string? SaladIngredient { get; set; }
protected override void OnInitialized() =>
saladToppers ??= string.Join(", ", SaladChef.SaladToppers);
}
@page "/starship-12"
@inject SaladChef SaladChef
<EditForm Model="this" autocomplete="off">
<DataAnnotationsValidator />
<p>
<label>
Salad topper (@saladToppers):
<input @bind="SaladIngredient" />
</label>
</p>
<button type="submit">Submit</button>
<ul>
@foreach (var message in context.GetValidationMessages())
{
<li class="validation-message">@message</li>
}
</ul>
</EditForm>
@code {
private string? saladToppers;
[SaladChefValidator]
public string? SaladIngredient { get; set; }
protected override void OnInitialized() =>
saladToppers ??= string.Join(", ", SaladChef.SaladToppers);
}
自定义验证 CSS 类属性
与 CSS 框架集成时,自定义验证 CSS 类属性非常有用,例如 Bootstrap。
若要指定自定义验证 CSS 类属性,请首先为自定义验证提供 CSS 样式。 在以下示例中,系统指定了有效样式 (validField
) 和无效样式 (invalidField
)。
将以下 CSS 类添加到应用的样式表:
.validField {
border-color: lawngreen;
}
.invalidField {
background-color: tomato;
}
创建一个从 FieldCssClassProvider 派生的类,用于检查字段验证消息,并应用相应的有效或无效样式。
CustomFieldClassProvider.cs
:
using Microsoft.AspNetCore.Components.Forms;
public class CustomFieldClassProvider : FieldCssClassProvider
{
public override string GetFieldCssClass(EditContext editContext,
in FieldIdentifier fieldIdentifier)
{
var isValid = editContext.IsValid(fieldIdentifier);
return isValid ? "validField" : "invalidField";
}
}
using Microsoft.AspNetCore.Components.Forms;
public class CustomFieldClassProvider : FieldCssClassProvider
{
public override string GetFieldCssClass(EditContext editContext,
in FieldIdentifier fieldIdentifier)
{
var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();
return isValid ? "validField" : "invalidField";
}
}
使用 SetFieldCssClassProvider 将 CustomFieldClassProvider
类设置为表单 EditContext 实例上的字段 CSS 类提供程序。
Starship13.razor
:
@page "/starship-13"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship13> Logger
<EditForm EditContext="editContext" OnValidSubmit="Submit" FormName="Starship13">
<DataAnnotationsValidator />
<ValidationSummary />
<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();
editContext = new(Model);
editContext.SetFieldCssClassProvider(new CustomFieldClassProvider());
}
private void Submit() => Logger.LogInformation("Submit: Processing form");
public class Starship
{
[Required]
[StringLength(10, ErrorMessage = "Id is too long.")]
public string? Id { get; set; }
}
}
@page "/starship-13"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship13> Logger
<EditForm EditContext="editContext" OnValidSubmit="Submit" FormName="Starship13">
<DataAnnotationsValidator />
<ValidationSummary />
<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();
editContext = new(Model);
editContext.SetFieldCssClassProvider(new CustomFieldClassProvider());
}
private void Submit() => Logger.LogInformation("Submit: Processing form");
public class Starship
{
[Required]
[StringLength(10, ErrorMessage = "Id is too long.")]
public string? Id { get; set; }
}
}
@page "/starship-13"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship13> Logger
<EditForm EditContext="editContext" OnValidSubmit="Submit">
<DataAnnotationsValidator />
<ValidationSummary />
<InputText @bind-Value="Model!.Id" />
<button type="submit">Submit</button>
</EditForm>
@code {
private EditContext? editContext;
public Starship? Model { get; set; }
protected override void OnInitialized()
{
Model ??= new();
editContext = new(Model);
editContext.SetFieldCssClassProvider(new CustomFieldClassProvider());
}
private void Submit()
{
Logger.LogInformation("Submit called: Processing the form");
}
public class Starship
{
[Required]
[StringLength(10, ErrorMessage = "Id is too long.")]
public string? Id { get; set; }
}
}
上面的示例检查所有窗体字段的有效性,并对每个字段应用样式。 如果窗体只应该将自定义样式应用于一部分字段,请让 CustomFieldClassProvider
有条件地应用样式。 下面的CustomFieldClassProvider2
示例仅将样式应用于 Name
字段。 对于名称与 Name
不符的任何字段,string.Empty
将返回,并且不应用任何样式。 使用反射,将字段与模型成员的属性或字段名称匹配,而不是与分配给 HTML 实体的 id
匹配。
CustomFieldClassProvider2.cs
:
using Microsoft.AspNetCore.Components.Forms;
public class CustomFieldClassProvider2 : FieldCssClassProvider
{
public override string GetFieldCssClass(EditContext editContext,
in FieldIdentifier fieldIdentifier)
{
if (fieldIdentifier.FieldName == "Name")
{
var isValid = editContext.IsValid(fieldIdentifier);
return isValid ? "validField" : "invalidField";
}
return string.Empty;
}
}
using Microsoft.AspNetCore.Components.Forms;
public class CustomFieldClassProvider2 : FieldCssClassProvider
{
public override string GetFieldCssClass(EditContext editContext,
in FieldIdentifier fieldIdentifier)
{
if (fieldIdentifier.FieldName == "Name")
{
var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();
return isValid ? "validField" : "invalidField";
}
return string.Empty;
}
}
注意
匹配前面示例中的字段名称需要区分大小写,因此指定了“Name
”的模型属性成员必须与“Name
”上的条件检查匹配:
- 正确匹配:
fieldId.FieldName == "Name"
- 匹配失败:
fieldId.FieldName == "name"
- 匹配失败:
fieldId.FieldName == "NAME"
- 匹配失败:
fieldId.FieldName == "nAmE"
向 Model
添加其他属性,例如:
[StringLength(10, ErrorMessage = "Description is too long.")]
public string? Description { get; set; }
向 CustomValidationForm
组件窗体添加 Description
:
<InputText @bind-Value="Model!.Description" />
更新组件 OnInitialized
方法中的 EditContext 实例以使用新字段 CSS 类提供程序:
editContext?.SetFieldCssClassProvider(new CustomFieldClassProvider2());
由于 CSS 验证类不应用于 Description
字段,因此它没有样式化。 但字段验证会正常运行。 如果提供了 10 个以上的字符,验证摘要将显示错误:
说明太长。
如下示例中:
自定义 CSS 样式应用于
Name
字段。任何其他字段都将应用类似于 Blazor 默认逻辑的逻辑,并使用 Blazor 默认字段 CSS 验证样式
modified
(valid
或invalid
)。 请注意,对于默认样式,如果应用程序基于 Blazor 项目模板,则不需要将这些样式添加到应用程序的样式表中。 对于不基于 Blazor 项目模板的应用程序,可将默认样式添加到应用程序的样式表中:.valid.modified:not([type=checkbox]) { outline: 1px solid #26b050; } .invalid { outline: 1px solid red; }
CustomFieldClassProvider3.cs
:
using Microsoft.AspNetCore.Components.Forms;
public class CustomFieldClassProvider3 : FieldCssClassProvider
{
public override string GetFieldCssClass(EditContext editContext,
in FieldIdentifier fieldIdentifier)
{
var isValid = editContext.IsValid(fieldIdentifier);
if (fieldIdentifier.FieldName == "Name")
{
return isValid ? "validField" : "invalidField";
}
else
{
if (editContext.IsModified(fieldIdentifier))
{
return isValid ? "modified valid" : "modified invalid";
}
else
{
return isValid ? "valid" : "invalid";
}
}
}
}
using Microsoft.AspNetCore.Components.Forms;
public class CustomFieldClassProvider3 : FieldCssClassProvider
{
public override string GetFieldCssClass(EditContext editContext,
in FieldIdentifier fieldIdentifier)
{
var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();
if (fieldIdentifier.FieldName == "Name")
{
return isValid ? "validField" : "invalidField";
}
else
{
if (editContext.IsModified(fieldIdentifier))
{
return isValid ? "modified valid" : "modified invalid";
}
else
{
return isValid ? "valid" : "invalid";
}
}
}
}
更新组件 OnInitialized
方法中的 EditContext 实例以使用上述字段 CSS 类提供程序:
editContext.SetFieldCssClassProvider(new CustomFieldClassProvider3());
使用 CustomFieldClassProvider3
:
Name
字段使用应用程序的自定义验证 CSS 样式。- 该
Description
字段使用类似于 Blazor 的逻辑和 Blazor 默认字段 CSS 验证样式的逻辑。
使用 IValidatableObject
进行类级验证
Blazor 表单模型支持使用 IValidatableObject
的类级验证(API 文档)。 IValidatableObject 仅当提交表单并且仅当所有其他验证成功时才执行验证。
Blazor 数据注释验证包
Microsoft.AspNetCore.Components.DataAnnotations.Validation
是使用 DataAnnotationsValidator 组件填补验证经验空白的包。 该包目前处于试验阶段。
警告
Microsoft.AspNetCore.Components.DataAnnotations.Validation
包在 NuGet.org 上具有最新版本的候选发布。目前,请继续使用实验性候选发布包。 提供实验性功能是为了探索功能的可用性,此类功能可能不会以稳定版本提供。 请观看公告 GitHub 存储库、dotnet/aspnetcore
GitHub 存储库或本主题部分,获取进一步更新。
[CompareProperty]
属性
CompareAttribute 不适用于 DataAnnotationsValidator 组件,因为 DataAnnotationsValidator 不会将验证结果与特定成员关联。 这可能会导致字段级验证的行为与提交时整个模型的验证行为不一致。 Microsoft.AspNetCore.Components.DataAnnotations.Validation
试验性包引入了一个附加的验证属性 ComparePropertyAttribute
,它可以克服这些限制。 在 Blazor 应用中,[CompareProperty]
可直接替代 [Compare]
特性。
嵌套模型、集合类型和复杂类型
Blazor 支持结合使用数据注释和内置的 DataAnnotationsValidator 来验证窗体输入。 但是,DataAnnotationsValidator 仅验证绑定到窗体的模型的顶级属性(不包括集合类型或复杂类型的属性)。
若要验证绑定模型的整个对象图(包括集合类型和复杂类型的属性),请使用试验性 Microsoft.AspNetCore.Components.DataAnnotations.Validation
包提供的 ObjectGraphDataAnnotationsValidator
:
<EditForm ...>
<ObjectGraphDataAnnotationsValidator />
...
</EditForm>
用 [ValidateComplexType]
注释模型属性。 在以下模型类中,ShipDescription
类包含附加数据注释,用于在将模型绑定到窗体时进行验证:
Starship.cs
:
using System;
using System.ComponentModel.DataAnnotations;
public class Starship
{
...
[ValidateComplexType]
public ShipDescription ShipDescription { get; set; } = new();
...
}
ShipDescription.cs
:
using System;
using System.ComponentModel.DataAnnotations;
public class ShipDescription
{
[Required]
[StringLength(40, ErrorMessage = "Description too long (40 char).")]
public string? ShortDescription { get; set; }
[Required]
[StringLength(240, ErrorMessage = "Description too long (240 char).")]
public string? LongDescription { get; set; }
}
基于窗体验证启用提交按钮
若要基于窗体验证启用和禁用提交按钮,请参阅以下示例:
- 使用“输入组件”一文的窗体示例部分的早期
Starfleet Starship Database
窗体(Starship3
组件)的缩写版本,它仅接受 Ship ID 的值。当创建Starship
类型的实例时,其他Starship
属性将接收有效的默认值。 - 使用窗体的 EditContext 在初始化组件时分配模型。
- 在上下文的 OnFieldChanged 回调中验证窗体,以启用和禁用提交按钮。
- 实现 IDisposable 并取消订阅
Dispose
方法中的事件处理程序。 有关详细信息,请参阅 ASP.NET Core Razor 组件生命周期。
注意
当分配给 EditForm.EditContext,不要亦将 EditForm.Model 分配给 EditForm。
Starship14.razor
:
@page "/starship-14"
@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: Processing form");
public void Dispose()
{
if (editContext is not null)
{
editContext.OnFieldChanged -= HandleFieldChanged;
}
}
}
@page "/starship-14"
@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: Processing form");
public void Dispose()
{
if (editContext is not null)
{
editContext.OnFieldChanged -= HandleFieldChanged;
}
}
}
@page "/starship-14"
@implements IDisposable
@inject ILogger<Starship14> Logger
<EditForm EditContext="editContext" OnValidSubmit="Submit">
<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;
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;
}
}
}
如果窗体未预先加载有效值并且你希望在窗体加载时禁用 Submit
按钮,请将 formInvalid
设置为 true
。
上述方法的副作用是在用户与任何一个字段进行交互后,验证摘要(ValidationSummary 组件)都会填充无效的字段。 采用以下两种方式之一解决此问题:
- 不在窗体上使用 ValidationSummary 组件。
- 选择提交按钮时,使 ValidationSummary 组件可见(例如,在
Submit
方法中)。
<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";
}
}