fix: validate processor parameter inputs
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user