Saturday, 19 November 2011

Data Validation in WPF

What we want to do is a simple entry form for an e-mail address. If the user enters an invalid e-mail address, the border of the textbox gets red and the tooltip is showing the reason.

Implementing a ValidationRule (.NET 3.0 style)

In this example I am implementing an generic validation rule that takes a regular expression as validation rule. If the expression matches the data is treated as valid.
 
/// <summary>
/// Validates a text against a regular expression
/// </summary>
public class RegexValidationRule : ValidationRule
{
    private string _pattern;
    private Regex _regex;
 
    public string Pattern
    {
        get { return _pattern; }
        set
        {
            _pattern = value;
            _regex = new Regex(_pattern, RegexOptions.IgnoreCase);
        }
    }
 
    public RegexValidationRule()
    {
    }
 
    public override ValidationResult Validate(object value, CultureInfo ultureInfo)
    {
        if (value == null || !_regex.Match(value.ToString()).Success)
        {
            return new ValidationResult(false, "The value is not a valid e-mail address");
        }
        else
        {
            return new ValidationResult(true, null);
        }
    }
}
 
 
First thing I need to do is place a regular expression pattern as string to the windows resources
 
<Window.Resources>
        <sys:String x:Key="emailRegex">^[a-zA-Z][\w\.-]*[a-zA-Z0-9]@
        [a-zA-Z0-9][\w\.-]*[a-zA-Z0-9]\.[a-zA-Z][a-zA-Z\.]
        *[a-zA-Z]$</sys:String>    
</Window.Resources>
 
 

Build a converter to convert ValidationErrors to a multi-line string

The following converter combines a list of ValidationErrors into a string. This makes the binding much easier. In many samples on the web you see the following binding expression:
{Binding RelativeSource={RelativeSource Self},Path=(Validation.Errors)[0].ErrorContent}
This expression works if there is one validation error. But if you don't have any validation errors the data binding fails. This slows down your application and causes the following message in your debug window:
System.Windows.Data Error: 16 : Cannot get ‘Item[]‘ value (type ‘ValidationError’) from ‘(Validation.Errors)’ (type ‘ReadOnlyObservableCollection`1′). BindingExpression:Path=(0).[0].ErrorContent; DataItem=’TextBox’...
The converter is both, a value converter and a markup extension. This allows you to create and use it at the same time.
 
[ValueConversion(typeof(ReadOnlyObservableCollection<ValidationError>), typeof(string))]
public class ValidationErrorsToStringConverter : MarkupExtension, IValueConverter
{
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return new ValidationErrorsToStringConverter();
    }
 
    public object Convert(object value, Type targetType, object parameter, 
        CultureInfo culture)
    {
        ReadOnlyObservableCollection<ValidationError> errors =
            value as ReadOnlyObservableCollection<ValidationError>;
 
        if (errors == null)
        {
            return string.Empty;
        }
 
        return string.Join("\n", (from e in errors 
                                  select e.ErrorContent as string).ToArray());
    }
 
    public object ConvertBack(object value, Type targetType, object parameter, 
        CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
 
 

Create an ErrorTemplate for the TextBox

Next thing is to create an error template for the text box.
 
<ControlTemplate x:Key="TextBoxErrorTemplate" TargetType="Control">
    <Grid ClipToBounds="False" >
        <Image HorizontalAlignment="Right" VerticalAlignment="Top" 
               Width="16" Height="16" Margin="0,-8,-8,0" 
               Source="{StaticResource ErrorImage}" 
               ToolTip="{Binding ElementName=adornedElement, 
                         Path=AdornedElement.(Validation.Errors), 
                         Converter={k:ValidationErrorsToStringConverter}}"/>
        <Border BorderBrush="Red" BorderThickness="1" Margin="-1">
            <AdornedElementPlaceholder Name="adornedElement" />
        </Border>
    </Grid>
</ControlTemplate>
 

The ValidationRule and the ErrorTemplate in Action

Finally we can add the validation rule to our binding expression that binds the Text property of a textbox to a EMail property of our business object.
 
<TextBox x:Name="txtEMail" Template={StaticResource TextBoxErrorTemplate}>
    <TextBox.Text>
        <Binding Path="EMail" UpdateSourceTrigger="PropertyChanged" >
            <Binding.ValidationRules>
                <local:RegexValidationRule Pattern="{StaticResource emailRegex}"/>
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>
 
 

How to manually force a Validation

If you want to force a data validation you can manually call UpdateSource() on the binding expression. A useful scenario could be to validate on LostFocus() even when the value is empty or to initially mark all required fields. In this case you cann call ForceValidation() in the Loaded event of the window. That is the time, when the databinding is established.

The following code shows how to get the binding expression from a property of a control.

 
private void ForceValidation()
{
  txtName.GetBindingExpression(TextBox.TextProperty).UpdateSource();    
}
 
 

No comments:

Post a Comment