diff --git a/src/BootstrapBlazor.Server/Components/Samples/ValidateForms.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/ValidateForms.razor.cs index 9ce3cf4d4fb..8a87539124f 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/ValidateForms.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/ValidateForms.razor.cs @@ -163,12 +163,12 @@ public IEnumerable Validate(ValidationContext validationContex _validMemberNames.AddRange([nameof(model.Email), nameof(model.ConfirmEmail)]); } } - return InvalidMemberNames(); + return GetInvalidMemberNames(); } - public List ValidMemberNames() => _validMemberNames; + public List GetValidMemberNames() => _validMemberNames; - public List InvalidMemberNames() => _invalidMemberNames; + public List GetInvalidMemberNames() => _invalidMemberNames; } class ComplexFoo : Foo diff --git a/src/BootstrapBlazor.Server/Data/CustomValidateCollectionModel.cs b/src/BootstrapBlazor.Server/Data/CustomValidateCollectionModel.cs index 54404435972..a757a0c7ec7 100644 --- a/src/BootstrapBlazor.Server/Data/CustomValidateCollectionModel.cs +++ b/src/BootstrapBlazor.Server/Data/CustomValidateCollectionModel.cs @@ -61,11 +61,11 @@ public IEnumerable Validate(ValidationContext validationContex /// /// /// - public List ValidMemberNames() => _validMemberNames; + public List GetValidMemberNames() => _validMemberNames; /// /// /// /// - public List InvalidMemberNames() => _invalidMemberNames; + public List GetInvalidMemberNames() => _invalidMemberNames; } diff --git a/src/BootstrapBlazor/BootstrapBlazor.csproj b/src/BootstrapBlazor/BootstrapBlazor.csproj index 54484625175..e0161cf7d29 100644 --- a/src/BootstrapBlazor/BootstrapBlazor.csproj +++ b/src/BootstrapBlazor/BootstrapBlazor.csproj @@ -1,7 +1,7 @@ - 8.9.4-beta05 + 8.9.4-beta06 diff --git a/src/BootstrapBlazor/Components/Validate/IValidateCollection.cs b/src/BootstrapBlazor/Components/Validate/IValidateCollection.cs index 2f5f3c8244e..e500017b51a 100644 --- a/src/BootstrapBlazor/Components/Validate/IValidateCollection.cs +++ b/src/BootstrapBlazor/Components/Validate/IValidateCollection.cs @@ -20,11 +20,11 @@ public interface IValidateCollection /// 返回合法成员集合 /// /// - List ValidMemberNames(); + List GetValidMemberNames(); /// /// 返回非法成员集合 /// /// - List InvalidMemberNames(); + List GetInvalidMemberNames(); } diff --git a/src/BootstrapBlazor/Components/Validate/ValidateBase.cs b/src/BootstrapBlazor/Components/Validate/ValidateBase.cs index 880818b8100..704b384ab63 100644 --- a/src/BootstrapBlazor/Components/Validate/ValidateBase.cs +++ b/src/BootstrapBlazor/Components/Validate/ValidateBase.cs @@ -301,6 +301,7 @@ protected override void OnParametersSet() if (ValidateForm != null) { + // IValidateCollection 支持组件间联动验证 var fieldName = FieldIdentifier?.FieldName; if (!string.IsNullOrEmpty(fieldName)) { diff --git a/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs b/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs index 257fb7ba4e8..591331500ba 100644 --- a/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs +++ b/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs @@ -21,7 +21,6 @@ public partial class ValidateForm /// is determined to be valid. /// [Parameter] - [NotNull] public Func? OnValidSubmit { get; set; } /// @@ -29,7 +28,6 @@ public partial class ValidateForm /// is determined to be invalid. /// [Parameter] - [NotNull] public Func? OnInvalidSubmit { get; set; } /// @@ -107,6 +105,11 @@ public partial class ValidateForm /// private readonly ConcurrentDictionary<(string FieldName, Type ModelType), (FieldIdentifier FieldIdentifier, IValidateComponent ValidateComponent)> _validatorCache = new(); + /// + /// 验证组件验证结果缓存 + /// + private readonly ConcurrentDictionary> _validateResults = new(); + private string? DisableAutoSubmitString => (DisableAutoSubmitFormByEnter.HasValue && DisableAutoSubmitFormByEnter.Value) ? "true" : null; /// @@ -165,7 +168,7 @@ internal void AddValidator((string FieldName, Type ModelType) key, (FieldIdentif /// /// /// - 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); /// /// 设置指定字段错误信息 @@ -186,19 +189,21 @@ public void SetError(Expression> 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 - { - new(errorMessage, new string[] { fieldName }) - }; - validator.ToggleMessage(results); + return; } + + var results = new List + { + new(errorMessage, [fieldName]) + }; + validator.ToggleMessage(results); } } @@ -213,7 +218,7 @@ public void SetError(string propertyName, string errorMessage) { var results = new List { - new(errorMessage, new string[] { fieldName }) + new(errorMessage, [fieldName]) }; validator.ToggleMessage(results); } @@ -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; @@ -256,6 +261,8 @@ private bool TryGetValidator(Type modelType, string fieldName, [NotNullWhen(true /// internal async Task ValidateObject(ValidationContext context, List results) { + _validateResults.Clear(); + if (ValidateAllProperties) { await ValidateProperty(context, results); @@ -266,30 +273,29 @@ internal async Task ValidateObject(ValidationContext context, List(); - var pi = key.ModelType.GetPropertyByName(key.FieldName); - if (pi != null) + continue; + } + + var messages = new List(); + 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 @@ -306,8 +312,8 @@ internal async Task ValidateObject(ValidationContext context, List 0) { foreach (var key in _validatorCache.Keys) { @@ -315,13 +321,19 @@ internal async Task ValidateObject(ValidationContext context, List _validateResults.Values.SelectMany(i => i).Any(i => i.MemberNames.Contains(name))); + foreach (var (validator, messages) in _validateResults) + { + validator.ToggleMessage(messages); + } } } @@ -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])); } } } @@ -507,7 +519,7 @@ private async Task ValidateAsync(IValidateComponent validator, ValidationContext if (messages.Count == 0) { // 自定义验证组件 - _tcs = new(); + _tcs = new TaskCompletionSource(); await validator.ValidatePropertyAsync(propertyValue, context, messages); _tcs.TrySetResult(messages.Count == 0); } @@ -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()); } } } diff --git a/test/UnitTest/Components/ValidateFormTest.cs b/test/UnitTest/Components/ValidateFormTest.cs index e67b8e627de..05b66440c37 100644 --- a/test/UnitTest/Components/ValidateFormTest.cs +++ b/test/UnitTest/Components/ValidateFormTest.cs @@ -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(pb => + { + pb.Add(a => a.Model, model); + pb.AddChildContent>(pb => + { + pb.Add(a => a.Value, model.Value1); + pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(model, "Value1", typeof(int))); + }); + pb.AddChildContent>(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>(); + var all = cut.FindComponents>(); + 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() { @@ -698,6 +735,7 @@ private class Dummy public string? File { get; set; } public string? Password1 { get; set; } + public string? Password2 { get; set; } } @@ -712,13 +750,68 @@ public IEnumerable 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 _validMemberNames = []; + + public List GetValidMemberNames() => _validMemberNames; + + private readonly List _invalidMemberNames = []; + + public List GetInvalidMemberNames() => _invalidMemberNames; + + public IEnumerable 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")] @@ -803,13 +896,13 @@ public IEnumerable Validate(ValidationContext validationContex /// /// /// - public List ValidMemberNames() => _validMemberNames; + public List GetValidMemberNames() => _validMemberNames; /// /// /// /// - public List InvalidMemberNames() => _invalidMemberNames; + public List GetInvalidMemberNames() => _invalidMemberNames; } private class MockInput : BootstrapInput