diff --git a/src/BootstrapBlazor.Shared/Locales/en-US.json b/src/BootstrapBlazor.Shared/Locales/en-US.json index 0d4f113c9908cfb86ce1533623e4328fec9571c3..c1090bbe72b301d13ea60436989835860c03edf4 100644 --- a/src/BootstrapBlazor.Shared/Locales/en-US.json +++ b/src/BootstrapBlazor.Shared/Locales/en-US.json @@ -88,6 +88,14 @@ "Foo.Address2": "Earth, China, Lane {0} of Jinshajiang Road, Putuo District, Shanghai. Here is an example of super long cell", "Foo.BindValue": "BindValue" }, + "BootstrapBlazor.Shared.Pages.ValidateForms.ComplexFoo": { + "Name": "Name", + "Name.Required": "{0} is required." + }, + "BootstrapBlazor.Shared.Pages.ValidateForms.Dummy2": { + "Name": "Name", + "Name.Required": "Dummy2 {0} is required" + }, "BootstrapBlazor.Shared.Pages.Components.EnumEducation": { "PlaceHolder": "Click to select ...", "Primary": "Primary", diff --git a/src/BootstrapBlazor.Shared/Locales/zh-CN.json b/src/BootstrapBlazor.Shared/Locales/zh-CN.json index 206bfbffb44c069af4a58d05fa0e8f756b889b08..571c3e377df8968dcc02f045b879b7e741b6bda7 100644 --- a/src/BootstrapBlazor.Shared/Locales/zh-CN.json +++ b/src/BootstrapBlazor.Shared/Locales/zh-CN.json @@ -88,6 +88,14 @@ "Foo.Address2": "地球、中国、上海市普陀区金沙江路 {0} 弄 这里是超长单元格示例", "Foo.BindValue": "绑定值" }, + "BootstrapBlazor.Shared.Pages.ValidateForms.ComplexFoo": { + "Name": "姓名", + "Name.Required": "{0} 值是必填项" + }, + "BootstrapBlazor.Shared.Pages.ValidateForms.Dummy2": { + "Name": "姓名", + "Name.Required": "Dummy2 {0} 值是必填项" + }, "BootstrapBlazor.Shared.Pages.Components.EnumEducation": { "PlaceHolder": "请选择 ...", "Primary": "小学", diff --git a/src/BootstrapBlazor.Shared/Pages/Samples/ValidateForms.razor b/src/BootstrapBlazor.Shared/Pages/Samples/ValidateForms.razor index 06fd0e507faa33db1f927cc781448023f74f6630..7c87c1a37a70662a200390c5d0c3e43eca7db7bd 100644 --- a/src/BootstrapBlazor.Shared/Pages/Samples/ValidateForms.razor +++ b/src/BootstrapBlazor.Shared/Pages/Samples/ValidateForms.razor @@ -76,7 +76,7 @@ 应用场景
客户端验证通过后进行数据库保存操作,如果出现其他问题,后仍然可以进行表单自定义错误提示,本例中数据验证合法后,点击 提交表单 按钮后,姓名字段会显示 数据库中已存在 这样的自定义提示信息

- +
@@ -127,6 +127,24 @@ + +

本示例中绑定的是一个超级复杂类型 ComplexModel.Dummy.Dummy2.Name 清空值后,点击 提交表单 会对数据进行验证

+ +
+
+ +
+
+ +
+
+
+
+
+ +
+ diff --git a/src/BootstrapBlazor.Shared/Pages/Samples/ValidateForms.razor.cs b/src/BootstrapBlazor.Shared/Pages/Samples/ValidateForms.razor.cs index 8650cfc13d26fbe2accf34d2677c94a3eacfde18..673fff40bd6d1dcf9a656a725f2e3174f7cc74d7 100644 --- a/src/BootstrapBlazor.Shared/Pages/Samples/ValidateForms.razor.cs +++ b/src/BootstrapBlazor.Shared/Pages/Samples/ValidateForms.razor.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Forms; using Microsoft.Extensions.Localization; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; @@ -28,6 +29,9 @@ namespace BootstrapBlazor.Shared.Pages [NotNull] private Logger? Trace3 { get; set; } + [NotNull] + private Logger? Trace4 { get; set; } + [Inject] [NotNull] private IStringLocalizer? Localizer { get; set; } @@ -46,6 +50,9 @@ namespace BootstrapBlazor.Shared.Pages [NotNull] private ValidateForm? Test { get; set; } + [NotNull] + private ComplexFoo? ComplexModel { get; set; } + /// baise /// OnInitialized 方法 /// @@ -56,6 +63,10 @@ namespace BootstrapBlazor.Shared.Pages // 初始化参数 Educations = typeof(EnumEducation).ToSelectList(new SelectedItem("", Localizer["PlaceHolder"] ?? "请选择 ...")); Hobbys = Foo.GenerateHobbys(LocalizerFoo); + ComplexModel = new ComplexFoo() + { + Dummy = new Dummy1() { Dummy2 = new Dummy2() }, + }; } private Task OnInvalidSubmit1(EditContext context) @@ -70,12 +81,6 @@ namespace BootstrapBlazor.Shared.Pages return Task.CompletedTask; } - private Task OnValidateNameSubmit(EditContext context) - { - Test.SetError(typeof(Foo), "Name", "数据库中已存在"); - return Task.CompletedTask; - } - private Task OnInvalidSubmit(EditContext context) { Trace2.Log("OnInvalidSubmit 回调委托"); @@ -88,6 +93,37 @@ namespace BootstrapBlazor.Shared.Pages return Task.CompletedTask; } + private Task OnInvalidComplexModel(EditContext context) + { + Trace4.Log("OnInvalidSubmit 回调委托"); + return Task.CompletedTask; + } + + private Task OnValidComplexModel(EditContext context) + { + Trace4.Log("OnValidSubmit 回调委托"); + Test.SetError(f => f.Dummy.Dummy2.Name, "数据库中已存在"); + return Task.CompletedTask; + } + + private class ComplexFoo : Foo + { + [NotNull] + public Dummy1? Dummy { get; set; } + } + + private class Dummy1 + { + [NotNull] + public Dummy2? Dummy2 { get; set; } + } + + private class Dummy2 + { + [Required] + public string? Name { get; set; } + } + #region 参数说明 private static IEnumerable GetAttributes() => new AttributeItem[] { diff --git a/src/BootstrapBlazor/BootstrapBlazor.csproj b/src/BootstrapBlazor/BootstrapBlazor.csproj index cae1d8c5c42fb99e43ad8c91896378e9d561bca4..708fe8880e9c37eec21c6e2f996d9f7ef8204100 100644 --- a/src/BootstrapBlazor/BootstrapBlazor.csproj +++ b/src/BootstrapBlazor/BootstrapBlazor.csproj @@ -4,7 +4,7 @@ net5.0 true logo.png - 5.0.22-beta01 + 5.0.22-beta02 https://gitee.com/LongbowEnterprise/BootstrapBlazor/wikis diff --git a/src/BootstrapBlazor/Components/Validate/ValidateBase.cs b/src/BootstrapBlazor/Components/Validate/ValidateBase.cs index 73216a50859331634c43f97f202d9b8c9bc17542..4e1c907778a363f967653dfe7e2a4c7334183d30 100644 --- a/src/BootstrapBlazor/Components/Validate/ValidateBase.cs +++ b/src/BootstrapBlazor/Components/Validate/ValidateBase.cs @@ -188,7 +188,7 @@ namespace BootstrapBlazor.Components [Parameter] public override string? Id { - get { return (EditForm != null && !string.IsNullOrEmpty(EditForm.Id) && FieldIdentifier != null) ? $"{EditForm.Id}_{FieldIdentifier.Value.FieldName}" : _id; } + get { return (EditForm != null && !string.IsNullOrEmpty(EditForm.Id) && FieldIdentifier != null) ? $"{EditForm.Id}_{FieldIdentifier.Value.Model.GetHashCode()}_{FieldIdentifier.Value.FieldName}" : _id; } set { _id = value; } } @@ -342,7 +342,7 @@ namespace BootstrapBlazor.Components if (EditForm != null && FieldIdentifier.HasValue) { - EditForm.AddValidator((FieldIdentifier.Value.Model.GetType(), FieldIdentifier.Value.FieldName), this); + EditForm.AddValidator(FieldIdentifier.Value, this); } // 显式设置显示标签时一定显示 @@ -408,13 +408,6 @@ namespace BootstrapBlazor.Components // 如果禁用移除验证信息 if (!IsDisabled && !SkipValidate) { - // 模型验证设置 验证属性名称 - // 验证组件内部使用 - if (string.IsNullOrEmpty(context.MemberName) && FieldIdentifier.HasValue) - { - context.MemberName = FieldIdentifier.Value.FieldName; - } - // 增加数值类型验证如 泛型 TValue 为 int 输入为 Empty 时 ValidateType(context, results); diff --git a/src/BootstrapBlazor/Components/ValidateForm/BootstrapBlazorDataAnnotationsValidator.cs b/src/BootstrapBlazor/Components/ValidateForm/BootstrapBlazorDataAnnotationsValidator.cs index 66f78c0c70ef168578fa8ce8d977fa680d91b50a..b9e0451bf3d32717df43030fa511447e304781d0 100644 --- a/src/BootstrapBlazor/Components/ValidateForm/BootstrapBlazorDataAnnotationsValidator.cs +++ b/src/BootstrapBlazor/Components/ValidateForm/BootstrapBlazorDataAnnotationsValidator.cs @@ -4,11 +4,13 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Forms; +using System; +using System.Diagnostics.CodeAnalysis; namespace BootstrapBlazor.Components { /// - /// DataAnnotationsValidator 验证组件 + /// BootstrapBlazorDataAnnotationsValidator 验证组件 /// public class BootstrapBlazorDataAnnotationsValidator : ComponentBase { @@ -29,7 +31,19 @@ namespace BootstrapBlazor.Components /// protected override void OnInitialized() { - CurrentEditContext!.AddEditContextDataAnnotationsValidation(EditForm!); + if (EditForm == null) + { + throw new InvalidOperationException($"{nameof(BootstrapBlazorDataAnnotationsValidator)} requires a cascading " + + $"parameter of type {nameof(ValidateForm)}. For example, you can use {nameof(BootstrapBlazorDataAnnotationsValidator)} " + + $"inside an {nameof(ValidateForm)}."); + } + + if (CurrentEditContext == null) + { + throw new InvalidOperationException($"{nameof(BootstrapBlazorDataAnnotationsValidator)} requires a cascading parameter of type {nameof(EditContext)}. For example, you can use {nameof(BootstrapBlazorDataAnnotationsValidator)} inside an EditForm."); + } + + CurrentEditContext.AddEditContextDataAnnotationsValidation(EditForm); } } } diff --git a/src/BootstrapBlazor/Components/ValidateForm/BootstrapBlazorEditContextDataAnnotationsExtensions.cs b/src/BootstrapBlazor/Components/ValidateForm/BootstrapBlazorEditContextDataAnnotationsExtensions.cs index fd83d483bf2080634b9ee5a3144ae2f25a98bbf2..a953e67c13a1fffe4c96fb156370dc57d2d915ca 100644 --- a/src/BootstrapBlazor/Components/ValidateForm/BootstrapBlazorEditContextDataAnnotationsExtensions.cs +++ b/src/BootstrapBlazor/Components/ValidateForm/BootstrapBlazorEditContextDataAnnotationsExtensions.cs @@ -2,26 +2,19 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Website: https://www.blazor.zone or https://argozhang.github.io/ -using BootstrapBlazor.Localization.Json; using Microsoft.AspNetCore.Components.Forms; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using System.Reflection; namespace BootstrapBlazor.Components { /// - /// EditContextDataAnotation 扩展操作类 + /// BootstrapBlazorEditContextDataAnnotationsExtensions 扩展操作类 /// internal static class BootstrapBlazorEditContextDataAnnotationsExtensions { - private static readonly ConcurrentDictionary<(Type ModelType, string FieldName), Func> PropertyValueInvokerCache = new(); - /// /// 添加数据合规检查 /// @@ -51,12 +44,9 @@ namespace BootstrapBlazor.Components { var validationContext = new ValidationContext(editContext.Model); var validationResults = new List(); - - TryValidateObject(editContext.Model, validationContext, validationResults, editForm); - editForm.ValidateObject(editContext.Model, validationContext, validationResults); + editForm.ValidateObject(validationContext, validationResults); messages.Clear(); - foreach (var validationResult in validationResults.Where(v => !string.IsNullOrEmpty(v.ErrorMessage))) { if (!validationResult.MemberNames.Any()) @@ -84,99 +74,12 @@ namespace BootstrapBlazor.Components DisplayName = fieldIdentifier.GetDisplayName() }; - var propertyValue = fieldIdentifier.GetPropertyValue(); - TryValidateProperty(propertyValue, validationContext, results); - editForm.ValidateProperty(propertyValue, validationContext, results); + editForm.ValidateField(validationContext, results, fieldIdentifier); messages.Clear(fieldIdentifier); messages.Add(fieldIdentifier, results.Where(v => !string.IsNullOrEmpty(v.ErrorMessage)).Select(result => result.ErrorMessage!)); editContext.NotifyValidationStateChanged(); } - - /// - /// 获取 FieldIdentifier 属性值 - /// - /// - /// - internal static object GetPropertyValue(this in FieldIdentifier fieldIdentifier) - { - var cacheKey = (fieldIdentifier.Model.GetType(), fieldIdentifier.FieldName); - var model = fieldIdentifier.Model; - var invoker = PropertyValueInvokerCache.GetOrAdd(cacheKey, key => LambdaExtensions.GetPropertyValueLambda(model, key.FieldName).Compile()); - - return invoker.Invoke(model); - } - - private static void TryValidateObject(object model, ValidationContext context, ICollection results, ValidateForm validateForm) - { - var modelType = model.GetType(); - var validateProperties = validateForm.ValidateAllProperties - ? modelType.GetRuntimeProperties().Where(p => p.IsPublic() && !p.GetIndexParameters().Any()) - : validateForm.GetPropertiesByModelType(model.GetType()).Select(p => model.GetType().GetProperty(p)); - foreach (var p in validateProperties) - { - if (p != null) - { - var fieldIdentifier = new FieldIdentifier(model, fieldName: p.Name); - var propertyValue = fieldIdentifier.GetPropertyValue(); - TryValidateProperty(propertyValue, context, results, p); - } - } - } - - private static void TryValidateProperty(object value, ValidationContext context, ICollection results, PropertyInfo? propertyInfo = null) - { - var modelType = context.ObjectType; - if (propertyInfo == null) - { - propertyInfo = modelType.GetProperty(context.MemberName!); - } - - if (propertyInfo != null) - { - var rules = propertyInfo.GetCustomAttributes(true).Where(i => i.GetType().BaseType == typeof(ValidationAttribute)).Cast(); - var displayName = new FieldIdentifier(context.ObjectInstance, propertyInfo.Name).GetDisplayName(); - var memberName = propertyInfo.Name; - var attributeSpan = "Attribute".AsSpan(); - foreach (var rule in rules) - { - if (!rule.IsValid(value)) - { - // 查找 resx 资源文件中的 ErrorMessage - var ruleNameSpan = rule.GetType().Name.AsSpan(); - var index = ruleNameSpan.IndexOf(attributeSpan, StringComparison.OrdinalIgnoreCase); - var ruleName = rule.GetType().Name.AsSpan().Slice(0, index); - var isResx = false; - if (!string.IsNullOrEmpty(rule.ErrorMessage)) - { - var resxType = ServiceProviderHelper.ServiceProvider.GetRequiredService>().Value.ResourceManagerStringLocalizerType; - if (resxType != null && JsonStringLocalizerFactory.TryGetLocalizerString(resxType, rule.ErrorMessage, out var resx)) - { - rule.ErrorMessage = resx; - isResx = true; - } - } - if (!isResx) - { - if (JsonStringLocalizerFactory.TryGetLocalizerString(rule.GetType(), nameof(rule.ErrorMessage), out var msg)) - { - rule.ErrorMessage = msg; - } - - if (JsonStringLocalizerFactory.TryGetLocalizerString(context.ObjectType, $"{memberName}.{ruleName.ToString()}", out msg)) - { - rule.ErrorMessage = msg; - } - } - - var errorMessage = rule.FormatErrorMessage(displayName ?? memberName); - results.Add(new ValidationResult(errorMessage, new string[] { memberName })); - } - } - } - } - - private static bool IsPublic(this PropertyInfo p) => p.GetMethod != null && p.SetMethod != null && p.GetMethod.IsPublic && p.SetMethod.IsPublic; } } diff --git a/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs b/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs index c69532895afcafe45cf851a6dc664cc7e7ff73a2..2e3a2e8801908d794e698540b61d861260f41d20 100644 --- a/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs +++ b/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs @@ -2,14 +2,19 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Website: https://www.blazor.zone or https://argozhang.github.io/ +using BootstrapBlazor.Localization.Json; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Forms; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Linq.Expressions; +using System.Reflection; using System.Threading.Tasks; namespace BootstrapBlazor.Components @@ -64,83 +69,223 @@ namespace BootstrapBlazor.Components [Parameter] public RenderFragment? ChildContent { get; set; } + [Inject] + [NotNull] + private IServiceProvider? ServiceProvider { get; set; } + /// /// 验证组件缓存 /// - private ConcurrentDictionary<(Type ModelType, string FieldName), IValidateComponent> ValidatorCache { get; } = new(); + private ConcurrentDictionary ValidatorCache { get; } = new(); /// /// 添加数据验证组件到 EditForm 中 /// /// /// - internal void AddValidator((Type ModelType, string FieldName) key, IValidateComponent comp) => ValidatorCache.AddOrUpdate(key, k => comp, (k, c) => c = comp); + internal void AddValidator(in FieldIdentifier key, IValidateComponent comp) => ValidatorCache.AddOrUpdate(key, k => comp, (k, c) => c = comp); /// /// 设置指定字段错误信息 /// - /// 数据类型 - /// 字段名称 + /// /// 错误描述信息,可为空,为空时查找资源文件 - public void SetError(Type modelType, string filedName, string errorMessage) + public void SetError(Expression> expression, string errorMessage) { - if (ValidatorCache.TryGetValue((modelType, filedName), out var validator)) + if (expression.Body is MemberExpression exp) { - var results = new List + var fieldName = exp.Member.Name; + var modelType = exp.Expression?.Type; + if (modelType != null) { - new ValidationResult(errorMessage, new string[] { filedName }) - }; - validator.ToggleMessage(results, true); + var validator = ValidatorCache.FirstOrDefault(c => c.Key.Model.GetType() == modelType && c.Key.FieldName == fieldName).Value; + if (validator != null) + { + var results = new List + { + new ValidationResult(errorMessage, new string[] { fieldName }) + }; + validator.ToggleMessage(results, true); + } + } } } /// /// 通过指定的模型类型获取当前表单中的绑定属性 /// - /// /// - internal IEnumerable GetPropertiesByModelType(Type modelType) => ValidatorCache.Keys.Where(k => k.ModelType == modelType).Select(k => k.FieldName); + internal IEnumerable<(FieldIdentifier, PropertyInfo PropertyInfo)> GetBindModelProperties() => ValidatorCache.Keys.Select(key => (key, key.Model.GetType().GetProperty(key.FieldName)!)); + + private static bool IsPublic(PropertyInfo p) => p.GetMethod != null && p.SetMethod != null && p.GetMethod.IsPublic && p.SetMethod.IsPublic; /// /// EditModel 数据模型验证方法 /// - /// /// /// - internal void ValidateObject(object model, ValidationContext context, List results) + internal void ValidateObject(ValidationContext context, List results) { - // 遍历所有可验证组件进行数据验证 - foreach (var key in ValidatorCache) + if (ValidateAllProperties) + { + ValidateProperty(context, results); + } + else { - if (key.Key.ModelType == context.ObjectType) + // 遍历所有可验证组件进行数据验证 + foreach (var key in ValidatorCache.Keys) { - var fi = new FieldIdentifier(model, key.Key.FieldName); - // 设置其关联属性字段 - var propertyValue = fi.GetPropertyValue(); - var validator = ValidatorCache[key.Key]; + var propertyValue = LambdaExtensions.GetPropertyValue(key.Model, key.FieldName); - // 数据验证 - context.MemberName = fi.FieldName; - validator.ValidateProperty(propertyValue, context, results); - validator.ToggleMessage(results, false); + // 验证 DataAnnotations + var pi = key.Model.GetType().GetProperty(key.FieldName); + if (pi != null) + { + var validator = ValidatorCache[key]; + var messages = new List(); + var propertyValidateContext = new ValidationContext(key.Model) + { + MemberName = key.FieldName, + DisplayName = key.GetDisplayName() + }; + ValidateDataAnnotations(propertyValue, propertyValidateContext, messages, pi); + + if (messages.Count == 0) + { + // 自定义验证组件 + validator.ValidateProperty(propertyValue, propertyValidateContext, messages); + } + + // 客户端提示 + validator.ToggleMessage(messages, false); + results.AddRange(messages); + } } } } /// - /// 字段验证方法 + /// 通过表单内绑定的字段验证方法 /// - /// /// /// - internal void ValidateProperty(object? propertyValue, ValidationContext context, List results) + /// + internal void ValidateField(ValidationContext context, List results, in FieldIdentifier fieldIdentifier) { - if (!string.IsNullOrEmpty(context.MemberName) && ValidatorCache.TryGetValue((context.ObjectType, context.MemberName), out var validator)) + if (ValidatorCache.TryGetValue(fieldIdentifier, out var validator)) { - validator.ValidateProperty(propertyValue, context, results); + var propertyValue = LambdaExtensions.GetPropertyValue(fieldIdentifier.Model, fieldIdentifier.FieldName); + var pi = fieldIdentifier.Model.GetType().GetProperty(fieldIdentifier.FieldName)!; + + // 验证 DataAnnotations + ValidateDataAnnotations(propertyValue, context, results, pi); + + if (results.Count == 0) + { + // 自定义验证组件 + validator.ValidateProperty(propertyValue, context, results); + } + + // 客户端提示 validator.ToggleMessage(results, true); } } + + /// + /// 通过属性设置的 DataAnnotation 进行数据验证 + /// + /// + /// + /// + /// + private void ValidateDataAnnotations(object? value, ValidationContext context, ICollection results, PropertyInfo propertyInfo) + { + var rules = propertyInfo.GetCustomAttributes(true).Where(i => i.GetType().BaseType == typeof(ValidationAttribute)).Cast(); + var displayName = context.DisplayName; + var memberName = propertyInfo.Name; + var attributeSpan = "Attribute".AsSpan(); + foreach (var rule in rules) + { + if (!rule.IsValid(value)) + { + // 查找 resx 资源文件中的 ErrorMessage + var ruleNameSpan = rule.GetType().Name.AsSpan(); + var index = ruleNameSpan.IndexOf(attributeSpan, StringComparison.OrdinalIgnoreCase); + var ruleName = rule.GetType().Name.AsSpan().Slice(0, index); + var isResx = false; + if (!string.IsNullOrEmpty(rule.ErrorMessage)) + { + var resxType = ServiceProvider.GetRequiredService>().Value.ResourceManagerStringLocalizerType; + if (resxType != null && JsonStringLocalizerFactory.TryGetLocalizerString(resxType, rule.ErrorMessage, out var resx)) + { + rule.ErrorMessage = resx; + isResx = true; + } + } + if (!isResx) + { + if (JsonStringLocalizerFactory.TryGetLocalizerString(rule.GetType(), nameof(rule.ErrorMessage), out var msg)) + { + rule.ErrorMessage = msg; + } + + if (JsonStringLocalizerFactory.TryGetLocalizerString(context.ObjectType, $"{memberName}.{ruleName.ToString()}", out msg)) + { + rule.ErrorMessage = msg; + } + } + + var errorMessage = rule.FormatErrorMessage(displayName ?? memberName); + results.Add(new ValidationResult(errorMessage, new string[] { memberName })); + } + } + } + + /// + /// 验证整个模型时验证属性方法 + /// + /// + /// + private void ValidateProperty(ValidationContext context, List results) + { + var properties = context.ObjectType.GetRuntimeProperties().Where(p => IsPublic(p) && !p.GetIndexParameters().Any()); + foreach (var pi in properties) + { + // 设置其关联属性字段 + var propertyValue = LambdaExtensions.GetPropertyValue(context.ObjectInstance, pi.Name); + + // 检查当前值是否为 Class + if (propertyValue != null && propertyValue is not string && propertyValue.GetType().IsClass) + { + var fieldContext = new ValidationContext(propertyValue) + { + MemberName = pi.Name + }; + ValidateProperty(fieldContext, results); + } + else + { + // 验证 DataAnnotations + var fieldIdentifier = new FieldIdentifier(context.ObjectInstance, pi.Name); + var messages = new List(); + context.DisplayName = fieldIdentifier.GetDisplayName(); + ValidateDataAnnotations(propertyValue, context, messages, pi); + + if (ValidatorCache.TryGetValue(fieldIdentifier, out var validator)) + { + if (messages.Count == 0) + { + // 自定义验证组件 + validator.ValidateProperty(propertyValue, context, messages); + } + + // 客户端提示 + validator.ToggleMessage(messages, true); + } + results.AddRange(messages); + } + } + } } } diff --git a/src/BootstrapBlazor/Extensions/LambdaExtensions.cs b/src/BootstrapBlazor/Extensions/LambdaExtensions.cs index d15f7143a7cf16b39258310a0a28a4080da387e7..9fbad52223742e7dc4bd418cea0759102e006a5f 100644 --- a/src/BootstrapBlazor/Extensions/LambdaExtensions.cs +++ b/src/BootstrapBlazor/Extensions/LambdaExtensions.cs @@ -3,6 +3,7 @@ // Website: https://www.blazor.zone or https://argozhang.github.io/ using BootstrapBlazor.Components; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; @@ -365,6 +366,21 @@ namespace System.Linq return Expression.Lambda>(body, param_p1, param_p2); } + private static readonly ConcurrentDictionary<(Type ModelType, string FieldName), Func> PropertyValueInvokerCache = new(); + + /// + /// 获取 指定对象的属性值 + /// + /// + /// + /// + public static object GetPropertyValue(object model, string fieldName) + { + var cacheKey = (model.GetType(), fieldName); + var invoker = PropertyValueInvokerCache.GetOrAdd(cacheKey, key => GetPropertyValueLambda(model, key.FieldName).Compile()); + return invoker.Invoke(model); + } + #region TryParse /// ///