Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UI not calling INotifyDataErrorInfo.GetErrors()

I have a model implementing both INotifyPropertyChanged and INotifyDataErrorInfo. The Property changed event fires when ever I have a property modified, but for some reason when I raise the Error event handler, the UI does ever invoke the GetErrors method. This results in the validation error not being rendered to the UI.

Can someone take a look at how I have the INotifyDataErrorInfo set up and tell me if I'm doing something wrong?

Base model implementation

public class BaseChangeNotify : INotifyPropertyChanged, INotifyDataErrorInfo
{
    private bool isDirty;

    private Dictionary<string, List<string>> errors = new Dictionary<string, List<string>>();

    public BaseChangeNotify()
    {
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    public bool IsDirty
    {
        get
        {
            return this.isDirty;
        }

        set
        {
            this.isDirty = value;
            this.OnPropertyChanged();
        }
    }

    public bool HasErrors
    {
        get
        {
            return this.errors.Count(e => e.GetType() == typeof(ErrorMessage)) > 0;
        }
    }

    public IEnumerable GetErrors(string propertyName)
    {
        if (string.IsNullOrEmpty(propertyName) ||
            !this.errors.ContainsKey(propertyName))
        {
            return null;
        }

        return this.errors[propertyName];/*.Where(e => (e is ErrorMessage));*/
    }

    protected virtual void AddError(string propertyName, string error, bool isWarning = false)
    {
        if (!this.errors.ContainsKey(propertyName))
        {
            this.errors[propertyName] = new List<string>();
        }

        if (!this.errors[propertyName].Contains(error))
        {
            if (isWarning)
            {
                this.errors[propertyName].Add(error);
            }
            else
            {
                this.errors[propertyName].Insert(0, error);
            }

            this.OnErrorsChanged(propertyName);
        }
    }

    protected virtual void RemoveError(string propertyName, string error)
    {
        if (this.errors.ContainsKey(propertyName) &&
            this.errors[propertyName].Contains(error))
        {
            this.errors[propertyName].Remove(error);

            if (this.errors[propertyName].Count == 0)
            {
                this.errors.Remove(propertyName);
            }

            this.OnErrorsChanged(propertyName);
        }
    }

    public virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
        // Perform the IsDirty check so we don't get stuck in a infinite loop.
        if (propertyName != "IsDirty")
        {
            this.IsDirty = true; // Each time a property value is changed, we set the dirty bool.
        }

        if (this.PropertyChanged != null)
        {
            // Invoke the event handlers attached by other objects.
            try
            {
                // When unit testing, this will always be null.
                if (Application.Current != null)
                {
                    try
                    {
                        Application.Current.Dispatcher.Invoke(() =>
                            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)));

                    }
                    catch (Exception)
                    {

                        throw;
                    }
                }
                else
                {
                    this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
                }
            }
            catch (Exception)
            {
                throw;
            }
        }
    }

    /// <summary>
    /// Called when an error has changed for this instance.
    /// </summary>
    /// <param name="propertyName">Name of the property.</param>
    public virtual void OnErrorsChanged([CallerMemberName] string propertyName = "")
    {
        if (string.IsNullOrWhiteSpace(propertyName))
        {
            return;
        }

        if (this.ErrorsChanged != null)
        {
            this.ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
        }
    }
}

Model using the implementation

public class PayItem : BaseChangeNotify
{
    private Section section;

    public Section Section
    {
        get
        {
            return this.section;
        }

        set
        {
            this.section = value;
            this.ValidateSection();
            this.OnPropertyChanged();
        }
    }

    private void ValidateSection([CallerMemberName] string propertyName = "")
    {
        const string sectionError = "You must select a Section.";
        if (this.Section == null || this.Section.Name.Length > 1)
        {
            this.AddError(propertyName, sectionError);
        }
        else
        {
            this.RemoveError(propertyName, sectionError);
        }
    }

The View trying to use it

<ComboBox Name="SectionComboBox"
          ItemsSource="{Binding Path=ProjectSections}"
          SelectedItem="{Binding Path=SelectedPayItem.Section, 
                         NotifyOnValidationError=True,
                         UpdateSourceTrigger=PropertyChanged}">

The app is being wrote in WPF, and the WPF docs are pretty scarce. I've read through the Silverlight documentation on it along with a few other blog posts I found on the internet and have implemented in each of the different ways the blog authors suggest. Each time the result is the same, the GetErrors() method never gets hit by the Binding engine.

Can anyone see something that I'm doing wrong? When my model has its property set, I can step through the debugger and ultimately end up within the OnErrorsChanged event handler, and the event gets invoked. Nothing happens when it gets invoked though, so I'm stumped.

Thanks in advance for any help.

Johnathon

EDIT

Also I would like to note that I had been using IDataErrorInfo in the base class for the last couple of months without any issues. The binding worked, the errors were reported to the View and everything was happy. When I changed from IDataErrorInfo to INotifyDataErrorInfo, the validation appeared to stop communicating with the View.

like image 641
Johnathon Sullinger Avatar asked Dec 26 '22 07:12

Johnathon Sullinger


1 Answers

The INotifyDataErrorInfo.HasErrors property must return true when raising the ErrorsChanged event. Otherwise the binding engine ignores the errors. Your HasErrors property will return false all the time. This happens because you are checking for items of type ErrorMessage but your dictionary contains items of type KeyValuePair<string, List<string>>. Besides that it is highly inefficent to count all the items. You should use .Any() instead.

By the way, the MSDN documentation of INotifyDataErrorInfo says the following:

Note that the binding engine never uses the HasErrors property, although you can use it in custom error reporting.

This is plain wrong and it took me hours to find that out.

like image 195
Tom Avatar answered Dec 27 '22 19:12

Tom