Introduction
WPF is a fairly new technology (OK, it's been around a while), so it's still growing and new techniques and ideas are developing all the time. As developers, we are expected to provide users with usable systems. As part of a usable system, we must provide input validation of some sort. Now, WPF does come with a standard mechanism for this, but as you shall see, it's not perfect, and that it is limited in certain areas.
What this article will attempt to outline is a possible solution to the inbuilt WPF shortcomings. It does assume you know a little bit about WPF and validation in general, but it should cover most ideas in enough detail that WPF newbies should be OK.
What Does This Article Attempt to Solve
As I stated, this article will attempt to show you an alternative validation concept that fills the gaps with the standard WPF validation mechanism.
But what's so wrong with the standard WPF validation mechanism? Well, typically, the inbuilt WPF validation mechanism provides means of validating a single object. So it provides bounds checking for that object if you will. So how does this work? Well, although that's now the main push for this article, it is worth a quick mention...so here goes.
The Standard WPF Validation Works Likes This:
In the .NET 3.0 days, you would have typically done something like this:
<Binding Source="{StaticResource data}" Path="Age"
UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<ExceptionValidationRule/>
</Binding.ValidationRules>
</Binding>
Where you add in ValidationRules
to your binding.
Which was OK, but if you wanted a specific type of validation, you had to either use the standard ValidationRule
or create your own, which may look like this:
class FutureDateRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
DateTime date;
try
{
date = DateTime.Parse(value.ToString());
}
catch (FormatException)
{
return new ValidationResult(false, "Value is not a valid date.");
}
if (DateTime.Now.Date > date)
{
return new ValidationResult(false, "Please enter a date in the future.");
}
else
{
return ValidationResult.ValidResult;
}
}
}
Ouch, painful.
Luckily, in .NET 3.5, things changed for the better. We were then able to use the IDataErrorInfo
interface directly on our business objects, which works like this:
public class Person : IDataErrorInfo
{
private int age;
public int Age
{
get { return age; }
set { age = value; }
}
public string Error
{
get
{
return null;
}
}
public string this[string name]
{
get
{
string result = null;
if (name == "Age")
{
if (this.age < 0 || this.age > 150)
{
result =
"Age must not be less than 0 or greater than 150.";
}
}
return result;
}
}
}
And the XAML (binding code really) would look something like this:
<Binding Source="{StaticResource data}" Path="Age"
UpdateSourceTrigger="PropertyChanged"
ValidatesOnDataErrors="True" />
This is also a tad painful, as we have to write loads of rules in the business objects. I have seen several people try to ease the pain of this mechanism using various techniques such as:
You can read more about the standard WPF validation mechanisms using these links:
The big problems that I personally have with all these standard methods are:
- You can only validate the object that you are binding to. Now, that is OK in some cases, but my business has pretty complicated rules for data, that span across multiple objects. This is where it all falls down using the standard WPF validation techniques. I am totally unaware of how I could cross-validate a field using a value from another object using the standard mechanisms.
- Some companies of mine included don't really gel with the idea of having business rules inside our business object; this seems to bloat the objects somehow. We prefer lean mean lite-weight business objects. Rocky Lhotka CSLA framework advocates the use of business objects holding their own rules, and he is a very, very smart guy, so maybe it is not all that bad. It is down to personal taste, I guess.
However, there is no denying that item 1 above just can not be solved by having each business object hold its own validation rules. I mean, how would you validate a business object if it needed to do so with some knowledge of another object. Even using some of the new WPFism Design Patterns such as MVVM, we may end up with a ViewModel holding a number of business objects that drive a single View. There will be a high probability that there may actually be some cross-business object validation required in these cases.
This led me to have a think about it a bit, and it led me to actually ditch what I had thought of as a good thing; so I ditched the use of the IDataErrorInfo
interface all together. But why did I do that?
So What's Your Idea?
Well, I personally think that validation logic doesn't actually belong in the objects themselves. I decided to move those rules out of the business objects. So where could I do my validating? I have to do it somewhere, right? Right indeed, so what I decided to do was stick to using a couple of WPF goodies such as:
- Model View View Model Design Pattern
INotifyPropertyChanged
, for happy binding
And then I decided to do the following:
- Remove all validation out of my business objects into a central validating object, which knew about all the relevant objects by having access to a ViewModel
- The ViewModel would make itself known to the validator
- The ViewModel would hold objects that drives the View
- The View would bind to the ViewModel
- The ViewModel would hold a list of all validation errors
- A new set of controls were made (I have only done
TextBox
, but the idea applies to any control you choose) that when bound would accept a parameter, which indicated which list of business rules it was interested in seeing validation failures for
That is pretty much it. So how does this look in code?
Well, let's see an example app (this is the demo app attached, but you can change it to suit your actual needs):
Ok, So How Does it Work
Well, quite simply, it works as follows. The actual business objects (Person
classes) simply have properties that notify bindings of changes via the INotifyPropertyChanged
interface mechanism. There is a single ViewModel (Window1ViewModel
) object per window (you may decide to have a number of View Models to govern your page, that is up to you), which holds a number of business objects (Person
classes) that the View (Window1
) needs to bind to. So basically, the View will bind directly to the ViewModel (this is the MVVM pattern). The ViewModel also holds a validator (Window1Validator
) which is responsible for running all business logic for a given ViewModel. As such, the validator (Window1Validator
) needs to know about the ViewModel (Window1ViewModel
), so that it can examine the ViewModel's (Window1ViewModel
) business objects (Person
classes) values, that are currently driving the View (Window1
).
So, what happens next is that the View (Window1
) will at some stage have to validate its contents. For the demo, that moment occurs when a validate button is clicked, which will call a Validate()
method inside the associated ViewModel (Window1ViewModel
). When the ViewModel is asked to validate, it simply passes the call on to its internal validator (Window1Validator
). As the internal ViewModel held validator knows about the ViewModel (Window1ViewModel
), it is just a case of running through whatever business validation logic you want.
When you do run through the business validation logic, a ViewModel (Window1ViewModel
) held ObservableCollection<ValidationFailure>
is added to, where the Key
property of the ValidationFailure
key will be some unique name. This could typically be the name of the property you are validating, such as "Age". The beauty of this approach is that you have access to all the objects that drive the View, which allows cross object validation. This is something that the standard WPF validation framework just doesn't cater for (to the best of my knowledge).
So once we rattle through all the validation logic, we end up with a bunch of broken rules in the form of a ObservableCollection<ValidationFailure>
within the ViewModel (Window1ViewModel
), that can be used to bind to. As each ValidationFailure
key will be some unique name, we can strip out only the ones that match a particular field on the View, which is done by using a ValueConverter
, or choose to view all of the ObservableCollection<ValidationFailure>
for the entire View.
Roughly, that is how it all works, so time for some code.
The Code Examples
So I guess it is time to start examining the code. Well, let's start with the business objects themselves. The demo uses the following business object:
public class Person : DomainObject
{
#region Data
private String firstName = String.Empty;
private String lastName = String.Empty;
private Boolean isAbleToVote = false;
private Int32 age = 0;
#endregion
#region Public Properties
public String FirstName
{
get { return firstName; }
set
{
firstName = value;
NotifyChanged("FirstName");
}
}
public String LastName
{
get { return lastName; }
set
{
lastName = value;
NotifyChanged("LastName");
}
}
public Boolean IsAbleToVote
{
get { return isAbleToVote; }
set
{
isAbleToVote = value;
NotifyChanged("IsAbleToVote");
}
}
public Int32 Age
{
get { return age; }
set
{
age = value;
NotifyChanged("Age");
}
}
#endregion
}
Where this class inherits from a base class called DomainObject
, which looks like this:
[Serializable()]
public abstract class DomainObject : INotifyPropertyChanged
{
#region Data
protected int id;
#endregion
#region Ctor
public DomainObject()
{
}
#endregion
#region Public Properties
public int ID
{
get { return id; }
set
{
id = value;
NotifyChanged("ID");
}
}
#endregion
#region INotifyPropertyChanged Implementation
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void NotifyChanged(params string[] propertyNames)
{
foreach (string name in propertyNames)
{
OnPropertyChanged(new PropertyChangedEventArgs(name));
}
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, e);
}
}
#endregion
}
As you can see, the DomainObject
class simply provides the INotifyPropertyChanged
interface implementation. Next, let us examine the ViewModel code which looks like this for the attached demo code:
public class Window1ViewModel : ViewModelBase
{
#region Data
private Window1Validator window1Validator = null;
private Person currentPerson1 = new Person();
private Person currentPerson2 = new Person();
private ObservableCollection<ValidationFailure>
validationErrors = new ObservableCollection<ValidationFailure>();
private ICommand validateCommand;
#endregion
#region Ctor
public Window1ViewModel()
{
window1Validator = new Window1Validator(this);
validateCommand = new SimpleCommand
{
CanExecuteDelegate = x => true,
ExecuteDelegate = x => Validate()
};
}
#endregion
#region Public Properties
public ICommand ValidateCommand
{
get { return validateCommand; }
}
public ObservableCollection<ValidationFailure> ValidationErrors
{
get { return validationErrors; }
set
{
validationErrors = value;
NotifyChanged("ValidationErrors");
}
}
public Person CurrentPerson1
{
get { return currentPerson1; }
set
{
currentPerson1 = value;
NotifyChanged("CurrentPerson1");
}
}
public Person CurrentPerson2
{
get { return currentPerson2; }
set
{
currentPerson2 = value;
NotifyChanged("CurrentPerson2");
}
}
#endregion
#region Public Methods
public void Validate()
{
window1Validator.Validate();
}
#endregion
}
Again, this class inherits from a base class that offers several useful things such as the INotifyPropertyChanged
interface implementation. It can also be seen that this example ViewModel holds several business objects that will be used by the View to bind to. This ViewModel is then used to bind against in the View, where the DataContext
of the View is set to be this ViewModel. Let us have a look at the View next.
public partial class Window1 : Window
{
private Window1ViewModel window1ViewModel = new Window1ViewModel();
public Window1()
{
InitializeComponent();
this.DataContext = window1ViewModel;
}
}
As you can see, the View is using the Window1ViewModel
ViewModel as its DataContext
, which allows controls on the View to bind to the ViewModel directly. I am not going to bore you with all the XAML for the View, but will just show you a typical binding for one of the ViewModel held business objects.
<!---->
<StackPanel Orientation="Horizontal" Margin="5">
<Label Content="FirstName" Width="100" />
<local:TextBoxEx x:Name="txtFirstName1" Width="150"
Height="25" FontSize="12" FontWeight="Bold"
ValidationErrors="{Binding Path=ValidationErrors, Mode=OneWay,
Converter={StaticResource ValidationErrorsLookupConv},
ConverterParameter='FirstName1'}"
Text="{Binding Path=CurrentPerson1.FirstName, UpdateSourceTrigger=PropertyChanged}"
Foreground="Black" HorizontalAlignment="Center"
HorizontalContentAlignment="Center" />
</StackPanel>
So you can see that we have a specialised TextBox
(TextBoxEx
) which is being bound to the ValidationErrors
property of the current DataContext
of the View (which is really the Window1ViewModel
ViewModel). It can also be seen that we are using a ValueConverter
(ValidationErrorsLookupConverter
) where we feed a parameter value into the ValueConverter
.
To understand that mechanism, we need to first understand how the ValidationErrors
property works within the Window1ViewModel
ViewModel, so let us examine that now.
Basically, what happens is when a new ViewModel (Window1ViewModel
) is constructed, it creates a new validator object (Window1Validator
) which is used to perform all validation on the ViewModel (Window1ViewModel
). The validator object (Window1Validator
) simply runs through a chunk of really boring rules code, and appends a new ValidationFailure
object to the list of ObservableCollection<ValidationFailure>
held within the ViewModel (Window1ViewModel
).
Let us examine the code for the validator object (Window1Validator
); it is pretty dull stuff, but that is the nature of validation code.
public class Window1Validator
{
#region Data
private Window1ViewModel window1ViewModel { get; set; }
#endregion
#region Ctor
public Window1Validator(Window1ViewModel window1ViewModel)
{
this.window1ViewModel = window1ViewModel;
}
#endregion
#region Public Methods
public void Validate()
{
ObservableCollection<ValidationFailure> localValidationErrors=
new ObservableCollection<ValidationFailure>();
#region Validate CurrentPerson1
if (window1ViewModel.CurrentPerson1.Age < 0)
localValidationErrors.Add(
new ValidationFailure("Age1",
"Person 1 Age cant be < 0"));
if (window1ViewModel.CurrentPerson1.Age > 65
&& window1ViewModel.CurrentPerson1.IsAbleToVote)
localValidationErrors.Add(
new ValidationFailure("Age1",
"Person 1 Age, You can't vote > 65"));
if (window1ViewModel.CurrentPerson1.FirstName == String.Empty)
localValidationErrors.Add(
new ValidationFailure("FirstName1",
"Person 1 FirstName can't be empty"));
if (window1ViewModel.CurrentPerson1.LastName == String.Empty)
localValidationErrors.Add(
new ValidationFailure("LastName1",
"Person 1 LastName can't be empty"));
#endregion
#region Validate CurrentPerson2
if (window1ViewModel.CurrentPerson1.Age < 18
&& window1ViewModel.CurrentPerson2.Age == 0)
localValidationErrors.Add(
new ValidationFailure(
"Age2",
"Person 2 Age cant be < 0 if Person1 Age < 18"));
if (window1ViewModel.CurrentPerson2.Age > 65
&& window1ViewModel.CurrentPerson2.IsAbleToVote)
localValidationErrors.Add(
new ValidationFailure("Age2",
"Person 2, You can't vote > 65"));
if (window1ViewModel.CurrentPerson2.FirstName == String.Empty)
localValidationErrors.Add(
new ValidationFailure("FirstName2",
"Person 2 FirstName can't be empty"));
if (window1ViewModel.CurrentPerson2.LastName == String.Empty)
localValidationErrors.Add(
new ValidationFailure("LastName2",
"Person 2 LastName can't be empty"));
#endregion
window1ViewModel.ValidationErrors = localValidationErrors;
}
#endregion
}
The important part to note here is that the ViewModel (Window1ViewModel
) holds a complete list of all the ValidationFailure
s that occurred, and that each ValidationFailure
has a key that can be used to grab only those ValidationFailure
s that are related to a specific TextBox
. Which is exactly what happens within the ValueConverter
(ValidationErrorsLookupConverter
) where we fed a parameter value into the ValueConverter
, which is used to obtain only those ValidationFailure
s that pertain to the TextBox
that has the correct key (value converter parameter value), which will be used to filter the list of all the ValidationFailure
s to only those that match the key (value converter parameter value).
Recall this binding for a single TextBoxEx
within the XAML:
I am not going to bore you with all the XAML for the View, but will just show you a typical binding for one of the ViewModel held business objects.
<!---->
<StackPanel Orientation="Horizontal" Margin="5">
<Label Content="FirstName" Width="100" />
<local:TextBoxEx x:Name="txtFirstName1" Width="150"
Height="25" FontSize="12" FontWeight="Bold"
ValidationErrors="{Binding Path=ValidationErrors, Mode=OneWay,
Converter={StaticResource ValidationErrorsLookupConv},
ConverterParameter='FirstName1'}"
Text="{Binding Path=CurrentPerson1.FirstName, UpdateSourceTrigger=PropertyChanged}"
Foreground="Black" HorizontalAlignment="Center"
HorizontalContentAlignment="Center" />
</StackPanel>
See the ValidationErrors
part, and the ConverterParameter='FirstName1'
, that is the part that enables us to grab only those validation errors that we need for the current TextBox
.
This may become clearer when you see the ValueConverter
(ValidationErrorsLookupConverter
).
[ValueConversion(typeof(ObservableCollection<ValidationFailure>),
typeof(ObservableCollection<ValidationFailure>))]
public class ValidationErrorsLookupConverter : IValueConverter
{
#region IValueConverter implementation
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
if (value != null)
{
ObservableCollection<ValidationFailure> validationLookup =
(ObservableCollection<ValidationFailure>)value;
List<ValidationFailure> failuresForKey =
(
from vf in validationLookup
where vf.Key.Equals(parameter.ToString())
select vf
).ToList();
return new ObservableCollection<ValidationFailure>(failuresForKey);
}
return null;
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new NotImplementedException("Can't convert back");
}
#endregion
}
The last step was in having a specialised TextBox
(TextBoxEx
) which knows how to show its own list of validation errors in a popup. Now, this is just what I chose to do, you may think up something different, but that is what I chose to do.
I should also point out that generally I am against creating specialised controls that inherit from System.Windows.Controls
, as most extra behavior can be added via attached properties, but this just didn't seem to fit in this case, as I wanted the popup within the XAML to be triggered by mouse moves etc.
Josh Smith will more than likely say, oh you just do this, then that, and there you have it. I will just smile back at the man and go "That's nice to know, thanks Josh".
Since writing this article, a reader, "SE_GEEK", has proposed a new control which inherits from ContentControl
instead of TextBox
; you can read more about that approach using the forum link: GlobalWPFValidation.aspx?msg=2895494#xx2895494xx.
Anyway, here is the code for the specialised TextBox
(TextBoxEx
) which knows how to show its own list of ValidationFailure
s within a popup. I have only done a specialised TextBox
(TextBoxEx
) for the attached demo code, but you could apply the same idea to any of the standard controls. At work, we have actually done that with CheckBox
/ComboBox
and other controls, and it works very well.
[TemplatePart(Name = "PART_PopupErrors", Type = typeof(Popup))]
[TemplatePart(Name = "PART_Close", Type = typeof(Button))]
public class TextBoxEx : TextBox
{
#region Data
private Popup errorPopup = null;
private Button cmdClose = null;
#endregion
#region Ctor
public TextBoxEx() : base()
{
this.MouseEnter += (s, e) =>
{
if (errorPopup != null && !IsValid)
{
errorPopup.IsOpen = false;
errorPopup.IsOpen = true;
}
};
}
#endregion
#region Private Methods
private void ErrorPopup_MouseUp(object sender,
System.Windows.Input.MouseButtonEventArgs e)
{
if (errorPopup.IsOpen)
errorPopup.IsOpen = false;
}
private void CmdClose_Click(object sender, RoutedEventArgs e)
{
Button button = e.OriginalSource as Button;
Popup pop = button.Tag as Popup;
if (pop != null)
pop.IsOpen = false;
}
#endregion
#region DPs
#region IsValid
public static readonly DependencyProperty IsValidProperty =
DependencyProperty.Register("IsValid", typeof(bool), typeof(TextBoxEx),
new FrameworkPropertyMetadata((bool)true));
public bool IsValid
{
get { return (bool)GetValue(IsValidProperty); }
set { SetValue(IsValidProperty, value); }
}
#endregion
#region NewStyle
public static readonly DependencyProperty NewStyleProperty =
DependencyProperty.Register("NewStyle", typeof(Style), typeof(TextBoxEx),
new FrameworkPropertyMetadata((Style)null,
new PropertyChangedCallback(OnNewStyleChanged)));
public Style NewStyle
{
get { return (Style)GetValue(NewStyleProperty); }
set { SetValue(NewStyleProperty, value); }
}
private static void OnNewStyleChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
((TextBoxEx)d).Style = e.NewValue as Style;
}
#endregion
#region ValidationErrors
public static readonly DependencyProperty ValidationErrorsProperty =
DependencyProperty.Register("ValidationErrors",
typeof(ObservableCollection<ValidationFailure>),
typeof(TextBoxEx),
new FrameworkPropertyMetadata(
(ObservableCollection<ValidationFailure>)null,
new PropertyChangedCallback(OnValidationErrorsChanged)));
public ObservableCollection<ValidationFailure> ValidationErrors
{
get { return (ObservableCollection<ValidationFailure>)
GetValue(ValidationErrorsProperty); }
set { SetValue(ValidationErrorsProperty, value); }
}
private static void OnValidationErrorsChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
ObservableCollection<ValidationFailure> failures =
e.NewValue as ObservableCollection<ValidationFailure>
TextBoxEx thisObj = (TextBoxEx)d;
if (failures == null)
thisObj.IsValid = true;
else thisObj.IsValid = failures.Count == 0 ? true : false;
}
#endregion
#endregion
#region Overrides
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
errorPopup = this.Template.FindName("PART_PopupErrors", this) as Popup;
if (errorPopup != null)
errorPopup.MouseUp += new MouseButtonEventHandler(ErrorPopup_MouseUp);
cmdClose = this.Template.FindName("PART_Close", this) as Button;
if (cmdClose != null)
cmdClose.Click += new RoutedEventHandler(CmdClose_Click);
}
#endregion
}
And, here is the XAML that provides an actual Style
for the TextBoxEx
control:
<Style TargetType="{x:Type local:TextBoxEx}">
<Setter Property="SnapsToDevicePixels" Value="true"/>
<Setter Property="OverridesDefaultStyle" Value="true"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:TextBoxEx}">
<Grid>
-->
<Popup x:Name="PART_PopupErrors"
PlacementTarget="{Binding ElementName=Bd}"
Placement="Relative"
AllowsTransparency="True"
PopupAnimation="Slide"
HorizontalOffset ="20"
StaysOpen="False"
VerticalOffset="20">
<Border HorizontalAlignment="Stretch"
BorderBrush="WhiteSmoke"
Background="Red"
Margin="0"
Width="250"
VerticalAlignment="Stretch"
Height="120"
Opacity="0.97"
BorderThickness="2"
CornerRadius="3" >
<Border HorizontalAlignment="Stretch"
BorderBrush="Red"
Background="White"
Margin="0"
VerticalAlignment="Stretch"
Opacity="1"
BorderThickness="5"
CornerRadius="0" >
......
......
......
......
<ScrollViewer Grid.Row="1" Margin="0"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<ItemsControl Margin="0"
BorderThickness="0"
ItemsSource="{Binding
RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type local:TextBoxEx}},
Path=ValidationErrors}"/>
</ScrollViewer>
</Grid>
</Border>
</Border>
</Popup>
-->
<Border SnapsToDevicePixels="true" x:Name="Bd"
Height="{TemplateBinding Height}"
Width="{TemplateBinding Width}"
VerticalAlignment="Center"
Background="{TemplateBinding Background}"
CornerRadius="2"
BorderBrush="Black"
BorderThickness="2">
<ScrollViewer x:Name="PART_ContentHost"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
TextElement.FontSize="{TemplateBinding FontSize}"
VerticalAlignment="Center"
VerticalContentAlignment="Center"
Margin="2,0,0,0"/>
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsValid" Value="False">
<Setter Property="BorderBrush"
TargetName="Bd" Value="Red"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
And that is pretty much it. With this mechanism, we are able to perform pretty much any cross business object validation we like for the current View. And to prove that, here are a couple of screenshots:
Here is a Single pop for a single TextBoxEx
object:
And here is what I get for all the errors for the current View:
That's it
That is all I have to say. I hope this article has helped you a little. If you liked it, could you please be kind enough to leave a vote or a message? Thanks.
Amendment
Since publishing this article, a colleague (Colin Eberhardt) has informed me that as of .NET 3.5 SP1, this is actually possible using the standard WPF mechanisms of BindingGroups. As .NET 3.5 SP1 is such a monster, I am not that surprised I missed this. Anyway, Colin has written an excellent post on this, and you can read all about it over at his blog. Here is a link: BindingGroups for Total View Validation.