Click here to Skip to main content
14,366,827 members

Binding using IDataErrorInfo with Attributes in WPF

Rate this:
4.92 (14 votes)
Please Sign up or sign in to vote.
4.92 (14 votes)
13 Sep 2015CPOL
This article presents an implementation to support IDataErrorInfo with attributes on the properties for WPF. It also covers other aspects of displaying error information using WPF.

Overview

In WPF, the interface IDataErrorInfo can be used in a ViewModel class to provide automatic error notifications to the user. There are also attributes that can be used on properties to validate user input, but the two are not integrated in WPF out of the box. Together they can create a highly maintainable way to display errors with minimal effort.

Introduction

I recently started a new project with two developers who had only started working with WPF a few months; they had also inherited a code base that they had to work with. I was given the task to create a simple form that is displayed when a button in a grid is pressed. I have used this task to create the infrastructure so as to make development and maintenance easier. One of these infrastructure pieces is the abstract class for the ViewModel. Initially I implemented this abstract class to support INotifyPropertyChanged. Once I had the basic functionality working, I started working on the IDataErrorInfo.

I had done something like this before, but had used the Enterprise Library from Microsoft, Interestingly enough, the existing ViewModel already had validation attributes associated with the properties, just no interface to allow the view to display the errors.

I had a general Idea that I wanted a standard to handle validations, and it seemed that IDataErrorInfo and the use of validation attributes was the way to go. The interface for IDataErrorInfo is as follows:

public interface IDataErrorInfo
{
       // Summary:
       //     Gets an error message indicating what is wrong with this object.
       //
       // Returns:
       //     An error message indicating what is wrong with this object. The default is
       //     an empty string ("").
       string Error { get; }

       // Summary:
       //     Gets the error message for the property with the given name.
       //
       // Parameters:
       //   columnName:
       //     The name of the property whose error message to get.
       //
       // Returns:
       //     The error message for the property. The default is an empty string ("").
       string this[string columnName] { get; }
}

In the Microsoft documentation, the implementation has a huge case statement to find errors for a property—this is a bad smell. I know that I really did not want to try to maintain this case statement. Another bad smell is the use of string to specify the property.

Did some searching and found something on the web which I hoped I would to use more or less intact. It used the validation property attributes that derived from the ValidationAttribute class and the IDataErrroInfo interface. I started to do some clean up and found that I was very dissatisfied with this solution. My biggest issue was performance—I binding to the Error property and the implementation had a LINQ statement for the Error property that would process all the validations each time. The INotifyPropertyChange will not trigger a refresh unless that is a noticeable change, like a change in its address. Because the getter was dynamic due to the LINQ statement, it could result in recalculations many times even when not required. What I wanted was to maintain a memory of the previous calculation, and compare the two, and if they are different, only then recalculate the Error property.

The design started with used a dictionary using a key of the property name and a value being basically an array of validators. I replaced this array with a class to contain the previous value and the array of validators, and they moved most of the processing into this class. I then replaced the dictionary with a class, and was able to reduce the body of the square-bracket operator for determining the validation of a property to a single line.

Implementation

The DataErrorInfoAttributeValidator class is initialized in the constructor with the class itself, and a function that is executed when there is a change in the validation, and returns a string of all the errors separated a line return. The constructor is responsible for getting the properties using reflection for a collection validation item class and putting the instances of this class into a dictionary that has a key of the property name:

  public DataErrorInfoAttributeValidator(IDataErrorInfo classInstance, 
                               Action<string> updateErrorFunction)
  {
   _classInstance = classInstance;
   _updateErrorFucntion = updateErrorFunction;
   var validators = classInstance.GetType()
       .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty)
       .Select(i => new PropertyValidator(i, classInstance)).Where(j => j.Validators.Any());
   _propertyValidations = validators.ToDictionary(p => p.PropertyName, p => p);
  }

The class instance is required by the process to find the properties, and is required in the PropertyValidator instances to find the values of the properties.

The PropertyValidator class is where these validator attributes of type PropertyValidator for the property are maintained, and this class also maintains the property that contains the last array of error notification strings. These strings are used to compare to the strings found when validation for the property is recalculated. The constructor of this class actually extracts the validators and maintains a collection of validators for the property.

   public PropertyValidator(PropertyInfo propertyInfo, object classInstance)
   {
    _propertyInfo = propertyInfo;
    _classInstance = classInstance;
    Validators = propertyInfo.GetCustomAttributes().Where(i => i is ValidationAttribute)
          .Cast<ValidationAttribute>().ToArray();
    Validators.Where(j => j is MethodValidator).Cast<MethodValidator>().ToList()
          .ForEach(i => i.SetClassInstance(_classInstance));
    ErrorMessages = new string[0];
   }

After the Validators are found, then each of the found Validators are checked to see if they are type MethodValidator. This is because the MethodValidator class needs to have an instance of the class available so that it can use reflection to find the Method that is provided the class as a string (an instance cannot be specified since this is specified as an Attribute.

As can be seen the ErrorMessages for the class instance is initially set to an empty collection so that the null condition does not need to be handled.

To validate a property, the Validate method of the DataErrorInfoAttributeValidator class is called with the name of the property:  

  public string Validate(string propertyName)
  {
   if (_propertyValidations.ContainsKey(propertyName))
   {
    var propertyValidation = _propertyValidations[propertyName];
    if (propertyValidation.CheckValidations())
    {
     Errors = _propertyValidations.SelectMany(i => i.Value.ErrorMessages).ToArray();
     _updateErrorFucntion?.Invoke(string.Join(Environment.NewLine, Errors));
    }
    return string.Join(Environment.NewLine, propertyValidation.ErrorMessages);
   }
   return string.Empty;
  }

This method finds the validation instance in the dictionary, and calls CheckValidations method of that instance. The return value of this method indicates if the validation issues are the same as the previous time the method was called. If there is a difference, then the errors array is recalculated and the function that was passed when this class was initialized is called with a string that indicates all the errors for the class. The errors for the property are then returned (and empty string if no errors).

public bool CheckValidations()
{
 var newValue = _propertyInfo.GetValue(_classInstance);

 var newErrorMessages = Validators.Where(i => !i.IsValid(newValue))
     .Select(j => GetErrorMessage(_propertyInfo.Name, j))
     .OrderBy(i => i).ToArray();
 var isChanged = !StringArraysEqual(newErrorMessages, ErrorMessages);
 ErrorMessages = newErrorMessages;
 return isChanged;
}

In the CheckValidations method, reflection is used to get the value of the property. The IsValid method of the validator indicates the validity of the value, and the Name property supplies the error messages associated with the validator.  The CheckValidation method only returns true is the validations have changed since the last call, that way, the error messages are only regenerated when there has been a change in validation. A collection of error message strings is saved in the ErrorMessages property for the DataErrorInfoAttributeValidator.Validate method to use to calculate the return value. There is an error message in this collection for each failed validation. The Validate method then joins these messages together with a return so that each error is on a different line. All the error messages for all the properties that have failed validation are then combined, and the Error property of the ViewModel is update with a new string containing the error messages joined with return characters.

You will notice that the GetErrorMessage method is used to get the error message, and the reason for this is that for the ValidationAttribute, the error message string is not required when specifying the attribute for the property. It seemed like instead of requiring that a message be provided, a message could be created using String.Format and the name of the field:

   private static string GetErrorMessage(string name, ValidationAttribute validator)
   {
    if (!string.IsNullOrWhiteSpace(validator.ErrorMessage)) return validator.ErrorMessage;
    if (validator is RequiredAttribute) return
      $"{FromCamelCase(name)} is required";
    if (validator is RangeAttribute) return
      $"{FromCamelCase(name)} is not between the range of {((RangeAttribute)validator).Minimum} and {((RangeAttribute)validator).Maximum}";
    if (validator is MaxLengthAttribute) return
     $"{name} text cannot be longer than {((MaxLengthAttribute) validator).Length} characters";
    throw new NotImplementedException(
     $"Validator {validator.GetType().Name} does have default error message or specific error message for property {name}");
   }

Only the Required attribute has a default message, and that was all that was required in the initial release of the initial form that I created; as I refine the designs I will add support for additional attributes.

The Method Validator

The MethodValidationAttribute was created because there was a need to do some dynamic testing of values. It takes a string which specifies the method to use in validation. There currently in no separate way to specify the error message to be generated since I figure that there would be a different validation method for each case, but I can see there may be a good reason to include this capability.

public class MethodValidationAttribute
{
    public MethodValidationAttribute(string methodName)
    {
        MethodName = methodName;
    }

    private MethodInfo _validationMethod;
    private object _classInstance;

    public void SetClassInstance(object instance)
    {
        _classInstance = instance;
        if (_validationMethod == null)
        {
            _validationMethod = instance.GetType()
                .GetMethod(MethodName, BindingFlags.Public | BindingFlags.NonPublic
                         | BindingFlags.Instance);
            Debug.Assert(_validationMethod != null,
             $"The method {MethodName} could not be found in class {_classInstance.GetType().Name}");
            Debug.Assert(_validationMethod.ReturnParameter?.ParameterType == typeof(string),
             $"The return type of method {MethodName} is of type
        {_validationMethod.ReturnParameter.ParameterType.Name},
        not type string in class {_classInstance.GetType().Name}");
        }
    }

    public string MethodName { get; }

    public override bool IsValid(object value)
    {
        if (_validationMethod == null) return true;
        ErrorMessage = (string)_validationMethod.Invoke(_classInstance, null);
        return (string.IsNullOrWhiteSpace(ErrorMessage));
    }
}

The MethodValidationAttribute needs the name of the method to use for validation, and an instance of the class. The method name is passed as an argument when in the decorator for the property to be checked. The class instance is provided when property is validated. To save processing, a pointer to the validation method is maintained so the method only has to be found the first time the property is validated. When the IsValid is called, the validation method is executed using Reflection. The method needs to be written to return a non-empty string or null if there is no error, otherwise a error message to be associated with the error notification. To do this the IsValid method assigns the ErrorMessage property the return value of the method found using Reflection.

I have found that there is a problem with the MethodValidationAttribute, and that is clearly seen in the sample. If two properties being validated have an interdependence, then can get in the situation that an error is being shown for one field, and is corrected in another field but the error will still show in the first field, also if an value is changed to an error condition in a property, it will not be shown in the dependent property.

The Required Enum Attribute Validator

There is another custom validator included in the solution, the RequiredEnumAttribute. This had to be created because there were cases where an enumeration was bound to a ComboBox and initial value was not and there was no "0" enumeration defined. The framework RequiredAttribute does not catch this problem, so I created this class to handle this situation. If you have problem where the RequiredAttribute is not working for an enumerated binding, try this decorator. See Required Enumeration Validation Attribute.

Interfacing to the DataErrorInfoAttributeValidator

There are to areas that need to be programmed to use the DataErrorInfoAttributeValidator class: the ViewModel and the View.

The ViewModel class implements the properties from the IDataErrorInfo interface. The DataErrorInfoAttributeValidator class needs to be instantiated, and this probably should be done as part of the class initialization since there may be errors that need to be communicated to the user because of initial errors in the model, including when creating a new instance. The Error property only needs to implement the INotifyPropertyChanged when the Error value is changed so that View will know that the Error value has changed. There are a number of ways to use and instantiate this class, but I have the class initialized on first use instead of as part of the initialization because it means that only have the overhead if an attempt is made to use the IDataErrorInfo interface:

private DataErrorInfoAttributeValidator _propertyValidations;
private string _error;

private DataErrorInfoAttributeValidator GetValidators()
{
   return _propertyValidations ?? (_propertyValidations
         = new DataErrorInfoAttributeValidator(this));
}

#region IDataErrorInfo

public string this[string propertyName]
{
   get { return GetValidators().Validate(propertyName); }
}

public string Error
{
       get { GetValidators(); return _error; }
       set { Set(ref _error, value); }
}

endregion

Using the DataErrorInfoAttributeValidator class makes implementation for IDataErrorInfo really cleaned up the ViewModel—I actually have this code in an abstract class that I use for a lot of the ViewModels in the project—the class that implements both INotifyPropertyChanged and IDataErrorInfo. In the project I am working in, there is no framework that is being used, so do not have any of the base classes to support WPF development that most significant projects use. I know that there are still some issues with the code, but if you do not use a framework to help with INotifyPropertyChanged, then may want to borrow this code.

XAML

The next part is what needs to be done in the View to bind to the error information:

<TextBox Text="{Binding Value,
         UpdateSourceTrigger=PropertyChanged,
         ValidatesOnDataErrors=True}" />

I have only shown the binding for the Text on the TextBox. The UpdateSourceTrigger set to PropertyChanged is required to ensure that validation is done on each keystroke. This obviously will decrease performance, but only when the user changes the value, so performance impact is minimal. The ValidatesOnDataErrors=True is required to cause the validation checking to occur.

I had implemented this in the applications I had worked on, and had gotten the red borders on a TextBox with an error and the tool tip with the error description, but when I tried the same on my sample, I only got the red box with no tool tip.  This was because straight WPF, without any themes, will display a red box around a control, but will not provide any error indication. What is needed to display the error in a ToolTip is to provide a ControlTemplate for the Validation.ErrorTemplate property. I have created a Style for this in the example:

<Style x:Key="Version1" TargetType="{x:Type Control}">
 <Setter Property="Validation.ErrorTemplate">
   <Setter.Value>
      <ControlTemplate x:Name="TextErrorTemplate">
        <Border BorderBrush="Red" BorderThickness="2">
           <AdornedElementPlaceholder/>
        </Brder>
      </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).CurrentItem}"/>
    </Trigger>
  </Style.Triggers>
</Style>

Note that originally I had "Path=(Validation.Errors)[0].ErrorContent" and discovered that this was causing issues, and changed to "Path=(Validation.Errors).CurrentItem", which solved the problem. I found this solution at Binding to (Validation.Errors)[0] without Creating Debug Spew.

The second group of TextBox has this style applied, while the first does not. If Theme is being used, then in all likelihood you will not need an ErrorTemplate to display the ErrorAdorner with the errors in the ToolTip. You may still want to change the way errors are displayed. The following is code that I found in a Theme, and is probably a good way to define the ErrorTemplate:

<ControlTemplate x:Key="ValidationTooltipTemplate">
  <Grid SnapsToDevicePixels="True"
        VerticalAlignment="Top">
    <Border Background="Transparent"
            HorizontalAlignment="Right"
            VerticalAlignment="Top"
            Width="3" Height="3"/>
    <AdornedElementPlaceholder x:Name="Holder"/>
    <Border BorderBrush="{StaticResource ValidationTooltipOuterBorder}"
            BorderThickness="1"
            CornerRadius="{StaticResource ValidationTooltip_CornerRadius}"/>
    <Path Data="M2,1 L6,1 6,5 Z"
          Fill="{StaticResource ValidationInnerTick}"
          Width="7" Height="7"
          HorizontalAlignment="Right"
          VerticalAlignment="Top"/>
    <Path Data="M0,0 L2,0 7,5 7,7 Z"
          Fill="{StaticResource ValidationOuterTick}"
          Width="7" Height="7"
          HorizontalAlignment="Right"
          VerticalAlignment="Top"/>
    <ToolTipService.ToolTip>
      <ToolTip x:Name="PART_ToolTip"
               DataContext="{Binding RelativeSource={RelativeSource Mode=Self},
               Path=PlacementTarget.DataContext}"
               Template="{StaticResource ErrorTooltipTemplate}"
               Placement="Right"/>
    </ToolTipService.ToolTip>
  </Grid>
</ControlTemplate>

This same template can now be used to define the ErrorTemplate for multiple controls. I have the code in the app.config file, and is used in the last group of TextBoxes. The way this is then used in a Style used:

<Style x:Key="Version4"

       TargetType="{x:Type Control}">
  <Setter Property="Validation.ErrorTemplate"

          Value="{StaticResource ValidationTooltipTemplate}" />
</Style>

You will also note that there is a Template for the used for the ToolTIp. In this Template, the Background for the ToolTip is defined as a red, and the Foreground is a white.

BaseViewModel

To make it real easy to incorporate this code into a project, I created a BaseViewModel which implements  INotifyPropertyChanged and IDataErrorInfo:

public abstract class BaseViewModel : INotifyPropertyChanged, IDataErrorInfo
{
 #region INotifyPropertyChanged
  ...
  ...
  ...
 #endregion

 #region validation (IDataErrorInfo)
 private DataErrorInfoAttributeValidator _propertyValidations;
 private string _error;

 private DataErrorInfoAttributeValidator GetValidators()
 {
  return _propertyValidations ?? (_propertyValidations
   = new DataErrorInfoAttributeValidator(this, str => Error = str));
 }

 #region IDataErrorInfo
 public string this[string propertyName] => GetValidators().Validate(propertyName);

 public string Error
 {
  get { GetValidators(); return _error; }
  set { Set(ref _error, value); }
 }
 #endregion
 #endregion
}

Using this class, I was able to very quickly include the DataErrorInfoAttributeValidator into my code.

The Code for Converting from Camel Case to Spaces

Here is the code I used to convert a Camel Case name to a name with spaces before each capital letter:

private static string FromCamelCase(string value) =>
 Regex.Replace(value,
 @"(?<a>(?<!^)((?:[A-Z][a-z])|(?:(?<!^[A-Z]+)[A-Z0-9]+(?:(?=[A-Z][a-z])|$))|(?:[0-9]+)))", @" ${a}");

Sample

I have five different examples of ErrorTemplates for the sample, including the out of the box WPF. They are all bound to the same two properties in the ViewModel (properties Key and Value). There is also a button that is only enabled if there are no errors. To do this I use a converter on the IDataErrorInfo Error property that only enables the button if the Error property is null, empty or white space. I also have an error adorner to the right of the button that is only visible if the Error property is null, empty, or white space. I use the same converter for this, a converter that evaluates if a value is null, empty, or white space, and returns the appropriate value from the ConverterParameter. The ToolTip for the error adorner is bound to the Error property.

Image 1

In the ViewModel for this form is:

public class ViewModel : BaseViewModel
{
 public ViewModel() { }

 [Required]
 [MethodValidator("KeyAndValueDifferent")]
 public string Key{get { return _key; }set { Set(ref _key, value); }}
 private string _key;

 [Required]
 [MethodValidator("KeyAndValueDifferent")]
 public string Value{get { return _value; }set { Set(ref _value, value); }}
 private string _value;

 private string KeyAndValueDifferent()
 {
  return (Key != null && Value != null && Key == Value ) ? "Key and value must be different" : null;
 }
}

While the XAML for each TextBox pair is similar to this:

<Border Margin="2"
        BorderBrush="Gray"
        BorderThickness="1 ">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="300" />
            <ColumnDefinition Width="50" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Label Grid.ColumnSpan="3"
               Margin="5 5 0 5"
               Content="Default WPF Error Template" />
        <Label Grid.Row="1"
               Grid.Column="0"
               Margin="5"
               Content="Key" />
        <TextBox Grid.Row="1"
                 Grid.Column="1"
                 Margin="5"
                 Text="{Binding Key,
                                UpdateSourceTrigger=PropertyChanged,
                                ValidatesOnDataErrors=True}" />
        <Label Grid.Row="2"
               Grid.Column="0"
               Margin="5"
               Content="Value" />
        <TextBox Grid.Row="2"
                 Grid.Column="1"
                 Margin="5"
                 Text="{Binding Value,
                                UpdateSourceTrigger=PropertyChanged,
                                ValidatesOnDataErrors=True}" />
    </Grid>
</Border>

Image 2

Conclusion

The code in this sample provides a very easy to use way to display errors in WPF applications, and there is a lot of information presented to allow customization of error presentation. I hope this is useful to you.

History

  • 09/12/2015: Initial Version
  • 03/03/2016: Updated Source Code
  • 03/04/2016: Added MethodValidator use to sample
  • 05/27/2016: Camel case to common name used for field names in automatic error message, and added automatic message generation for the Range Validation
  • 07/11/2016: Replaced Path=(Validation.Errors)[0].ErrorContent with Path=(Validation.Errors).CurrentItem which eliminates debug spew.
  • 08/02/2016: Added a specialized RangeValidator (RangeUnitValidator) to that code that has a Unit argument.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Clifford Nelson
Software Developer (Senior) Clifford Nelson Consulting
United States United States
Has been working as a C# developer on contract for the last several years, including 3 years at Microsoft. Previously worked with Visual Basic and Microsoft Access VBA, and have developed code for Word, Excel and Outlook. Started working with WPF in 2007 when part of the Microsoft WPF team. For the last eight years has been working primarily as a senior WPF/C# and Silverlight/C# developer. Currently working as WPF developer with BioNano Genomics in San Diego, CA redesigning their UI for their camera system. he can be reached at qck1@hotmail.com.

Comments and Discussions

 
Suggestion2 Thoughts Pin
Mr.PoorEnglish15-Sep-15 3:56
memberMr.PoorEnglish15-Sep-15 3:56 
AnswerRe: 2 Thoughts Pin
Clifford Nelson15-Sep-15 5:30
mvaClifford Nelson15-Sep-15 5:30 
AnswerRe: 2 Thoughts Pin
Clifford Nelson7-Mar-16 13:28
mvaClifford Nelson7-Mar-16 13:28 
SuggestionSample, please Pin
Mr.PoorEnglish14-Sep-15 0:43
memberMr.PoorEnglish14-Sep-15 0:43 
AnswerRe: Sample, please Pin
Clifford Nelson14-Sep-15 4:05
mvaClifford Nelson14-Sep-15 4:05 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Article
Posted 13 Sep 2015

Tagged as

Stats

14.2K views
338 downloads
17 bookmarked