Files
qhmes/yy-admin-master/YY.Admin.Core/FluentValidation/FluentValidationHelper.cs

333 lines
13 KiB
C#
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
/// <summary>
/// 入口方法:从 ViewModel 自动获取 Validator并绑定到所有子控件
/// </summary>
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);
}
/// <summary>
/// 递归绑定所有子控件
/// </summary>
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<DependencyObject>())
{
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<T>(FrameworkElement root, T model, IValidator<T> 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) // 同步等待TaskValidationRule 不支持 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<string> GetSkipValidationPropertyNames(DependencyObject root)
{
var skipPropertyNames = new HashSet<string>(StringComparer.Ordinal);
foreach (var control in FindVisualChildren<FrameworkElement>(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<FrameworkElement>(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<FrameworkElement> FindBoundControls(DependencyObject parent, string propertyName)
{
return FindVisualChildren<FrameworkElement>(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<T> FindVisualChildren<T>(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<T>(child))
yield return subChild;
}
}
#endregion
}
}