I am practicing how to use the TextBox
with different Type
bound to the Text
property.
There are a lot of threads out there covering this issue. However, I am missing some steps and will appreciate some support.
space
, backspace
and delete
keystrokes must be handled to prevent a System.Windows.Data Error
The View contains three TextBox
controls which are bound to the same property of the ViewModel. The CommunityToolkit.Mvvm
is used to reduce boilerplate.
[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
with standard behaviorTextBox
where event handlers are added in HomeView.xaml.cs
IntegerTextBox.xaml
where event handlers are added and a DependencyProperty
named Value
is registered. <!-- 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}" />
Within a ResourceDictionary
the behavior of all TextBox
controls is modified to remove the "red box" if a validation error was found and to show the validation error provided in the 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>
Additionally some events are added to validate the input while entering data into the TextBox
control. The threads out there showed two solutions I like most. One is using Regex
and the other is using int.TryParse()
. To catch input like space
, backspace
and delete
the event handlers are handling those.
[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);
}
Visualization of validation errors
TextBox
is working as intended (no red box; red error message beneath the control)Custom TextBox
is working as intended (no red box; red error message beneath the control)Custom IntegerTextBox
is showing the red box and not displaying the error messageCatching input
TextBox
is throwing a lot of exceptions and accepts all keys and values (however, that is the expected result)Custom TextBox
is allowing integers only; no comma, no decimal point, no -
; but pressing space
, backspace
or delete
throws exceptionsCustom IntegerTextBox
is working as intended (no exceptions at all; only accepting positive integer values)That is where I do need your help!
I am missing something, why the TextBox
controls are acting differently. At least the Custom IntegerTextBox
I would like to work as intended.
The 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>
The 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.
}
}
}
The 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>
The 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
}
}
}
}
So far the passing through of the NotifyDataErrorInfos is working, now.
There is still a issue with data binding, but that is another question.
the Custom IntegerTextBox is showing the red box and not displaying the error message
The difference is that IntegerTextBox
is a UserControl
that simply wraps an ordinary TextBox
. It's not a custom TextBox
. Your custom Validation.ErrorTemplate
won't apply to the control.
You should modify your IntegerTextBox
class to be a custom control that extends TextBox
. Remove the XAML file and create a stand-alone class:
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
}
}
}
Then you define a Style
for the custom control that is based on your TextBox
style in TextBoxStyle.xaml
:
<Style TargetType="customControls:IntegerTextBox" BasedOn="{StaticResource {x:Type TextBox}}" />