Thursday, June 21, 2012

WPF TextBox Validation with IDataErrorInfo

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.

WPF TextBox Validation with IDataErrorInfo


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 TextBoxThis 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 trueOnce it is true, "Confirm" button becomes enabled and Confirm_Executed method can be processed.
Download source code of this example (Visual Studio 2010 project)

10 comments:

  1. Excellent!!! Thanks for sharing!!!

    Just 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".

    ReplyDelete
    Replies
    1. In order to fire validation only on lost focus just change UpdateSourceTrigger=PropertyChanged to UpdateSourceTrigger=LostFocus

      Delete
  2. This 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.

    ReplyDelete
  3. Very nice code :) !!

    ReplyDelete
  4. NotImplementedException is illegal in a property getter.

    ReplyDelete
  5. Hi, is there a way to prevent the IDataErrorInfo firing without an Error on the Load?

    Thanks,

    O

    ReplyDelete
  6. Hi, thanks for this exapmle. I've one question:
    how set disable for IsEnabled button property when textbox has error?

    ReplyDelete