Blazor:手动填充验证消息存储后不会显示验证消息



我正在尝试:

  1. 从Blazor页面将Excel上传到控制器
  2. 处理控制器中的行,并对它们执行一些操作(如果成功,则返回空响应(
  3. 如果在分析Excel时出现验证错误,请将其返回到页面

我不知道之前会有多少行,所以我也会从控制器返回那个数字。我也没有模型,所以我只会在Blazor中伪造它来显示验证错误。ValidationMessages不会显示。这里有一个测试代码,不需要服务器调用:

@using System.Collections
<button @onclick="GenerateErrors">
Generate
</button>
<EditForm EditContext="@editContext">
@if (Errors != null)
{
for (int i = 0; i < Errors.Rows; i++)
{
<ValidationMessage For="()=>cmd.Users[i].Name" />
}
}
</EditForm>
@code {
private record FakeValidationError(string Identifier, string ErrorMessage);
// This is what I would get from the controller
private record FakeValidationErrors(List<FakeValidationError> Errors, int? Rows); 
private class FakeList<T> : IList<T> where T : new()
{
public T this[int index] { get => _factory == null ? new T() : _factory(); set => _ = value; }
private Func<T> _factory;
public int Count => throw new NotImplementedException();
public FakeList(Func<T> factory = null)
{
_factory = factory;
}
public bool IsReadOnly => false;
public void Add(T item) => throw new NotImplementedException();
public void Clear() => throw new NotImplementedException();
public bool Contains(T item) => throw new NotImplementedException();
public void CopyTo(T[] array, int arrayIndex) => throw new NotImplementedException();
public IEnumerator<T> GetEnumerator() { yield return _factory == null ? new T() : _factory(); }
public int IndexOf(T item) => throw new NotImplementedException();
public void Insert(int index, T item) => throw new NotImplementedException();
public bool Remove(T item) => throw new NotImplementedException();
public void RemoveAt(int index) => throw new NotImplementedException();
IEnumerator IEnumerable.GetEnumerator() { yield return _factory == null ? new T() : _factory(); }
}
private class FakeUser
{
public string Name { get; set; }
}
private class FakeUsers
{
public IList<FakeUser> Users { get; set; } = new FakeList<FakeUser>();
}
// This is the model, which I have to fake, because it only lives in the controller when it parses the Excel
// It's not returned by it because it won't validate, so it's meaningless.
FakeUsers cmd = new();
protected override async Task OnInitializedAsync()
{
editContext = new(cmd);
messageStore = new(editContext);
base.OnInitialized();
}
FakeValidationErrors Errors = null;
protected EditContext editContext;
protected ValidationMessageStore messageStore;
private void GenerateErrors()
{
List<FakeValidationError> l = new();
l.Add(new("Users[5].Name", "Bad name at #5"));
Errors = new(l, 8);
messageStore?.Clear();
foreach (var x in Errors.Errors)
{
FieldIdentifier f = new FieldIdentifier(cmd, x.Identifier);
messageStore.Add(f, x.ErrorMessage);
}
editContext.NotifyValidationStateChanged();
}
}

我知道有很多务实的方法可以实现这一点。但我想知道,具体地说,为什么这不起作用。这段代码

messageStore?.Clear();
foreach (var x in Errors.Errors)
{
FieldIdentifier f = new FieldIdentifier(cmd, x.Identifier);
messageStore.Add(f, x.ErrorMessage);
}
editContext.NotifyValidationStateChanged();

一直在其他EditForms中工作。

更新:代码中有一个拼写错误。它已被删除(Errors=new(((

验证摘要工作

我试过用一个变量,这样lambda就不会捕获I。没有区别

for (int i = 0; i < Errors.Rows; i++)
{
var j=i;
<ValidationMessage For="()=>cmd.Users[j].Name" />
}

ValidatationMessage需要一个FieldIdentifier来在消息存储中进行查找。它通过将For中定义的Expression传递给FieldIdentifier上的Create方法来获得一个。对Expression的解码是通过一种称为ParseAccessor的内部方法来完成的。

它无法解码()=>cmd.Users[j].Name,因此不知道显示什么。我在这个答案的底部包含了FieldIdentifier的相关代码,以供参考。

一种解决方案是编写自己的ValidationMessage,它直接接受FieldIdfentifier作为For。这是一个简单的版本:

public class MyValidationMessage : ComponentBase, IDisposable
{
[Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }
[CascadingParameter] EditContext CurrentEditContext { get; set; } = default!;
[Parameter, EditorRequired] public FieldIdentifier For { get; set; } = new FieldIdentifier();
protected override void OnParametersSet()
{
if (CurrentEditContext == null)
throw new InvalidOperationException($"{GetType()} requires a cascading parameter ");
CurrentEditContext.OnValidationStateChanged += OnValidationStateChanged;
}
private void OnValidationStateChanged(object? sender, ValidationStateChangedEventArgs e)
=> StateHasChanged();
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
foreach (var message in CurrentEditContext.GetValidationMessages(For))
{
builder.OpenElement(0, "div");
builder.AddAttribute(1, "class", "validation-message");
builder.AddMultipleAttributes(2, AdditionalAttributes);
builder.AddContent(3, message);
builder.CloseElement();
}
}
public void Dispose()
{
if (CurrentEditContext is not null)
CurrentEditContext.OnValidationStateChanged += OnValidationStateChanged;
}
}

这是一个经典的表单演示页面:

@page "/"
@using System.ComponentModel.DataAnnotations
<PageTitle>Index</PageTitle>
<EditForm EditContext=this.editContext class="form">
<DataAnnotationsValidator />
<div class="form-label">Enter more than two chars:</div>
<InputText class="form-control" @bind-Value=model.Name />
<MyValidationMessage For="@(new FieldIdentifier(model, "Name"))" />
<div class="m-2 text-end">
<button type="submit" class="btn btn-success">Submit</button>
</div>
</EditForm>
@code {
private ModelData model = new ModelData();
private ValidationMessageStore? myStore;
private EditContext? editContext;
public class ModelData {
[Required]
[StringLength(2, ErrorMessage = "Name is too long.")]
public string? Name { get; set; }
}
protected override void OnInitialized()
{
editContext = new EditContext(model);
myStore = new ValidationMessageStore(editContext);
}
}

还有你的简化版。请注意,EditContext指向一个不同的简单对象。它只用于ValidationMessageStore和活动!

@page "/"
@using System.ComponentModel.DataAnnotations
<PageTitle>Index</PageTitle>
<EditForm EditContext=this.editContext class="form">
@foreach (var item in models)
{
<MyValidationMessage For="@(new FieldIdentifier(item, "Value"))" />
}
</EditForm>
<div class="m-2">
<button class="btn btn-danger" @onclick="LogMessages">Create Errors</button>
<button class="btn btn-primary" @onclick="() => LogMessage(0)">Error on Row 0</button>
<button class="btn btn-primary" @onclick="() => LogMessage(1)">Error on Row 1</button>
<button class="btn btn-primary" @onclick="() => LogMessage(2)">Error on Row 2</button>
</div>
@code {
private RowData model = new RowData();
private List<RowData> models = new() {
new RowData {Value=1},
new RowData {Value=2},
new RowData {Value=3},
};
private ValidationMessageStore? myStore;
private EditContext? editContext;
public class RowData
{
public int Value { get; set; }
}
protected override void OnInitialized()
{
editContext = new EditContext(model);
myStore = new ValidationMessageStore(editContext);
}
private void LogMessage(int index)
{
this.myStore?.Clear();
this.myStore?.Add(new FieldIdentifier(models[index], "Value"), $"Row {index} error");
this.editContext?.NotifyValidationStateChanged();
}
private void LogMessages()
{
this.myStore?.Clear();
this.myStore?.Add(new FieldIdentifier(models[0], "Value"), $"Row {0} error");
this.myStore?.Add(new FieldIdentifier(models[2], "Value"), $"Row {2} error");
this.editContext?.NotifyValidationStateChanged();
}
}

供参考

这是相关的FieldIdentifier代码。


public static FieldIdentifier Create<TField>(Expression<Func<TField>> accessor)
{
if (accessor == null)
{
throw new ArgumentNullException(nameof(accessor));
}
ParseAccessor(accessor, out var model, out var fieldName);
return new FieldIdentifier(model, fieldName);
}
private static void ParseAccessor<T>(Expression<Func<T>> accessor, out object model, out string fieldName)
{
var accessorBody = accessor.Body;
// Unwrap casts to object
if (accessorBody is UnaryExpression unaryExpression
&& unaryExpression.NodeType == ExpressionType.Convert
&& unaryExpression.Type == typeof(object))
{
accessorBody = unaryExpression.Operand;
}
if (!(accessorBody is MemberExpression memberExpression))
{
throw new ArgumentException($"The provided expression contains a {accessorBody.GetType().Name} which is not supported. {nameof(FieldIdentifier)} only supports simple member accessors (fields, properties) of an object.");
}
// Identify the field name. We don't mind whether it's a property or field, or even something else.
fieldName = memberExpression.Member.Name;
// Get a reference to the model object
// i.e., given a value like "(something).MemberName", determine the runtime value of "(something)",
if (memberExpression.Expression is ConstantExpression constantExpression)
{
if (constantExpression.Value is null)
{
throw new ArgumentException("The provided expression must evaluate to a non-null value.");
}
model = constantExpression.Value;
}
else if (memberExpression.Expression != null)
{
// It would be great to cache this somehow, but it's unclear there's a reasonable way to do
// so, given that it embeds captured values such as "this". We could consider special-casing
// for "() => something.Member" and building a cache keyed by "something.GetType()" with values
// of type Func<object, object> so we can cheaply map from "something" to "something.Member".
var modelLambda = Expression.Lambda(memberExpression.Expression);
var modelLambdaCompiled = (Func<object?>)modelLambda.Compile();
var result = modelLambdaCompiled();
if (result is null)
{
throw new ArgumentException("The provided expression must evaluate to a non-null value.");
}
model = result;
}
else
{
throw new ArgumentException($"The provided expression contains a {accessorBody.GetType().Name} which is not supported. {nameof(FieldIdentifier)} only supports simple member accessors (fields, properties) of an object.");
}
}

关键是FieldIdentifier中的Member必须是一个简单的属性访问器。因此,我从Blazor Validation 中获取了GetParentObjectAndPropertyName

这解决了手动验证任何复杂对象的问题,从(Identifier,ErrorMessage)记录中获取消息,该记录可能来自FluentValidation,但可以从任何其他验证器生成。它具有对客户端或服务器验证不可知的优点。

悬而未决的问题可能没有解决方案,那就是拥有一个只有错误值的模型。我对GetParentObjectAndPropertyName做了一点更改,这样它就可以很好地与我的FakeList配合使用,但老实说,现在它没有什么作用,因为我已经意识到有错误的项目必须保留它们的值,否则FieldIdentifier就找不到它们。如果有人能想出一种只填充有错误的列表项的方法,我将不胜感激。

对不起,所以不让我在这里张贴代码。以下是(可运行的(片段:https://try.mudblazor.com/snippet/cYmcbbYlIIpKpoho

相关内容

  • 没有找到相关文章

最新更新