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:
this is what I am exactly looking for..Thanks for your post.
this was very helpful. Thank yout
Excellent article, Muhammad! Well stated, comprehensive, and informational. I wish many others wrote as you did!
Post a Comment