fix: validate processor parameter inputs

This commit is contained in:
zhengxuan.zhang
2026-08-11 13:03:12 +08:00
parent 35faa70e9c
commit ef21965a53
2 changed files with 201 additions and 0 deletions
@@ -3,6 +3,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:XP.Common.Localization.Extensions;assembly=XP.Common"
xmlns:validation="clr-namespace:XplorePlane.Views.ImageProcessing"
xmlns:controls="clr-namespace:XP.ImageProcessing.CfgControl;assembly=XP.ImageProcessing.CfgControl"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -12,6 +13,16 @@
mc:Ignorable="d">
<UserControl.Resources>
<Style TargetType="TextBox">
<Style.Triggers>
<Trigger Property="validation:ParameterInputValidationBehavior.HasError" Value="True">
<Setter Property="BorderBrush" Value="#FFD32F2F" />
<Setter Property="BorderThickness" Value="2" />
<Setter Property="Background" Value="#FFFFE5E5" />
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(validation:ParameterInputValidationBehavior.ErrorMessage)}" />
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="PanelBorderStyle" TargetType="Border">
<Setter Property="Background" Value="{StaticResource NovaSurfaceContainerBrush}" />
@@ -156,6 +167,7 @@
<GroupBox Header="{loc:Localization Key=ImageProcessing_Parameters}">
<controls:ProcessorParameterControl
x:Name="parameterControl"
validation:ParameterInputValidationBehavior.IsEnabled="True"
MinHeight="200"
ParameterChanged="OnParameterChanged" />
</GroupBox>
@@ -0,0 +1,189 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace XplorePlane.Views.ImageProcessing;
/// <summary>
/// 为外部 ProcessorParameterControl 动态生成的 TextBox 提供统一的类型和范围校验。
/// </summary>
public static class ParameterInputValidationBehavior
{
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached(
"IsEnabled",
typeof(bool),
typeof(ParameterInputValidationBehavior),
new PropertyMetadata(false, OnIsEnabledChanged));
public static readonly DependencyProperty HasErrorProperty =
DependencyProperty.RegisterAttached(
"HasError",
typeof(bool),
typeof(ParameterInputValidationBehavior),
new PropertyMetadata(false));
public static readonly DependencyProperty ErrorMessageProperty =
DependencyProperty.RegisterAttached(
"ErrorMessage",
typeof(string),
typeof(ParameterInputValidationBehavior),
new PropertyMetadata(string.Empty));
public static void SetIsEnabled(DependencyObject element, bool value) => element.SetValue(IsEnabledProperty, value);
public static bool GetIsEnabled(DependencyObject element) => (bool)element.GetValue(IsEnabledProperty);
public static void SetHasError(DependencyObject element, bool value) => element.SetValue(HasErrorProperty, value);
public static bool GetHasError(DependencyObject element) => (bool)element.GetValue(HasErrorProperty);
public static void SetErrorMessage(DependencyObject element, string value) => element.SetValue(ErrorMessageProperty, value);
public static string GetErrorMessage(DependencyObject element) => (string)element.GetValue(ErrorMessageProperty);
private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not FrameworkElement root)
return;
if ((bool)e.NewValue)
{
root.AddHandler(FrameworkElement.LoadedEvent, new RoutedEventHandler(OnDescendantLoaded), true);
root.AddHandler(FrameworkElement.UnloadedEvent, new RoutedEventHandler(OnDescendantUnloaded), true);
}
else
{
root.RemoveHandler(FrameworkElement.LoadedEvent, new RoutedEventHandler(OnDescendantLoaded));
root.RemoveHandler(FrameworkElement.UnloadedEvent, new RoutedEventHandler(OnDescendantUnloaded));
}
}
private static void OnDescendantLoaded(object sender, RoutedEventArgs e)
{
if (e.OriginalSource is TextBox textBox && !GetIsHooked(textBox))
{
SetIsHooked(textBox, true);
textBox.TextChanged += OnTextChanged;
Validate(textBox);
}
}
private static void OnDescendantUnloaded(object sender, RoutedEventArgs e)
{
if (e.OriginalSource is TextBox textBox && GetIsHooked(textBox))
{
textBox.TextChanged -= OnTextChanged;
SetIsHooked(textBox, false);
}
}
private static void OnTextChanged(object sender, TextChangedEventArgs e)
{
if (sender is TextBox textBox)
Validate(textBox);
}
private static void Validate(TextBox textBox)
{
var bindingSource = textBox.GetBindingExpression(TextBox.TextProperty)?.ResolvedSource;
var metadata = FindMetadata(bindingSource ?? textBox.DataContext);
if (metadata.Type == null || metadata.Type == typeof(string))
{
SetError(textBox, false, string.Empty);
return;
}
var text = textBox.Text?.Trim() ?? string.Empty;
if (text.Length == 0)
{
SetError(textBox, true, "Value is required");
return;
}
object? value;
try
{
value = ConvertValue(text, metadata.Type);
}
catch
{
SetError(textBox, true, $"Invalid {metadata.Type.Name} value");
return;
}
if (metadata.Min != null && Compare(value, metadata.Min) < 0)
{
SetError(textBox, true, $"Value must be ≥ {metadata.Min}");
return;
}
if (metadata.Max != null && Compare(value, metadata.Max) > 0)
{
SetError(textBox, true, $"Value must be ≤ {metadata.Max}");
return;
}
SetError(textBox, false, string.Empty);
}
private static object ConvertValue(string text, Type type)
{
var targetType = Nullable.GetUnderlyingType(type) ?? type;
if (targetType == typeof(bool))
{
if (bool.TryParse(text, out var boolean)) return boolean;
if (text == "0") return false;
if (text == "1") return true;
throw new FormatException();
}
if (targetType.IsEnum)
return Enum.Parse(targetType, text, true);
return TypeDescriptor.GetConverter(targetType).ConvertFrom(null, CultureInfo.InvariantCulture, text)
?? throw new FormatException();
}
private static int Compare(object value, object bound)
{
try
{
var converted = ConvertValue(Convert.ToString(bound, CultureInfo.InvariantCulture)!, value.GetType());
return ((IComparable)value).CompareTo(converted);
}
catch
{
return 0;
}
}
private static (Type? Type, object? Min, object? Max) FindMetadata(object? dataContext)
{
if (dataContext == null) return (null, null, null);
var source = GetProperty(dataContext, "ValueType") != null
? dataContext
: GetProperty(dataContext, "Parameter");
if (source == null) return (null, null, null);
return (
GetProperty(source, "ValueType") as Type,
GetProperty(source, "MinValue"),
GetProperty(source, "MaxValue"));
}
private static object? GetProperty(object source, string name)
=> source.GetType().GetProperty(name, BindingFlags.Instance | BindingFlags.Public)?.GetValue(source);
private static void SetError(TextBox textBox, bool hasError, string message)
{
SetHasError(textBox, hasError);
SetErrorMessage(textBox, message);
textBox.ToolTip = hasError ? message : null;
}
private static readonly DependencyProperty IsHookedProperty =
DependencyProperty.RegisterAttached("IsHooked", typeof(bool), typeof(ParameterInputValidationBehavior), new PropertyMetadata(false));
private static void SetIsHooked(DependencyObject element, bool value) => element.SetValue(IsHookedProperty, value);
private static bool GetIsHooked(DependencyObject element) => (bool)element.GetValue(IsHookedProperty);
}