333 lines
13 KiB
C#
333 lines
13 KiB
C#
|
|
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) // 同步等待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<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
|
|||
|
|
}
|
|||
|
|
}
|