Thursday, November 3, 2011

Data Annotation in WPF user control editor for generic class editor.

In WPF we all end up building editor for an data object, especially when we are dealing with databases. I cannot remember how many times I end up redoing similar things over and over again. This time I would like to take a stab in building a generic editor for any data object. There probably different way to approach this problem but the solution I am investigating is in the spirit of ASP MVC "html.editorFor" functionality and its validation model, more info about the ASP MVC can be found here.

First in ASP MVC there is a way to annotate your model with different validation attributes, for example consider our test model object here:


TestPersonModel.Email property is annotated with 2 validation attributes:
  1. Required meaning this property must have a value
  2. The value of the property must conform with the reqular expression patterned defined in the attribute.
In ASP MVC the "html.editorfor" is smart enough to generate correct type of fields for each property and also validate the input for you based on these attributes. Wouldn't it be great to have a similar helper exist in WPF ?

So lets figure out what we need want to have as an end result:
  1. A User Control that have 2 columns, left for name of property right for editing the property value.
  2. Take advantage of the Validation Attributes on the property and use it for automatic error checking.
  3. Do it without forcing the generic object to implement anything special except using validation attributes for checking. We want to have a MVVM solution for this.
First, creating the User Control:
The User Control going to have this appearance:







The user control show property name and the value editor in a list. I am using ListView to display the listing.
The following is the code snippet for the ListView declaration:

















The Property column is not that interesting since its only displaying a static TextBlock element for displaying name of the property. The Value column is the interesting part since it have to display different editor for different types, to achieve this we uitlize DataTemplateSelector which is explained here. If you are familiar with DataTemplateSelector it is very usefull in building a generic UI, so in our DataDisplayViewTemplateSelector what it needs to do is display correct template for different Type object. Note I declared two templates for a string type and boolean type only, it can be extended to more types.

We have the user control and the display template selector but what do we bound this component too? We cannot bound to the object directly since that will tie the user control definition to a specific object only and we do not want that, following MVVM pattern we are going to need a View Model to mediate between the User Control and the object we are inspecting. Here comes the View Model!

The view model will use Reflection to query the object properties and provide bindable list that will provide enough information for the user control. Here is a design for the view model:
 







The view model essentially need to use reflection to build the property model list. The following code does that purpose:
      /// <summary>
        /// Initializes a new instance of the DataDisplayViewModel class
        /// </summary>
        /// <param name="objectToInspect">The object to inspect</param>
        public DataDisplayViewModel(object objectToInspect)
        {
            this.data = objectToInspect;
            this.Properties = new ObservableCollection<PropertyViewModel>();
            PropertyInfo[] properties = this.data.GetType().GetProperties();
            foreach (PropertyInfo property in properties)
            {
                //// Make sure we only do set and get on a public property
                if (property.GetSetMethod().IsPublic && property.GetGetMethod().IsPublic)
                {
                    PropertyViewModel newProperty = new PropertyViewModel(objectToInspect, property);
                    this.properties.Add(newProperty);
                    newProperty.PropertyChanged += new PropertyChangedEventHandler(this.PropertyValueChanged);
                }
            }
        }


The Property Model code is the following:

        public class PropertyViewModel : INotifyPropertyChanged, IDataErrorInfo
        {
           ....
            /// <summary>
            /// Store all the attribute that inherit from DataAnnotation.ValidationAttribute class.
            /// </summary>
            private ValidationAttribute[] validation;
            ...


            public PropertyViewModel(object objectToInspect, PropertyInfo propertyInfo)
            {
                 ...
                this.validation = (ValidationAttribute[])propertyInfo.GetCustomAttributes(typeof(ValidationAttribute), false);
            }
            ...


            /// <summary>
            /// Get the property value for this property
            /// </summary>
            public object Value
            {
                get
                {
                    return this.propertyInfo.GetValue(this.objectToInspect, null);
                }


                set
                {
                    if (this.propertyInfo.GetValue(this.objectToInspect, null) != value)
                    {
                        this.propertyInfo.SetValue(this.objectToInspect, value, null);
                        this.NotifyPropertyChanged("Value");
                    }
                }
            }


            ...
            
            /// <summary>
            /// IDataErrorInfo implementation for getting error for a property 
            /// </summary>
            /// <param name="columnName">The property to get error for</param>
            /// <returns>Error string</returns>
            public string this[string columnName]
            {
                get
                {
                    if (columnName == "Value")
                        foreach (ValidationAttribute validator in this.validation)
                            if (validator.IsValid(this.Value) == false)
                                return validator.ErrorMessage;


                    return String.Empty;
                }
            }
        }

In this property class wrapper we query the property for all ValidationAttributes attached to it. This is where we tie in the data annotation for providing error checking the property value. The class also expose a Value property which is where we tie in the error notification for the property. Every single time the property value is changed then WPF will query the IDataErrorInfo function to check for error and we just need to return the result from the ValidationAttribute evaluation. This makes the error propagate using the usual Validation.ErrorTemplate functionality in WPF.

So that's it by combining Reflection, DataTemplateSelector and ValidationAttribute we can make a generic user control to set values of an object properties without enforcing specialized interface the user need to adhere too.

The complete solution for the code is attached, there are additional things to make the user control more usable, namely the following:

  1. Expose ItemSource dependency property to allow binding from xaml.
  2. Expose IsValid and Errors read only dependency property to allow checking for errors in the object. After all if you are building a dialog you want to prevent action if the object is bad correct?
Of course there are more things to do to make it more functional but i think the plumbing for validation is there. 

Download Source files.


No comments:

Post a Comment