ASP.NET Core Blazor 窗体验证

注意

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

警告

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

重要

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

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

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

窗体验证

在基本窗体验证场景中,EditForm 实例可以使用声明的 EditContextValidationMessageStore 实例来验证表单域。 OnValidationRequestedEditContext 事件处理程序执行自定义验证逻辑。 处理程序的结果会更新 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 框架的实现:

有关验证行为的详细信息,请参阅 DataAnnotationsValidator 验证行为 部分。

如果需要在代码中为 EditContext 启用数据注解验证支持,请在 EnableDataAnnotationsValidation 处使用注入的 IServiceProvider@inject IServiceProvider ServiceProvider)调用 EditContext。 有关高级示例,请参阅 NotifyPropertyChangedValidationComponent ASP.NET Core Blazor 框架(BasicTestAppdotnet/aspnetcoreGitHub 存储库)中的组件。 在示例的生产版本中,将服务提供商的 new TestServiceProvider() 参数替换为注入的 IServiceProvider

注意

指向 .NET 参考源的文档链接通常会加载存储库的默认分支,该分支表示针对下一个 .NET 版本的当前开发。 要为特定版本选择标记,请使用“切换分支或标记”下拉列表。 有关详细信息,请参阅如何选择 ASP.NET Core 源代码的版本标记 (dotnet/AspNetCore.Docs #26205)

Blazor 执行两种类型的验证:

  • 当用户从某个字段中跳出时,将执行字段验证。 在字段验证期间,DataAnnotationsValidator 组件将报告的所有验证结果与该字段相关联。
  • 当用户提交窗体时,将执行模型验证。 在模型验证期间,DataAnnotationsValidator 组件尝试根据验证结果报告的成员名称来确定字段。 与单个成员无关的验证结果将与模型而不是字段相关联。

在自定义验证方案中:

实现自定义验证有两种常规方法,本文接下来的两个部分对此进行介绍:

  • 使用 OnValidationRequested 事件进行手动验证:在通过分配给 OnValidationRequested 事件的事件处理程序请求验证时,使用数据注释手动验证表单字段并使用自定义代码进行字段检查。
  • 验证器组件:一个或多个自定义验证器组件可用于在同一页面上对不同表单进行验证,也可以用于同一表单在不同处理步骤中的验证(例如,客户端验证与后续的服务器验证)。

使用 OnValidationRequested 事件进行手动验证

可以使用分配给EditContext.OnValidationRequested事件的自定义事件处理程序手动验证表单以管理ValidationMessageStore

该 Blazor 框架提供组件 DataAnnotationsValidator ,用于根据 验证属性(数据注释)将其他验证支持附加到表单。

回顾前面的 Starship8 组件示例,HandleValidationRequested 方法被分配给 OnValidationRequested,在 C# 代码中可以执行手动验证。 通过 DataAnnotationsValidator 和应用于 Holodeck 模型的验证属性,一些更改演示如何将现有手动验证与数据批注验证相结合。

在组件定义文件顶部的System.ComponentModel.DataAnnotations指令中引用Razor命名空间。

@using System.ComponentModel.DataAnnotations

Id模型添加一个Holodeck属性,并使用验证属性将字符串长度限制为6个字符:

[StringLength(6)]
public string? Id { get; set; }

DataAnnotationsValidator 组件 (<DataAnnotationsValidator />) 添加到窗体。 通常,组件通常放置在<EditForm>标签的正下方,但你可以将其放置在表单中的任何位置。

<DataAnnotationsValidator />

将表单的提交行为在 <EditForm> 标记中从 OnSubmit 更改为 OnValidSubmit,以保证在执行指定的事件处理程序方法之前表单有效。

- OnSubmit="Submit"
+ OnValidSubmit="Submit"

<EditForm>中添加一个用于Id属性的字段:

<div>
    <label>
        <InputText @bind-Value="Model!.Id" />
        ID (6 characters max)
    </label>
    <ValidationMessage For="() => Model!.Id" />
</div>

进行上述更改后,窗体的行为与以下规范匹配:

  • Id 属性上的数据批注验证在 Id 字段仅失去焦点时不会触发验证失败。 当用户选择 Update 按钮时,将执行验证。
  • 当用户选择窗体的 HandleValidationRequested 按钮时,你要在分配给窗体 OnValidationRequested 事件的 Update 方法中执行的任何手动验证都会执行。 在组件示例的现有代码 Starship8 中,用户必须选中或两个复选框来验证表单。
  • 在数据注释和手动验证通过之前,表单不会处理 Submit 该方法。

验证器组件

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

Blazor 框架提供了 DataAnnotationsValidator 组件,以将验证支持附加到基于验证属性(数据批注)的窗体。 可以创建自定义验证器组件,以处理同一页上不同窗体或同一窗体上不同处理步骤的验证消息,例如先进行客户端验证,再进行服务器端验证。 本文的以下部分将使用本部分 CustomValidation 中所示的验证器组件示例:

在数据注释内置验证程序中,[Remote] 仅不支持

注意

在许多情况下,可使用自定义数据注释验证属性来代替自定义验证器组件。 应用于窗体模型的自定义属性使用 DataAnnotationsValidator 组件激活。 当与服务器验证一起使用时,应用于模型的所有自定义属性都必须可在服务器上执行。 有关详细信息,请参阅自定义验证属性部分。

ComponentBase 创建验证器组件:

  • 窗体的 EditContext 是组件的级联参数
  • 初始化验证器组件时,将创建一个新的 ValidationMessageStore 来维护当前的窗体错误列表。
  • 当窗体组件中的开发人员代码调用 DisplayErrors 方法时,消息存储接收错误。 这些错误会传递到 DisplayErrors 中的 Dictionary<string, List<string>> 方法。 在字典中,键是具有一个或多个错误的窗体字段的名称。 值为错误列表。
  • 发生以下任一情况时,将清除消息:
    • 引发 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部分的 组件。
  • 如果用户选择 Description ship 分类 (Defense),则需要 ship 说明 (Classification) 的值才能验证。

在组件中设置验证消息时,这些消息将被添加到验证器的 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),
                [ "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),
                [ "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。 如果用户选择 Description ship 分类 (Defense),除了客户端和服务器的数据批注验证,控制器还验证是否为 ship 说明 (Classification) 提供了值。

  • Blazor WebAssembly 项目模板创建的托管Blazor WebAssembly解决方案。 Blazor中所述的任何安全托管 Blazor WebAssembly 解决方案都支持此方法。
  • “输入组件”一文的Starship部分的 Starship.cs 模型 ()。
  • CustomValidation部分中显示的 组件。

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

注意

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

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

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 = [ "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);
    }
}
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 Browser Developer 或 Postman。

如果服务器 API 返回上述的默认 JSON 响应,则客户端可分析开发人员代码中的响应,为窗体验证错误处理进程获取 errors 节点的子节点。 你无法轻松通过编写开发人员代码来分析文件。 手动分析 JSON 需要在调用 Dictionary<string, List<string>> 后生成错误 ReadFromJsonAsync。 理想情况下,服务器 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 的响应,使其仅返回验证错误,请更改在 ApiControllerAttribute 文件中注释了 Program 的操作上调用的委托。 对于 API 终结点 (/StarshipValidation),返回具有 BadRequestObjectResultModelStateDictionary。 对于任何其他 API 终结点,通过使用新的 ValidationProblemDetails 返回对象结果来保留默认行为。

Microsoft.AspNetCore.Mvc 命名空间添加到 Program 的主项目中的 Blazor Web App 文件顶部:

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 项目中,借助 Starfleet Starship Database 组件的支持,系统更新 CustomValidation 窗体,以显示服务器验证错误和。 当服务器 API 返回验证消息时,这些消息将添加到 CustomValidation 组件的 ValidationMessageStore。 此错误会按窗体的验证摘要显示在窗体的 EditContext 中。

在以下组件中,将共享项目 (@using BlazorSample.Shared) 的命名空间更新为共享项目的命名空间。 请注意,窗体需要授权,因此用户必须登录到应用程序以导航到窗体。

Microsoft.AspNetCore.Mvc 命名空间添加到 Program 应用程序的 Server 文件上方:

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 项目中,借助 Starfleet Starship Database 组件的支持,系统更新 CustomValidation 窗体,以显示服务器验证错误和。 当服务器 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
        {
            using 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.";
        }
    }
}

.Client 的 Blazor Web App 项目还必须为针对后端 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
        {
            using 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);

自定义验证属性

当使用自定义验证属性时,为确保验证结果与字段正确关联,请在创建 MemberName 时传递验证上下文的 ValidationResult

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.",
            [ validationContext.MemberName! ]);
    }
}
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! });
    }
}
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" };
}

SaladChef 文件的应用 DI 容器中注册 Program

builder.Services.AddTransient<SaladChef>();

以下 IsValid 类的 SaladChefValidatorAttribute 方法从 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";
    }
}

使用 CustomFieldClassProviderEditContext 类设置为表单 SetFieldCssClassProvider 实例上的字段 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; } 

Description 组件窗体添加 CustomValidationForm

<InputText @bind-Value="Model!.Description" />

更新组件 EditContext 方法中的 OnInitialized 实例以使用新字段 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";
            }
        }
    }
}

更新组件 EditContext 方法中的 OnInitialized 实例以使用上述字段 CSS 类提供程序:

editContext.SetFieldCssClassProvider(new CustomFieldClassProvider3());

使用 CustomFieldClassProvider3

  • Name 字段使用应用程序的自定义验证 CSS 样式。
  • Description 字段使用类似于 Blazor 的逻辑和 Blazor 默认字段 CSS 验证样式的逻辑。

使用 IValidatableObject 进行类级验证

IValidatableObject 窗体模型支持使用 的类级验证(Blazor)。 仅当提交表单并且仅当所有其他验证成功时才执行 IValidatableObject 验证。

Blazor 数据注释验证包

注意

Microsoft.AspNetCore.Components.DataAnnotations.Validation 不再建议将包用于面向 .NET 10 或更高版本的应用。 有关详细信息,请参阅 嵌套对象、集合类型和复杂类型 部分。

Microsoft.AspNetCore.Components.DataAnnotations.Validation 使用 DataAnnotationsValidator 组件填补了验证体验空白。 该包目前处于试验阶段

警告

Microsoft.AspNetCore.Components.DataAnnotations.ValidationNuGet.org 具有最新版本的候选版本。目前继续使用试验性发布候选包。 提供实验性功能是为了探索功能的可用性,此类功能可能不会以稳定版本提供。 请观看公告 GitHub 存储库dotnet/aspnetcore GitHub 存储库或本主题部分,获取进一步更新。

[CompareProperty] 属性

CompareAttribute 不适用于 DataAnnotationsValidator 组件,因为 DataAnnotationsValidator 不会将验证结果与特定成员关联。 这可能会导致字段级验证的行为与提交时整个模型的验证行为不一致。 Microsoft.AspNetCore.Components.DataAnnotations.Validation 引入了一个额外的验证属性,ComparePropertyAttribute该属性适用于这些限制。 在 Blazor 应用中,[CompareProperty] 可直接替代 [Compare] 属性

使用不同的程序集中的验证模型

对于在不同程序集中定义的模型验证,例如库或 .Client 以下 Blazor Web App项的项目:

  • 如果库是纯类库(它不基于 Microsoft.NET.Sdk.WebMicrosoft.NET.Sdk.Razor SDK),请为 NuGet 包添加对库的 Microsoft.Extensions.Validation引用。
  • 在库或 .Client 项目中创建一个方法,该方法 IServiceCollection 接收实例作为参数并对其进行调用 AddValidation
  • 在应用中,调用方法和 AddValidation.

上述方法会导致对这两个程序集中的类型进行验证。

在下面的示例中,为了验证.Client项目,使用.Client项目中定义的Blazor Web App类型来创建AddValidationForTypesInClient方法。

ServiceCollectionExtensions.cs (在 .Client 项目中):

namespace BlazorSample.Client.Extensions;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddValidationForTypesInClient(
        this IServiceCollection collection)
    {
        return collection.AddValidation();
    }
}

在服务器项目的 Program 文件中,添加命名空间并调用 .Client 项目的服务集合扩展方法(AddValidationForTypesInClient)和 AddValidation

using BlazorSample.Client.Extensions;

...

builder.Services.AddValidationForTypesInClient();
builder.Services.AddValidation();

嵌套对象和集合类型

Blazor 表单验证支持使用内置 DataAnnotationsValidator进行嵌套对象和集合项属性的验证。

若要创建已验证的表单,请使用 DataAnnotationsValidator 组件内部的 EditForm 组件,就像以前一样。

若要选择加入嵌套对象和集合类型验证功能,请执行以下作:

  1. 在注册服务的文件 AddValidation 中调用扩展方法 Program
  2. 在 C# 类文件中声明表单模型类型,而不是在组件 Razor (.razor) 中。
  3. 使用 [ValidatableType] 特性批注根窗体模型类型。

如果不遵循上述步骤,表单验证行为不包括嵌套模型和集合类型验证。

以下示例演示了采用改进的表单验证的客户订单(为简洁起见省略的详细信息):

Program.cs服务集合中,调用 AddValidation

builder.Services.AddValidation();

在以下 Order 类中,顶级模型类型需要该 [ValidatableType] 属性。 将自动发现其他类型。 OrderItem 并且 ShippingAddress 不为简洁起见而显示,但如果显示嵌套验证和集合验证,则这些类型的工作方式相同。

Order.cs:

[ValidatableType]
public class Order
{
    public Customer Customer { get; set; } = new();
    public List<OrderItem> OrderItems { get; set; } = [];
}

public class Customer
{
    [Required(ErrorMessage = "Name is required.")]
    public string? FullName { get; set; }

    [Required(ErrorMessage = "Email is required.")]
    public string? Email { get; set; }

    public ShippingAddress ShippingAddress { get; set; } = new();
}

在以下 OrderPage 组件中, DataAnnotationsValidator 组件存在于组件中 EditForm

OrderPage.razor:

<EditForm Model="Model">
    <DataAnnotationsValidator />

    <h3>Customer Details</h3>
    <div class="mb-3">
        <label>
            Full Name
            <InputText @bind-Value="Model!.Customer.FullName" />
        </label>
        <ValidationMessage For="@(() => Model!.Customer.FullName)" />
    </div>

    // ... form continues ...
</EditForm>

@code {
    public Order? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();
}

声明组件(Razor文件)之外的.razor模型类型的要求是由于新的验证功能和Razor编译器本身都使用源生成器。 目前,一个源生成器的输出不能用作另一个源生成器的输入。

嵌套对象、集合类型和复杂类型

注意

对于面向 .NET 10 或更高版本的应用,我们不再建议使用Microsoft.AspNetCore.Components.DataAnnotations.Validation本部分所述的实验和方法。 建议使用组件的内置验证功能 DataAnnotationsValidator

Blazor 支持结合使用数据注释和内置的 DataAnnotationsValidator 来验证窗体输入。 但是,. DataAnnotationsValidator NET 9 或更早版本中仅验证绑定到不是集合或复杂类型属性的表单的模型的顶级属性。

若要验证绑定模型的整个对象图(包括集合和复杂类型属性),请使用 ObjectGraphDataAnnotationsValidator .NET 9 或更早版本中 的实验Microsoft.AspNetCore.Components.DataAnnotations.Validation 提供的:

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

DataAnnotationsValidator 验证行为

组件 DataAnnotationsValidator 具有相同的验证顺序和短路行为 System.ComponentModel.DataAnnotations.Validator。 验证类型的 T实例时,将应用以下规则:

  1. 验证的成员属性 T ,包括递归验证嵌套对象。
  2. 验证的类型 T 级别属性。
  3. IValidatableObject.Validate如果T实现该方法,则执行该方法。

如果上述步骤之一生成验证错误,则会跳过其余步骤。