Validation in WPF






4.92/5 (58 votes)
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.
The architecture attempts to decrease the complexity of an applications by organising it into layers, following the doctrine of "separation of concerns".
- The database provides persistence services.
- A repository provides a simple interface into the database and hides details of the actual storage. Services provide interfaces to replaceable/shared modules.
- The controller responds to user input that directs the execution of code.
- The model contains "business objects" which contain logic (and possibly transient data) that models the operation of the business.
- 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.
- 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.
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)
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.
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.
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:
- Validation errors detected by the framework including data conversion errors and validation rules registered with the view.
- 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.
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!
![]() 3. On start up.
|
![]() figure 4. After text has been cleared.
|
Validation Processing
Built-in ValidationRule
s
-
If the
ValidateOnDataError=True
then the built-inDataErrorValidationRule
is added to the control'sValidationRules
list. -
If the
ValidateOnExceptions=True
, theExceptionValidationRule
is added to the control'sValidationRules
list. Note that even if theExceptionValidationRule
is in the control'sValidationRules
list, any handler is called only ifNotifyOnValidationError = "True"
. -
If the
ValidatesOnNotifyDataErrors=True
, theNotifyDataErrorValidationRule
is added to the control'sValidationRules
list. TheNotifyDataErrorValidationRule.Validate
calls theINotifyDataErrorInfo.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>
Validation processing proceeds as follows.
-
Validation is triggered. If
UpdateSourceTrigger=PropertyChange
, validation is triggered when new data in entered. IfUpdateSourceTrigger=LoseFocus
, validation is triggered when the control looses focus. -
Any
ValidationRule
withValidationStep = RawProposedValue
is applied. -
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 theExceptionValidationRule
has been added to the controlsValidationRules
list, either explicitly or by puttingValidateOnExceptions=True
, then theValidation.Error
event (of typeSystem.Windows.Controls.Validation.ErrorEvent
) will be raised and potentially trapped by user code.The ValidatioError includes a reference to the thrown exception
-
Any
ValidationRule
withValidationStep = ConvertedProposedValue
is applied. -
The underlying value is set to the converted value.
-
Any
ValidationRule
withValidationStep = UpdatedValue
is applied. This includes theDataValidationErrorRule
which calls into the view-modelIDataErrorInfo
. IfValidateOnDataError=True
is used to triggerIDataErrorInfo
validation, it will be called after all other rules withValidationStep = UpdatedValue
have been applied. Note that the order in whichIDataErrorInfo
is called can be manipulated by usingDataValidationErrorRule
changing its position in the ValidationRules list. -
If
NotifyOnSourceUpdate=True
theBinding.SourceUpdated
event is fired. -
Any
ValidationRule
withValidationStep = CommittedValue
is applied.
Once a ValidationRule
error occurs, no further ValidationRule
s will be applied. If there is a validation error then
-
The binding engine creates a
ValidationError
and adds it to the control'sValidation.Errors
collection.The behaviour here differs for different versions of .NET. Prior to .NET4, any existing
ValidationError
was removed fromValidation.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-definedErrorTemplate
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.
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
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).
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.
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:
ValidationRule
IDataErrorInfo
INotifyDataErrorInfo
- Custom controls
- Attribute based validation.(http://blog.magnusmontin.net/2013/08/26/data-validation-in-wpf/)
- Language based declarative schemes
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)
.
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.
<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:
-
ValidationRule
s are defined in the view. Validation should be testable - that means that it should be declared in the view-model, not the view.ValidationRule
s 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). -
ValidationRule
s provides a very modular approach to single control validation - it is not difficult to code up aValidationRule
so it is reusable.
Despite its limitation, ValidationRule
s 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.
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 theError
property. It is hard to understand why MS would define a method as part of its standard infra-structure and then not use it. TheError
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 largeswitch
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 ofValidationRule
objects, such asMandatoryRule
andNumberRangeRule
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.
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 implementstring 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.
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.
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 ValidationRule
s 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.
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.
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.
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 TextBox
s 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.