我正在练习如何将
TextBox
与绑定到 Type
属性的不同 Text
一起使用。
有很多线程讨论这个问题。但是,我缺少一些步骤,希望得到一些支持。
space
、backspace
和 delete
击键,以防止 System.Windows.Data Error
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;
TextBox
TextBox
,其中事件处理程序添加在
HomeView.xaml.cs
中
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);
}
存在的问题
TextBox
正在按预期工作(没有红色框;控件下方有红色错误消息)
Custom TextBox
正在按预期工作(没有红色框;控件下方有红色错误消息)
Custom IntegerTextBox
显示红色框且不显示错误消息
TextBox
抛出很多异常并接受所有键和值(但是,这是预期的结果)
Custom TextBox
只允许整数;没有逗号,没有小数点,没有
-
;但按
space
、
backspace
或
delete
会引发异常
Custom IntegerTextBox
按预期工作(完全没有例外;仅接受正整数值)
我遗漏了一些东西,为什么
TextBox
控件的行为不同。至少
Custom IntegerTextBox
我想按预期工作。完整代码
<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
}
}
}
}
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
}
}
}
自定义 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}}" />