Click here to Skip to main content
15,883,901 members
Articles / Web Development / HTML
Article

Validation in WPF

Rate me:
Please Sign up or sign in to vote.
4.92/5 (59 votes)
9 Jan 2015CPOL22 min read 151.9K   6.6K   98   15
General exploration of WPF validation

Introduction

The project is about validation in WPF MVVM projects. The various options are explored and a simple WPF application is built that adds two numbers together. The Adder application is implemented as a user control containing two TextBox for input and a Calculate button.

Architecture Review

The diagram below shows a typical "Enterprise" WPF application.

Image 1

1. Enterprise Architecture including MVVM.

The architecture attempts to decrease the complexity of an applications by organising it into layers, following the doctrine of "separation of concerns".

  1. The database provides persistence services.
  2. A repository provides a simple interface into the database and hides details of the actual storage. Services provide interfaces to replaceable/shared modules.
  3. The controller responds to user input that directs the execution of code.
  4. The model contains "business objects" which contain logic (and possibly transient data) that models the operation of the business.
  5. The view-model contains the data that is displayed in the view. The data in the view-model needs to be in close correspondence with the view since the binding provides a 1-1 correspondence between elements in the view and properties in the view-model. It provides a "facia" over the model that interacts directly with the view - it has the same purpose as a View in a relational databases.
  6. The view displays content from the view-model and interacts directly with the user.

The use of Code-behind is generally considered bad practice. There are two good reasons for that. Firstly, validation defined on the view-model allows different representations of the data in the view. For example, an option type of call or put could be selected in the view using radio buttons, or a ComboBox. If the validation logic is defined in the view-model, it can be applied in either case. It should be possible to change from one representation to another without changing anything outside the XAML. Secondly, and more importantly, it is relatively easy to define units tests on the view-model so putting validation and other user-interface logic there brings them within the scope of unit testing.

Command Processing

Forms require some triggering device such as a button or menu to invoke processing. It is not uncommon for the same processing to be invoked by both (an application may have a 'Save' button as well as a 'Save' menu option). The code is much cleaner if the same mechanism is used for buttons and menu options and those familiar with Win32 know that such a creature already exists - Windows commands.

.NET components that receive Windows-like commands implement the ICommand interface shown below.

C#
interface ICommand
{
    bool CanExecute(Object parameter);
    void Execute(Object parameter);
}

The diagram below shows the flow of control associated with command processing. The WFP infra-structure queries the ICommand interface of the object (RelayCommand) bound to the visual component (button or menu item), the RelayCommand in turn queries the view-model to determine whether it is in a state that would allow processing to proceed. If it is the visual component (button or menu item) is enabled; if it is not the visual component is not enabled (greyed out).

If that visual component (button or menu item) is enabled, and the user clicks on it, execution will pass to the controller via the RelayCommand. RelayCommand by-passes the standard event processing (bubbling, tunneling)

Image 2

2. Enterprise Architecture including MVVM and Command Processing.

In the code below, a button is bound to the CalculateCommand (ICommand) object in the view-model. When the user clicks on the button in the view, the framework calls the Execute method on CalculateCommand.ICommand in the view-model.

The CalculateCommand is initially null - the view-model should not know anything about the command processing.

C#
CalculatorView.xaml:

	<Button Content="Calculate" ... Command="{Binding Path=CalculateCommand}"... />

AdderViewModel.cs:

	public ICommand CalculateCommand { get { return CalculateCmd; } }   // Must be a property to allow the button to bind to it.

	public ICommand CalculateCmd = null;

At start-up, the controller "injects" an instance of RelayCommand into the view-model, whose purpose is to relay the command to the controller where it is processed. The RelayCommand is initialised with a delegate to call to execute the command processing, and the method to call on the view-model to determine if it is a state where command processing can take place.

C#
Controller.cs:

	ViewModel.CalculateCmd = new RelayCommand((object z) =>
	{
		try
		{
			// Execute command.
		}
		catch(Exception)
		{
			...
		}
	},
	ViewModel.CanCalculate);

What is Validation?

It is not always clear in discussions what is considered "validation rules" and what is considered "business rules". For example, a constraint such as "Option type must be Call or Put" would be identified by most people as a validation rule, while a constraint such as "Buyer must be a pre-existing customer" would be identified by most people as a business rule. The difference seems to be a pragmatic one - the validation rule can be applied at the view/view-model level, while the business rule requires a trip into the model to access business logic. What is a validation rule and what is a business rule can therefore depend on the implementation!

Others argue that a constraint should be regards as a business rule if it is likely to change. For example, the rule "A valid CUSIP code must be supplied" could be superseded by the rule "A valid ISIN code must be supplied" so the rule should viewed as a business rule. That classification requires an accurate assessment of the likelihood of change, so in practice is not particularly useful.

We will assume that validation is primarily concerned with finding errors in data in the view and view-model.

It should be noted that users do not make such a distinction. Both are just "rules" to them, some simple, some complex, and when the rules are broken, the result is just seen as an input error.

The Validation Error Collection

The MVVM model requires that calculations and control functions be based on the data in the view-model, and not visual elements in the view. The view-model is not updated when a validation errors occur, in which case the view will contain invalid data and view-model will contain the last valid input. i.e. the view and view-model become out-of-sync. The calculation and control logic which only looks at the view-model therefore needs to be aware what validation errors have occurred so it can determine the true state of the data being displayed to the user.

The framework associates a System.Windows.Controls.ValidationError collection with each input control, however it is not easy or desirable to access from within the view-model - the view-model can be used with multiple views so it should not contain view-specific logic. Instead it is simpler to trap validation errors as they occur and store them in a separate user-defined validation error collection in the view-model.

There are two sources of validation errors:

  1. Validation errors detected by the framework including data conversion errors and validation rules registered with the view.
  2. User validation code in the view-model - in which case, the user code can directly add or remove validation errors from the collection.

If the ValidatesOnExceptions binding attribute is set to True, the framework will check for exceptions thrown during the update of the underlying data in the view-model. If an exception is thrown, the framework will add a system ValidationError to the Validation.Errors collection associated with that control. If the NotifyOnValidationError binding attribute is also set to True, the framework will raise a routed System.Windows.Controls.Validation.ErrorEvent that can be trapped by a user framework.

CalculateView.xaml:

	<TextBox ...>
		<Binding ... Path="y" ValidatesOnExceptions="True" NotifyOnValidationError="True" .../>
	</TextBox>

The code below demonstrates how to trap the validation error event. The event handlers adds or removes the validation error from the view-model validation error collection.

ViewBase.cs:

	public virtual void OnLoad(object sender, System.Windows.RoutedEventArgs e)
	{
		...
		AddHandler(System.Windows.Controls.Validation.ErrorEvent, new RoutedEventHandler(listener.Handler), true);
	}

	public virtual void OnUnload(object sender, System.Windows.RoutedEventArgs e)
	{
		RemoveHandler(System.Windows.Controls.Validation.ErrorEvent, new RoutedEventHandler(listener.Handler));
	}

Implementation of the Adder CanCalcute method becomes trivial - In the code below, HasErrors returns true if the validation error collection is not empty.

C#
AdderViewModel.cs:

	public bool CanCalculate(object z)
	{
		return    x.HasValue
			   && y.HasValue 
			   && !HasErrors;
	}

The project contains library code to create and maintain a user-defined error validation collection in the view-model.

Validation Triggers

Validation can occur as a result of the following events:

  • The user presses a key to type text into an control.
  • The user makes a selection using the mouse.
  • The control loses focus.
  • The user clicks on a button to execute some action.

The application can ensure that users only selects valid data, say in a dropdown, by actively managing the options displayed to the user. For this reason, this mode of input will not be discussed further.

The developer can choose between the immediate validation of keyboard input or validation on lose of focus by setting the control's binding UpdateSourceTrigger attribute to PropertyChange or LoseFocus respectively.

Immediate validation of keyboard input can be problematic. A user can pass through a state which is not valid without doing anything "wrong". For example, suppose that a TextBox accepts integer values between 880 and 1100. The TextBox contains "999" but the user needs to change it to "899" - the user has no choice but to delete one of the 9s, in which case the control passes through an invalid state.

The routine use of messages boxes to display validation errors is considered bad practice as they are generally considered to be too intrusive. (Imagine what the user would be thinking if a message box was displayed ever time an invalid input was detected in the previous example!) This does not preclude their use to report failure of the main command processing, although even here better choices are possible.

Type Conversion

Implicit validation occurs as a result of the framework converting the user input into the bound data type in the view-model. For example, if a TextBox is bound to a Double in the view-model, the framework will attempt to convert the user text to a Double. If the framework fails, it will generate a validation error (E.g. "Input string was not in a correct format").

More precisely, if the binding property ValidatesOnExceptions=True, the framework will trap any exception thrown during the update of the underlying data. If the binding property NotifyOnValidationError="True", then a handler can be set up to catch an System.Windows.Controls.Validation.ErrorEvent; the handler can then add the error to a user-defined error container. (See ViewBase.cs)

Mandatory Fields

If a TextBox is bound to a numeric field (double, int, decimal etc.) and the user attempts to delete the text, the framework will regard it as a validation error since it cannot convert the blank text to a numeric value. In effect, the value converter treats simple numeric fields as if they were mandatory.

If a field is mandatory, it is much better though to use a separate error messages to say so rather than relying on the user to correctly interpret "Input string was not in a correct format" when faced with a empty control in error.

It is recommended to always use Nullable types for numeric fields in the view-model. The value specified by the binding property TargetNullValue will be converted to a null value (typically TargetNullValue="" so a blank input maps to a null data value).

Note that WPF generally validates user input, not field values, so interestingly when a form with mandatory fields starts up there are no validation errors. (See figure 3) Once a field has been used, it will generate validation errors when cleared. (See figure 4). However IDataErrorInfo is inconsistent in this regard; if an application uses IDataErrorInfo, the framework will query the interface for errors on startup!

Image 3
3. On start up.
Image 4
figure 4. After text has been cleared.

Validation Processing

Built-in ValidationRules

  • If the ValidateOnDataError=True then the built-in DataErrorValidationRule is added to the control's ValidationRules list.

  • If the ValidateOnExceptions=True, the ExceptionValidationRule is added to the control's ValidationRules list. Note that even if the ExceptionValidationRule is in the control's ValidationRules list, any handler is called only if NotifyOnValidationError = "True".

  • If the ValidatesOnNotifyDataErrors=True, the NotifyDataErrorValidationRule is added to the control's ValidationRules list. The NotifyDataErrorValidationRule.Validate calls the INotifyDataErrorInfo.GetErrors method with the bound property name as its parameter.

The DataErrorValidationRule, ExceptionValidationRule and ValidatesOnNotifyDataErrors can be added directly to a binding via XAML, rather than implicitly via ValidateOnDataError, ValidateOnExceptions or ValidatesOnNotifyDataErrors.

Note that if both the built-in ValidationRule is specified in the XAML and the matching binding attribute set, two instances of the validation rule will be associated with the control, and both instances of the rule applied.

<TextBox Name="xInput" Height="Auto" Width="60" Margin="0,5,0,0" Grid.Column="2" Grid.Row="1">
      <Binding Path="x" TargetNullValue="" ValidatesOnExceptions="True" UpdateSourceTrigger="PropertyChanged">
          <Binding.ValidationRules>
              <ExceptionValidationRule/>
          </Binding.ValidationRules>
      </Binding>
  </TextBox>
figure 4.5. Two instances of ExceptionValidationRule are defined and applied.

Validation processing proceeds as follows.

  1. Validation is triggered. If UpdateSourceTrigger=PropertyChange, validation is triggered when new data in entered. If UpdateSourceTrigger=LoseFocus, validation is triggered when the control looses focus.

  2. Any ValidationRule with ValidationStep = RawProposedValue is applied.

  3. The entered data is converted internally to the required underlying type by the appropriate IDataConverter, if it exists. The binding engine will throw an exception if an error occurs during conversion of the text to the underlying data type. If the ExceptionValidationRule has been added to the controls ValidationRules list, either explicitly or by putting ValidateOnExceptions=True, then the Validation.Error event (of type System.Windows.Controls.Validation.ErrorEvent) will be raised and potentially trapped by user code.

    The ValidatioError includes a reference to the thrown exception

  4. Any ValidationRule with ValidationStep = ConvertedProposedValue is applied.

  5. The underlying value is set to the converted value.

  6. Any ValidationRule with ValidationStep = UpdatedValue is applied. This includes the DataValidationErrorRule which calls into the view-model IDataErrorInfo. If ValidateOnDataError=True is used to trigger IDataErrorInfo validation, it will be called after all other rules with ValidationStep = UpdatedValue have been applied. Note that the order in which IDataErrorInfo is called can be manipulated by using DataValidationErrorRule changing its position in the ValidationRules list.

  7. If NotifyOnSourceUpdate=True the Binding.SourceUpdated event is fired.

  8. Any ValidationRule with ValidationStep = CommittedValue is applied.

Once a ValidationRule error occurs, no further ValidationRules will be applied. If there is a validation error then

  • The binding engine creates a ValidationError and adds it to the control's Validation.Errors collection.

    The behaviour here differs for different versions of .NET. Prior to .NET4, any existing ValidationError was removed from Validation.Errors before the new errors was added. From .NET 4, the new error is added before the old error is removed.

  • Visual notification of the error is provided to the user. If a custom ErrorTemplate is defined for the control, it is applied. If no user-defined ErrorTemplate is available, the default (a red-border around the control) is used.

If the Adder application is run in Visual Studio, the output from Debug.WriteLine calls can be seen in the Output window. The top most TextBox in each view has multiple attached TraceValidationRules whose sole purpose is to show the progress of the validation processing described above.

Note that calling INotifyPropertyChanged.PropertyChanged or INotifyDataErrorInfo.RaiseErrorChanged causes re-validation and so has the potential for unexpected recursion.

Displaying Validation Errors

Validation error messages are often displayed using one of the following three mechanisms:

  • ErrorTemplates.
  • ContentPresenters.
  • Error Bars.

ErrorTemplate

The most common way of implementing adjacent errors messages is probably to use a control template to define what the control (including the error message) should look like when a validation error occurs. The control template is specified by the input control's Validation.ErrorTemplate property and typically defined as an application/window/control resource.

CalculatorViewWithErrorTemplate.xaml:

	<TextBox ... Validation.ErrorTemplate="{StaticResource ValidatedTextBoxTemplate}">

The control templates is rendered as an adornment. I.e. the newly specified control appearance is painted in a layer over the top of the control. The original control (e.g. TextBox) is not visible unless it is specified in the control template using the AdornedElementPlaceholder element.

Image 5

figure 5. Errors displayed using an ErrorTemplate.

The AdornedElementPlaceholder element is used to determine where the layer should be rendered relative to the control. The AdornedElementPlaceholder only works for error templates. A survey of the web indicates that it is not uncommon to attempt to use it outside of its original purpose, which generally leads to problems.

It is not uncommon for error templates to only display the error message in a tool tip (figure 5) however that requires the user to have some level of sophistication, particularly if they are only casual users of the application. If an error occurs, a red "star" or any other symbol for that matter, does not immediately trigger the user response - "Oh, I need to hover over this bit of the screen to get the error message". It requires patience that some users, such as traders, are not known to exhibit. It is arguably very poor UI design and is not recommended.

Using a tool tip to display errors is probably an attempt to save on screen real estate since it is not displayed until there is an error and no space on the form is specifically allocated for it.

ContentPresenter

C#
CalculatorViewUsingContentPresenter.xaml:

	...
	<TextBox Name="yInput" ... TextBox>
	<ContentPresenter ... Content="{Binding ElementName=yInput, Path=(Validation.Errors).CurrentItem}" />
	...

If screen real estate is not a concern, the associating a content presenter with each input control is a particularly simple and effective method of displaying error messages (figure 6).

Image 6

figure 6. System validation errors displayed using a ContentPresenter

The ContentPresenter requires a DataTemplate so that WPF knows how to render a System.Windows.Controls.ValidationError.

CalculatorViewUsingContentPresenter.xaml:

	<UserControl.Resources>
		<DataTemplate DataType="{x:Type ValidationError}">
			<TextBlock FontStyle="Italic" Foreground="Red" HorizontalAlignment="Right" Margin="0,1" Text="{Binding Path=ErrorContent}"/>
		</DataTemplate>
	</UserControl.Resources>

Displaying the error messages beside or below the input control (figure 6) should be regarded as good GUI design.

Error Bars

If screen real estate is at a premium, displaying error messages in a "bar" at the bottom of the form can be the solution.(figure 7).

This method requires that validation errors are stored in a user-defined validation error collection, the error display binding to the most recent or relevant error in the collection.

The major issue with this approach is that it can often be difficult to decide which error message should be displayed if there is more than one. The Adder application chooses to display the last error in the list of errors for the last validated control.

A custom display algorithm can be implemented by overloading the CurrentValidationError property. Probably the only hard rule for any custom algorithm to implement is that if one of the input controls has focus and has an error, the displayed error message should relate to that control.

Alternatively the complete error list could be displayed in a separate section of the application.

Image 7

7. Adder with error bar.

The following shows the approach taken by the Adder application. It displays the CurrentValidationError property of the user defined validation error collection in a TextBox.

CalculatorView.xaml:

	<!-- Error Display -->
	<TextBox ... Text="{Binding Path=CurrentValidationError, Mode=OneWay}" />

Setting Up Validation

There are a multitude of ways to do validation, including:

This article is only concern ourselves with the first 3 methods.

Caveat: Do not implement both the IDataErrorInfo and INotifyDataErrorInfo on the same objects. Do not use methods that have the same name as those in any of the IDataErrorInfo or INotifyDataErrorInfo interfaces. The author did just that during the development of the Adder application, resulting in some very hard to understand behaviour.

ValidationRule

ValidationRule objects can be used to implement and attach validation rules to specific fields.

Custom validation rules are derived from ValidationRule and implement the Validate method. The Validate method return a ValidationResult - the first parameter to the constructor indicates whether the checked field is valid or not. The second parameter contains the "error content",usually an error message.

The code for MandatoryRule below is taken from the Adder project. The Name property is setup in the associated XAML and is used to construct the error message. The code also uses ValidationResult.ValidResult which is a constant equivalent to ValidationResult(true, null).

C#
public class MandatoryRule : ValidationRule
{
    public string Name
    {
        get;
        set;
    }

    public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
    {
        if(String.IsNullOrEmpty((string)value))
        {
            if (Name.Length == 0)
                Name = "Field";
            return new ValidationResult(false, Name + " is mandatory.");
        }
        return ValidationResult.ValidResult;
    }
}

The MandatoryRule is attached to the control by declaring it in the Binding.ValidationRules section of the control's XAML. Note that the Name property is also setup in the XAML.

C#
<TextBox Height="Auto" Width="60" Margin="0,5,0,0" Grid.Column="1" Grid.Row="3" Validation.ErrorTemplate="{StaticResource ValidatedTextBoxTemplate}">
      <Binding Path="y" TargetNullValue="" ValidatesOnExceptions="True" NotifyOnValidationError="True" UpdateSourceTrigger="PropertyChanged">
          <Binding.ValidationRules>
              <common:MandatoryRule Name="y"/>
          </Binding.ValidationRules>
      </Binding>
  </TextBox>

Note that:

  • ValidationRules are defined in the view. Validation should be testable - that means that it should be declared in the view-model, not the view. ValidationRules are not a good fit with MVVM.

  • A ValidationRule is attached to a single visual component. It is not really suited to cross-control validation. (For example, if the sum of two inputs must be less than some fixed amount, the rule must be defined on both input controls).

  • ValidationRules provides a very modular approach to single control validation - it is not difficult to code up a ValidationRule so it is reusable.

Despite its limitation, ValidationRules are the workhorses of WPF validation. However since a non-MVVM application may be converted at some point in the future to MVVM, the use of ValidationRule is not whole-heartedly recommended.

The mechanics of using ValidationRule is not covered in this article - it is well documented elsewhere and the Adder application contains multiple instances of the object

IDataErrorInfo

IDataErrorInfo (below) can be used to replace ValidationRules. The indexer is used to retrieve validation errors associated with a specific field. No error is indicated by returning a empty string.

C#
public interface IDataErrorInfo
{
	string Error { get; }
	string this[string propertyName] { get; }
}

IDataErrorInfo has a number of idiosyncrasies:

  • The Error property is supposed to return a "whole object" validation error. (E.g. Holidays plus days worked must add to the total number of days in a specific period). The binding engine however never uses the Error property. It is hard to understand why MS would define a method as part of its standard infra-structure and then not use it. The Error property does however provide the type of functionality that the framework requires to determine if command processing can take place.

  • IDataErrorInfo is not "modular". IDataErrorInfo implementations tend to be large switch statements, and the re-use of validation logic requires an additional framework or coding conventions. In contrast, it is possible to build up a library of ValidationRule objects, such as MandatoryRule and NumberRangeRule in the Adder application, which can easily be re-used.

  • Generally WPF validates input - when a form is first displayed, empty fields are not validated to prevent the user being greeted by a sea of red. (See above). That's not the case with IDataErrorInfo.

IDataErrorInfo is a closer fit with MVVM than ValidationRules since the validation is done in the view-model and is therefore testable. However there is a better alternative.

INotifyDataErrorInfo

INotifyDataErrorInfo is a Microsoft-defined interface onto user-defined validation error containers. Containers that support INotifyDataErrorInfo can be directly queried by the WPF framework.

INotifyDataErrorInfo has been imported into WPF from Silverlight where it is part of the infra-structure support for asynchronous validation. It is available in .NET 4.5.

C#
public interface INotifyDataErrorInfo
{
	bool HasErrors { get; }
	IEnumerable GetErrors(string propertyName);
	event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
}

Note that

  • Any changes to the errors in the error container should raise the ErrorsChanged event.
  • The error objects returned by INotifyDataErrorInfo.GetErrors must implement string ToString().

The INotifyDataErrorInfo interface incorporates many of the lessons that became apparent to the author during the development of the Adder application. There is a striking resemblance with the user-defined error container interface. See below. In fact before they were changes, the method names used in the original version of the IValidationErrorContainer clashed with INotifyDataErrorInfo and had to be changed! For practical reasons, the ErrorsChanged event is shared by both interfaces.

C#
public interface IValidationErrorContainer
{
    bool AddError(ValidationError error, bool isWarning = false);
    bool RemoveError(string propertyName, string errorID);

    int ErrorCount { get; }
    IEnumerable GetPropertyErrors(string propertyName);
    event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
}

INotifyDataErrorInfo suffers from some of the same limitations as IDataErrorInfo. For example, it is not particularly modular - implementations tend to consist of large switch statements. However IDataErrorInfo implementations treat the user-defined error container is an additional after-thought, required only to disable command controls and update error bars. INotifyDataErrorInfo on the other hand treats the user-defined error container as the primary structure for organising validation logic.

Why is the ErrorsChanged event necessary? Visual components such as an error bar need to be updated with new errors. The error container should not know about these components in which notification by subscription to an update event is the obvious solution.

The implementation of the INotifyDataErrorInfo based collection in the Adder application is a bit clumsy since it implements the INotifyDataErrorInfo interface over the top of a container that implements the almost identical IValidationErrorContainer interface. The implementation could be significantly streamlined by implementing the INotifyDataErrorInfo interface directly on ValidationErrorContainer. That was not done for pedagogical and practical reasons (see "Setting Up Validation" above). The ValidationToolkit library should not be viewed in any way as a definitive implementation since it serves two masters: IDataErrorInfo and INotifyDataErrorInfo. If you are interested in such a thing (which of course you are), please refer to the validation ErrorContainer in Microsoft Prism.

The project

Description

The project implements 4 instances of a simple Adder control in the same window. Each Adder adds two numbers together and displays the result. Each uses the MVVM pattern and each is implemented in a slightly different way. The 4 Adders can be described as:

  • Top-Left: Validation is done using ValidationRule. Errors displayed suing a content presenter under the input control.
  • Bottom-Left: Validation is done using ValidationRule. Errors displayed using a error template.
  • Top-Right: Validation uses IDataErrorInfo. Errors in the user-defined validation error collection are displayed in an error bar at the base on the control.
  • Bottom-Right: Validation uses INotifyDataErrorInfo. Errors in the user-defined validation error collection are displayed in an error bar at the base on the control.

Image 8

8. Adder Application.

The inputs to the Adders are mandatory and must be greater or equal to 0.

Each Adder uses a RelayCommand object to pass the commands to the controller, and each ICommand.CanCalculate refers to the view-model only.

The project includes library code which implements the error container and base classes for the view-model and view. Please note that the primary purpose of the library is to demonstrate potential usage and design rather then re-usability.

ValidationRule

The following ValidationRules are defined in the Adder application.

  • MandatoryRule
  • IntegerRangeRule
  • TraceValidationRule

ValidationRules are only used in the two left adders.

 

The TraceValidationRule sole purpose is to trace the execution of validation rules in the Output window in Visual Studio.

INotifyErrorDataInfo

The project defines a custom ValidationError object. The bottom right adder implements INotifyErrorDataInfo on the error container as shown below.

C#
AdderViewModel_INotifyDataErrorInfo.cs:

	// INotifyErrorDataInfo.
	public System.Collections.IEnumerable GetErrors(string propertyName)
	{
		return base.GetPropertyErrors(propertyName);
	}

	// INotifyErrorDataInfo.
	public bool HasErrors
	{
		get { return ErrorCount != 0; }
	}

	// Helper
	protected void RaiseErrorsChanged(string propertyName)
	{
		if (this.ErrorsChanged != null)
			this.ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
	}

Validation occurs in the property setter, in the ValidateProperty method. See below.

C#
AdderViewModel.cs:

	public Nullable<double> y
	{
		get { return y_; }
		set 
		{ 
			y_ = value;
			ValidateProperty("y");
			NotifyPropertyChanged("y");
			Sum = null;
		}
	}

The validation logic specific to the application (below) shows the use of the AddError and RemoveError methods to update the error collection. The ValidateProperty is called from the view-model setters.

C#
AdderViewModel_INotifyDataErrorInfo.cs:

	public const string Constraint_Mandatory = "IsMandatory";
	public const string Constraint_MustBeNonNegative = "NonNegative";

	...

	void ValidateNonNegative(Nullable<double> x, string fieldName)
	{
		if (x.HasValue && x.Value < 0.0)
			AddError(new ValidationError(fieldName, Constraint_MustBeNonNegative, fieldName + ": must be non-negative"));
		else
			RemoveError(fieldName, Constraint_MustBeNonNegative);
	}

	void ValidateMandatory(Nullable<double> x, string fieldName)
	{
		if (!x.HasValue)
			AddError(new ValidationError(fieldName, Constraint_Mandatory, fieldName + ": is mandatory"));
		else
			RemoveError(fieldName, Constraint_Mandatory);
	}

	public override void ValidateProperty(string propertyName)
	{
		Tracer.LogValidation("INotifyDataErrorInfo.ValidateProperty called. Validating " + propertyName);
		switch (propertyName)
		{
			case "x":
				{
					ValidateNonNegative(x, "x");
					ValidateMandatory(x, "x");
				}
				break;

			case "y":
				{
					ValidateNonNegative(y, "y");
					ValidateMandatory(y, "y");
				}
				break;
		}
		if (String.IsNullOrEmpty(propertyName))
		{
			Tracer.LogValidation("No cross-property validation errors.");
		}
	}

Unit Tests

Finally, the project contains unit tests to exercise the service and view-model code for the INotifyDataErrorInfo adder.

The unit test cannot test the code to handle invalid numeric input (e.g. typing "hello" when the control expects a number). The purpose of the view-model is to put the data in a format that can be easily consumed by the view. Microsoft suggests going one step further: the view should always bind to data of the same type as that expected by the view controls. In the case of the Adder application, the TextBoxs expects strings so the control would bind to string values within the view-model. The conversion from strings to numbers would be accomplished in an additional layer within the view-model, which could also be subject to unit testing.

Conclusion

Validation in MVVM application is anything but trivial. There are multiple ways in which it can be done, however a user validation error container is central to all MVVM approaches.

This article only scratches the surface of what is possible. Other topics of interest include asynchronous validation, attribute based validation, custom controls, rule-based validation, calculator fields (that accept input such as "45/360+3") and Prism.

Hopefully this project will be useful to others who need to make decisions about how to go about validation.

History

No updates yet.

License

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


Written By
Founder Bowmain
United Kingdom United Kingdom
Shaun O'Kane is a software developer with over 25 years experience. His interests include C++, C#, Windows, Linux, WPF, sockets, finance, .. just about everything.

Comments and Discussions

 
PraiseExcellent Pin
David Deley6-May-23 8:02
David Deley6-May-23 8:02 
QuestionNo code behind - what about ViewBase? Pin
Member 1455649024-Feb-20 14:12
Member 1455649024-Feb-20 14:12 
PraiseWhen to use ValidationRules, and when to use INotifyDataErrorInfo? Pin
Nick Alexeev19-Sep-18 13:25
professionalNick Alexeev19-Sep-18 13:25 
BugError is removed in ValidationRule case. Pin
MrDaedra3-Oct-16 23:50
MrDaedra3-Oct-16 23:50 
Questionvote of 5 Pin
Beginner Luck15-Aug-16 15:39
professionalBeginner Luck15-Aug-16 15:39 
thanks
Questionwhy on't you just copy and paste simple example next time you gd sob fuk Pin
Member 121136024-Nov-15 4:20
Member 121136024-Nov-15 4:20 
AnswerRe: why on't you just copy and paste simple example next time you gd sob fuk Pin
Member 149029704-Feb-22 23:12
Member 149029704-Feb-22 23:12 
GeneralMy vote of 5 Pin
hutz2-Jul-15 0:00
hutz2-Jul-15 0:00 
Generalvery nice Pin
BillW3317-Feb-15 5:00
professionalBillW3317-Feb-15 5:00 
AnswerNice effort Pin
Liju Sankar9-Feb-15 0:36
professionalLiju Sankar9-Feb-15 0:36 
QuestionVery good Pin
RugbyLeague13-Jan-15 5:51
RugbyLeague13-Jan-15 5:51 
GeneralMy vote of 5 Pin
Agent__00712-Jan-15 15:47
professionalAgent__00712-Jan-15 15:47 
QuestionMy vote of 4: constraints need to be defined in the model! Pin
Gerd Wagner12-Jan-15 2:15
professionalGerd Wagner12-Jan-15 2:15 
GeneralMy vote of 5 Pin
Afzaal Ahmad Zeeshan9-Jan-15 16:56
professionalAfzaal Ahmad Zeeshan9-Jan-15 16:56 
Questionbrilliant article Pin
Sacha Barber9-Jan-15 11:54
Sacha Barber9-Jan-15 11:54 

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

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