提问人:Killnine 提问时间:11/16/2023 更新时间:11/20/2023 访问量:89
嵌套 Blazor 组件中的验证
Validation within nested Blazor components
问:
我正在 Blazor (.net 8) 中设计一个组件,其中包含许多子组件。如下图所示:
- CreateBill 组件是主容器
- Contact 组件有 3 个实例
- 有一个 ChargeTerms 组件的单一实例
目前,我的应用程序有一个提交按钮(屏幕外),可以一次对整个模型执行验证:
<EditForm EditContext="_editContext" OnValidSubmit="CreateBillOnSubmit">
这样做的缺点是,模型上的任何验证失败都只是一个大列表,并且没有任何来自特定组件的上下文。例如,如果所有联系表单都是空的,则它们都没有任何突出显示,并且所有验证错误都会重复 3 次(见下文)
我不确定嵌套验证的模式应该是什么,因为根对象只有一个,并且子组件(联系人、收费条款)不包含表单,只包含类似的东西等等。<Form>
<InputText>
是否有建议的模式来显示每个字段的验证错误?
答:
对于 Blazor 嵌套组件,可以尝试使用 CascadingParameter
和 CascadingValue
为子组件提供验证上下文。这允许每个组件参与表单验证并根据上下文显示错误。
在每个子组件中定义一个类型。并通过调用 在子组件的方法中注册每个输入的验证。
使用或自定义验证程序来验证子模型。将每个输入绑定到子组件模型的属性。
在父组件上,处理事件以验证所有子组件。CascadingParameter
EditContext
OnInitialized
editContext.AddValidationMessageStore
DataAnnotationsValidator
OnValidSubmit
在组件中:Contact
@code {
[CascadingParameter]
private EditContext ParentEditContext { get; set; }
protected override void OnInitialized() {
if (ParentEditContext != null) {
// Logic to tie this component's validation to the parent EditContext.
}
}
}
在组件中:CreateBill
<EditForm EditContext="_editContext" OnValidSubmit="CreateBillOnSubmit">
<CascadingValue Value="_editContext">
<Contact />
<Contact />
<Contact />
<ChargeTerms />
</CascadingValue>
</EditForm>
@code {
private EditContext _editContext;
protected override void OnInitialized() {
_editContext = new EditContext(new CreateBillModel());
}
private void CreateBillOnSubmit() {
// Perform the submission logic here
}
}
请在提交时调用 以触发所有子组件的验证。Validate
EditContext
这将为每个子组件提供单独的验证上下文,同时仍然是整个表单验证的一部分。这应该可以消除重复错误消息的问题,并向用户提供上下文反馈。
另请参阅“ASP.NET Core Blazor 窗体概述”。
评论
我非常感谢@VonC的解决方案。
我还想提供史蒂夫·桑德森(Steve Sanderson,@MS)在这里链接的解决方案,因为它是一个更充实的解决方案。
关键部分是验证类:
using FluentValidation;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using System;
namespace CustomValidationSample
{
public class FluentValidator<TValidator> : ComponentBase where TValidator: IValidator, new()
{
private readonly static char[] separators = new[] { '.', '[' };
private TValidator validator;
[CascadingParameter] private EditContext EditContext { get; set; }
protected override void OnInitialized()
{
validator = new TValidator();
var messages = new ValidationMessageStore(EditContext);
// Revalidate when any field changes, or if the entire form requests validation
// (e.g., on submit)
EditContext.OnFieldChanged += (sender, eventArgs)
=> ValidateModel((EditContext)sender, messages);
EditContext.OnValidationRequested += (sender, eventArgs)
=> ValidateModel((EditContext)sender, messages);
}
private void ValidateModel(EditContext editContext, ValidationMessageStore messages)
{
var context = new ValidationContext<object>(editContext.Model);
var validationResult = _validator.Validate(context);
messages.Clear();
foreach (var error in validationResult.Errors)
{
var fieldIdentifier = ToFieldIdentifier(editContext, error.PropertyName);
messages.Add(fieldIdentifier, error.ErrorMessage);
}
editContext.NotifyValidationStateChanged();
}
private static FieldIdentifier ToFieldIdentifier(EditContext editContext, string propertyPath)
{
// This method parses property paths like 'SomeProp.MyCollection[123].ChildProp'
// and returns a FieldIdentifier which is an (instance, propName) pair. For example,
// it would return the pair (SomeProp.MyCollection[123], "ChildProp"). It traverses
// as far into the propertyPath as it can go until it finds any null instance.
var obj = editContext.Model;
while (true)
{
var nextTokenEnd = propertyPath.IndexOfAny(separators);
if (nextTokenEnd < 0)
{
return new FieldIdentifier(obj, propertyPath);
}
var nextToken = propertyPath.Substring(0, nextTokenEnd);
propertyPath = propertyPath.Substring(nextTokenEnd + 1);
object newObj;
if (nextToken.EndsWith("]"))
{
// It's an indexer
// This code assumes C# conventions (one indexer named Item with one param)
nextToken = nextToken.Substring(0, nextToken.Length - 1);
var prop = obj.GetType().GetProperty("Item");
var indexerType = prop.GetIndexParameters()[0].ParameterType;
var indexerValue = Convert.ChangeType(nextToken, indexerType);
newObj = prop.GetValue(obj, new object[] { indexerValue });
}
else
{
// It's a regular property
var prop = obj.GetType().GetProperty(nextToken);
if (prop == null)
{
throw new InvalidOperationException($"Could not find property named {nextToken} on object of type {obj.GetType().FullName}.");
}
newObj = prop.GetValue(obj);
}
if (newObj == null)
{
// This is as far as we can go
return new FieldIdentifier(obj, nextToken);
}
obj = newObj;
}
}
}
}
然后将其添加到 Razor 组件中:
<EditForm Model="customer" OnValidSubmit="SaveCustomer">
<FluentValidator TValidator="CustomerValidator" />
<h3>Your name</h3>
<InputText placeholder="First name" @bind-Value="customer.FirstName" />
<InputText placeholder="Last name" @bind-Value="customer.LastName" />
<ValidationMessage For="@(() => customer.FirstName)" />
<ValidationMessage For="@(() => customer.LastName)" />
<h3>Your address</h3>
<div>
<InputText placeholder="Line 1" @bind-Value="customer.Address.Line1" />
<ValidationMessage For="@(() => customer.Address.Line1)" />
</div>
<div>
<InputText placeholder="City" @bind-Value="customer.Address.City" />
<ValidationMessage For="@(() => customer.Address.City)" />
</div>
<div>
<InputText placeholder="Postcode" @bind-Value="customer.Address.Postcode" />
<ValidationMessage For="@(() => customer.Address.Postcode)" />
</div>
<h3>
Payment methods
[<a href @onclick="AddPaymentMethod">Add new</a>]
</h3>
<ValidationMessage For="@(() => customer.PaymentMethods)" />
@foreach (var paymentMethod in customer.PaymentMethods)
{
<p>
<InputSelect @bind-Value="paymentMethod.MethodType">
<option value="@PaymentMethod.Type.CreditCard">Credit card</option>
<option value="@PaymentMethod.Type.HonourSystem">Honour system</option>
</InputSelect>
@if (paymentMethod.MethodType == PaymentMethod.Type.CreditCard)
{
<InputText placeholder="Card number" @bind-Value="paymentMethod.CardNumber" />
}
else if (paymentMethod.MethodType == PaymentMethod.Type.HonourSystem)
{
<span>Sure, we trust you to pay us somehow eventually</span>
}
<button type="button" @onclick="@(() => customer.PaymentMethods.Remove(paymentMethod))">Remove</button>
<ValidationMessage For="@(() => paymentMethod.CardNumber)" />
</p>
}
<p><button type="submit">Submit</button></p>
</EditForm>
@code {
private Customer customer = new Customer();
void AddPaymentMethod()
{
customer.PaymentMethods.Add(new PaymentMethod());
}
void SaveCustomer()
{
Console.WriteLine("TODO: Actually do something with the valid data");
}
}
唯一的缺点(对我来说不是问题)是它会在提交时触发,而如果您想在其他点触发验证,Von 的解决方案可能会提供更大的灵活性。我希望这对您有所帮助!
评论