Search code examples
wpfentity-frameworkvalidationmvvmidataerrorinfo

Business logic validation with Entity Framework and IDataErrorInfo


I'm working on a project using WPF and MVVM with Entity Framework 4.3, and I would like to know how can I perform business logic validation implementing the IDataErrorInfo interface.

All of my models (POCO classes) are implementing it in order to perform raw validations, like maxlength, non-negative numbers, and so on...

But what about bussiness logic validation, such as to prevent duplicate records?

Imagine I have a textbox for a material "reference", which must be unique, defined liked this:

 <TextBox Text="{Binding Material.Reference, ValidatesOnDataErrors=True, NotifyOnValidationError=true, 
                                 UpdateSourceTrigger=PropertyChanged}">

The model will successfully validate the reference's length, but if there's already a material in my viewmodel's materials observablecollection, how should I notify the user of this fact from my ViewModel, yet taking advantage of the IDataErrorInfo messages?


Solution

  • I've done this in the past by exposing a validation delegate from my models that my ViewModels can hook into for additional business logic validation

    The end result ends up looking like this:

    public class MyViewModel
    {
        // Keeping these generic to reduce code here, but they
        // should be full properties with PropertyChange notification
        public ObservableCollection<MyModel> MyCollection { get; set; }
        public MyModel SelectedModel { get; set; }
    
        public MyViewModel()
        {
            MyCollection = DAL.GetAllModels();
    
            // Add the validation delegate to each object
            foreach(var model in MyCollection)
                model.AddValidationErrorDelegate(ValidateModel);
        }
    
        // Validation Delegate to verify the object's name is unique
        private string ValidateObject(object sender, string propertyName)
        {
            if (propertyName == "Name")
            {
                var obj = (MyModel)sender;
                var existingCount = MyCollection.Count(p => 
                    p.Name == obj.Name && p.Id != obj.Id);
    
                if (existingCount > 0)
                    return "This name has already been taken";
            }
            return null;
        }
    }
    

    Most of my models inherit from a generic base class, which includes this validation delegate. Here's the relevant code from that base class, taken from my blog article on Validating Business Rules in MVVM

    #region IDataErrorInfo & Validation Members
    
    /// <summary>
    /// List of Property Names that should be validated.
    /// Usually populated by the Model's Constructor
    /// </summary>
    protected List<string> ValidatedProperties = new List<string>();
    
    #region Validation Delegate
    
    public delegate string ValidationErrorDelegate(
        object sender, string propertyName);
    
    private List<ValidationErrorDelegate> _validationDelegates = new List<ValidationErrorDelegate>();
    
    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)
        {
            string s = null;
    
            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
    

    This allows me to keep the basic data validation in my Models, and my ViewModels can attach any customized business logic validation they want to the Model as well.