blazor-university.zh-cn

原文链接:https://blazor-university.com/forms/writing-custom-validation/

编写自定义验证

源代码

请注意,与有关 EditContext、FieldIdentifiers 和 FieldState 的部分一样,这是一个高级主题。

如前所述,FieldState 类保存表单数据的元状态。除了指示值是否已被手动编辑外,Blazor 还存储验证错误消息的集合。为了了解它的工作原理,本节将说明如何创建我们自己的自定义验证机制,该机制可与 Blazor 一起使用来验证用户输入。

下面的 UML 图显示了 EditForm 和存储此元状态的各种类(在图中分组)之间的关系。请记住,每当 EditForm.Model 发生更改时,EditForm 都会创建一个新的 EditContext 实例。然后可以对以前的 EditContext(不再需要它,因为它包含有关以前模型的信息)进行垃圾收集,并且可以对图表中分组的所有类实例进行垃圾收集。

我们的自定义验证将基于 FluentValidation。完成本节后(或者如果您只是想要一些可以立即使用的东西),请查看 blazor-validation

创建验证器组件

我们的验证器组件不必为了提供验证而从任何特定类继承。唯一的要求是它来自 Blazor ComponentBase 类,以便我们可以将其添加到视图中的 <EditForm> 标记中。嵌入 <EditForm> 标记的目的是为了让我们可以定义一个级联参数,以便在 EditFormModel 参数更改时获取当前由 EditForm 创建的 EditContext

首先,创建一个新的 Blazor 应用并添加对 FluentValidation NuGet 包的引用。然后创建一个名为 FluentValidationValidator 的类。

public class FluentValidationValidator : ComponentBase
{
  [CascadingParameter]
  private EditContext EditContext { get; set; }

  [Parameter]
  public Type ValidatorType { get; set; }

  private IValidator Validator;
  private ValidationMessageStore ValidationMessageStore;
  [Inject]
  private IServiceProvider ServiceProvider { get; set; }
}
public override async Task SetParametersAsync(ParameterView parameters)
{
  // Keep a reference to the original values so we can check if they have changed
  EditContext previousEditContext = EditContext;
  Type previousValidatorType = ValidatorType;

  await base.SetParametersAsync(parameters);

  if (EditContext == null)
    throw new NullReferenceException($"{nameof(FluentValidationValidator)} must be placed within an {nameof(EditForm)}");

  if (ValidatorType == null)
    throw new NullReferenceException($"{nameof(ValidatorType)} must be specified.");

  if (!typeof(IValidator).IsAssignableFrom(ValidatorType))
    throw new ArgumentException($"{ValidatorType.Name} must implement {typeof(IValidator).FullName}");

  if (ValidatorType != previousValidatorType)
    ValidatorTypeChanged();

  // If the EditForm.Model changes then we get a new EditContext
  // and need to hook it up
  if (EditContext != previousEditContext)
    EditContextChanged();
}

创建一个新的 ValidatorType 实例就像指示我们的 ServiceProvider 检索一个实例一样简单。

private void ValidatorTypeChanged()
{
  Validator = (IValidator)ServiceProvider.GetService(ValidatorType);
}

为此,我们必须在应用程序的 Startup.ConfigureServices 方法中注册验证器——一旦我们有了验证器和要验证的东西,我们就会这样做。

每当 EditContext 发生变化时,我们都需要一个新的 ValidationMessagesStore 来存储我们的验证错误消息。

void EditContextChanged()
{
  ValidationMessageStore = new ValidationMessageStore(EditContext);
  HookUpEditContextEvents();
}

我们还需要连接一些事件,以便我们可以验证用户输入并将错误添加到我们的 ValidationMessageStore

private void HookUpEditContextEvents()
{
  EditContext.OnValidationRequested += ValidationRequested;
  EditContext.OnFieldChanged += FieldChanged;
}
async void ValidationRequested(object sender, ValidationRequestedEventArgs args)
{
  ValidationMessageStore.Clear();
  var validationContext =
    new ValidationContext<object>(EditContext.Model);
  ValidationResult result =
    await Validator.ValidateAsync(validationContext);
  AddValidationResult(EditContext.Model, result);
}

将错误消息添加到 ValidationMessageStore 只是创建一个 FieldIdentifier 以准确识别哪个对象/属性有错误并使用该标识符添加任何错误消息,然后让 EditContext 知道验证状态已更改的情况。

请注意,当验证涉及长时间运行的异步调用(例如,对 WebApi 以检查 UserName 可用性)时,我们可以更新验证错误并多次调用 EditContext.NotifyValidationStateChanged 以在用户界面中提供验证状态的增量显示。

void AddValidationResult(object model, ValidationResult validationResult)
{
  foreach (ValidationFailure error in validationResult.Errors)
  {
    var fieldIdentifier = new FieldIdentifier(model, error.PropertyName);
    ValidationMessageStore.Add(fieldIdentifier, error.ErrorMessage);
  }
  EditContext.NotifyValidationStateChanged();
}

最后,当用户在表单输入控件中编辑值时,我们需要验证单个对象/属性。当发生这种情况时,我们会通过 EditContext.OnFieldChanged 事件得到通知。除了前两行和最后一行,以下代码是 FluentValidator 特定的。

async void FieldChanged(object sender, FieldChangedEventArgs args)
{
  FieldIdentifier fieldIdentifier = args.FieldIdentifier;
  ValidationMessageStore.Clear(fieldIdentifier);

  var propertiesToValidate = new string[] { fieldIdentifier.FieldName };
  var fluentValidationContext =
    new ValidationContext<object>(
      instanceToValidate: fieldIdentifier.Model,
      propertyChain: new FluentValidation.Internal.PropertyChain(),
      validatorSelector: new FluentValidation.Internal.MemberNameValidatorSelector(propertiesToValidate)
    );

  ValidationResult result = await Validator.ValidateAsync(fluentValidationContext);

  AddValidationResult(fieldIdentifier.Model, result);
}

使用组件

首先创建一个模型供我们的用户编辑。

namespace CustomValidation.Models
{
  public class Person
  {
    public string Name { get; set; }
    public int Age { get; set; }
  }
}

接下来,使用 FluentValidation 为 Person 创建一个验证器。

using CustomValidation.Models;
using FluentValidation;

namespace CustomValidation.Validators
{
  public class PersonValidator : AbstractValidator<Person>
  {
    public PersonValidator()
    {
      RuleFor(x => x.Name).NotEmpty();
      RuleFor(x => x.Age).InclusiveBetween(18, 80);
    }
  }
}

因为我们的验证组件使用 IServiceProvider 来创建验证器的实例,所以我们需要在 Startup.ConfigureServices 中注册它。

public void ConfigureServices(IServiceCollection services)
{
  services.AddScoped<Validators.PersonValidator>();
}

最后,我们需要设置我们的用户界面来编辑我们的 Person 类的一个实例。

@page "/"
@using Models

<EditForm Model=@Person OnValidSubmit=@ValidFormSubmitted>
  <FluentValidationValidator ValidatorType=typeof(Validators.PersonValidator)/>
  <p>Validation summary</p>
  <ValidationSummary />
  <p>Edit object</p>
  <div class="form-group">
    <label for="Name">Name</label>
    <InputText @bind-Value=Person.Name class="form-control" id="Name" />
    <ValidationMessage For="() => Person.Name" />
  </div>
  <div class="form-group">
    <label for="Age">Age</label>
    <InputNumber @bind-Value=Person.Age class="form-control" id="Age" />
    <ValidationMessage For=@(() => Person.Age) />
  </div>
  <input type="submit" class="btn btn-primary" value="Save" />
</EditForm>

@code {
  Person Person = new Person();

  void ValidFormSubmitted()
  {
    Person = new Person();
  }
}

执行流程

页面显示

  1. 我们的 EditForm 组件是从 <EditForm Model=@Person> 标记创建的。
  2. EditForm.OnParametersSet 被执行,因为 EditForm.Model 已经从 null 变成了我们的 Person,它创建了一个新的 EditContext 实例。
  3. 新的 EditContext 实例通过级联值级联到所有子组件。
  4. 对于这种级联值的变化,InputBase<T> 的每个派生类都会执行其 SetParametersAsync,并通过创建 FieldIdentifier 的新实例来做出反应。

我们的验证组件已初始化

  1. 我们的验证组件的 SetParametersAsync 方法是通过引用新的 EditContext 来执行的。
  2. 我们的组件创建了一个新的 ValidationMessageStore
  3. 我们的组件侦听 EditContext 上的事件以获取验证请求和输入更改通知。

用户更改数据

  1. 用户在 InputBase<T> 派生类中编辑数据。
  2. 组件通过 EditContext.NotifyFieldChanged 通知此状态更改(从未修改到已修改),并传递其 FieldIdentifier
  3. EditContext 触发其 OnFieldChanged,传递 FieldIdentifier
  4. 我们组件的事件订阅告诉我们的 ValidationMessageStore 清除由 FieldIdentifierModelFieldName 属性标识的状态的所有错误消息。
  5. 我们的组件对单个属性执行其自定义验证。
  6. 验证错误被添加到我们组件的 ValidationMessageStore 中,由 FieldIdentifier 作为键。
  7. ValidationMessageStore 执行 EditContext.GetFieldState 以检索当前 FieldIdentifierFieldState
  8. ValidationMessageStore 被添加到 FieldState,以便 FieldState.GetValidationMessages 能够从所有 ValidationMessageStore 实例中检索所有错误消息。

第 8 步特别重要,因为 Blazor 需要能够检索特定输入的所有验证错误消息,无论它们被添加到哪个 ValidationMessageStore

用户提交表单

  1. <EditForm> 执行 EditContext.Validate
  2. EditContext 触发其 OnValidationRequested 事件。
  3. 我们组件的事件订阅告诉我们的 ·ValidationMessageStore· 清除所有字段的所有先前验证错误消息。
  4. 我们的组件对整个 EditContext.Model 对象执行其自定义验证。
  5. 与对单个更改的验证一样,错误被添加到 ·ValidationMessageStore·,它使用 ·EditContext· 中的所有相关 ·FieldState· 实例注册自身。
  6. <EditForm> 根据是否有错误消息触发相关的有效/无效事件。

EditForm.Model 已更改

如果这是一个用于创建 people 的用户界面,那么在成功将我们的新 People 提交到服务器后,我们的应用程序可能会创建一个新 People 供我们的表单进行编辑。这将丢弃与前一个 People 实例相关的所有状态(在虚线框中表示),并从新实例重新开始。

在演示代码中添加了一些日志记录,我们会看到以下输出。

WASM:编辑上下文已更改
WASM:创建了新的 ValidationMessageStore
WASM:连接 EditContext 事件(OnValidationRequested 和 OnFieldChanged)
WASM:触发 OnFieldChanged:验证类 Person 上名为 Name 的单个属性
WASM:触发 OnFieldChanged:验证 Person 类上名为 Age 的单个属性
WASM:OnValidationRequested 触发:验证整个对象
WASM:EditContext 已更改
WASM:创建了新的 ValidationMessageStore
WASM:连接 EditContext 事件(OnValidationRequested 和 OnFieldChanged)

下一篇 - 组件库