Click here to Skip to main content
11,415,390 members (90,201 online)
Click here to Skip to main content

Validating User Input - WPF MVVM

, 6 Aug 2010 CPOL
Rate this:
Please Sign up or sign in to vote.
Validating user input in a WPF MVVM Application using the IDataErrorInfo Interface

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 NotImplementedExceptions 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()
{
//Insert code to save new Product to database etc 
} 

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)
{
// Create the ViewModel and expose it using the View's DataContext
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>

<!-- PRODUCT NAME-->
<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}"/>

<!-- HEIGHT-->
<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}"/>

<!-- WIDTH-->
<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}"/>

<!-- SAVE BUTTON -->
<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:

<!-- SAVE BUTTON -->
<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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Wayne Gaylard
Software Developer DynaByte Solutions
Zimbabwe Zimbabwe
No Biography provided

Comments and Discussions

 
QuestionThanks Pin
RogerDavidSmith10-Apr-15 7:10
memberRogerDavidSmith10-Apr-15 7:10 
Questionthere's an extra } Pin
Lukasz Szyrmer26-Nov-13 5:30
memberLukasz Szyrmer26-Nov-13 5:30 
AnswerRe: there's an extra } Pin
Wayne Gaylard26-Nov-13 6:59
mentorWayne Gaylard26-Nov-13 6:59 
GeneralMy vote of 5 Pin
exsquare28-Aug-13 3:28
professionalexsquare28-Aug-13 3:28 
GeneralRe: My vote of 5 Pin
Wayne Gaylard28-Aug-13 3:33
mentorWayne Gaylard28-Aug-13 3:33 
QuestionUsing SQL Server Database with the example Pin
priom2230-Jul-13 7:58
memberpriom2230-Jul-13 7:58 
GeneralMy vote of 2 Pin
MahBulgaria30-Jan-13 5:05
memberMahBulgaria30-Jan-13 5:05 
GeneralMy vote of 5 Pin
tripathy.rajendra@gmail.com31-Aug-12 0:08
membertripathy.rajendra@gmail.com31-Aug-12 0:08 
GeneralRe: My vote of 5 Pin
Wayne Gaylard31-Aug-12 6:28
mentorWayne Gaylard31-Aug-12 6:28 
GeneralMy vote of 5 Pin
RenatoK6-Aug-12 14:09
memberRenatoK6-Aug-12 14:09 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.150427.4 | Last Updated 6 Aug 2010
Article Copyright 2010 by Wayne Gaylard
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid