Validating Business Rules in MVVM

January 22, 2012

I’ve always thought that raw data validation should occur in the data Model, while Business Rule validation should occur in the ViewModel.

For example, verifying that a UserName is no longer than X length long should occur in the data model, while verifying that the UserName is unique would occur in the ViewModel. The reason for this is that the User Model is simply a raw data object. It doesn’t contain any advanced functionality like database connectivity, or knowing about other User objects. It’s a selfish little thing which only cares about it’s own data.

Since I use IDataErrorInfo for my validation and like to expose the entire Model to the View from my ViewModel, this presents a problem. Using the above example, I could bind a TextBox to SelectedUser.UserName, and it would automatically show an ErrorTemplate if the string was too long, however it wouldn’t show an error template if the UserName already exists.

After some thought, I decided to add a Validation Delegate to my Models to solve this problem. This is a delegate which ViewModels can use to add Business Logic Validation to its Models.

In the above example, the UsersViewModel might look like this:

public class UsersViewModel
{
    // Keeping these generic to reduce code here, but they
    // should be full properties with PropertyChange notification
    public ObservableCollection<UserModel> UserCollection { get; set; }
    public UserModel SelectedUser { get; set; }

    public UsersViewModel()
    {
        UserCollection = DAL.GetAllUsers();

        // Add the validation delegate to the UserModels
        foreach(var user in UserCollection)
            user.AddValidationErrorDelegate(ValidateUser);
    }

    // User Validation Delegate to verify UserName is unique
    private string ValidateUser(object sender, string propertyName)
    {
        if (propertyName == "UserName")
        {
            var user = (UserModel)sender;
            var existingCount = UserCollection.Count(p => 
                p.UserName == user.UserName && p.Id != user.Id);

            if (existingCount > 0)
                return "This username has already been taken";
        }
        return null;
    }
}

The actual implementation of my IDataErrorInfo on my Model class would look like the code below. It’s generic, so I usually put it into some kind of base class for my Models.


    #region IDataErrorInfo & Validation Members
    
    /// <summary>
    /// List of Property Names that should be validated.
    /// Usually populated by the Model's Constructor
    /// </summary>
    protected List ValidatedProperties = new List();
    
    #region Validation Delegate
    
    public delegate string ValidationErrorDelegate(
        object sender, string propertyName);
    
    private List _validationDelegates = new List();
    
    public void AddValidationErrorDelegate(
        ValidationErrorDelegate func)
    {
        _validationDelegates.Add(func);
    }
    
    #endregion // Validation Delegate
    
    #region IDataErrorInfo for binding errors
    
    string IDataErrorInfo.Error { get { return null; } }
    
    string IDataErrorInfo.this[string propertyName]
    {
        get { return this.GetValidationError(propertyName); }
    }
    
    public string GetValidationError(string propertyName)
    {
        // Check to see if this property has any validation
        if (ValidatedProperties.IndexOf(propertyName)  0)
        {
            foreach (var func in _validationDelegates)
            {
                s = func(this, propertyName);
                if (s != null)
                {
                    return s;
                }
            }
        }
    
        return s;
    }
    
    #endregion // IDataErrorInfo for binding errors
    
    #region IsValid Property
    
    public bool IsValid
    {
        get
        {
            return (GetValidationError() == null);
        }
    }
    
    public string GetValidationError()
    {
        string error = null;
    
        if (ValidatedProperties != null)
        {
            foreach (string s in ValidatedProperties)
            {
                error = GetValidationError(s);
                if (error != null)
                {
                    return error;
                }
            }
        }
    
        return error;
    }
    
    #endregion // IsValid Property
    
    #endregion // IDataErrorInfo & Validation Members

The idea is that your Models should only contain raw data, therefore they should only validate raw data. This can include validating things like maximum lengths, required fields, and allowed characters. Business Logic, which includes business rules, should be validated in the ViewModel, and by exposing a Validation Delegate that they can subscribe to, this can happen.


Follow

Get every new post delivered to your Inbox.