Introduction
When writing data-centric apps, validating user input becomes an important design consideration. One of the most important considerations is when to validate the input. Do you validate the input only after the user has tried to save the data, or do you validate as the user is busy entering the data. Personally, I believe that it is better to validate as the user is busy entering the data, before the user tries to submit it. Luckily for us, the .NET Framework comes with an Interface for just such situations, the IDataErrorInfo
Interface. By implementing this interface in your models, combined with WPF data binding, user input validation becomes automatic, and implementing validation becomes merely a choice of how to present the validation errors to the user. When working with the MVVM design pattern, the interface makes it easy to keep code out of your code behind and in your ViewModel and Model.
This article is a tutorial on implementing this interface in an MVVM WPF application, ensuring no code is written in the View. The demo app will consist simply of a form where a user can enter a new Product.
Requirements
To follow the tutorial only requires Visual Studio (I have used Visual C# 2010 Express, but you can use any version). I have also used an MVVM Application Template which can be downloaded from here. I will also assume you have a basic understanding of the MVVM pattern, as well as of WPF Data Binding, as the tutorial will concentrate on the IDataErrorInfo
Interface implementation.
So let's begin...
The Model
After installing the MVVM application template, open up Visual Studio, start a new project using the template and that will give you a working skeleton of an MVVM app. I start off most projects creating the models, as I tend to develop the models and Data storage structures side by side. As this example won't actually do any saving or retrieving of data, we can just create a model. Our application spec requires only one form where a user can enter and save a Product's Product Name, Height and Width. This means we only need one model, a Product with three Properties, ProductName
, Width
and Height
. Right click on the models folder, and add a new class called Product
. Make the class public
so we can use it. Add three public
properties to your Product
class ProductName(string)
, Width
and Height
, both int
. Add the using statement System.ComponentModel
at the top of the file. This is the namespace where the IDataErrorInfo
Interface resides. Make sure your class implements the IDataErrorInfo
interface by adding the name of the interface after the class declaration like this:
class Contact : IDataErrorInfo
Once you have done that, intellisense should show you a little blue line underneath the declaration. If you hover the cursor over the blue line, you will be given the choice of whether you want to implement the Interface or explicitly implement the interface. To implement the IDataErrorInfo
Interface, a class needs to expose two readonly
properties, an error property and an item property which is an indexer that takes a string parameter representing the name of one of the implementing classes properties. Whether you implicitly or explicitly implement the interface will depend on whether you have any other indexers in your class and need to differentiate them. For us, we only need to implicitly implement the interface so go ahead and choose this option. This will generate the property stubs for the required properties with generic throw new NotImplementedException
s and should look like this:
public string Error
{
get
{
throw new NotImplementedException();}
}
}
public string this[string columnName]
{
get
{
throw new NotImplementedException();
}
}
The error property returns a string
that indicates what is wrong with the whole object, while the Item
property (implemented as an indexer so will show as public string this[columnName]
) returns a string
indicating the error associated with the particular property passed as the parameter. If the property is valid according to the validation rules you specify, then the Item
property returns an empty string. For the most part, one can leave the Error
property as NotImplemented
, while implementing individual validations for each property of your class within the indexer. Basically, the way it works, it checks which property is being validated, using the parameter passed as input, and then validates that property according to rules you specify. For our validations, let's assume that each product's name must be longer than 5 letters, that the Height should not be greater than the Width and obviously each property should not be null.
Let's implement each validation in its own method which can be called from the Item
property as required. Let each validation method return a string
. If the validation fails, the method should return an appropriate error message, else it should return an empty string. Each validation method is very similar, they each check first whether the property has a value, if so they check whether the value conforms to the right rule, else it returns an error message. If the property is valid, it passes all tests and an empty string is returned. In the Item indexer itself, we declare a string validationResult
to hold our error message, and then use a switch
statement to call the right validation method depending on the property being validated, assigning the result to our validationResult string
which is then returned to the calling function. That completes our Contact model and also all that is required to implement the IDataErrorInfo
interface and our code should now look like this:
public class Product:IDataErrorInfo
{
#region state properties
public string ProductName{ get; set; }
public int Width { get; set; }
public int Height { get; set; }
#endregion
public void Save()
{
}
public string Error
{
get { throw new NotImplementedException(); }
}
public string this[string propertyName]
{
get
{
string validationResult = null;
switch (propertyName)
{
case "ProductName":
validationResult = ValidateName();
break;
case "Height":
validationResult = ValidateHeight();
break;
case "Width":
validationResult = ValidateWidth();
break;
default:
throw new ApplicationException("Unknown Property being validated on Product.");
}
return validationResult;
}
}
private string ValidateName()
{
if (String.IsNullOrEmpty (this.ProductName))
return "Product Name needs to be entered.";
else if(this.ProductName.Length < 5)
return "Product Name should have more than 5 letters.";
else
return String.Empty;
}
private string ValidateHeight()
{
if (this.Height <= 0)
return "Height should be greater than 0";
if (this.Height > this.Width)
return "Height should be less than Width.";
else
return String.Empty;
}
private string ValidateWidth()
{
if (this.Width <= 0)
return "Width should be greater than 0";
if (this.Width < this.Height)
return "Width should be greater than Height.";
else
return String.Empty;
}
}
I added a Save
method which I left blank. This is where in a real app, you would save the product to your database or XML file, etc.
The ViewModel
Now that we have our model all worked out, we need to decide how to represent that model to the user. In our case, the best plan would be to create a current instance of our Product
class with empty properties, which the user can fill in via a TextBox
and 2 Sliders and then save if all properties are valid. So, delete the default MainView.xaml and MainViewModel.cs files from your project, and add a new class NewProductViewModel.cs in your ViewModels folder, and a new Window NewProductView.xaml in your Views folder. Expand the App.xaml node in your solution explorer and open App.xaml.cs. This is where the application's OnStartup
method is located and we need to change the method to look like this:
private void OnStartup(object sender, StartupEventArgs e)
{
Views.NewProductView newProductView = new Views.NewProductView();
NewProducts.Models.Product newProduct = new Models.Product();
newProductView.DataContext = new ViewModels.NewProductViewModel(newProduct);
newProductView.Show();
}
Open up your NewProductViewModel file and add System.Windows
, System.Windows.Input
, System.ComponentModel
, Contacts.Commands
and Contacts.Models
using directives. Make sure that NewProductViewModel
inherits from ViewModelBase
. Add a private
method Exit
where we will close the application like so:
private void Exit()
{
Application.Current.Shutdown();
}
and an ICommand ExitCommand
which we will use to bind to our Exit MenuItem
like so:
private DelegateCommand exitCommand;
public ICommand ExitCommand
{
get
{
if (exitCommand == null)
{
exitCommand = new DelegateCommand(Exit);
}
return exitCommand;
}
}
Let your NewProductViewModel
class implement the IDataErrorInfo
and INotifyPropertyChanged
interfaces by adding them after ViewModelBase
in your class declaration. Let intellisense generate the property stubs for the IDataErrorInfo
interface for you automatically.
Add a readonly
instance of your Product
class as a private
field in your NewProductViewModel
class called currentProduct
, and add a constructor that accepts a Product
as a parameter and sets currentProduct
to point to that instance like this:
private readonly Product currentProduct;
public NewProductViewModel(Product newProduct)
{
this.currentProduct = newProduct;
}
We then need to set up some public
properties for our view to bind to, that represent the properties for currentProduct
. These properties need to implement INotifyPropertyChanged
, and must set and get the relevant properties on our currentProduct
and need to look like so, remembering that NewProductViewModel
inherits ViewModelBase
which implements an OnPropertyChanged
handler:
public string ProductName
{
get { return currentProduct.ProductName; }
set
{
if (currentProduct.ProductName != value)
{
currentProduct.ProductName = value;
base.OnPropertyChanged("ProductName");
}
}
}
For the Width
and Height
properties, we have a dependency in that the Height
must not be more than the Width
. This means that if one property changes, then we need to check and see if the other property is still valid. The easiest way to accomplish this is to act as if both properties have changed when either property changes. We can do this by calling base.OnPropertyChanged
for both properties when either one changes. So our Property
declarations should now look like this:
public int Width
{
get { return currentProduct.Width; }
set
{
if (this.currentProduct.Width != value)
{
this.currentProduct.Width = value;
base.OnPropertyChanged("Width");
base.OnPropertyChanged("Height");
}
}
}
public int Height
{
get { return currentProduct.Height; }
set
{
if (this.currentProduct.Height != value)
{
this.currentProduct.Height = value;
base.OnPropertyChanged("Height");
base.OnPropertyChanged("Width");
}
}
}
All that is left now is to set our IDataErrorInfo
members to return the relevant error and item properties from our base Product
class. For the error property, we just convert currentProduct
to an IDataErrorInfo
and call the error property. For the Item
property the procedure is the same, just adding a call to ensure that CommandManager
updates all validations like so:
public string Error
{
get
{
return (currentProduct as IDataErrorInfo).Error;
}
}
public string this[string columnName]
{
get
{
string error = (currentProduct as IDataErrorInfo)[columnName];
CommandManager.InvalidateRequerySuggested();
return error;
}
}
The only thing left to do is to implement an ICommand
(SaveCommand
) to save the currentProduct
to file and a save method that the SaveCommand
can call. The save
method simply calls currentProduct.Save()
, and the SaveCommand
creates a DelegateCommand
that passes the Save
method as a parameter like so:
private DelegateCommand saveCommand;
public ICommand ExitCommand
{
get
{
if (exitCommand == null)
{
exitCommand = new DelegateCommand(Exit);
}
return exitCommand;
}
}
private void Save()
{
currentContact.Save();
}
This will give us a Command
to bind to our Save button on our NewProductView
. This concludes the basics of our NewProductViewModel
, completing everything required to ensure that the user is kept informed about the validity of each property. All that's left to implement now is the actual view.
The View
Our View for this exercise will be very simple. It will consist of a menu at the top with the typical File - Exit menu items which the user can use to close the app, a labelled TextBox
which the user will use to enter the Product Name for the new Product, 2 labelled sliders, each with its value presented with a label, which the user will use to input the Height
and Width
, and a button which the user can use to save the Product
.
Open up the XAML file for NewProductView
and add a namespace reference to your Commands
namespace, this will allow us to add a CommandReference
to our View which we can use to create an InputBinding
so that the user can close the application using Ctrl-X.
xmlns:c="clr-namespace:NewProducts.Commands"
then create a reference to our ExitCommand
in our ContactViewModel
like so:
<Window.Resources>
<c:CommandReference x:Key="ExitCommandReference" Command="{Binding ExitCommand}" />
</Window.Resources>
then create the InputBinding
like this:
<Window.InputBindings>
<KeyBinding Key="X" Modifiers="Control"
Command="{StaticResource ExitCommandReference}" />
</Window.InputBindings>
Next, delete the default grid, and replace it with a DockPanel
. Inside the dockpanel
add a Menu and set its DockPanel.Dock
property to top. In this Menu, add a MenuItem
headered _File
, and within this MenuItem
create another MenuItem
headered E_xit
. Set the Command
property of this Exit MenuItem
to Bind to our ExitCommand
command and set its InputGestureText
to Ctrl-X
like this:
<DockPanel>
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<MenuItem Command="{Binding ExitCommand}"
Header="E_xit" InputGestureText="Ctrl-X" />
</MenuItem>
</Menu>
</DockPanel>
In the DockPanel
, below the Menu, add a Grid
and define 3 Columns and 4 Rows on the grid. Set the first Column's width to Auto
and the second to 5*
, and the third to *
, set all the Row Heights to Auto. In the first row of the grid, place a Label
in the first column and a TextBox
in the second. In the next 2 Rows, place a label in the first Column, a slider in the second Column and another label in the third Column. Name the sliders using x
: notation
, sliHeight
and sliWidth
respectively. Set the Content of the labels to Product Name :
, Width :
and Height :
respectively, and name the TextBoxes
appropriately, i.e. txtProductName
, txtHeight
and txtWidth
. For the Text
property of txtProductName
, create a Binding and set the Path of the Binding to the ProductName
Property on our NewProductViewModel
. For the sliders, create the Binding on the Value
Property.
Within each Binding, set ValidatesOnDataErrors
to True
and the UpdateSourceTrigger
property to PropertyChanged
. For the labels in the third Column, which will display the sliders value, we set up a Binding for the Content
property setting the Binding's ElementName
to the appropriate slider and the Path
to the slider's Value
property.
In the fourth Row of the Grid place a Button, set its Content to _Save
and create a Binding for its Command
property whose Path points to our SaveCommand
on the ContactViewModel
. The XAML for the Window content should now look something like this:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="5*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
-->
<Label Grid.Row="0" Grid.Column="0" Content="Product Name:"
HorizontalAlignment="Right" Margin="3"/>
<TextBox x:Name="txtProductName" Grid.Row="0" Grid.Column="1" Margin="3"
Text="{Binding Path=ProductName, ValidatesOnDataErrors=True,
UpdateSourceTrigger=PropertyChanged}"/>
-->
<Label Grid.Row="1" Grid.Column="0" Content="Height:"
HorizontalAlignment="Right" Margin="3"/>
<Slider Grid.Row="1" Grid.Column="1" Margin="3" x:Name="sliHeight" Maximum="100"
Value="{Binding Path=Height, ValidatesOnDataErrors=True,
UpdateSourceTrigger=PropertyChanged}"/>
<Label Grid.Row="1" Grid.Column="2"
Content="{Binding ElementName=sliHeight, Path=Value}"/>
-->
<Label Grid.Row="2" Grid.Column="0" Content="Width:"
HorizontalAlignment="Right" Margin="3"/>
<Slider Grid.Row="2" Grid.Column="1" Margin="3" x:Name="sliWidth" Maximum="100"
Value="{Binding Path=Width, ValidatesOnDataErrors=True,
UpdateSourceTrigger=PropertyChanged}"/>
<Label Grid.Row="2" Grid.Column="2"
Content="{Binding ElementName=sliWidth, Path=Value}"/>
-->
<Button Grid.Row="3" Grid.Column="1" Command="{Binding Path=SaveCommand}" Content="_Save"
HorizontalAlignment="Right" Margin="4,2" MinWidth="60"/>
</Grid>
And there we have it! If you run the application now, you will see that each data entry control has a red border around it, and that once you enter a valid entry in each Control, the red border disappears. Unfortunately, the application still has some serious shortcomings. It is still possible to Save the Product even though the Data is not valid. Also the default red border thing is really not as informative as a serious application should try to be, so we really should try and implement something a little more user friendly to let the user know more precisely what is going on.
Cleaning Up
Saving Invalid Data
With regard to the user being able to Save invalid data, the best solution would be to disable the Save button until all properties are valid. This would mean defining a way in which the Button's IsEnabled
property is set to false
if any of the properties are invalid. Seeing how the IsEnabled
property is a boolean, it would be simplest if we had a boolean property in our NewProductViewModel
to which we could bind to the Button's IsEnabled
property.
So open up the NewProductViewModel.cs file, and define a bool property AllPropertiesValid
which returns a private
field allPropertiesValid
like this:
private bool allPropertiesValid = false;
public bool AllPropertiesValid
{
get { return allPropertiesValid; }
set
{
if (allPropertiesValid != value)
{
allPropertiesValid = value;
base.OnPropertyChanged("AllPropertiesValid");
}
The next question arises as to where to set this property. The IDataErrorInfo
Item indexer validates each property, but has no information with regard to any other properties' validation status, so we need some way to store the validation status of each property and then only change each specific status in the Item
indexer. Since the Item
indexer has an input parameter declaring the name of the property being checked, I thought the best option would be to use a Dictionary
, where the Key
could be the name of the property and the Value
a boolean representing the status of that property. So in our indexer, we can set the status of the current property being checked, and then search the Dictionary
to see if all properties are valid and set AllPropertiesValid
as appropriate. Thus we need to declare a Dictionary<string,bool>
called validProperties
and initiate the Dictionary
with the three properties in the constructor like this:
private Dictionary<string,bool> validProperties;
public NewProductViewModel(Product newProduct)
{
this.currentProduct = newProduct;
this.validProperties = new Dictionary<string, bool>();
this.validProperties.Add("ProductName", false);
this.validProperties.Add("Height", false);
this.validProperties.Add("Width", false);
}
We then need to implement a private
method that will check whether all the properties are valid and set AllPropertiesValid
appropriately:
private void ValidateProperties()
{
foreach(bool isValid in validProperties.Values )
{
if (isValid == false)
{
this.AllPropertiesValid = false;
return;
}
}
this.AllPropertiesValid = true;
}
Then we need to set the correct status in the Item
indexer and call this method to set our boolean, so we need to change the Item indexer to this:
string IDataErrorInfo.this[string propertyName]
{
get
{
string error = (currentContact as IDataErrorInfo)[propertyName];
validProperties[propertyName] = String.IsNullOrEmpty(error) ? true : false;
ValidateProperties();
CommandManager.InvalidateRequerySuggested();
return error;
}
}
Once that is complete, we just need to set up the Binding in the NewProductView
. Open up the NewProductView.xaml file and change the Button declaration to this:
<!---->
<Button Grid.Row="3" Grid.Column="1" Command="{Binding Path=SaveCommand}" Content="_Save"
HorizontalAlignment="Right" Margin="4,2" MinWidth="60"
IsEnabled="{Binding Path=AllPropertiesValid}"/>
And we are good to go. Press F5 and you should see that the button is only enabled once all properties are valid. As soon as a property becomes invalid, the button becomes disabled.
Informing the User
There are various ways to inform the user what is going on, and the exact implementation would obviously be subject to company standards, etc. One of the most common ways would be to show a ToolTip when the user hovers over the TextBox
showing them an error message. Another way would be to have labels underneath the TextBox
showing the error message. Knowing that the TextBox
has a Validation.HasError
property and that the property returns a list of validation errors each of whose ErrorContent
property returns the specific error message, implementing a Trigger that binds to the HasError
property and that shows a ToolTip when the TextBox
has an error is relatively trivial.
In the Grid
, add a Grid.Resources
tag (You could define this under Window.Resources
or even under Application.Resources
to ensure Application wide adherence). Under grid resources, define a Style
whose TargetType
is TextBox
. Under Style.Triggers
, add a Trigger
whose Property
is set to Validation.HasError
and fires when the property returns true
. In this Trigger
add a Setter
where the Property
is set to ToolTip
, and create a Binding
for the Value
property, that binds to itself (TextBox
) and where the Path
returns the ErrorContent
of the first error in the list like this:
<Grid.Resources>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</Grid.Resources>
We then need to create another Style
for the Sliders, which I have implemented in exactly the same manner. And there you have it. A user friendly form where a user can add a new Product
, and can only save the information if she has entered valid data in all fields. If any field is incorrect, she is informed as to the reason in a fairly unobstrusive way, and can easily correct any errors.
Conclusion
The IDataErrorInfo
interface is really a useful interface to use in data input validation, making things really simple to implement. I hope you had as much fun reading this tutorial as I had writing it. .NET rocks!!!
History
- 03rd August, 2010: Initial post