Sunday, July 18, 2010

WPF Validation - Using Validation Rules in BindingGroup

This post is part of series of discussions about different validation techniques in WPF. In this post we will be discussing about using Validation rules with Binding Groups. WPF allows us to group bindings in the form of a Binding Group. It allows to associate Validation rules with these binding groups. After validation is done, it provides us notification in the form of an event which can be used for notifying the user about failed validation. We can mark the controls with invalid values or notify the user in the form of a message box or whatever we want to do as a result of passed / failed validation. This event is fired as a result of both new validation error or old validation errors which are now passed because the user has entered new valid values.

Simple Example:
Let's create a window with three text boxes and a button. These three text boxes are used for entering Name, Age and City by the user. Our validation rule suggest that for certain cities, providing Age information is mandatory. The validation logic should be executed when the user clicks the Validate button.
<Window x:Class="wpf_validation.Window4"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:wpf_validation"
Title="Window4" Height="300" Width="641" Validation.Error="Window_Error">
<Window.BindingGroup>
<BindingGroup NotifyOnValidationError="True">
<BindingGroup.ValidationRules>
<local:MyValidationRule ValidationStep="ConvertedProposedValue" />
</BindingGroup.ValidationRules>
</BindingGroup>
</Window.BindingGroup>
<Grid>
<TextBox Height="22" Margin="97,27,97,0" Name="textBox1" VerticalAlignment="Top" Text="{Binding Name}" />
<Label Height="28" HorizontalAlignment="Left" Margin="21,25,0,0" VerticalAlignment="Top" Width="70">Name</Label>
<TextBox Height="22" Margin="97,55,97,0" Name="textBox2" VerticalAlignment="Top" Text="{Binding Age}" />
<Label Height="28" HorizontalAlignment="Left" Margin="21,53,0,0" VerticalAlignment="Top" Width="70">Age</Label>
<TextBox Height="22" Margin="97,83,97,0" Name="textBox3" VerticalAlignment="Top" Text="{Binding City}"/>
<Label Height="28" HorizontalAlignment="Left" Margin="21,81,0,0" VerticalAlignment="Top" Width="70">City</Label>
<Button HorizontalAlignment="Left" Margin="97,111,0,126" Name="button1" Width="118" Click="button1_Click">Validate</Button>
</Grid>
</Window>

WPF allows us to define various binding group within the same control / window. In the case of different binding groups within the same control, we need to specify the binding group name with each binding which we want to be validated with that group. Since ours is a simple example, we have just created one Binding group for the whole window. Setting NotifyOnValidationError as true allows WPF runtime to fire Validation.Error event specified. Let's have a look at the code behind of our window. Here we have specified Window4ViewModel as datacontext of the window. We also have specified click event of Validate button where we have checked for validation result by calling CommitEdit() method on binding group. This returns true for passed validation and false otherwise. For passed validation we are again setting the binding group as being edited. We have also provided the definition of Window_Error event as specified in the XAML above.

We have specified business rule, MyValidationRule, for this Biding group. With the validation rules, we can also specify when we want these validation rules to be executed. This is specified using ValidationStep property of Validation rule. Setting it as ConvertedProposedValue would be evaluating this rule after the entered value is converted. The other options are RawProposedValue, UpdatedValue and CommittedValue.
public partial class Window4 : Window
{
Window4ViewModel _vm;
public Window4()
{
InitializeComponent();
_vm = new Window4ViewModel();
this.DataContext = _vm;
this.BindingGroup.BeginEdit();
}

private void button1_Click(object sender, RoutedEventArgs e)
{
if (this.BindingGroup.CommitEdit())
{
this.BindingGroup.BeginEdit();
}
}

private void Window_Error(object sender, ValidationErrorEventArgs e)
{
if (e.Action == ValidationErrorEventAction.Added)
{
if (e.Error.RuleInError.GetType() == typeof(MyValidationRule))
{
BindingGroup bg = (BindingGroup)e.Error.BindingInError;
BindingExpression BindingExprssionAge = (BindingExpression)bg.BindingExpressions[1];
BindingExpression BindingExprssionCity = (BindingExpression)bg.BindingExpressions[2];
ExceptionValidationRule dummyRule = new ExceptionValidationRule();
Validation.MarkInvalid(BindingExprssionAge, new ValidationError(dummyRule, "Age is Invalid!"));
Validation.MarkInvalid(BindingExprssionCity, new ValidationError(dummyRule, "City is invalid!"));
//MessageBox.Show(e.Error.ErrorContent.ToString());

}
}
else if (e.Action == ValidationErrorEventAction.Removed)
{
if (e.Error.RuleInError.GetType() == typeof(MyValidationRule))
{
BindingGroup bg = (BindingGroup)e.Error.BindingInError;
BindingExpression BindingExprssionAge = (BindingExpression)bg.BindingExpressions[1];
BindingExpression BindingExprssionCity = (BindingExpression)bg.BindingExpressions[2];
Validation.ClearInvalid(BindingExprssionAge);
Validation.ClearInvalid(BindingExprssionCity);
}
}
}
}

The Window_Error is fired when a validation results in an error or validation is passed after failing it last time. If the validation has not resulted in failure when last time this validation was executed then WPF runtime does not fire this event. Firing it once when a validation error is removed allows the clean up activities to be executed like clearing the invalid messages. This also provides the error message as specified when ValidationRule is executed resulting in a failed validation. We can get this value with e.Error.ErrorContent.

Below we have provided the definition of Window4ViewModel which was specified as the data context of the window. We have specified three dependency properties Name, Age and City.
class Window4ViewModel : System.Windows.DependencyObject
{
public string Name
{
get { return (string)GetValue(NameProperty); }
set { SetValue(NameProperty, value); }
}
public static DependencyProperty NameProperty = DependencyProperty.Register("Name", typeof(string), typeof(Window4ViewModel));

public int? Age
{
get { return (int?)GetValue(AgeProperty); }
set { SetValue(AgeProperty, value); }
}
public static DependencyProperty AgeProperty = DependencyProperty.Register("Age", typeof(int?), typeof(Window4ViewModel));

public string City
{
get { return (string)GetValue(CityProperty); }
set { SetValue(CityProperty, value); }
}
public static DependencyProperty CityProperty = DependencyProperty.Register("City", typeof(string), typeof(Window4ViewModel));

}

Finally we need to provide the definition of the business rule, MyValidationRule. The ValidationRule is evaluated by its Validate method. For passed validation, this should return ValidationResult with isValid as true. For failed validation, this should result in a ValidationResult with isValid set as false. Additionally, it can also provide the error message.

The data context is provided as the first item of the parameter value. After getting values from data context, we can run our validation logic and return the appropriate validation result.
public class MyValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
ValidationResult validationResult = new ValidationResult(true, null);

BindingGroup bg = value as BindingGroup;
Window4ViewModel vm = bg.Items[0] as Window4ViewModel;

object objAge;
object objCity;

bg.TryGetValue(vm, "Age", out objAge);
bg.TryGetValue(vm, "City", out objCity);

int? Age = (int?)(objAge == DependencyProperty.UnsetValue? null : objAge);
string City = (string)objCity;

if (new string[] { "Karachi", "Shanghai", "New York" }.Contains(City))
{
if (!Age.HasValue)
validationResult = new ValidationResult(false, "For this city, specification of age is mandatory");
}
return validationResult;
}
}

Note:
Validation rules allow to execute the validation logic at different steps controlled through ValidationStep. It can be executed before even the value is converted by the converter, after it is converted and before it is committed to the source or after it is copied to the source. This provides the developer great control over the flow of the application.

3 comments:

ReDs said...

this is what I am exactly looking for..Thanks for your post.

Anonymous said...

this was very helpful. Thank yout

Tom Battista said...

Excellent article, Muhammad! Well stated, comprehensive, and informational. I wish many others wrote as you did!