Thursday, June 21, 2012

WPF TextBox Validation with IDataErrorInfo

There are 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)

17 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
  7. Very clean and understandable, but indeed please answer the question how to prevent the form from being invalid at startup time. Some of us would like to wait for that until user clicks Confirm.
    I thought of adding "if (this.IsLoaded)" in Validation_Error but that did not help.

    ReplyDelete
  8. Thank you for the tutorial.

    I have an issue though. I wanted to get the selected value on a combobox, but instead it returns me the selected text. How can I get the selected value?

    Thanks in advance.

    ReplyDelete
  9. can we do same thing from class file(code behind) ??

    ReplyDelete
  10. This comment has been removed by the author.

    ReplyDelete
    Replies
    1. For me it works on Integers, but not on Strings...
      Any idea why??

      Delete
    2. Can you believe, it took me HOURS and then I saw it... I forgot ": IDataErrorInfo".
      Works like a charm!

      Delete
  11. Thanks for the demo. Very helpful.

    ReplyDelete