原文鏈接:https://blazor-university.com/forms/writing-custom-validation/
編寫自定義驗證
源代碼[1]
請注意,與有關 EditContext、FieldIdentifiers 和 FieldState[2] 的部分一樣,這是一個高級主題。
如前所述,FieldState
類保存表單數據的元狀態。除了指示值是否已被手動編輯外,Blazor 還存儲驗證錯誤消息的集合。為了了解它的工作原理,本節將說明如何創建我們自己的自定義驗證機制,該機制可與 Blazor 一起使用來驗證用戶輸入。
下面的 UML 圖顯示了 EditForm
和存儲此元狀態的各種類(在圖中分組)之間的關系。請記住,每當 EditForm.Model
發生更改時,EditForm
都會創建一個新的 EditContext
實例。然后可以對以前的 EditContext
(不再需要它,因為它包含有關以前模型的信息)進行垃圾收集,并且可以對圖表中分組的所有類實例進行垃圾收集。
我們的自定義驗證將基于 FluentValidation[3]。完成本節后(或者如果您只是想要一些可以立即使用的東西),請查看 blazor-validation[4]。
創建驗證器組件
我們的驗證器組件不必為了提供驗證而從任何特定類繼承。唯一的要求是它來自 Blazor ComponentBase
類,以便我們可以將其添加到視圖中的 <EditForm>
標記中。嵌入 <EditForm>
標記的目的是為了讓我們可以定義一個級聯參數[5],以便在 EditForm
的 Model
參數更改時獲取當前由 EditForm
創建的 EditContext
。
首先,創建一個新的 Blazor 應用并添加對 FluentValidation NuGet 包[6]的引用。然后創建一個名為 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?changedEditContext?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?upif?(EditContext?!=?previousEditContext)EditContextChanged();
}
第 4-5 行
每當我們的參數之一更改(包括我們的
EditContext
級聯參數)時,都會執行SetParametersAsync
。我們需要做的第一件事是保留對一些原始值的引用,以便我們可以查看它們是否已更改并做出相應的反應。第 7 行
調用
base.SetParametersAsync
會將我們對象的屬性更新為新值。第 9-16 行
確保我們有一個
EditContext
和一個作為IValidator
的ValidatorType
。第 18-19 行
如果
ValidatorType
已更改,那么我們需要創建該類型的新實例并將其分配給我們的私有Validator
字段以驗證我們的EditContext.Model
。第 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);
}
第 3 行
首先,我們從任何先前的驗證中清除所有錯誤消息。
第 4 行
接下來,我們指示
FluentValidation.IValidator
驗證在EditForm
(我們通過EditContext.Model
訪問)中正在編輯的模型。第 5 行
最后,我們將任何驗證錯誤添加到我們的
ValidationMessageStore
,這是在一個單獨的方法中完成的,因為我們將在驗證整個對象時使用它,并且在通過EditContext.OnFieldChanged
通知時驗證單個更改的屬性時也會使用它。
將錯誤消息添加到 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);
}
第 3-4 行
從事件
args
中獲取FieldIdentifier
(ObjectInstance/PropertyName 對),并僅清除該屬性的所有先前錯誤消息。第 16 行
使用
ValidationRequested
使用的相同方法將來自 FluentValidation 的錯誤添加到我們的ValidationMessageStore
。
使用組件
首先創建一個模型供我們的用戶編輯。
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();}
}
執行流程
頁面顯示
我們的
EditForm
組件是從<EditForm Model=@Person>
標記創建的。EditForm.OnParametersSet
被執行,因為EditForm.Model
已經從 null 變成了我們的Person
,它創建了一個新的EditContext
實例。新的
EditContext
實例通過級聯值[7]級聯到所有子組件。對于這種級聯值的變化,
InputBase<T>
的每個派生類都會執行其SetParametersAsync
,并通過創建FieldIdentifier
的新實例來做出反應。
我們的驗證組件已初始化
我們的驗證組件的
SetParametersAsync
方法是通過引用新的EditContext
來執行的。我們的組件創建了一個新的
ValidationMessageStore
。我們的組件偵聽
EditContext
上的事件以獲取驗證請求和輸入更改通知。
用戶更改數據
用戶在
InputBase<T>
派生類中編輯數據。組件通過
EditContext.NotifyFieldChanged
通知此狀態更改(從未修改到已修改),并傳遞其FieldIdentifier
。EditContext
觸發其OnFieldChanged
,傳遞FieldIdentifier
。我們組件的事件訂閱告訴我們的
ValidationMessageStore
清除由FieldIdentifier
的Model
和FieldName
屬性標識的狀態的所有錯誤消息。我們的組件對單個屬性執行其自定義驗證。
驗證錯誤被添加到我們組件的
ValidationMessageStore
中,由FieldIdentifier
作為鍵。ValidationMessageStore
執行EditContext.GetFieldState
以檢索當前FieldIdentifier
的FieldState
。ValidationMessageStore
被添加到FieldState
,以便FieldState.GetValidationMessages
能夠從所有ValidationMessageStore
實例中檢索所有錯誤消息。
第 8 步特別重要,因為 Blazor 需要能夠檢索特定輸入的所有驗證錯誤消息,無論它們被添加到哪個 ValidationMessageStore
。
用戶提交表單
<EditForm>
執行EditContext.Validate
。EditContext
觸發其OnValidationRequested
事件。我們組件的事件訂閱告訴我們的 ·ValidationMessageStore· 清除所有字段的所有先前驗證錯誤消息。
我們的組件對整個
EditContext.Model
對象執行其自定義驗證。與對單個更改的驗證一樣,錯誤被添加到 ·ValidationMessageStore·,它使用 ·EditContext· 中的所有相關 ·FieldState· 實例注冊自身。
<EditForm>
根據是否有錯誤消息觸發相關的有效/無效事件。
EditForm.Model 已更改
如果這是一個用于創建 people
的用戶界面,那么在成功將我們的新 People
提交到服務器后,我們的應用程序可能會創建一個新 People
供我們的表單進行編輯。這將丟棄與前一個 ?People
實例相關的所有狀態(在虛線框中表示),并從新實例重新開始。
EditContext
FieldState
ValidationMessageStore
在演示代碼中添加了一些日志記錄,我們會看到以下輸出。
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)
第 1-3 行
創建相關狀態實例以支持編輯
Person
實例。第 4 行
輸入了一個名字
第 5 行
輸入年齡
第 6 行
用戶提交表單
第 7 行
Index.razor 中的
Person
實例被更改,導致元狀態實例被丟棄并為新的EditForm.Model
創建新實例。
參考資料
[1]
源代碼: https://github.com/mrpmorris/blazor-university/tree/master/src/Forms/CustomValidation
[3]FluentValidation: https://github.com/JeremySkinner/FluentValidation
[4]blazor-validation: https://github.com/mrpmorris/blazor-validation
[6]FluentValidation NuGet 包: https://www.nuget.org/packages/FluentValidation/