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

333 lines
13 KiB
C#
Raw Normal View History

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
}
}