Introduction
While writing WPF applications, validation in MVVM is primarily done through the IDataErrorInfo
interface. Data is binded to the control through a viewmodel
implementing the IDataErrorInfo
interface.
We shall cover some concepts of a base viewmodel calling it ViewModelBase
and extend it to ValidationViewModelBase
.
Using the Code
Most of the boilerplate code involved in the implementation of the IDataErrorInfo
is the evaluation of error of individual properties and looking at the state of the entire object and qualifying it as valid or invalid.
We build towards a sample that has:
- User input as
string
whose length follows 3 simple business rules:
- Must be multiple of 2
- Greater than 10 digits
- Less than 32 digits
- OK button that can be clicked if only the user input follows the rules (is valid).
The invalid state shall have the OK button disabled.
As soon as the user input is correct, the error clears and the OK button is enabled.
The implementation is based upon our base class ValidationViewModel.cs that will be explained later. The UI contains a regular TextBox
and a Button
.
The DataContext
is set and binded to the TextBox Text
property as:
<TextBox Text="{Binding Aid,UpdateSourceTrigger=PropertyChanged,Mode=TwoWay,ValidatesOnDataErrors=True}"
Override the default ErrorTemplate
for changing the Background
color:
<TextBox.Style>
<Style TargetType="TextBox">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="Background" Value="Pink"/>
<Setter Property="ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</TextBox.Style>
Implementing the ValidationViewModel
on the sample viewmodel
can be done as such corresponding to our original two use cases.
1. Implementing the Business Rule
The rule is added as Func<bool>
to the rule dictionary using the AddRule()
method.
public ViewModel()
{
base.AddRule(() => Aid, () =>
Aid.Length >= (5 * 2) &&
Aid.Length <= (16 * 2) &&
Aid.Length % 2 == 0, "Invalid AID.");
}
2. Defining behavior of the ‘OK’ button
This is implemented by using the RelayCommand
which uses the HasErrors
to evaluate the ICommand.CanExecute
.
public ICommand OkCommand
{
get
{
return Get(()=>OkCommand, new RelayCommand(
()=> MessageBox.Show("Ok pressed"),
()=> !base.HasErrors));
}
}
Also as a side note, private
field for the command is not needed, as the result is cached and the same command is returned every time the getter is called.
Implementing the ViewModelBase
First up is the generic ViewModelBase
which will implement the INotifyPropertyChanged
. Also in the base, we tackle quite a few generic points.
-
Removing the “Magic String” in PropertyChanged Event
This is a common problem and having an Expression
removes the need of property string
is a pretty neat solution. This is nice as it removes typing errors and makes refactoring easy.
The code is primarily the NotificationObject
of the PRISM library.
protected static string GetPropertyName<T>(Expression<Func<T>> expression)
{
if (expression == null)
throw new ArgumentNullException("expression");
Expression body = expression.Body;
MemberExpression memberExpression = body as MemberExpression;
if (memberExpression == null)
{
memberExpression = (MemberExpression)((UnaryExpression)body).Operand;
}
return memberExpression.Member.Name;
}
-
Generic Getter
We have property name to value map for mapping last known value of corresponding property.
private Dictionary<string, object> propertyValueMap;
protected ViewModelBase()
{
propertyValueMap = new Dictionary<string, object>();
}
We have Get
that takes an Expression
that is used to extract the property name and default value.
protected T Get<T>(Expression<Func<T>> path)
{
return Get(path, default(T));
}
protected virtual T Get<T>(Expression<Func<T>> path, T defaultValue)
{
var propertyName = GetPropertyName(path);
if (propertyValueMap.ContainsKey(propertyName))
{
return (T)propertyValueMap[propertyName];
}
else
{
propertyValueMap.Add(propertyName, defaultValue);
return defaultValue;
}
}
-
Generic Setter
Building up on the property map, we have generic setter that raises the PropertyChanged
event.
protected void Set<T>(Expression<Func<T>> path, T value)
{
Set(path, value, false);
}
protected virtual void Set<T>(Expression<Func<T>> path, T value, bool forceUpdate)
{
var oldValue = Get(path);
var propertyName = GetPropertyName(path);
if (!object.Equals(value, oldValue) || forceUpdate)
{
propertyValueMap[propertyName] = value;
OnPropertyChanged(path);
}
}
Implementing the ValidationViewModel
Building up on the previous ViewModelBase
, we implement IDataErrorInfo
interface on ValidationViewModel
. The features that it exposes are:
1. Method to add rule corresponding to specific property
The class exposes a AddRule()
method taking in the property, a delegate that is a function that evaluates to bool
, and the error message as string
that is displayed if the rule fails. This delegate is added to ruleMap
corresponding to the property name.
The functionality to add multiple rules for the same property is left to the discretion of the client and AddRule()
will throw ArgumentException if property name (key) is present.
private Dictionary<string, Binder> ruleMap = new Dictionary<string, Binder>();
public void AddRule<T>(Expression<Func<T>> expression, Func<bool> ruleDelegate, string errorMessage)
{
var name = GetPropertyName(expression);
ruleMap.Add(name, new Binder(ruleDelegate, errorMessage));
}
The implementation of the Binder
class is straightforward, it exists only to encapsulate the functionality of data validation.
The Binder
class has a IsDirty
property that qualifies that the current values is dirty or not. This property is set whenever the property value is updated. Also an Update()
method that evaluates the rule that was passed while registering the rule.
internal string Error { get; set; }
internal bool HasError { get; set; }
internal bool IsDirty { get; set; }
internal void Update()
{
if (!IsDirty)
return;
Error = null;
HasError = false;
try
{
if (!ruleDelegate())
{
Error = message;
HasError = true;
}
}
catch (Exception e)
{
Error = e.Message;
HasError = true;
}
}
The Update()
method performs little optimization as not to reevaluate the ruleDelegate
if the property is not dirty.
2. Override the Set method to set IsDirty flag
protected override void Set<T>(Expression<Func<T>> path, T value, bool forceUpdate)
{
ruleMap[GetPropertyName(path)].IsDirty = true;
base.Set<T>(path, value, forceUpdate);
}
3. Global HasErrors to check validity of the entire view model state
public bool HasErrors
{
get
{
var values = ruleMap.Values.ToList();
values.ForEach(b => b.Update());
return values.Any(b => b.HasError);
}
}
4. Implementation of IDataErrorInfo. The Error property concatenates the error messages into a single message.
public string Error
{
get
{
var errors = from b in ruleMap.Values where b.HasError select b.Error;
return string.Join("\n", errors);
}
}
public string this[string columnName]
{
get
{
if (ruleMap.ContainsKey(columnName))
{
ruleMap[columnName].Update();
return ruleMap[columnName].Error;
}
return null;
}
}
This finishes my take on WPF validation.
The entire code essentially aggregates information and presents you with an encapsulated base class to work with your custom business rules.
Hope someone finds it useful.
Please leave your comments…
History
- 25th September, 2014 - Added dependency DLL
- 10th June, 2014 - First draft