如何将NotifyDataErrorInfo传递到自定义TextBox用户控件?

问题描述 投票:0回答:1

目标

我正在练习如何将

TextBox
与绑定到
Type
属性的不同
Text
一起使用。

有很多线程讨论这个问题。但是,我缺少一些步骤,希望得到一些支持。

  • 输入值必须在特定范围内
  • 仅允许正整数
  • 必须处理
  • space
    backspace
    delete
    击键,以防止
    System.Windows.Data Error

设定

  • 使用 .Net 8 的 Wpf 应用程序
  • MVVM模式;依赖注入
  • 社区工具包.Mvvm
  • GitHub 上的公共存储库

View包含三个TextBox

控件,它们绑定到
ViewModel的相同属性。 CommunityToolkit.Mvvm
 用于减少样板代码。

[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveDataButtonCommand))] [NotifyCanExecuteChangedFor(nameof(DiscardButtonCommand))] [NotifyDataErrorInfo] [Range(9, 999, ErrorMessage = "Value is out of range")] private int _intValue = 0; private int _backupIntValue = 0;

  • Integer TextBox 是具有标准行为的 TextBox
    
    
  • CustomTextBox 是一个 TextBox
    ,其中事件处理程序添加在 
    HomeView.xaml.cs
  • 自定义 IntegerTextBox 是一个名为 IntegerTextBox.xaml
     的自定义用户控件,其中添加了事件处理程序并注册了名为 
    DependencyProperty
    Value
<!-- Integer components --> <Label Grid.Row="2" Grid.Column="1" Content="Integer TextBox" /> <TextBox Grid.Row="3" Grid.Column="1" MinWidth="200" Text="{Binding IntValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}" /> <Label Grid.Row="2" Grid.Column="3" Content="Custom TextBox" /> <TextBox x:Name="CustomTextBox" Grid.Row="3" Grid.Column="3" HorizontalContentAlignment="Right" MinWidth="200" Text="{Binding IntValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}" /> <Label Grid.Row="2" Grid.Column="5" Content="Custom IntegerTextBox" /> <customControls:IntegerTextBox Grid.Row="3" Grid.Column="5" MinWidth="200" Value="{Binding IntValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}" />

实现目标的步骤

ResourceDictionary

 中,所有 
TextBox
 控件的行为都会被修改,以便在发现验证错误时删除“红框”,并显示 
ViewModel 中提供的验证错误。

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Style TargetType="TextBox"> <!-- Sets basic look of the TextBox --> <Setter Property="Padding" Value="2 1" /> <Setter Property="BorderBrush" Value="LightGray" /> <Setter Property="BorderThickness" Value="1" /> <!-- Removes the red border around the TextBox, if validation found errors --> <Setter Property="Validation.ErrorTemplate"> <Setter.Value> <ControlTemplate> <AdornedElementPlaceholder /> </ControlTemplate> </Setter.Value> </Setter> <!-- Enables the UI to show the error messages --> <Setter Property="Template"> <Setter.Value> <ControlTemplate> <StackPanel> <Border Padding="{TemplateBinding Padding}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="3"> <ScrollViewer x:Name="PART_ContentHost" /> </Border> <ItemsControl ItemsSource="{TemplateBinding Validation.Errors}" Margin="0 5 0 5"> <ItemsControl.ItemTemplate> <DataTemplate> <TextBlock Foreground="Red" FontStyle="Italic" Text="{Binding ErrorContent}" /> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </StackPanel> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
此外,还添加了一些事件来在将数据输入到 

TextBox

 控件时验证输入。那里的线程显示了我最喜欢的两个解决方案。一种是使用 
Regex
,另一种是使用 
int.TryParse()
。为了捕获像 
space
backspace
delete
 这样的输入,事件处理程序正在处理这些输入。

[GeneratedRegex("[^0-9]+")] private static partial Regex IntegerPositiveValuesOnlyRegex(); // one option private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e) { e.Handled = IntegerPositiveValuesOnlyRegex().IsMatch(e.Text); } // another option private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e) { e.Handled = !int.TryParse((sender as TextBox)!.Text, NumberStyles.Integer, CultureInfo.InvariantCulture, out int validInteger); }
存在的问题

验证错误的可视化

    标准
  1. TextBox
    正在按预期工作(没有红色框;控件下方有红色错误消息)
  2. Custom TextBox
     正在按预期工作(没有红色框;控件下方有红色错误消息)
  3. Custom IntegerTextBox
    显示红色框且不显示错误消息
捕捉输入

    标准
  1. TextBox
    抛出很多异常并接受所有键和值(但是,这是预期的结果)
  2. Custom TextBox
     只允许整数;没有逗号,没有小数点,没有
    -
    ;但按 
    space
    backspace
    delete
     会引发异常
  3. Custom IntegerTextBox
    按预期工作(完全没有例外;仅接受正整数值)
这就是我需要你帮助的地方!

我遗漏了一些东西,为什么

TextBox

控件的行为不同。至少
Custom IntegerTextBox
我想按预期工作。

完整代码

HomeView.xaml

<UserControl x:Class="SampleTextBoxValidation.Views.Screens.HomeView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:SampleTextBoxValidation.Views.Screens" xmlns:viewModels="clr-namespace:SampleTextBoxValidation.ViewModels" xmlns:customControls="clr-namespace:SampleTextBoxValidation.Views.CustomControls" mc:Ignorable="d" d:DataContext="{d:DesignInstance Type=viewModels:HomeViewModel}" d:DesignHeight="600" d:DesignWidth="800"> <Border CornerRadius="10" BorderThickness="1" BorderBrush="Black" Background="MintCream" Padding="5"> <DockPanel> <!-- *** The UI's title *** --> <Label DockPanel.Dock="Top" HorizontalAlignment="Center" Content="Home Screen" Margin="0 0 0 20" FontSize="14" FontWeight="Bold"> </Label> <!-- *** The UI's footer *** --> <Label DockPanel.Dock="Bottom" HorizontalAlignment="Center" Content="Testing CommunityToolkit.Mvvm => ObservableValidator on several user controls." Margin="0 20 0 0" FontSize="10" FontWeight="Bold"> </Label> <!-- Button components --> <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Center"> <Button Margin="15 20 15 5" Command="{Binding DiscardButtonCommand}" Content="Discard" /> <Button Margin="15 20 15 5" Command="{Binding LoadDataButtonCommand}" Content="Load Data" /> <Button Margin="15 20 15 5" Command="{Binding SaveDataButtonCommand}" Content="Save Data" /> </StackPanel> <!-- *** The UI's body *** --> <Grid DockPanel.Dock="Top" HorizontalAlignment="Center"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <!-- A column to seperate the user components on the left and right --> <Rectangle Grid.Column="2" Grid.Row="0" Grid.RowSpan="10" MinWidth=" 20" /> <Rectangle Grid.Column="4" Grid.Row="0" Grid.RowSpan="10" MinWidth=" 20" /> <!-- FirstName components --> <Label Grid.Row="0" Grid.Column="1" Content="First Name" /> <TextBox Grid.Row="1" Grid.Column="1" MinWidth="200" Text="{Binding FirstName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}" /> <!-- LastName components --> <Label Grid.Row="0" Grid.Column="3" Content="Last Name" /> <TextBox Grid.Row="1" Grid.Column="3" MinWidth="200" Text="{Binding LastName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}" /> <!-- Integer components --> <Label Grid.Row="2" Grid.Column="1" Content="Integer TextBox" /> <TextBox Grid.Row="3" Grid.Column="1" MinWidth="200" Text="{Binding IntValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}" /> <Label Grid.Row="2" Grid.Column="3" Content="Custom TextBox" /> <TextBox x:Name="CustomTextBox" Grid.Row="3" Grid.Column="3" HorizontalContentAlignment="Right" MinWidth="200" Text="{Binding IntValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}" /> <Label Grid.Row="2" Grid.Column="5" Content="Custom IntegerTextBox" /> <customControls:IntegerTextBox Grid.Row="3" Grid.Column="5" MinWidth="200" Value="{Binding IntValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}" /> <!-- Decimal components --> <Label Grid.Row="4" Grid.Column="1" Content="Decimal StringFormat {0:C}" /> <TextBox Grid.Row="5" Grid.Column="1" MinWidth="200" Text="{Binding DecimalValue, StringFormat={}{0:C}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}" /> <Label Grid.Row="4" Grid.Column="3" Content="Decimal As String" /> <TextBox Grid.Row="5" Grid.Column="3" MinWidth="200" Text="{Binding DecimalValueAsString, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}" /> <!-- Double components --> <Label Grid.Row="6" Grid.Column="1" Content="Double StringFormat {0:F2}" /> <TextBox Grid.Row="7" Grid.Column="1" MinWidth="200" Text="{Binding DoubleValue, StringFormat={}{0:F2}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}" /> <Label Grid.Row="6" Grid.Column="3" Content="Double As String" /> <TextBox Grid.Row="7" Grid.Column="3" MinWidth="200" Text="{Binding DoubleValueAsString, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}" /> </Grid> </DockPanel> </Border> </UserControl>
HomeView.xaml.cs

using System.Diagnostics; using System.Globalization; using System.Text.RegularExpressions; using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace SampleTextBoxValidation.Views.Screens; public partial class HomeView : UserControl { [GeneratedRegex("[^0-9]+")] private static partial Regex IntegerPositiveValuesOnlyRegex(); public HomeView() { InitializeComponent(); CustomTextBox.GotFocus += TextBox_GotFocus; CustomTextBox.PreviewTextInput += TextBox_PreviewTextInput; CustomTextBox.PreviewKeyUp += TextBox_PreviewKeyDown; CustomTextBox.TextChanged += TextBox_TextChanged; } private void TextBox_GotFocus(object sender, RoutedEventArgs e) { Debug.WriteLine($"Entered {nameof(TextBox_GotFocus)}"); (sender as TextBox)!.SelectAll(); } private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e) { Debug.WriteLine($"Entered {nameof(TextBox_PreviewTextInput)}"); e.Handled = IntegerPositiveValuesOnlyRegex().IsMatch(e.Text); } private void TextBox_PreviewKeyDown(object sender, KeyEventArgs e) { Debug.WriteLine($"Entered {nameof(TextBox_PreviewKeyDown)}"); if (e.Key == Key.Space) { e.Handled = true; } else if (e.Key == Key.Back) { if ((sender as TextBox)!.Text.Length == 1) { (sender as TextBox)!.SelectAll(); e.Handled = true; } } else if (e.Key == Key.Delete) { if ((sender as TextBox)!.Text.Length == 1) { (sender as TextBox)!.SelectAll(); e.Handled = true; } } } private void TextBox_TextChanged(object sender, TextChangedEventArgs e) { Debug.WriteLine($"Entered {nameof(TextBox_TextChanged)}"); bool valid = int.TryParse((sender as TextBox)!.Text, NumberStyles.Integer, CultureInfo.InvariantCulture, out int validInteger); if (valid) { CustomTextBox.Text = validInteger.ToString(); } else { CustomTextBox.Text = 999.ToString(); // For testing, only, to show there was a problem. } } }
IntegerTextBox.xaml

<UserControl x:Class="SampleTextBoxValidation.Views.CustomControls.IntegerTextBox" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:SampleTextBoxValidation.Views.CustomControls" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> <!-- Refer to the code behind file to see this user control's logic! --> <!-- Binding Value => this is a custom dependency property that is registered to this user control --> <!-- RelativeSource => this is needed to enable the databinding in both directions --> <TextBox x:Name="CustomIntegerTextBox" HorizontalContentAlignment="Right" Text="{Binding Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}}" /> </UserControl>
IntegerTextBox.xaml.cs

using System.Diagnostics; using System.Globalization; using System.Text.RegularExpressions; using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace SampleTextBoxValidation.Views.CustomControls { public partial class IntegerTextBox : UserControl { [GeneratedRegex("[^0-9]+")] private static partial Regex IntegerPositiveValuesOnlyRegex(); public static readonly DependencyProperty DependencyPropertyOfValue = DependencyProperty.Register(nameof(Value), typeof(int), typeof(IntegerTextBox), new PropertyMetadata(0)); public int Value { get { return (int)GetValue(DependencyPropertyOfValue); } set { SetValue(DependencyPropertyOfValue, value); } } public IntegerTextBox() { InitializeComponent(); CustomIntegerTextBox.TextChanged += TextBox_TextChanged; CustomIntegerTextBox.GotFocus += TextBox_GotFocus; CustomIntegerTextBox.PreviewTextInput += TextBox_PreviewTextInput; CustomIntegerTextBox.PreviewKeyDown += TextBox_PreviewKeyDown; } private void TextBox_GotFocus(object sender, RoutedEventArgs e) { Debug.WriteLine($"Entered {nameof(TextBox_GotFocus)}"); (sender as TextBox)!.SelectAll(); } private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e) { Debug.WriteLine($"Entered {nameof(TextBox_PreviewTextInput)}"); e.Handled = IntegerPositiveValuesOnlyRegex().IsMatch(e.Text); } private void TextBox_PreviewKeyDown(object sender, KeyEventArgs e) { Debug.WriteLine($"Entered {nameof(TextBox_PreviewKeyDown)}"); if (e.Key == Key.Space) { e.Handled = true; } else if (e.Key == Key.Back) { if ((sender as TextBox)!.Text.Length == 1) { (sender as TextBox)!.SelectAll(); e.Handled = true; } } else if (e.Key == Key.Delete) { if ((sender as TextBox)!.Text.Length == 1) { (sender as TextBox)!.SelectAll(); e.Handled = true; } } } private void TextBox_TextChanged(object sender, TextChangedEventArgs e) { Debug.WriteLine($"Entered {nameof(TextBox_TextChanged)}"); bool valid = int.TryParse((sender as TextBox)!.Text, NumberStyles.Integer, CultureInfo.InvariantCulture, out int validInteger); if (valid) { Value = validInteger; } else { Value = 999; // for testing, only, to show an error was not handled } } } }

>><<< Edit after implementing the changes provided by >

更新代码后,仍然有一个问题。

    如果在
  • Custom TextBox 中输入值,则 Custom IntegerTextBox 正在正确验证输入;它显示错误消息,但没有红色框;但Custom IntegerTextBox中没有显示任何内容
  • 如果在
  • Custom IntegerTextBox 中输入值,则验证正常工作并且其他两个控件显示内容
不知何故,属性

Value

需要更新和显示。这是如何实现的?

添加

PropertyChanged

 并没有改变行为。

public int Value { get { return (int)GetValue(DependencyPropertyOfValue); } set { SetValue(DependencyPropertyOfValue, value); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value))); } }
代码现在看起来像这样。请注意,类名称暂时从 

IntegerTextBox

 更改为 
CustomTextBox

<customControls:CustomTextBox Grid.Row="3" Grid.Column="5" MinWidth="200" HorizontalContentAlignment="Right" Value="{Binding IntValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}" /> />
using System.Diagnostics;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace SampleTextBoxValidation.Views.CustomControls;

public partial class CustomTextBox : TextBox
{
    [GeneratedRegex("[^0-9]+")]
    private static partial Regex IntegerPositiveValuesOnlyRegex();

    public static readonly DependencyProperty DependencyPropertyOfValue =
        DependencyProperty.Register(nameof(Value), typeof(int), typeof(CustomTextBox), new PropertyMetadata(0));

    public int Value
    {
        get { return (int)GetValue(DependencyPropertyOfValue); }
        set { SetValue(DependencyPropertyOfValue, value); }
    }

    public CustomTextBox()
    {
        TextChanged += TextBox_TextChanged;
        GotFocus += TextBox_GotFocus;
        PreviewTextInput += TextBox_PreviewTextInput;
        PreviewKeyDown += TextBox_PreviewKeyDown;
    }

    private void TextBox_GotFocus(object sender, RoutedEventArgs e)
    {
        Debug.WriteLine($"Entered {nameof(TextBox_GotFocus)}");

        (sender as TextBox)!.SelectAll();
    }

    private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
    {
        Debug.WriteLine($"Entered {nameof(TextBox_PreviewTextInput)}");

        e.Handled = IntegerPositiveValuesOnlyRegex().IsMatch(e.Text);
    }

    private void TextBox_PreviewKeyDown(object sender, KeyEventArgs e)
    {
        Debug.WriteLine($"Entered {nameof(TextBox_PreviewKeyDown)}");

        if (e.Key == Key.Space)
        {
            e.Handled = true;
        }
        else if (e.Key == Key.Back)
        {
            if ((sender as TextBox)!.Text.Length == 1)
            {
                (sender as TextBox)!.SelectAll();
                e.Handled = true;
            }
        }
        else if (e.Key == Key.Delete)
        {
            if ((sender as TextBox)!.Text.Length == 1)
            {
                (sender as TextBox)!.SelectAll();
                e.Handled = true;
            }
        }
    }

    private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
    {
        Debug.WriteLine($"Entered {nameof(TextBox_TextChanged)}");

        bool valid = int.TryParse((sender as TextBox)!.Text,
            NumberStyles.Integer,
            CultureInfo.InvariantCulture,
            out int validInteger);

        if (valid)
        {
            Value = validInteger;
        }
        else
        {
            Value = 999; // for testing, only, to show an error was not handled
        }
    }
}

c# wpf xaml textbox
1个回答
1
投票
自定义 IntegerTextBox 显示红色框且不显示错误消息

不同之处在于,

IntegerTextBox

是一个简单包装普通
UserControl
TextBox
。这不是定制的
TextBox
。您的自定义 
Validation.ErrorTemplate
 将不适用于该控件。

您应该将

IntegerTextBox

 类修改为扩展 
TextBox
 的自定义控件。删除 XAML 文件并创建一个独立的类:

public partial class IntegerTextBox : TextBox { [GeneratedRegex("[^0-9]+")] private static partial Regex IntegerPositiveValuesOnlyRegex(); public static readonly DependencyProperty DependencyPropertyOfValue = DependencyProperty.Register(nameof(Value), typeof(int), typeof(IntegerTextBox), new PropertyMetadata(0)); public int Value { get { return (int)GetValue(DependencyPropertyOfValue); } set { SetValue(DependencyPropertyOfValue, value); } } public IntegerTextBox() { TextChanged += TextBox_TextChanged; GotFocus += TextBox_GotFocus; PreviewTextInput += TextBox_PreviewTextInput; PreviewKeyDown += TextBox_PreviewKeyDown; } private void TextBox_GotFocus(object sender, RoutedEventArgs e) { Debug.WriteLine($"Entered {nameof(TextBox_GotFocus)}"); (sender as TextBox)!.SelectAll(); } private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e) { Debug.WriteLine($"Entered {nameof(TextBox_PreviewTextInput)}"); e.Handled = IntegerPositiveValuesOnlyRegex().IsMatch(e.Text); } private void TextBox_PreviewKeyDown(object sender, KeyEventArgs e) { Debug.WriteLine($"Entered {nameof(TextBox_PreviewKeyDown)}"); if (e.Key == Key.Space) { e.Handled = true; } else if (e.Key == Key.Back) { if ((sender as TextBox)!.Text.Length == 1) { (sender as TextBox)!.SelectAll(); e.Handled = true; } } else if (e.Key == Key.Delete) { if ((sender as TextBox)!.Text.Length == 1) { (sender as TextBox)!.SelectAll(); e.Handled = true; } } } private void TextBox_TextChanged(object sender, TextChangedEventArgs e) { Debug.WriteLine($"Entered {nameof(TextBox_TextChanged)}"); bool valid = int.TryParse((sender as TextBox)!.Text, NumberStyles.Integer, CultureInfo.InvariantCulture, out int validInteger); if (valid) { Value = validInteger; } else { Value = 999; // for testing, only, to show an error was not handled } } }
然后,根据 

Style

 中的 
TextBox
 样式为自定义控件定义 
TextBoxStyle.xaml

<Style TargetType="customControls:IntegerTextBox" BasedOn="{StaticResource {x:Type TextBox}}" />
    
© www.soinside.com 2019 - 2024. All rights reserved.