Blazor 验证控件






3.20/5 (2投票s)
一个 Blazor 验证控件,用于管理和监控表单中的验证状态。
概述 - Blazor ValidationFormState 控件
这是系列文章中的第二篇,描述了一组有用的 Blazor 编辑控件,它们无需购买昂贵的工具包即可解决开箱即用编辑体验中的一些当前缺点。
本文介绍了表单验证的工作原理,并展示了如何从头开始构建一个相对简单但功能齐全的验证系统。一旦定义了基本结构和类,就可以很容易地为任何新的验证要求或自定义类的验证器编写额外的验证链方法。
代码和示例
此存储库包含一个项目,其中实现了本系列所有文章的控件。您可以在此处找到它。
示例网站位于https://cec-blazor-database.azurewebsites.net/。
本文末尾描述的示例表单可以在https://cec-blazor-database.azurewebsites.net//validationeditor查看。
该存储库是未来文章的“进行中”项目,因此会发生变化和发展。
Blazor 编辑设置
首先,让我们看看开箱即用的表单控件以及验证的工作原理。一个经典的表单看起来像这样
<EditForm Model="@exampleModel" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <InputText id="name" @bind-Value="exampleModel.Name" />
    <ValidationMessage For="@(() => exampleModel.Name)" />
    <button type="submit">Submit</button>
</EditForm>
第一篇文章描述了 EditForm 和 EditContext 的基本交互,所以我们跳过它,专注于验证过程。
当用户点击提交按钮时,EditForm 会:
- 如果注册了 OnSubmit的委托,则触发它并忽略验证。
- 如果没有 OnSubmit委托,它会调用EditContext.Validate。根据结果,要么触发OnValidSubmit,要么触发OnInvalidSubmit。
EditContext.Validate 检查是否有为 OnValidationRequested 注册的委托,如果有,则同步运行它。完成后,它检查 ValidationMessageStore 中是否有任何消息。如果为空,则表单通过验证并调用 OnValidSubmit,否则调用 OnInvalidSubmit。
验证器是一个没有发出标记的表单组件。它放置在 EditForm 内并捕获级联的 EditContext。初始化时,它向 EditContext.OnValidationRequested 注册一个事件处理程序以触发验证。在验证时,验证器执行其编码功能,将验证失败消息记录到 EditContext 的 ValidationMessageStore,最后调用 EditContext.NotifyValidationStateChanged,后者触发 EditContext.OnValidationStateChanged。
验证控件
诸如 ValidationMessage 和 ValidationSummary 等控件捕获级联的 EditContext 并在 EditContext.OnValidationStateChanged 上注册事件处理程序。当触发时,它们检查任何相关消息并显示它们。
在上面显示的表单中,<DataAnnotationsValidator /> 将 DataAnnotationsValidator 控件添加到表单中。它按上述方式进行挂钩,并使用模型类上的自定义属性注释来验证值。
验证器
Validator 是基本的验证器类。它被声明为 abstract 并使用泛型。验证器基于链式原理工作。基类包含所有常见的样板代码。
- 第一次调用是对要验证的对象类型定义的扩展方法。每种对象类型都需要自己的扩展方法来调用其特定的验证器。此扩展方法返回适合该对象类型的验证器。
- 一旦有了验证器实例,您就可以根据需要链接任意数量的验证方法。每个方法都经过编码,用于运行其验证测试,将任何特定消息记录到验证器,必要时触发触发器,并返回验证器实例。
- 验证通过调用 Validate完成,该方法在必要时触发传入的触发器,并将所有验证消息记录到ValidationMessageStore。
Validator 的属性/字段是
public bool IsValid => !Trip;
public List<string> Messages { get; } = new List<string>();
protected bool Trip { get; set; } = false;
protected string FieldName { get; set; }
protected T Value { get; set; }
protected string DefaultMessage { get; set; } = "The value failed validation";
protected ValidationMessageStore ValidationMessageStore { get; set; }
protected object Model { get; set; }
构造函数填充 validator
public Validator(T value, string fieldName, object model, 
ValidationMessageStore validationMessageStore, string message)
{
    this.FieldName = fieldName;
    this.Value = value;
    this.Model = model;
    this.ValidationMessageStore = validationMessageStore;
    this.DefaultMessage = string.IsNullOrWhiteSpace(message) ? this.DefaultMessage : message;
}
有两个 Validate 方法:一个用于外部使用的 public 方法和一个供特定验证器重写的 protected 方法。
public virtual bool Validate(ref bool tripwire, string fieldname, string message = null)
{
    if (string.IsNullOrEmpty(fieldname) || this.FieldName.Equals(fieldname))
    {
        this.Validate(message);
        if (!this.IsValid)
            tripwire = true;
    }
    else this.Trip = false;
    return this.IsValid;
}
protected virtual bool Validate(string message = null)
{
    if (!this.IsValid)
    {
        message ??= this.DefaultMessage;
        // Check if we've logged specific messages. If not, add the default message
        if (this.Messages.Count == 0) Messages.Add(message);
        //set up a FieldIdentifier and 
        //add the message to the Edit Context ValidationMessageStore
        var fi = new FieldIdentifier(this.Model, this.FieldName);
        this.ValidationMessageStore.Add(fi, this.Messages);
    }
    return this.IsValid;
}
protected void LogMessage(string message)
{
    if (!string.IsNullOrWhiteSpace(message)) Messages.Add(message);
}
字符串验证器
让我们以 StringValidator 为例,了解验证器的实现。完整的验证器集在 Repo 中。有两个类
- StringValidatorExtensions是一个- static类,声明为- string的扩展方法。
- StringValidator是一个专门针对- string类型的- Validator实现。
StringValidatorExtensions 为 string 声明了一个静态扩展方法 Validation。它返回一个 StringValidator 实例。在任何 string 上调用 StringValidator 以初始化验证链。
public static class StringValidatorExtensions
{
    public static StringValidator Validation(this string value, string fieldName, 
    object model, ValidationMessageStore validationMessageStore, string message = null)
    {
        var validation = new StringValidator(value, fieldName, model, 
                                             validationMessageStore, message);
        return validation;
    }
}
StringValidator 继承自 Validator,并为 string 声明了特定的验证链方法。每个方法都运行其测试。如果验证失败,它将任何提供的消息记录到消息存储并触发警报。最后,它返回 this。对于 string,我们有两个长度方法和一个 RegEx 方法来涵盖大多数情况。
public class StringValidator : Validator<string>
{
    public StringValidator(string value, string fieldName, 
    object model, ValidationMessageStore validationMessageStore, string message) : 
    base(value, fieldName, model, validationMessageStore, message) { }
    public StringValidator LongerThan(int test, string message = null)
    {
        if (string.IsNullOrEmpty(this.Value) || !(this.Value.Length > test))
        {
            Trip = true;
            LogMessage(message);
        }
        return this;
    }
    public StringValidator ShorterThan(int test, string message = null)
    {
            
        if (string.IsNullOrEmpty(this.Value) || !(this.Value.Length < test))
        {
            Trip = true;
            LogMessage(message);
        }
        return this;
    }
    public StringValidator Matches(string pattern, string message = null)
    {
        if (!string.IsNullOrWhiteSpace(this.Value))
        {
            var match = Regex.Match(this.Value, pattern);
            if (match.Success && match.Value.Equals(this.Value)) return this;
        }
        this.Trip = true;
        LogMessage(message);
        return this;
    }
}
IValidation
IValidation 接口如下所示。它只定义了一个 Validate 方法。
public interface IValidation
{
    public bool Validate(ValidationMessageStore validationMessageStore, string fieldname, object model = null);
}
WeatherForecast
WeatherForecast 是一个典型的数据类。
- 它实现了 IValidation,因此该控件可以运行验证。
- 每个字段都声明为具有默认值的属性。
- 它实现了 IValidation.Validate,后者调用了三次验证。
每次验证
- 调用类型上的 Validation扩展方法。
- 调用一个或多个验证链方法。
- 调用 Validate将任何验证消息记录到EditContext上的ValidationMessageStore,并在必要时触发警报。
public class WeatherForecast : IValidation
{
    public int ID { get; set; } = -1;
    public DateTime Date { get; set; } = DateTime.Now;
    public int TemperatureC { get; set; } = 0;
    [NotMapped] public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    public string Summary { get; set; } = string.Empty;
    public bool Validate(ValidationMessageStore validationMessageStore, 
                         string fieldname, object model = null)
    {
        model = model ?? this;
        bool trip = false;
        this.Summary.Validation("Summary", model, validationMessageStore)
            .LongerThan(2, "Your description needs to be a little longer! 3 letters minimum")
            .Validate(ref trip, fieldname);
        this.Date.Validation("Date", model, validationMessageStore)
            .NotDefault("You must select a date")
            .LessThan(DateTime.Now.AddMonths(1), true, "Date can only be up to 1 month ahead")
            .Validate(ref trip, fieldname);
        this.TemperatureC.Validation("TemperatureC", model, validationMessageStore)
            .LessThan(70, "The temperature must be less than 70C")
            .GreaterThan(-60, "The temperature must be greater than -60C")
            .Validate(ref trip, fieldname);
        return !trip;
    }
}
ValidationFormState 控件
ValidationFormState 控件替换了 Blazor 提供的基本 Validator。
- 它捕获了级联的 EditContext。
- DoValidationOnFieldChange控制字段级验证。如果为- true,则在用户退出字段时验证字段。如果为- false,则仅响应通过- EditContext发出的表单级验证请求。
- ValidStateChanged是一个回调,供父级在需要时附加事件处理程序。
- IsValid是一个- public readonly属性,公开当前的验证状态。它检查- EditContext是否有任何验证消息。
- ValidationMessageStore是- EditContext的- ValidationMessageStore。
- validating是一个布尔字段,用于确保我们不会堆叠验证。
- disposedValue是- IDisposable实现的一部分。
[CascadingParameter] public EditContext EditContext { get; set; }
[Parameter] public bool DoValidationOnFieldChange { get; set; } = true;
[Parameter] public EventCallback<bool> ValidStateChanged { get; set; }
public bool IsValid => !EditContext?.GetValidationMessages().Any() ?? true;
private ValidationMessageStore validationMessageStore;
private bool validating = false;
private bool disposedValue;
当组件初始化时,它从 EditContext 获取 ValidationMessageStore。它检查是否正在运行字段级验证,如果是,则向 EditContext.OnFieldChanged 事件注册 FieldChanged。最后,它向 EditContext.OnValidationRequested 注册 ValidationRequested。
protected override Task OnInitializedAsync()
{
    Debug.Assert(this.EditContext != null);
    if (this.EditContext != null)
    {
        // Get the Validation Message Store from the EditContext
        this.validationMessageStore = new ValidationMessageStore(this.EditContext);
        // Wires up to the EditContext OnFieldChanged event
        if (this.DoValidationOnFieldChange)
            this.EditContext.OnFieldChanged += FieldChanged;
        // Wires up to the Editcontext OnValidationRequested event
        this.EditContext.OnValidationRequested += ValidationRequested;
    }
    return Task.CompletedTask;
}
这两个事件处理程序都调用 Validate,一个带字段名,一个不带字段名。
private void FieldChanged(object sender, FieldChangedEventArgs e)
    => this.Validate(e.FieldIdentifier.FieldName);
private void ValidationRequested(object sender, ValidationRequestedEventArgs e)
    => this.Validate();
Validate 中的注释解释了它正在做什么。它将 Model 转换为 IValidator 并检查其是否有效。如果有效,它会调用接口上的 Validate 方法。我们已经在 WesatherForecast 数据类中看到了 *model.*Validate。当它将 fieldname 传递给 Validate 时,它只清除该特定 fieldname 的任何验证消息。
private void Validate(string fieldname = null)
{
    // Checks to see if the Model implements IValidation
    var validator = this.EditContext.Model as IValidation;
    if (validator != null || !this.validating)
    {
        this.validating = true;
        // Check if we are doing a field level or form level validation
        // Form level - clear all validation messages
        // Field level - clear any field specific validation messages
        if (string.IsNullOrEmpty(fieldname))
            this.validationMessageStore.Clear();
        else
            validationMessageStore.Clear
                (new FieldIdentifier(this.EditContext.Model, fieldname));
        // Run the IValidation interface Validate method
        validator.Validate(validationMessageStore, fieldname, this.EditContext.Model);
        // Notify the EditContext that the Validation State has changed
        // This precipitates a OnValidationStateChanged event 
        // which the validation message controls are all plugged into
        this.EditContext.NotifyValidationStateChanged();
        // Invoke ValidationStateChanged
        this.ValidStateChanged.InvokeAsync(this.IsValid);
        this.validating = false;
    }
}
代码的其余部分包括实用方法和 IDisposable 实现。
public void Clear()
    => this.validationMessageStore.Clear();
<span class="pl-c">// IDisposable Implementation
protected virtual void Dispose(bool disposing)
{
    if (!disposedValue)
    {
        if (disposing)
        {
            if (this.EditContext != null)
            {
                this.EditContext.OnFieldChanged -= this.FieldChanged;
                this.EditContext.OnValidationRequested -= this.ValidationRequested;
            }
        }
        disposedValue = true;
    }
}
public void Dispose()
{
    <span class="pl-c">// Do not change this code. 
                       // Put cleanup code in 'Dispose(bool disposing)' method
    Dispose(disposing: true);
    GC.SuppressFinalize(this);
}
一个简单的实现
为了测试该组件,这里有一个简单的测试页面。
上下更改温度,您应该会看到按钮的颜色和文本发生变化,并且启用/禁用状态也会随之改变。将温度更改为 200 以获取验证消息。
你可以在https://cec-blazor-database.azurewebsites.net//validationeditor查看此内容。
@using Blazor.Database.Data
@page "/validationeditor"
<EditForm Model="@Model" OnValidSubmit="@HandleValidSubmit">
    <EditFormState @ref="editFormState" EditStateChanged="this.EditStateChanged">
    </EditFormState>
    <ValidationFormState @ref="validationFormState"></ValidationFormState>
    <label class="form-label">ID:</label> <InputNumber class="form-control" 
     @bind-Value="Model.ID" />
    <label class="form-label">Date:</label> <InputDate class="form-control" 
     @bind-Value="Model.Date" /><ValidationMessage For="@(() => Model.Date)" />
    <label class="form-label">Temp C:</label> <InputNumber class="form-control" 
     @bind-Value="Model.TemperatureC" /><ValidationMessage For="@(() => Model.TemperatureC)" />
    <label class="form-label">Summary:</label> <InputText class="form-control" 
     @bind-Value="Model.Summary" /><ValidationMessage For="@(() => Model.Summary)" />
    <div class="mt-2">
        <div>Validation Messages:</div>
        <ValidationSummary />
    </div>
    <div class="text-right mt-2">
        <button class="btn @btnStateColour" disabled>@btnStateText</button>
        <button class="btn @btnValidColour" disabled>@btnValidText</button>
        <button class="btn btn-primary" type="submit" disabled="@_btnSubmitDisabled">
         Submit</button>
    </div>
</EditForm>
@code {
    protected bool _isDirty = false;
    protected bool _isValid => validationFormState?.IsValid ?? true;
    protected string btnStateColour => _isDirty ? "btn-danger" : "btn-success";
    protected string btnStateText => _isDirty ? "Dirty" : "Clean";
    protected string btnValidColour => !_isValid ? "btn-danger" : "btn-success";
    protected string btnValidText => !_isValid ? "Invalid" : "Valid";
    protected bool _btnSubmitDisabled => !(_isValid && _isDirty);
    protected EditFormState editFormState { get; set; }
    protected ValidationFormState validationFormState { get; set; }
    private WeatherForecast Model = new WeatherForecast()
    {
        ID = 1,
        Date = DateTime.Now,
        TemperatureC = 22,
        Summary = "Balmy"
    };
    private void HandleValidSubmit()
        => this.editFormState.UpdateState();
    private void EditStateChanged(bool editstate)
        => this._isDirty = editstate;
}
总结
希望我已经解释了验证的工作原理以及如何构建一个简单但全面且可扩展的验证系统。
验证最常见的问题是 ValidationMessage 控件不显示消息。通常有两个原因:
- UI 未更新。逐步调试代码以检查何时发生什么。
- 由 ValidationMessage的For属性生成的FieldIdentifier与验证存储中的FieldIdentifier不匹配。请检查您生成并记录到验证存储中的FieldIdentifier。
下一篇文章将展示如何在表单脏时锁定表单并防止导航。
如果您在很久以后才找到这篇文章,最新版本将在此处提供。
历史
- 2021 年 3 月 16 日:初始版本



