ASP.NET Core Blazor 窗体验证

注意

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

警告

此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本

重要

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

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

本文介绍如何在 Blazor 窗体中使用验证。

窗体验证

在基本窗体验证场景中,EditForm 实例可以使用声明的 EditContextValidationMessageStore 实例来验证表单域。 EditContextOnValidationRequested 事件处理程序执行自定义验证逻辑。 处理程序的结果会更新 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 组件尝试根据验证结果报告的成员名称来确定字段。 与单个成员无关的验证结果将与模型而不是字段相关联。

验证器组件

验证器组件通过管理窗体的 EditContextValidationMessageStore 来支持窗体验证。

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 表达式是前面的示例中 OnValidationRequestedOnFieldChanged 的已注册的事件处理程序。 在此方案中,无需实现 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) 和字段验证错误。
  • 成功时禁用窗体,否则显示错误。

如果窗体模型是在托管窗体的组件中定义的,无论是直接定义为组件上的成员,还是在子类中定义,基本验证都是有用的。 在多个组件中使用独立模型类时,建议使用验证程序组件。

下面的示例基于:

Starship 模型 (Starship.cs) 放入共享类库项目中,以便客户端和服务器项目都可使用该模型。 添加或更新命名空间,使之与共享应用程序的命名空间相匹配,例如 namespace BlazorSample.Shared。 由于模型需要数据注释,请确认共享类库使用共享框架或将 System.ComponentModel.Annotations 包添加到共享项目。

备注

有关将包添加到 .NET 应用的指南,请参阅包使用工作流(NuGet 文档)中“安装和管理包”下的文章。 在 NuGet.org 中确认正确的包版本。

在 Blazor Web App 的主项目中,添加控制器来处理 Starship 验证请求并返回失败的验证消息。 更新共享类库项目最后一条 using 语句中的命名空间和控制器类中的 namespace。 如果用户选择 Defense ship 分类 (Classification),除了客户端和服务器的数据批注验证,控制器还验证是否为 ship 说明 (Description) 提供了值。

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,则需要进行其他自定义。

有关代理安全性的详细信息,请参阅:

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),返回具有 ModelStateDictionaryBadRequestObjectResult。 对于任何其他 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.csswwwroot/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";
    }
}

使用 SetFieldCssClassProviderCustomFieldClassProvider 类设置为表单 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 验证样式 modifiedvalidinvalid)。 请注意,对于默认样式,如果应用程序基于 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 组件)都会填充无效的字段。 采用以下两种方式之一解决此问题:

<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";
    }
}