Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -163,12 +163,12 @@ public IEnumerable<ValidationResult> Validate(ValidationContext validationContex
_validMemberNames.AddRange([nameof(model.Email), nameof(model.ConfirmEmail)]);
}
}
return InvalidMemberNames();
return GetInvalidMemberNames();
}

public List<string> ValidMemberNames() => _validMemberNames;
public List<string> GetValidMemberNames() => _validMemberNames;

public List<ValidationResult> InvalidMemberNames() => _invalidMemberNames;
public List<ValidationResult> GetInvalidMemberNames() => _invalidMemberNames;
}

class ComplexFoo : Foo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,11 @@ public IEnumerable<ValidationResult> Validate(ValidationContext validationContex
/// <inheritdoc/>
/// </summary>
/// <returns></returns>
public List<string> ValidMemberNames() => _validMemberNames;
public List<string> GetValidMemberNames() => _validMemberNames;

/// <summary>
/// <inheritdoc/>
/// </summary>
/// <returns></returns>
public List<ValidationResult> InvalidMemberNames() => _invalidMemberNames;
public List<ValidationResult> GetInvalidMemberNames() => _invalidMemberNames;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ public interface IValidateCollection
/// 返回合法成员集合
/// </summary>
/// <returns></returns>
List<string> ValidMemberNames();
List<string> GetValidMemberNames();

/// <summary>
/// 返回非法成员集合
/// </summary>
/// <returns></returns>
List<ValidationResult> InvalidMemberNames();
List<ValidationResult> GetInvalidMemberNames();
}
1 change: 1 addition & 0 deletions src/BootstrapBlazor/Components/Validate/ValidateBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ protected override void OnParametersSet()

if (ValidateForm != null)
{
// IValidateCollection 支持组件间联动验证
var fieldName = FieldIdentifier?.FieldName;
if (!string.IsNullOrEmpty(fieldName))
{
Expand Down
88 changes: 50 additions & 38 deletions src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,13 @@ public partial class ValidateForm
/// <see cref="EditContext"/> is determined to be valid.
/// </summary>
[Parameter]
[NotNull]
public Func<EditContext, Task>? OnValidSubmit { get; set; }

/// <summary>
/// A callback that will be invoked when the form is submitted and the
/// <see cref="EditContext"/> is determined to be invalid.
/// </summary>
[Parameter]
[NotNull]
public Func<EditContext, Task>? OnInvalidSubmit { get; set; }

/// <summary>
Expand Down Expand Up @@ -107,6 +105,11 @@ public partial class ValidateForm
/// </summary>
private readonly ConcurrentDictionary<(string FieldName, Type ModelType), (FieldIdentifier FieldIdentifier, IValidateComponent ValidateComponent)> _validatorCache = new();

/// <summary>
/// 验证组件验证结果缓存
/// </summary>
private readonly ConcurrentDictionary<IValidateComponent, List<ValidationResult>> _validateResults = new();

private string? DisableAutoSubmitString => (DisableAutoSubmitFormByEnter.HasValue && DisableAutoSubmitFormByEnter.Value) ? "true" : null;

/// <summary>
Expand Down Expand Up @@ -165,7 +168,7 @@ internal void AddValidator((string FieldName, Type ModelType) key, (FieldIdentif
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
internal bool TryRemoveValidator((string FieldName, Type ModelType) key, [MaybeNullWhen(false)] out (FieldIdentifier FieldIdentifier, IValidateComponent IValidateComponent) value) => _validatorCache.TryRemove(key, out value);
internal bool TryRemoveValidator((string FieldName, Type ModelType) key, out (FieldIdentifier FieldIdentifier, IValidateComponent IValidateComponent) value) => _validatorCache.TryRemove(key, out value);

/// <summary>
/// 设置指定字段错误信息
Expand All @@ -186,19 +189,21 @@ public void SetError<TModel>(Expression<Func<TModel, object?>> expression, strin

private void InternalSetError(MemberExpression exp, string errorMessage)
{
var fieldName = exp.Member.Name;
if (exp.Expression != null)
{
var fieldName = exp.Member.Name;
var modelType = exp.Expression.Type;
var validator = _validatorCache.FirstOrDefault(c => c.Key.ModelType == modelType && c.Key.FieldName == fieldName).Value.ValidateComponent;
if (validator != null)
if (validator == null)
{
var results = new List<ValidationResult>
{
new(errorMessage, new string[] { fieldName })
};
validator.ToggleMessage(results);
return;
}

var results = new List<ValidationResult>
{
new(errorMessage, [fieldName])
};
validator.ToggleMessage(results);
}
}

Expand All @@ -213,7 +218,7 @@ public void SetError(string propertyName, string errorMessage)
{
var results = new List<ValidationResult>
{
new(errorMessage, new string[] { fieldName })
new(errorMessage, [fieldName])
};
validator.ToggleMessage(results);
}
Expand Down Expand Up @@ -241,7 +246,7 @@ private bool TryGetModelField(string propertyName, [MaybeNullWhen(false)] out Ty
return propNames.IsEmpty;
}

private bool TryGetValidator(Type modelType, string fieldName, [NotNullWhen(true)] out IValidateComponent validator)
private bool TryGetValidator(Type modelType, string fieldName, out IValidateComponent validator)
{
validator = _validatorCache.FirstOrDefault(c => c.Key.ModelType == modelType && c.Key.FieldName == fieldName).Value.ValidateComponent;
return validator != null;
Expand All @@ -256,6 +261,8 @@ private bool TryGetValidator(Type modelType, string fieldName, [NotNullWhen(true
/// <param name="results"></param>
internal async Task ValidateObject(ValidationContext context, List<ValidationResult> results)
{
_validateResults.Clear();

if (ValidateAllProperties)
{
await ValidateProperty(context, results);
Expand All @@ -266,30 +273,29 @@ internal async Task ValidateObject(ValidationContext context, List<ValidationRes
foreach (var key in _validatorCache.Keys)
{
// 验证 DataAnnotations
var validatorValue = _validatorCache[key];
var validator = validatorValue.ValidateComponent;
var fieldIdentifier = validatorValue.FieldIdentifier;
if (validator.IsNeedValidate)
var (fieldIdentifier, validator) = _validatorCache[key];
if (!validator.IsNeedValidate)
{
var messages = new List<ValidationResult>();
var pi = key.ModelType.GetPropertyByName(key.FieldName);
if (pi != null)
continue;
}

var messages = new List<ValidationResult>();
var pi = key.ModelType.GetPropertyByName(key.FieldName);
if (pi != null)
{
var propertyValidateContext = new ValidationContext(fieldIdentifier.Model, context, null)
{
var propertyValidateContext = new ValidationContext(fieldIdentifier.Model, context, null)
{
MemberName = fieldIdentifier.FieldName,
DisplayName = fieldIdentifier.GetDisplayName()
};
MemberName = fieldIdentifier.FieldName,
DisplayName = fieldIdentifier.GetDisplayName()
};

// 设置其关联属性字段
var propertyValue = Utility.GetPropertyValue(fieldIdentifier.Model, fieldIdentifier.FieldName);
// 设置其关联属性字段
var propertyValue = Utility.GetPropertyValue(fieldIdentifier.Model, fieldIdentifier.FieldName);

await ValidateAsync(validator, propertyValidateContext, messages, pi, propertyValue);
}
// 客户端提示
validator.ToggleMessage(messages);
results.AddRange(messages);
await ValidateAsync(validator, propertyValidateContext, messages, pi, propertyValue);
}
_validateResults.TryAdd(validator, messages);
results.AddRange(messages);
}

// 验证 IValidatableObject
Expand All @@ -306,22 +312,28 @@ internal async Task ValidateObject(ValidationContext context, List<ValidationRes
}
if (validate != null)
{
var messages = validate.Validate(context);
if (messages.Any())
var messages = validate.Validate(context).ToList();
if (messages.Count > 0)
{
foreach (var key in _validatorCache.Keys)
{
var validatorValue = _validatorCache[key];
var validator = validatorValue.ValidateComponent;
if (validator.IsNeedValidate)
{
validator.ToggleMessage(messages);
_validateResults[validator].AddRange(messages);
}
}
results.AddRange(messages);
}
}
}

ValidMemberNames.RemoveAll(name => _validateResults.Values.SelectMany(i => i).Any(i => i.MemberNames.Contains(name)));
foreach (var (validator, messages) in _validateResults)
{
validator.ToggleMessage(messages);
}
}
}

Expand Down Expand Up @@ -425,7 +437,7 @@ private void ValidateDataAnnotations(object? value, ValidationContext context, L
var errorMessage = !string.IsNullOrEmpty(rule.ErrorMessage) && rule.ErrorMessage.Contains("{0}")
? rule.FormatErrorMessage(displayName)
: rule.ErrorMessage;
results.Add(new ValidationResult(errorMessage, new string[] { memberName }));
results.Add(new ValidationResult(errorMessage, [memberName]));
}
}
}
Expand Down Expand Up @@ -507,7 +519,7 @@ private async Task ValidateAsync(IValidateComponent validator, ValidationContext
if (messages.Count == 0)
{
// 自定义验证组件
_tcs = new();
_tcs = new TaskCompletionSource<bool>();
await validator.ValidatePropertyAsync(propertyValue, context, messages);
_tcs.TrySetResult(messages.Count == 0);
}
Expand All @@ -527,8 +539,8 @@ private async Task ValidateAsync(IValidateComponent validator, ValidationContext
if (validate != null)
{
messages.AddRange(validate.Validate(context));
ValidMemberNames.AddRange(validate.ValidMemberNames());
InvalidMemberNames.AddRange(validate.InvalidMemberNames());
ValidMemberNames.AddRange(validate.GetValidMemberNames());
InvalidMemberNames.AddRange(validate.GetInvalidMemberNames());
}
}
}
Expand Down
101 changes: 97 additions & 4 deletions test/UnitTest/Components/ValidateFormTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,43 @@ public void MetadataTypeIValidatableObject_Ok()
Assert.Equal("两次密码必须一致。", message);
}

[Fact]
public async Task MetadataTypeIValidateCollection_Ok()
{
var model = new Dummy2() { Value1 = 0, Value2 = 0 };
var cut = Context.RenderComponent<ValidateForm>(pb =>
{
pb.Add(a => a.Model, model);
pb.AddChildContent<MockInput<int>>(pb =>
{
pb.Add(a => a.Value, model.Value1);
pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(model, "Value1", typeof(int)));
});
pb.AddChildContent<MockInput<int>>(pb =>
{
pb.Add(a => a.Value, model.Value2);
pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(model, "Value2", typeof(int)));
});
});
var form = cut.Find("form");
await cut.InvokeAsync(() => form.Submit());
var input = cut.FindComponent<MockInput<int>>();
var all = cut.FindComponents<MockInput<int>>();
var input2 = all[all.Count - 1];
Assert.Null(input.Instance.GetErrorMessage());
Assert.Equal("Value2 必须大于 0", input2.Instance.GetErrorMessage());

model.Value1 = 0;
model.Value2 = 2;
cut.SetParametersAndRender(pb =>
{
pb.Add(a => a.Model, model);
});
await cut.InvokeAsync(() => form.Submit());
Assert.Equal("Value1 必须大于 Value2", input.Instance.GetErrorMessage());
Assert.Equal("Value1 必须大于 Value2", input2.Instance.GetErrorMessage());
}

[Fact]
public void Validate_Class_Ok()
{
Expand Down Expand Up @@ -698,6 +735,7 @@ private class Dummy
public string? File { get; set; }

public string? Password1 { get; set; }

public string? Password2 { get; set; }
}

Expand All @@ -712,13 +750,68 @@ public IEnumerable<ValidationResult> Validate(ValidationContext validationContex
if (validationContext.ObjectInstance is Dummy dy)
{
if (!string.Equals(dy.Password1, dy.Password2, StringComparison.InvariantCultureIgnoreCase))
result.Add(new ValidationResult("两次密码必须一致。",
[nameof(Dummy.Password1), nameof(Dummy.Password2)]));
{
result.Add(new ValidationResult("两次密码必须一致。", [nameof(Dummy.Password1), nameof(Dummy.Password2)]));
}
}
return result;
}
}

[MetadataType(typeof(Dummy2MetadataCollection))]
private class Dummy2
{
public int Value1 { get; set; }

public int Value2 { get; set; }
}

public class Dummy2MetadataCollection : IValidateCollection
{
[Required]
public int Value1 { get; set; }

[CustomValidation(typeof(Dummy2MetadataCollection), nameof(CustomValidate), ErrorMessage = "{0} 必须大于 0")]
[Required]
public int Value2 { get; set; }

private readonly List<string> _validMemberNames = [];

public List<string> GetValidMemberNames() => _validMemberNames;

private readonly List<ValidationResult> _invalidMemberNames = [];

public List<ValidationResult> GetInvalidMemberNames() => _invalidMemberNames;

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
_invalidMemberNames.Clear();
_validMemberNames.Clear();
if (validationContext.ObjectInstance is Dummy2 dummy)
{
if (dummy.Value1 < dummy.Value2)
{
_invalidMemberNames.Add(new ValidationResult("Value1 必须大于 Value2", [nameof(Dummy2.Value1), nameof(Dummy2.Value2)]));
}
else
{
_validMemberNames.AddRange([nameof(Dummy2.Value1), nameof(Dummy2.Value2)]);
}
}
return _invalidMemberNames;
}

public static ValidationResult? CustomValidate(object value, ValidationContext context)
{
ValidationResult? ret = null;
if (value is int v && v < 1)
{
ret = new ValidationResult("Value2 必须大于 0", ["Value2"]);
}
return ret;
}
}

private class MockFoo
{
[Required(ErrorMessage = "{0} is Required")]
Expand Down Expand Up @@ -803,13 +896,13 @@ public IEnumerable<ValidationResult> Validate(ValidationContext validationContex
/// <inheritdoc/>
/// </summary>
/// <returns></returns>
public List<string> ValidMemberNames() => _validMemberNames;
public List<string> GetValidMemberNames() => _validMemberNames;

/// <summary>
/// <inheritdoc/>
/// </summary>
/// <returns></returns>
public List<ValidationResult> InvalidMemberNames() => _invalidMemberNames;
public List<ValidationResult> GetInvalidMemberNames() => _invalidMemberNames;
}

private class MockInput<TValue> : BootstrapInput<TValue>
Expand Down