MTR
MTR

Reputation: 1740

Red border doesn't disappear with INotifyDataErrorInfo

I'm trying to implement INotifyDataErrorInfo but don't have success when I try to validate ObservableCollection properties.

The problem is, I get the red border, if the collection is wrong, but if I correct the collection, the red border doesn't go away anymore.

Does anybody have an idea how to solve this?

I setup a small sample to demonstrate the problem:

<Window x:Class="Validation.ValidationWindow3"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="ValidationWindow3" Height="300" Width="300">
<DockPanel LastChildFill="True">
    <TextBlock DockPanel.Dock="Top">Click button Add two times.<LineBreak/>
        => Red border should appear.<LineBreak/>
        <LineBreak/>
        Select second line in listbox then click button Remove.<LineBreak/>
        =>Red border should disappear.</TextBlock>
    <Button DockPanel.Dock="Bottom" Click="OnOk">Ok</Button>
    <Button DockPanel.Dock="Top" Click="OnAdd">Add</Button>
    <Button DockPanel.Dock="Top" Click="OnRemove">Remove</Button>
    <ListBox ItemsSource="{Binding ListOfNumbers, NotifyOnValidationError=True}" SelectedItem="{Binding SelectedNumber}" />
</DockPanel>

Code behind:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;

namespace Validation
{
/// <summary>
/// Interaction logic for ValidationWindow2.xaml
/// </summary>
public partial class ValidationWindow3 : Window, INotifyDataErrorInfo
{
    public ObservableCollection<int> ListOfNumbers
    {
        get { return (ObservableCollection<int>)GetValue(ListOfNumbersProperty); }
        set { SetValue(ListOfNumbersProperty, value); }
    }
    public static readonly DependencyProperty ListOfNumbersProperty =
        DependencyProperty.Register("ListOfNumbers", typeof(ObservableCollection<int>), typeof(ValidationWindow3), new PropertyMetadata(null, OnPropertyChanged));



    public int SelectedNumber
    {
        get { return (int)GetValue(SelectedNumberProperty); }
        set { SetValue(SelectedNumberProperty, value); }
    }
    public static readonly DependencyProperty SelectedNumberProperty =
        DependencyProperty.Register("SelectedNumber", typeof(int), typeof(ValidationWindow3), new PropertyMetadata(-1));



    public ValidationWindow3()
    {
        InitializeComponent();
        ListOfNumbers = new ObservableCollection<int>();
        DataContext = this;
    }

    private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ValidationWindow3 instance = d as ValidationWindow3;
        ObservableCollection<int> coll = (ObservableCollection<int>)e.NewValue;
        coll.CollectionChanged += instance.coll_CollectionChanged;
    }

    void coll_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        CheckProperty("ListOfNumbers");
    }



    private void OnOk(object sender, RoutedEventArgs e)
    {
        if(HasErrors)
        {
            IEnumerable list = GetErrors(null);
            string msg = "";
            foreach(var item in list)
            {
                msg += item.ToString();
            }
            MessageBox.Show(msg);
            return;
        }
        DialogResult = true;
    }


    void CheckProperty([CallerMemberName] string propertyName = "")
    {
        bool isValid = true;
        string msg = null;

        switch(propertyName)
        {
        case "ListOfNumbers":
            msg = "Only even numbers allowed!";
            foreach(int item in ListOfNumbers)
            {
                if(item % 2 > 0)
                {
                    isValid = false;
                }
            }
            break;
        default:
            break;
        }
        if(!isValid)
        {
            AddError(propertyName, msg);
        }
        else if(msg != null)
        {
            RemoveError(propertyName, msg);
        }
    }

    // Adds the specified error to the errors collection if it is not 
    // already present, inserting it in the first position if isWarning is 
    // false. Raises the ErrorsChanged event if the collection changes. 
    public void AddError(string propertyName, string error, bool isWarning=false)
    {
        if(!errors.ContainsKey(propertyName))
            errors[propertyName] = new List<string>();

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

    // Removes the specified error from the errors collection if it is
    // present. Raises the ErrorsChanged event if the collection changes.
    public void RemoveError(string propertyName, string error)
    {
        if(errors.ContainsKey(propertyName) &&
            errors[propertyName].Contains(error))
        {
            errors[propertyName].Remove(error);
            if(errors[propertyName].Count == 0)
                errors.Remove(propertyName);
            RaiseErrorsChanged(propertyName);
        }
    }

    public void RaiseErrorsChanged(string propertyName)
    {
        if(ErrorsChanged != null)
            ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
    }




    #region INotifyDataErrorInfo Members

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

    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    public System.Collections.IEnumerable GetErrors(string propertyName)
    {
        if (errors.Count < 1)
        {
            return null;
        }
        if(String.IsNullOrEmpty(propertyName))
        {
            return errors.SelectMany(err => err.Value.ToList());
        }
        if(!errors.ContainsKey(propertyName))
            return null;
        return errors[propertyName];
    }

    public bool HasErrors
    {
        get { return errors.Count > 0; }
    }

    #endregion

    static int _nextNumber = 0;

    private void OnAdd(object sender, RoutedEventArgs e)
    {
        ListOfNumbers.Add(_nextNumber++);
    }

    private void OnRemove(object sender, RoutedEventArgs e)
    {
        ListOfNumbers.Remove(SelectedNumber);
    }
}
}

EDIT:

I found out that there is nothing wrong with the validation itself. It seems to be a problem related to the ListBox. If I bind an additional TextBox to the ListOfNumbers I can see that the border on this TextBox works correct.

This is what I added:

        <TextBox DockPanel.Dock="Top" Text="{Binding ListOfNumbers, NotifyOnValidationError=True}" />

So why is the red border on the ListBox wrong?

Upvotes: 2

Views: 1187

Answers (2)

Ohadra
Ohadra

Reputation: 19

I also had this problem. I solved it with removing the Validation.ErrorTemplate like this:

<Style>
    <Setter Property="Validation.ErrorTemplate">
        <Setter.Value>
            <ControlTemplate>
                <AdornedElementPlaceholder/>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

And add error indication in the DataTemplate of the binded element, like this:

 <TextBlock Text="!" FontWeight="Bold" Foreground="Red" Margin="5" Visibility="{Binding HasErrors, Converter={StaticResource BoolToVisibilityConverter}}"/>

Upvotes: 0

MTR
MTR

Reputation: 1740

I think I have found the cause:

The ListBox is validating the SelectedItem. So if I delete this item the variable SelectedNumber, which is bound to SelectedItem, has a value that is not in the collection anymore. This gives me the red box.

I don't think that this is correct behaviour of the ListBox, but if I keep this in mind, there are some workarounds that I can use dependent on the situation:

  1. After removing the SelectedItem, set the SelectedItem to a different item in the collection, or to -1.
  2. Use SelectedIndex instead of SelectedItem, because this doesn't have such problem.
  3. Use Mode=OneWay for the SelectedItem binding

Upvotes: 1

Related Questions