There so many ways for data validation in WPF that sometimes it confuses people. I decided to figure out what's the best way for me to do TextBox validation, which will be enough versatile for different tasks, but also as simple as possible for implementation. I believe a good code must be simple and easy to understand. So today I'll show, for my opinion, the best validation method on TextBox example.
Ok, let's start from XAML, this is the Grid:
<Window.Resources> <Style TargetType="{x:Type Label}"> <Setter Property="Margin" Value="5,0,5,0" /> <Setter Property="HorizontalAlignment" Value="Left" /> </Style> <Style TargetType="{x:Type TextBox}"> <Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="Margin" Value="0,2,40,2" /> <Setter Property="Validation.ErrorTemplate"> <Setter.Value> <ControlTemplate> <DockPanel LastChildFill="true"> <Border Background="OrangeRed" DockPanel.Dock="right" Margin="5,0,0,0" Width="20" Height="20" CornerRadius="5" ToolTip="{Binding ElementName=customAdorner, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}"> <TextBlock Text="!" VerticalAlignment="center" HorizontalAlignment="center" FontWeight="Bold" Foreground="white" /> </Border> <AdornedElementPlaceholder Name="customAdorner" VerticalAlignment="Center" > <Border BorderBrush="red" BorderThickness="1" /> </AdornedElementPlaceholder> </DockPanel> </ControlTemplate> </Setter.Value> </Setter> </Style> </Window.Resources> <Grid x:Name="grid_EmployeeData" Margin="0,20"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="auto" /> <RowDefinition Height="auto" /> <RowDefinition Height="auto" /> <RowDefinition Height="auto" /> </Grid.RowDefinitions> <Grid.CommandBindings> <CommandBinding Command="New" CanExecute="Confirm_CanExecute" Executed="Confirm_Executed" /> </Grid.CommandBindings> <Label Target="{Binding ElementName=textBox_Name}" Content="Name:" Grid.Column="0" Grid.Row="0" /> <Label Target="{Binding ElementName=textBox_Position}" Content="Position:" Grid.Column="0" Grid.Row="1" /> <Label Target="{Binding ElementName=textBox_Salary}" Content="Salary:" Grid.Column="0" Grid.Row="2" /> <TextBox x:Name="textBox_Name" Grid.Row="0" Grid.Column="1" Validation.Error="Validation_Error" Text="{Binding UpdateSourceTrigger=PropertyChanged, Path=Name, ValidatesOnDataErrors=true, NotifyOnValidationError=true}" /> <TextBox x:Name="textBox_Position" Grid.Row="1" Grid.Column="1" Validation.Error="Validation_Error" Text="{Binding UpdateSourceTrigger=PropertyChanged, Path=Position, ValidatesOnDataErrors=true, NotifyOnValidationError=true}" /> <TextBox x:Name="textBox_Salary" Grid.Row="2" Grid.Column="1" Width="50" HorizontalAlignment="left" Validation.Error="Validation_Error" MaxLength="5" Text="{Binding UpdateSourceTrigger=PropertyChanged, Path=Salary, ValidatesOnDataErrors=true, NotifyOnValidationError=true}" /> <Button Content="Confirm" Grid.ColumnSpan="2" Grid.Row="3" Grid.Column="1" Margin="0,0,10,0" HorizontalAlignment="right" VerticalAlignment="Center" Command="New"/> </Grid>
Note that we have a style for TextBox. In that style we defining how our validation will look like. But the main key of the whole thing is CommandBinding of the Grid. Let's see the code behind first, then things become clearer.
public partial class MainWindow : Window { private int _errors = 0; private Employee _employee = new Employee(); public MainWindow() { InitializeComponent(); grid_EmployeeData.DataContext = _employee; } private void Confirm_CanExecute(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = _errors == 0; e.Handled = true; } private void Confirm_Executed(object sender, ExecutedRoutedEventArgs e) { _employee = new Employee(); grid_EmployeeData.DataContext = _employee; e.Handled = true; } private void Validation_Error(object sender, ValidationErrorEventArgs e) { if (e.Action == ValidationErrorEventAction.Added) _errors++; else _errors--; } }And our Employee class looks like this:
public class Employee : IDataErrorInfo { public string Name { get; set; } public string Position { get; set; } public int Salary { get; set; } public string Error { get { throw new NotImplementedException(); } } public string this[string columnName] { get { string result = null; if (columnName == "Name") { if (string.IsNullOrEmpty(Name) || Name.Length < 3) result = "Please enter a Name"; } if (columnName == "Position") { if (string.IsNullOrEmpty(Position) || Name.Length < 3) result = "Please enter a Position"; } if (columnName == "Salary") { if (Salary <= 1000 || Salary >= 50000) result = "Please enter a valid salary amount"; } return result; } } }
All the validation logic is handled in indexer method of Employee class. Validation process occurs every time as the text is changing inside the TextBox. This is because we set the UpdateSourceTrigger property to PropertyChanged.
The Validation_Error method calculates number of errors for the whole Grid, thus Confirm_CanExecute method knows when to set CanExecute parameter to true. Once it is true, "Confirm" button becomes enabled and Confirm_Executed method can be processed.
Download source code of this example (Visual Studio 2010 project)
Genial
ReplyDeleteawesome
ReplyDeleteExcellent!!! Thanks for sharing!!!
ReplyDeleteJust one question: is it possible to fire the validation only on "focus out" of the text field or when clicking the confirm button? I'd rather not show validation errors when opening the "form".
In order to fire validation only on lost focus just change UpdateSourceTrigger=PropertyChanged to UpdateSourceTrigger=LostFocus
DeleteThis is a fabulous tutorial, being new to ASP.NET this will prove fruitful to me. Will be trying this soon to know how exactly it works.
ReplyDeleteVery nice code :) !!
ReplyDeleteNotImplementedException is illegal in a property getter.
ReplyDeleteSure, just replace with your code
DeleteHi, is there a way to prevent the IDataErrorInfo firing without an Error on the Load?
ReplyDeleteThanks,
O
Hi, thanks for this exapmle. I've one question:
ReplyDeletehow set disable for IsEnabled button property when textbox has error?