using FluentValidation; using HandyControl.Controls; using System.Reflection; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Data; using System.Windows.Media; using static Dm.util.ByteArrayQueue; using ComboBox = System.Windows.Controls.ComboBox; using DatePicker = System.Windows.Controls.DatePicker; using PasswordBox = System.Windows.Controls.PasswordBox; using TextBox = System.Windows.Controls.TextBox; namespace YY.Admin.Core.FluentValidation { public static class FluentValidationHelper { #region ✅ SkipValidation 附加属性 public static readonly DependencyProperty SkipValidationProperty = DependencyProperty.RegisterAttached( "SkipValidation", typeof(bool), typeof(FluentValidationHelper), new System.Windows.PropertyMetadata(false)); public static void SetSkipValidation(DependencyObject element, bool value) => element.SetValue(SkipValidationProperty, value); public static bool GetSkipValidation(DependencyObject element) => (bool)element.GetValue(SkipValidationProperty); #endregion /// /// 入口方法:从 ViewModel 自动获取 Validator,并绑定到所有子控件 /// public static void Attach(FrameworkElement root, Type validatorOwnerType) { if (root == null || validatorOwnerType == null) return; // 找出 validator 属性 var validatorProp = validatorOwnerType .GetProperties(BindingFlags.Instance | BindingFlags.Public) .FirstOrDefault(p => typeof(IValidator).IsAssignableFrom(p.PropertyType) && p.Name.EndsWith("Validator", StringComparison.OrdinalIgnoreCase)); if (validatorProp == null) return; var validatorInstance = root.DataContext != null ? validatorProp.GetValue(root.DataContext) : null; if (validatorInstance == null) return; var validatedType = validatorProp.PropertyType.GetInterfaces() .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IValidator<>)) ?.GetGenericArguments()[0]; if (validatedType == null) return; AttachRecursive(root, validatorInstance, validatedType); } /// /// 递归绑定所有子控件 /// private static void AttachRecursive(DependencyObject parent, object validatorInstance, Type validatedType) { if (parent == null) return; // 先尝试走逻辑树 —— 能穿透 ScrollViewer、Row、Col、ContentPresenter 等容器 foreach (var childObj in LogicalTreeHelper.GetChildren(parent).OfType()) { var child = childObj; if (child is FrameworkElement feLogical) { // ⚠️ 先设置 ErrorTemplate,保证 skip 的控件也能显示样式 var template = feLogical.TryFindResource("BottomLeftErrorTemplate_ForInfoElement") as ControlTemplate; if (template != null) { Validation.SetErrorTemplate(feLogical, template); } // 🧭 再判断是否跳过校验 if (GetSkipValidation(feLogical)) { AttachRecursive(child, validatorInstance, validatedType); continue; } // 🪄 普通控件 / PasswordBox AttachValidationForElement(feLogical, validatorInstance, validatedType); } // 递归逻辑树的子元素 AttachRecursive(child, validatorInstance, validatedType); } // 再补充视觉树 —— 仅在 parent 是 Visual 或 Visual3D 时调用,避免 ColumnDefinition 等非 Visual 抛错 if (parent is Visual || parent is System.Windows.Media.Media3D.Visual3D) { int childrenCount = VisualTreeHelper.GetChildrenCount(parent); for (int i = 0; i < childrenCount; i++) { var child = VisualTreeHelper.GetChild(parent, i); if (child is FrameworkElement fe) { // ⚠️ 先设置 ErrorTemplate,保证 skip 的控件也能显示样式 var template = fe.TryFindResource("BottomLeftErrorTemplate_ForInfoElement") as ControlTemplate; if (template != null) { Validation.SetErrorTemplate(fe, template); } // 🧭 再判断是否跳过校验 if (GetSkipValidation(fe)) { AttachRecursive(child, validatorInstance, validatedType); continue; } // 🪄 普通控件 / PasswordBox AttachValidationForElement(fe, validatorInstance, validatedType); } // 递归子元素 AttachRecursive(child, validatorInstance, validatedType); } } } #region 普通控件处理 private static void AttachValidationForElement(FrameworkElement fe, object validatorInstance, Type validatedType) { var localValues = fe.GetLocalValueEnumerator(); while (localValues.MoveNext()) { var entry = localValues.Current; var binding = BindingOperations.GetBinding(fe, entry.Property); if (binding == null) continue; AddValidationRule(binding, validatorInstance, validatedType); // 重新应用绑定才能生效 BindingOperations.ClearBinding(fe, entry.Property); BindingOperations.SetBinding(fe, entry.Property, binding); } } private static void AddValidationRule(Binding binding, object validatorInstance, Type validatedType) { var propertyPath = binding.Path?.Path; if (string.IsNullOrEmpty(propertyPath)) return; var propertyName = propertyPath.Split('.').Last(); var ruleType = typeof(FluentAutoValidationRule<>).MakeGenericType(validatedType); var rule = (ValidationRule)Activator.CreateInstance(ruleType)!; ruleType.GetProperty("Validator")?.SetValue(rule, validatorInstance); ruleType.GetProperty("PropertyName")?.SetValue(rule, propertyName); binding.ValidationRules.Add(rule); } #endregion #region 校验触发与跳过 public static bool ValidateAll(FrameworkElement root, T model, IValidator validator) where T : class { // 1️⃣ 获取所有 skip 的属性名 var skipPropertyNames = GetSkipValidationPropertyNames(root); // 2️⃣ 获取模型所有属性名 var allPropertyNames = typeof(T) .GetProperties(BindingFlags.Instance | BindingFlags.Public) .Select(p => p.Name) .ToList(); // 3️⃣ 排除 skip,剩下的就是要验证的属性 var includePropertyNames = allPropertyNames .Except(skipPropertyNames, StringComparer.Ordinal) .ToArray(); // 4️⃣ 验证时只验证这些属性 var result = validator.ValidateAsync(model, options => { options.IncludeProperties(includePropertyNames); }).ConfigureAwait(false) // 同步等待Task(ValidationRule 不支持 async → 只能同步等待。) .GetAwaiter() .GetResult(); // 5️⃣ 清除之前的错误 ClearAllValidationErrors(root); // 6️⃣ 只对未 skip 的属性标记错误 foreach (var error in result.Errors) { var controls = FindBoundControls(root, error.PropertyName); foreach (var control in controls) { var depProperty = GetBoundDependencyProperty(control); if (depProperty == null) continue; var bindingExpr = BindingOperations.GetBindingExpression(control, depProperty); if (bindingExpr != null) { Validation.MarkInvalid( bindingExpr, new ValidationError( new ExceptionValidationRule(), bindingExpr, error.ErrorMessage, null)); } } } return result.IsValid; } private static HashSet GetSkipValidationPropertyNames(DependencyObject root) { var skipPropertyNames = new HashSet(StringComparer.Ordinal); foreach (var control in FindVisualChildren(root)) { if (!GetSkipValidation(control)) continue; var dp = GetBoundDependencyProperty(control); if (dp == null) continue; var binding = BindingOperations.GetBinding(control, dp); if (binding?.Path?.Path is string propertyPath && !string.IsNullOrEmpty(propertyPath)) { var propertyName = propertyPath.Split('.').Last(); skipPropertyNames.Add(propertyName); } } return skipPropertyNames; } public static void ClearAllValidationErrors(DependencyObject parent) { foreach (var control in FindVisualChildren(parent)) { if (GetSkipValidation(control)) continue; var depProperty = GetBoundDependencyProperty(control); if (depProperty == null) continue; var expr = BindingOperations.GetBindingExpression(control, depProperty); if (expr != null) Validation.ClearInvalid(expr); } } #endregion #region 工具方法 private static DependencyProperty? GetBoundDependencyProperty(FrameworkElement control) { return control switch { // 原生控件 TextBox => TextBox.TextProperty, // ✅ WPF 原生 PasswordBox(注意:WPF 原生 PasswordBox 没有现成的依赖属性绑定 Password,需要额外处理) PasswordBox => PasswordBoxHelper.PasswordProperty, ComboBox cb => BindingOperations.GetBinding(cb, Selector.SelectedItemProperty) != null ? Selector.SelectedItemProperty : BindingOperations.GetBinding(cb, Selector.SelectedValueProperty) != null ? Selector.SelectedValueProperty : null, DatePicker => DatePicker.SelectedDateProperty, CheckBox => ToggleButton.IsCheckedProperty, // HandyControl控件 HandyControl.Controls.PasswordBox => HandyControl.Controls.PasswordBox.UnsafePasswordProperty, TimePicker => TimePicker.SelectedTimeProperty, NumericUpDown => NumericUpDown.ValueProperty, CheckComboBox => CheckComboBox.SelectedItemsProperty, _ => null }; } private static IEnumerable FindBoundControls(DependencyObject parent, string propertyName) { return FindVisualChildren(parent) .Where(fe => { if (GetSkipValidation(fe)) return false; var dp = GetBoundDependencyProperty(fe); if (dp == null) return false; var expr = BindingOperations.GetBindingExpression(fe, dp); return expr != null && expr.ParentBinding.Path.Path.EndsWith(propertyName); }); } private static IEnumerable FindVisualChildren(DependencyObject depObj) where T : DependencyObject { if (depObj == null) yield break; for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++) { var child = VisualTreeHelper.GetChild(depObj, i); if (child is T tChild) yield return tChild; foreach (var subChild in FindVisualChildren(child)) yield return subChild; } } #endregion } }