请注意,与有关EditContext、FieldIdentifier和FieldState的部分一样,这是一个高级主题。
如前所述,FieldState
类保存表单数据的元状态。除了指示值是否已手动编辑外,Blazor
还存储一组验证错误消息。为了理解它是如何工作的,本节将解释如何创建我们自己的自定义验证机制,该机制可以与Blazor一起用于验证用户输入。
下面的UML图显示了EditForm
和存储此元状态的各种类(在图中分组)之间的关系。请记住,每当EditForm.Model
更改时,EditForm
都会创建EditContext
的新实例。然后之前的EditContext
(不再需要它,因为它包含关于以前模型的信息)会被垃圾回收,以及图中分组的类的所有实例。
我们的自定义验证将基于FluentValidation
。一旦你完成了这一部分(或者如果你只是想要一些你可以立即使用的东西),请看一看Blazor-Validation。
Creating a validator component
我们的验证器组件不必从任何特定类派生来提供验证。唯一的要求是它是从Blazor ComponentBase
类派生出来的,这样我们就可以将它添加到视图中的<EditForm>
标记中。嵌入到<EditForm>
标记中的目的是,每当EditForm
的Model
参数更改时,我们可以定义一个级联参数来拾取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; }
}
- EditContext
从其父<EditForm>
组件传递到组件的级联参数。每次EditForm.Model
更改时,此设置都会更改。 - ValidatorType
这将指定用于执行实际验证的类类型。我们将检查这是否为IValidator
(FluentValidation接口)。 - Validator
这将包含对指定ValidatorType
的实例的引用,以执行实际的对象验证。 - ValidationMessageStore
每次EditContext
更改时(因为EditForm.Model
已经更改),我们都会创建一个新的。 ServiceProvider
对IServiceProvider
的注入依赖项,我们可以使用它来创建ValidatorType
的实例。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();
}
LInes 4-5
每当我们的一个参数(包括EditContext
级联参数)发生变化时,就会执行SetParametersAsync
。我们需要做的第一件事是保持对一些原始值的引用,这样我们就可以看到它们是否发生了变化并做出相应的反应。
- Line 7
调用base.SetParametersAsync
会将对象的属性更新为新值。
- Lines 9-16
确保我们有EditContext
和作为IValidator
的ValidatorType
。
- Lines 18-19
如果ValidatorType
已更改,则需要创建该类型的新实例,并将其分配给私有Validator
字段以验证EditContext.Model
。
- Lines 23-24
如果EditContext
已经更改,那么我们需要连接到一些事件,以便可以验证用户输入,并且需要一个新的ValidationMessageStore
来存储任何验证错误。
创建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;
}
- OnValidationRequested
此事件在需要验证EditContext.Model
的所有属性时触发。当用户尝试发布EditForm
以便Blazor可以确定输入是否有效时,就会发生这种情况。
- OnFieldChanged
只要用户通过在Blazor的InputBase<T>
子代组件之一中编辑EditContext.Model
的属性值来更改该属性值,就会触发此事件。
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);
}
- Line 3
首先,我们从之前的任何验证中清除所有错误消息。
- Line 4
接下来,我们指示FluentValidation.IValidator
验证正在EditForm
(我们通过EditContext.Model
访问)中编辑的模型。
- Line 5
最后,我们将所有验证错误添加到ValidationMessageStore
中,这是在一个单独的方法中完成的,因为我们将在验证整个对象以及在通过EditContext.OnFieldChanged
通知时验证单个更改的属性时使用它。
将错误消息添加到ValidationMessageStore
只需创建一个FieldIdentifier
来准确标识哪个对象/属性有错误,并使用该标识符来添加任何错误消息,然后让EditContext
知道验证状态已更改。
请注意,当验证涉及长时间运行的异步调用时(例如,对WebApi进行检查用户名可用性的调用),我们可以更新验证错误并多次调用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);
}
- LInes 3-4
从事件参数中获取FieldIdentifier
(ObjectInstance/PropertyName对),并仅清除该属性以前的所有错误消息。
- Line 16
使用与ValidationRequsted
相同的方法将错误从FluentValidation
添加到我们的ValidationMessageStore
。
Using the component
首先创建一个模型供我们的用户编辑。
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();
}
}
Process flow
Page is displayed
- 我们的
EditForm
组件是从<EditForm Model=@Person>
标记创建的。 - 执行
EditForm.OnParametersSet
,因为EditForm.Model
已从null
更改为OurPerson
,它将创建一个新的EditContext
实例。 - 新的
EditContext
实例通过级联值向下级联到所有子组件。 作为这种级联值更改的结果,
InputBase<T>
的每个子代都执行了其SetParametersAsync
,并通过创建一个新的FieldIdentifier
实例进行响应。Our validation component is initialised
我们的验证组件的
SetParametersAsync
方法通过引用新的EditContext
来执行。- 我们的组件创建一个新的
ValidationMessageStore
。 我们的组件侦听
EditContext
上的事件,以获取验证请求和输入更改通知。User alters data
用户编辑
InputBase<T>
子体中的数据。- 该组件通过
EditContext.NotifyFieldChanged
通知此状态更改(从未修改到已修改),并传递其FieldIdentifier
。 EditContext
通过传递FieldIdentifier
来触发其OnFieldChanged
。- 组件的事件订阅告诉
ValidationMessageStore
清除由FieldIdentifier
的Model
和FieldName
属性标识的状态的所有错误消息。 - 我们的组件对单个属性执行自定义验证。
- 验证错误被添加到组件的
ValidationMessageStore
中,以FieldIdentifier
为关键字。 ValidationMessageStore
执行EditContext.GetFieldState
以检索当前FieldIdentifier
的FieldState
。- 将
ValidationMessageStore
添加到FieldState
,以便FieldState.GetValidationMessages
能够从所有ValidationMessageStore
实例检索所有错误消息。步骤8特别重要,因为Blazor需要能够检索特定输入的所有验证错误消息,而不管它们被添加到哪个ValidationMessageStore。
User submits the form
<EditForm>
执行EditContext.Validate
。EditContext
触发其OnValidationRequsted
事件。- 我们组件的订阅告诉我们的
ValidationMessageStore
清除所有字段以前的所有验证错误消息。 - 组件对整个
EditContext.Model
对象执行其自定义验证。 - 与单个更改的验证一样,错误被添加到
ValidationMessageStore
,该存储库向EditContext
中的所有相关FieldState
实例注册自身。 <EditForm>
根据是否有错误消息触发相关的有效/无效事件。EditForm.Model is changed
如果这是一个用于创建新人的用户界面,那么在成功地将我们的新Person
提交到服务器之后,我们的应用程序可能会为我们的表单创建一个要编辑的新Person
。这将丢弃与前一个Person
实例相关联的所有状态(在虚线框中指示),并从新实例重新开始。
- EditContext
- FieldState
- ValidationMessageStore
在演示源代码中添加了一些日志记录后,我们可以看到以下输出。
WASM: EditContext has changed
WASM: New ValidationMessageStore created
WASM: Hooked up EditContext events (OnValidationRequested and OnFieldChanged)
WASM: OnFieldChanged triggered: Validating a single property named Name on class Person
WASM: OnFieldChanged triggered: Validating a single property named Age on class Person
WASM: OnValidationRequested triggered: Validating whole object
WASM: EditContext has changed
WASM: New ValidationMessageStore created
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
创建新实例。