Saturday, May 16, 2009

MVVM Validation With NHibernate Validator

MVVM-Validation

Simplest way to do Validation in WPF is usually implementing IDataErrorInfo interface, and do the validation in the indexer’s getter. It turns out to be ugly and gets out of hand when your model gets a little larger. Implementing the IDataErrorInfo is as simple as this:

public string this[string propertyName]
{
get
{
//Validate property that is being set,
//and return an error message if there's
//any error.

return null;
}
}
public string Error
{
get { return string.Empty; }
}

I intend to use NHibernate Validator attributes to add non-intrusive validation to my ViewModel classes. So you’d just decorate properties of your ViewModel and let WPF and NHibernate Validator do the rest for you. Before we do that, let’s create a more elegant way to show the errors by restyling the TextBox control and add an Error Icon and tooltip to it in case of an error existing:

<Style TargetType="{x:Type TextBox}">
<
Setter Property="Validation.ErrorTemplate">
<
Setter.Value>
<
ControlTemplate>
<
DockPanel LastChildFill="True">
<
Image x:Name="ErrorIcon" Source="Images/FieldError.png" DockPanel.Dock="Right" Width="16" Height="16" Margin="4,0,0,0" />
<
Border BorderBrush="Red" BorderThickness="1">
<
AdornedElementPlaceholder />
</
Border>
</
DockPanel>
</
ControlTemplate>
</
Setter.Value>
</
Setter>
<
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>
Now that styles are all set, let’s configure NHibenrate Validator’s engine on application startup. Nothing fancy, just straightforeward configuration. We’ll use a shared validation engine:
public void RegisterValidatorEngine()
{
var config = new NHVConfigurationBase();

config.Properties[Environment.ApplyToDDL] = "false";
config.Properties[Environment.AutoregisterListeners] = "true";
config.Properties[Environment.ValidatorMode] = "UseAttribute";
config.Properties[Environment.SharedEngineClass] = typeof(ValidatorEngine).FullName;
config.Mappings.Add(new MappingConfiguration(DomainAssemblyName, null));

Environment.SharedEngineProvider = new NHibernateSharedEngineProvider();
Environment.SharedEngineProvider.GetEngine().Configure(config);

ValidatorInitializer.Initialize(NHibernateConfig);
}
And to make things reusable, why not create a base ViewModel:
public abstract class ValidatableViewModel : IDataErrorInfo
{
private readonly ValidatorEngine validation;

protected ValidatableViewModel()
{
validation = Environment.SharedEngineProvider.GetEngine();
}

public string this[string propertyName]
{
get
{
var rules = GetInvalidRules(propertyName);
if (rules != null && rules.Count > 0)
{
return rules[0].Message;
}

return null;
}
}

public string Error
{
get { return string.Empty; }
}

public IList<InvalidValue> GetInvalidRules(string propertyName)
{
var type = this.GetType();

return validation.ValidatePropertyValue(type, propertyName, GetPropertyValue(type, propertyName));
}

public IList<InvalidValue> GetAllInvalidRules()
{
return validation.Validate(this);
}

private object GetPropertyValue(Type objectType, string properyName)
{
return objectType.GetProperty(properyName).GetValue(this, null);
}
}

The rest is just to inherit the ValidatableViewModel and add necessary attributes to our binded properties. A sample ViewModel containing a Save command, which is invocable only when there’s no error on the model and a couple of other business properties would look like this:

public class NewAccountViewModel : ValidatableViewModel, INotifyPropertyChanged
{
private string _firstName;
private string _lastName;
private string _currentBalance;

[NotNullNotEmpty]
public string Firstname
{
get { return _firstName; }
set
{
_firstName = value;
this.Notify(this.PropertyChanged, o => o.Firstname);
}
}

[NotNullNotEmpty]
public string Lastname
{
get { return _lastName; }
set
{
_lastName = value;
this.Notify(this.PropertyChanged, o => o.Lastname);
}
}

[IsNumeric]
[NotNullNotEmpty]
public string CurrentBalance
{
get { return _currentBalance; }
set
{
_currentBalance = value;
this.Notify(this.PropertyChanged, o => o.CurrentBalance);
}
}

public event PropertyChangedEventHandler PropertyChanged = delegate { };

private ICommand _cmdSave;
public ICommand SaveCommand
{
get
{
if (_cmdSave == null)
_cmdSave = new RelayCommand(Save, CanSave);

return _cmdSave;
}
}

public void Save()
{
MessageBox.Show("Save");
}

public bool CanSave()
{
return GetAllInvalidRules().Count == 0;
}
}
<UserControl x:Class="Jimba.UI.View.NewAccountView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

<
StackPanel>

<
StackPanel Orientation="Horizontal">
<
Label Width="100">Firstname:</Label>
<
TextBox Text="{Binding Path=Firstname, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
</
StackPanel>

<
StackPanel Orientation="Horizontal">
<
Label Width="100">Lastname:</Label>
<
TextBox Text="{Binding Path=Lastname, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
</
StackPanel>

<
StackPanel Orientation="Horizontal">
<
Label Width="100">Current Balance:</Label>
<
TextBox Text="{Binding Path=CurrentBalance, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
</
StackPanel>

</
StackPanel>

</
UserControl>


Submit this story to DotNetKicks Shout it

No comments: