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 日:初始版本