请注意,与有关EditContext、FieldIdentifier和FieldState的部分一样,这是一个高级主题。
如前所述,FieldState类保存表单数据的元状态。除了指示值是否已手动编辑外,Blazor还存储一组验证错误消息。为了理解它是如何工作的,本节将解释如何创建我们自己的自定义验证机制,该机制可以与Blazor一起用于验证用户输入。
下面的UML图显示了EditForm和存储此元状态的各种类(在图中分组)之间的关系。请记住,每当EditForm.Model更改时,EditForm都会创建EditContext的新实例。然后之前的EditContext(不再需要它,因为它包含关于以前模型的信息)会被垃圾回收,以及图中分组的类的所有实例。
image.png
我们的自定义验证将基于FluentValidation。一旦你完成了这一部分(或者如果你只是想要一些你可以立即使用的东西),请看一看Blazor-Validation

Creating a validator component

我们的验证器组件不必从任何特定类派生来提供验证。唯一的要求是它是从Blazor ComponentBase类派生出来的,这样我们就可以将它添加到视图中的<EditForm>标记中。嵌入到<EditForm>标记中的目的是,每当EditFormModel参数更改时,我们可以定义一个级联参数来拾取EditForm创建的当前EditContext
首先,创建一个新的Blazor应用程序,并添加对FluentValidation NuGet包的引用。然后创建一个名为FluentValidationValidator的类。

  1. public class FluentValidationValidator : ComponentBase
  2. {
  3. [CascadingParameter]
  4. private EditContext EditContext { get; set; }
  5. [Parameter]
  6. public Type ValidatorType { get; set; }
  7. private IValidator Validator;
  8. private ValidationMessageStore ValidationMessageStore;
  9. [Inject]
  10. private IServiceProvider ServiceProvider { get; set; }
  11. }
  • EditContext
    从其父<EditForm>组件传递到组件的级联参数。每次EditForm.Model更改时,此设置都会更改。
  • ValidatorType
    这将指定用于执行实际验证的类类型。我们将检查这是否为IValidator(FluentValidation接口)。
  • Validator
    这将包含对指定ValidatorType的实例的引用,以执行实际的对象验证。
  • ValidationMessageStore
    每次EditContext更改时(因为EditForm.Model已经更改),我们都会创建一个新的。
  • ServiceProvider
    IServiceProvider的注入依赖项,我们可以使用它来创建ValidatorType的实例。

    1. public override async Task SetParametersAsync(ParameterView parameters)
    2. {
    3. // Keep a reference to the original values so we can check if they have changed
    4. EditContext previousEditContext = EditContext;
    5. Type previousValidatorType = ValidatorType;
    6. await base.SetParametersAsync(parameters);
    7. if (EditContext == null)
    8. throw new NullReferenceException($"{nameof(FluentValidationValidator)} must be placed within an {nameof(EditForm)}");
    9. if (ValidatorType == null)
    10. throw new NullReferenceException($"{nameof(ValidatorType)} must be specified.");
    11. if (!typeof(IValidator).IsAssignableFrom(ValidatorType))
    12. throw new ArgumentException($"{ValidatorType.Name} must implement {typeof(IValidator).FullName}");
    13. if (ValidatorType != previousValidatorType)
    14. ValidatorTypeChanged();
    15. // If the EditForm.Model changes then we get a new EditContext
    16. // and need to hook it up
    17. if (EditContext != previousEditContext)
    18. EditContextChanged();
    19. }
  • LInes 4-5

每当我们的一个参数(包括EditContext级联参数)发生变化时,就会执行SetParametersAsync。我们需要做的第一件事是保持对一些原始值的引用,这样我们就可以看到它们是否发生了变化并做出相应的反应。

  • Line 7

调用base.SetParametersAsync会将对象的属性更新为新值。

  • Lines 9-16

确保我们有EditContext和作为IValidatorValidatorType

  • Lines 18-19

如果ValidatorType已更改,则需要创建该类型的新实例,并将其分配给私有Validator字段以验证EditContext.Model

  • Lines 23-24

如果EditContext已经更改,那么我们需要连接到一些事件,以便可以验证用户输入,并且需要一个新的ValidationMessageStore来存储任何验证错误。
创建ValidatorType的新实例就像指示ServiceProvider检索实例一样简单。

  1. private void ValidatorTypeChanged()
  2. {
  3. Validator = (IValidator)ServiceProvider.GetService(ValidatorType);
  4. }

要实现这一点,我们必须在应用程序的Startup.ConfigureServices方法中注册我们的验证器-一旦有了验证器和要验证的内容,我们就会这样做。
每当EditContext更改时,我们都需要一个新的ValidationMessagesStore来存储验证错误消息。

  1. void EditContextChanged()
  2. {
  3. ValidationMessageStore = new ValidationMessageStore(EditContext);
  4. HookUpEditContextEvents();
  5. }

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

  1. private void HookUpEditContextEvents()
  2. {
  3. EditContext.OnValidationRequested += ValidationRequested;
  4. EditContext.OnFieldChanged += FieldChanged;
  5. }
  • OnValidationRequested

此事件在需要验证EditContext.Model的所有属性时触发。当用户尝试发布EditForm以便Blazor可以确定输入是否有效时,就会发生这种情况。

  • OnFieldChanged

只要用户通过在Blazor的InputBase<T>子代组件之一中编辑EditContext.Model的属性值来更改该属性值,就会触发此事件。

  1. async void ValidationRequested(object sender, ValidationRequestedEventArgs args)
  2. {
  3. ValidationMessageStore.Clear();
  4. var validationContext =
  5. new ValidationContext<object>(EditContext.Model);
  6. ValidationResult result =
  7. await Validator.ValidateAsync(validationContext);
  8. AddValidationResult(EditContext.Model, result);
  9. }
  • Line 3

首先,我们从之前的任何验证中清除所有错误消息。

  • Line 4

接下来,我们指示FluentValidation.IValidator验证正在EditForm(我们通过EditContext.Model访问)中编辑的模型。

  • Line 5

最后,我们将所有验证错误添加到ValidationMessageStore中,这是在一个单独的方法中完成的,因为我们将在验证整个对象以及在通过EditContext.OnFieldChanged通知时验证单个更改的属性时使用它。
将错误消息添加到ValidationMessageStore只需创建一个FieldIdentifier来准确标识哪个对象/属性有错误,并使用该标识符来添加任何错误消息,然后让EditContext知道验证状态已更改。
请注意,当验证涉及长时间运行的异步调用时(例如,对WebApi进行检查用户名可用性的调用),我们可以更新验证错误并多次调用EditContext.NotifyValidationStateChanged,以在用户界面中提供验证状态的增量显示。

  1. void AddValidationResult(object model, ValidationResult validationResult)
  2. {
  3. foreach (ValidationFailure error in validationResult.Errors)
  4. {
  5. var fieldIdentifier = new FieldIdentifier(model, error.PropertyName);
  6. ValidationMessageStore.Add(fieldIdentifier, error.ErrorMessage);
  7. }
  8. EditContext.NotifyValidationStateChanged();
  9. }

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

  1. async void FieldChanged(object sender, FieldChangedEventArgs args)
  2. {
  3. FieldIdentifier fieldIdentifier = args.FieldIdentifier;
  4. ValidationMessageStore.Clear(fieldIdentifier);
  5. var propertiesToValidate = new string[] { fieldIdentifier.FieldName };
  6. var fluentValidationContext =
  7. new ValidationContext<object>(
  8. instanceToValidate: fieldIdentifier.Model,
  9. propertyChain: new FluentValidation.Internal.PropertyChain(),
  10. validatorSelector: new FluentValidation.Internal.MemberNameValidatorSelector(propertiesToValidate)
  11. );
  12. ValidationResult result = await Validator.ValidateAsync(fluentValidationContext);
  13. AddValidationResult(fieldIdentifier.Model, result);
  14. }
  • LInes 3-4

从事件参数中获取FieldIdentifier(ObjectInstance/PropertyName对),并仅清除该属性以前的所有错误消息。

  • Line 16

使用与ValidationRequsted相同的方法将错误从FluentValidation添加到我们的ValidationMessageStore

Using the component

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

  1. namespace CustomValidation.Models
  2. {
  3. public class Person
  4. {
  5. public string Name { get; set; }
  6. public int Age { get; set; }
  7. }
  8. }

接下来,使用FluentValidationPerson创建验证器。

  1. using CustomValidation.Models;
  2. using FluentValidation;
  3. namespace CustomValidation.Validators
  4. {
  5. public class PersonValidator : AbstractValidator<Person>
  6. {
  7. public PersonValidator()
  8. {
  9. RuleFor(x => x.Name).NotEmpty();
  10. RuleFor(x => x.Age).InclusiveBetween(18, 80);
  11. }
  12. }
  13. }

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

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

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

  1. @page "/"
  2. @using Models
  3. <EditForm Model=@Person OnValidSubmit=@ValidFormSubmitted>
  4. <FluentValidationValidator ValidatorType=typeof(Validators.PersonValidator)/>
  5. <p>Validation summary</p>
  6. <ValidationSummary />
  7. <p>Edit object</p>
  8. <div class="form-group">
  9. <label for="Name">Name</label>
  10. <InputText @bind-Value=Person.Name class="form-control" id="Name" />
  11. <ValidationMessage For="() => Person.Name" />
  12. </div>
  13. <div class="form-group">
  14. <label for="Age">Age</label>
  15. <InputNumber @bind-Value=Person.Age class="form-control" id="Age" />
  16. <ValidationMessage For=@(() => Person.Age) />
  17. </div>
  18. <input type="submit" class="btn btn-primary" value="Save" />
  19. </EditForm>
  20. @code {
  21. Person Person = new Person();
  22. void ValidFormSubmitted()
  23. {
  24. Person = new Person();
  25. }
  26. }

Process flow

Page is displayed

  1. 我们的EditForm组件是从<EditForm Model=@Person>标记创建的。
  2. 执行EditForm.OnParametersSet,因为EditForm.Model已从null更改为Our Person,它将创建一个新的EditContext实例。
  3. 新的EditContext实例通过级联值向下级联到所有子组件。
  4. 作为这种级联值更改的结果,InputBase<T>的每个子代都执行了其SetParametersAsync,并通过创建一个新的FieldIdentifier实例进行响应。

    Our validation component is initialised

  5. 我们的验证组件的SetParametersAsync方法通过引用新的EditContext来执行。

  6. 我们的组件创建一个新的ValidationMessageStore
  7. 我们的组件侦听EditContext上的事件,以获取验证请求和输入更改通知。

    User alters data

  8. 用户编辑InputBase<T>子体中的数据。

  9. 该组件通过EditContext.NotifyFieldChanged通知此状态更改(从未修改到已修改),并传递其FieldIdentifier
  10. EditContext通过传递FieldIdentifier来触发其OnFieldChanged
  11. 组件的事件订阅告诉ValidationMessageStore清除由FieldIdentifierModelFieldName属性标识的状态的所有错误消息。
  12. 我们的组件对单个属性执行自定义验证。
  13. 验证错误被添加到组件的ValidationMessageStore中,以FieldIdentifier为关键字。
  14. ValidationMessageStore执行EditContext.GetFieldState以检索当前FieldIdentifierFieldState
  15. ValidationMessageStore添加到FieldState,以便FieldState.GetValidationMessages能够从所有ValidationMessageStore实例检索所有错误消息。

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

User submits the form

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

    EditForm.Model is changed

    如果这是一个用于创建新人的用户界面,那么在成功地将我们的新Person提交到服务器之后,我们的应用程序可能会为我们的表单创建一个要编辑的新Person。这将丢弃与前一个Person实例相关联的所有状态(在虚线框中指示),并从新实例重新开始。
  • EditContext
  • FieldState
  • ValidationMessageStore

image.png
在演示源代码中添加了一些日志记录后,我们可以看到以下输出。

  1. WASM: EditContext has changed
  2. WASM: New ValidationMessageStore created
  3. WASM: Hooked up EditContext events (OnValidationRequested and OnFieldChanged)
  4. WASM: OnFieldChanged triggered: Validating a single property named Name on class Person
  5. WASM: OnFieldChanged triggered: Validating a single property named Age on class Person
  6. WASM: OnValidationRequested triggered: Validating whole object
  7. WASM: EditContext has changed
  8. WASM: New ValidationMessageStore created
  9. WASM: Hooked up EditContext events (OnValidationRequested and OnFieldChanged)
  • Lines 1-3

创建相关的状态实例以支持编辑Person实例。

  • Line 4

输入了一个名称

  • Line 5

输入了年龄

  • Line 6

用户提交表单

  • Line 7

Index.razor中的Person实例被更改,导致元状态实例被丢弃,并为新的EditForm.Model创建新实例。