Friday, July 31, 2009

Integrating Validation Application Block with WPF

Business objects can be directly validated in WPF using the IDataErrorInfo interface provided in WPF 3.5. With the help of the VAB and IDataErrorInfo we can implement a very easy and convenient Attribute based validation of the business objects in WPF. In this post, I’ll show an example how to achieve VAB + WPF integration to validate the business objects.

I am going to use the IDataErrorInfo implementation as in my previous article.

Using IDataErrorInfo requires you to implement the methods

public string Error{ get; }

public string this[string columnName]{ get; }

My base entity implements the IDataErrorInfo and INotifyPropertyChanged interfaces

public abstract class BaseEntity : INotifyPropertyChanged, IDataErrorInfo

{

[NotNullValidator(MessageTemplate="Id cannot be null", Tag="Id")]

public T Id { get; set; }

public abstract ValidationResults SelfValidate();

public abstract string GetError(ValidationResults results);

public abstract bool IsValid { get; }

#region IDataErrorInfo Members

public string Error

{

get

{

var __ValidationResults = SelfValidate();

return GetError(__ValidationResults);

}

}

public string this[string columnName]

{

get

{

var __ValidationResults = SelfValidate();

if (__ValidationResults != null)

{

var __ColumnResults = __ValidationResults.FirstOrDefault<ValidationResult>(x => string.Compare(x.Tag, columnName, true) == 0);

return __ColumnResults != null ? __ColumnResults.Message : string.Empty;

}

return string.Empty;

}

}

#endregion

public event PropertyChangedEventHandler PropertyChanged;

public void OnPropertyChanged(string propertyName)

{

if (PropertyChanged != null)

PropertyChanged(this, new PropertyChangedEventArgs(propertyName));

}

}

Every business class inherits the base entity class and implements the abstract methods

[HasSelfValidation]

public class User : BaseEntity<Guid>

{

private string ___name;

private decimal ___cost;

private DateTime ___dateOfJoining;

[ValidatorComposition(Microsoft.Practices.EnterpriseLibrary.Validation.CompositionType.And)]

[StringLengthValidator(

0, RangeBoundaryType.Exclusive,

45, RangeBoundaryType.Inclusive,

MessageTemplate="Name should be in the range 0 - 45", Tag="Name")]

[NotNullValidator(MessageTemplate="Name cannot be null")]

public string Name

{

get { return ___name; }

set

{

___name = value;

OnPropertyChanged("Name");

}

}

[RangeValidator(

typeof(decimal),

"0.00", RangeBoundaryType.Inclusive,

"999.99", RangeBoundaryType.Inclusive,

MessageTemplate="Cost should be in the range 0 - 999.99", Tag="Cost")]

public decimal Cost

{

get { return ___cost; }

set

{

___cost = value;

OnPropertyChanged("Cost");

}

}

public DateTime DateOfJoining

{

get { return ___dateOfJoining; }

set

{

___dateOfJoining = value;

OnPropertyChanged("DateOfJoining");

}

}

[SelfValidation]

public void Validate(ValidationResults results)

{

if (DateOfJoining < new DateTime(2003, 10, 1) || DateOfJoining > DateTime.Now)

results.AddResult(new ValidationResult("Date of joining should be between 01-Oct-2003 and today", this, string.Empty, "DateOfJoining", null));

}

public override ValidationResults SelfValidate()

{

return EntityValidator<User>.ValidateEntity(this);

}

public override string GetError(ValidationResults results)

{

return EntityValidator<User>.GetValidationErrors(results);

}

public override bool IsValid

{

get{ return SelfValidate().IsValid; }

}

}

I have created an EntityValidator class that encapsulates my VAB method calls to validate the entity.

Finally the WPF Code

<Style x:Key="__TextBoxStyle" TargetType="{x:Type TextBox}">

<Setter Property="Width" Value="160" />

<Setter Property="Height" Value="30" />

<Setter Property="HorizontalAlignment" Value="Left" />

<Setter Property="VerticalAlignment" Value="Center" />

<Setter Property="Validation.ErrorTemplate">

<Setter.Value>

<ControlTemplate>

<DockPanel DockPanel.Dock="Right" >

<AdornedElementPlaceholder />

<Image Source="{StaticResource Error}"

Width="15" Height="15"

ToolTip="{Binding Path=AdornedElement.ToolTip,

RelativeSource={RelativeSource Mode=FindAncestor,

AncestorType={x:Type Adorner}}}" />

DockPanel>

ControlTemplate>

Setter.Value>

Setter>

<Style.Triggers>

<Trigger Property="Validation.HasError" Value="True">

<Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}" />

Trigger>

Style.Triggers>

Style>

<TextBox Style="{StaticResource __TextBoxStyle}"

Text="{Binding Path=User.Cost,

Mode=TwoWay,

ValidatesOnDataErrors=True, Converter={StaticResource __DecimalConverter},

UpdateSourceTrigger=PropertyChanged}"

Grid.Column="1" Grid.Row="1" />

I have my SaveCommand implemented in the ViewModel as

public ICommand SaveCommand

{

get

{

if (___SaveCommand == null) ___SaveCommand = new DelegateCommand

(

() => Save(User),

() => User.IsValid

);

return ___SaveCommand;

}

}

<Button Grid.Row="3" Grid.Column="1" Width="120"

Grid.ColumnSpan="2"

HorizontalAlignment="Left"

VerticalAlignment="Center"

Content="Save" Margin="20 0 0 0"

Command="{Binding SaveCommand}" />

The IsValid method checks whether the entity is valid for save.

Output