Search code examples
.netwpfvalidationmvvmidataerrorinfo

Cross-property validation in WPF


Currently I am using the IDataErrorInfo interface to implement validation in a WPF application. The indexer which is part of that interface allows to validate a single property, like so:

public string this[string columnName]
{
    get
    {
        switch (columnName)
        {
            case "LastName":
                if (string.IsNullOrEmpty(this.LastName))
                    return "LastName must not be empty.";
                break;
            // case, case, case, etc., etc.
        }
        return null;
    }
}

If a validation error occurs I display an asterisk with a tooltip next to the TextBox.

What can I do if I have validation rules which are not strictly related to a single property? For instance, if a domain entity representing an order has a shipping date and an invoice date and I want to validate the rule that the invoice date must be later than or equal to the shipping date?

Of course I could force this rule into the indexer as well by checking the relationship twice, once for columnName "ShippingDate" and once for columnName "InvoiceDate" and then marking the error with an asterisk at both input fields in the UI, like so:

public string this[string columnName]
{
    get
    {
        switch (columnName)
        {
            case "ShippingDate":
            case "InvoiceDate":
                if (this.ShippingDate > this.InvoiceDate)
                    return "Invoice date must not be before shipping date.";
                break;
        }
        return null;
    }
}

But I would prefer to have an "object level" or "cross property" validation independently from the indexer (the indexer should only report an invalid "single property state") and display those object level or relationship errors separately on the UI.

I was hoping that the Error property of the IDataErrorInfo interface might have this purpose of object level validation. WPF calls the indexer for property validation when I specify ValidatesOnDataErrors=True in the Binding expression of a TextBox, for instance. But I couldn't find a way to tell WPF to call the Error property whenever I change some data in my input fields. Maybe my guess about the purpose of this property is wrong?

How can I implement cross-property validation in WPF?

Thank you for suggestions in advance!


Solution

  • Regarding my question if I can setup a binding so that WPF tests automatically the Error property of the IDataErrorInfo interface I found the following negative answer here:

    Question from someone:

    Basically, I'd like to know of a Binding property that will trigger the testing of IDataErrorInfo.Error, in the way that ValidatesOnDataErrors causes the testing of IDataErrorInfo.Item.

    Answer from Microsoft Online Community Support:

    Setting the ValidatesOnDataErrors property of the Binding class only tests of the IDataErrorInfo.Item and not of the IDataErrorInfo.Error.

    The Binding class doesn't provide a property to check the IDataErrorInfo.Error as the ValidatesOnDataErrors property to check the IDataErrorInfo.Item so far.

    To get what yo want, we have to set up a data binding to the IDataError.Error...

    So, the Error property doesn't have any more value than defining my own hand-made property (like CrossPropertyErrors) in the domain entities. WPF doesn't support testing of the Error property in an easy built-in way.

    Edit: The quotes above are from March 2008, so very likely related to .NET 3.5. But I couldn't find any sign that this did change in .NET 4.0.

    Edit: Finally I had to create my own hand-written binding to the Error property and fill it with appropriate cross-property error messages. Every change of any other property in the class raises now a PropertyChanged Event of both the changed property itself and of the Error property to refresh the Error message on the UI.

    Edit 2

    It looks roughly like this:

    Model (or ViewModel) classes:

    public class SomeModel : NotificationObject, IDataErrorInfo
    {
        private string _someProperty;
        public string SomeProperty
        {
            get { return _someProperty; }
            set
            {
                if (_someProperty != value)
                {
                    _someProperty = value;
                    RaisePropertyChanged("SomeProperty", "Error");
                    // That's the key: For every changed property a change
                    // notification also for the Error property is raised
                }
            }
        }
        // The above repeats for every property of the model
    
        #region IDataErrorInfo Member
        public string Error
        {
            get
            {
                var sb = new StringBuilder();
                // for example...
                if (InvoiceDate < ShippingDate)
                    sb.AppendLine("InvoiceDate must not be before ShippingDate.");
    
                // more cross-property validations... We have only one Error
                // string, therefore we append the messages with
                // sb.AppendLine("Another message...") ... etc.
                // could all be moved into a separate validation class
                // to keep the model class cleaner
    
                return sb.ToString();
            }
        }
    
        public string this[string columnName]
        {
            get
            {
                switch (columnName)
                {
                    case "ShippingDate":
                        // property-level validations
                    case "InvoiceDate":
                        // property-level validations
                    // etc.
                }
                return null;
            }
        }
        #endregion
    }
    

    NotificationObject implements RaisePropertyChanged:

    public abstract class NotificationObject : INotifyPropertyChanged
    {
        #region INotifyPropertyChanged Member
        public event PropertyChangedEventHandler PropertyChanged;
        #endregion
    
        protected virtual void RaisePropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
                handler(this, new PropertyChangedEventArgs(propertyName));
        }
    
        protected void RaisePropertyChanged(params string[] propertyNames)
        {
            if (propertyNames == null)
                throw new ArgumentNullException("propertyNames");
    
            foreach (var name in propertyNames)
                RaisePropertyChanged(name);
        }
    
        // ...
    }
    

    Then in a view the Error property is bound to - for instance - a TextBlock which displays the cross-property validation errors:

    <TextBlock Text="{Binding SomeModel.Error}" TextWrapping="Wrap" ... />
    

    So: Every changed property on the model will notify the WPF binding engine about a (potential) change of the Error property causing therefore an update of the cross-property validation text.