User In-Experience

A discussion about Rich Internet Applications, and user experience focusing on architecture, design patterns and methodologies.

Tuesday, November 16, 2010

Dynamic Entity Binding–Validation

Last post, I described a method to create a loosely typed business object that supports binding with change notification.  This post I’m going to add validation support. 

I have chosen to use the INotifyDataErrorInfo interface to implement validation notification since it provides the richest functionality for this purpose.  IDataErrorInfo uses an indexer to report errors, which conflicts with the indexer I already have for retrieving fields from the entity.

So first step is to add that interface to the class:

public class DynamicEntity : INotifyCollectionChanged, INotifyPropertyChanged, INotifyDataErrorInfo


This interface defines two methods and an event:



bool HasErrors { get; }
event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
IEnumerable GetErrors(string propertyName);


What we need is a place to store errors.  To keep the example simple, I’ll just create a list of strings on the Field class:



public List<String> Errors { get; private set; }


Now the interface can be implemented by querying this property:



	private void RaiseErrorsChanged(string propertyName)
        {
            string arg = string.Format("Item[{0}]", propertyName);
            if (ErrorsChanged != null) { ErrorsChanged(this, new DataErrorsChangedEventArgs(arg)); }
        }
        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
        public System.Collections.IEnumerable GetErrors(string propertyName)
        {
            if (string.IsNullOrEmpty(propertyName)) return null;
                        
            string key = GetBindingKey(propertyName);
            Field field = Fields[key];
            return field.Errors;
        }
        public bool HasErrors
        {
            get
            {
                var query = from field in Fields.Values
                            where field.Errors.Count > 0
                            select field;
                return query.ToList().Count > 0;
            }
        }


The first is just a helper method for raising the event.  The key is the property value used to bind to the field using the indexer format Item[PropertyName].  GetErrors retrieves the list of errors for the indicated field.  HasErrors simply counts the number of errors in all fields and returns true if there is at least one.



Now to populate the errors we need some validation rules defined.  I started with a base validator which implements the core functionality:



 public abstract class Validator : LocalizedObject
    {
        private string _ErrorMessage;
        public virtual string ErrorMessage
        {
            get
            {
                return _ErrorMessage;
            }
            protected set
            {
                _ErrorMessage = value;
            }
        }
        public Validator() { }
        public Validator(string errorMessage)
        {
            ErrorMessage = errorMessage;
        }
        public abstract bool Validate(object value);
    }


This is simply a property to set the message on and a method to call to validate the entity with.  A required validator can now be created using this base class:



    public class RequiredValidator : Validator
    {
        public RequiredValidator(string errorMessage) : base(errorMessage) { }
        public override bool Validate(object value)
        {
            if (value == null)
            {
                return false;
            }
            string str = value as string;
            if (str != null)
            {
                return (str.Trim().Length != 0);
            }
            return true;
        }
    }


The validation rules are part of the field metadata, so we add a place to store them and a fluent interface to add them to the FieldMetadata class:



public List<Validator> Validators { get; private set; }
public FieldMetadata AddValidator(string propertyName, Validator validator)
{
        Validators.Add(validator);
        return this;
}


Now on the DynamicEntity we add a method to perform the validation:



private bool Validate(object value, Field field)
{
	ClearErrors(field);
        foreach (var validator in field.Metadata.Validators)
        {
        	if (!validator.Validate(value))
                {
                    AddError(field, validator.ErrorMessage);
                }
            }
            return true;
        }
}
private void ClearErrors(Field field)
{
	field.Errors.Clear();
        RaiseErrorsChanged(field.Metadata.Name);
}
private void AddError(Field field, string error)
{
    	field.Errors.Add(error);
    	RaiseErrorsChanged(field.Metadata.Name);
}


And we call it from the indexer setter:



        public object this[string propertyName]
        {
            get
            {
                return Fields[propertyName].Value;
            }
            set
            {
                var field = Fields[propertyName];
                object oldValue = field.Value;
                if (Validate(value, field)) field.Value = value;
                if (oldValue != value)
                {
                    RaisePropertyChanged(propertyName);
                    RaiseCollectionChanged(NotifyCollectionChangedAction.Replace, value, oldValue, -1);
                }
            }
        }


I have the validate function returning a boolean that specifies whether the setter should allow the value to be set, but I have not added logic to support this yet.



Now we can add validation rules to our entity:




            field.AddValidator("FirstName", new RequiredValidator("Firstname is a required field."));

The last step is to enable NotifyDataErrorInfo on the binding:



<TextBox x:Name="textBox" Text="{Binding Entity[FirstName], Mode=TwoWay, ValidatesOnNotifyDataErrors=True}"/>


image

About Me

My photo
Toronto, ON, Canada
I am a technical architect at Navantis, with over 12 years experience in IT. I have been involved in various projects, including a Hospital information system called CCIS for which my team received the 2007 Tech-Net innovation award. I have been working with Silverlight since beta 1, and am very keen on practically applying this technology and WPF to line of business applications.