BindingList<T> INotifyPropertyChanged unexpected behavior
Asked Answered
S

4

4

Suppose, I have objects:

public interface ITest
{
    string Data { get; set; }
}
public class Test1 : ITest, INotifyPropertyChanged
{
    private string _data;
    public string Data
    {
        get { return _data; }
        set
        {
            if (_data == value) return;
            _data = value;
            OnPropertyChanged("Data");
        }
    }
    protected void OnPropertyChanged(string propertyName)
    {
        var h = PropertyChanged;
        if (null != h) h(this, new PropertyChangedEventArgs(propertyName));
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

and its holder:

    private BindingList<ITest> _listTest1;
    public BindingList<ITest> ListTest1 { get { return _listTest1 ?? (_listTest1 = new BindingList<ITest>() { RaiseListChangedEvents = true }); }
    }

Also, I subscribe to ListChangedEvent

    public MainWindow()
    {
        InitializeComponent();            
        ListTest1.ListChanged += new ListChangedEventHandler(ListTest1_ListChanged);
    }
    void ListTest1_ListChanged(object sender, ListChangedEventArgs e)
    {
        MessageBox.Show("ListChanged1: " + e.ListChangedType);
    }

And 2 test handlers: For adding object

    private void AddITestHandler(object sender, RoutedEventArgs e)
    {
        ListTest1.Add(new Test1 { Data = Guid.NewGuid().ToString() });
    }

and for changing

    private void ChangeITestHandler(object sender, RoutedEventArgs e)
    {
        if (ListTest1.Count == 0) return;
        ListTest1[0].Data = Guid.NewGuid().ToString();
        //if (ListTest1[0] is INotifyPropertyChanged)
        //    MessageBox.Show("really pch");
    }

ItemAdded occurs, but ItemChanged not. Inside seeting proprty "Data" I found that no subscribers for my event PropertyChanged:

    protected void OnPropertyChanged(string propertyName)
    {
        var h = PropertyChanged; // h is null! why??
        if (null != h) h(this, new PropertyChangedEventArgs(propertyName));
    }

    public event PropertyChangedEventHandler PropertyChanged;

Digging deeper i took reflector and discover BindingList:

    protected override void InsertItem(int index, T item)
    {
        this.EndNew(this.addNewPos);
        base.InsertItem(index, item);
        if (this.raiseItemChangedEvents)
        {
            this.HookPropertyChanged(item);
        }
        this.FireListChanged(ListChangedType.ItemAdded, index);
    }
private void HookPropertyChanged(T item)
    {
        INotifyPropertyChanged changed = item as INotifyPropertyChanged;
        if (changed != null) // Its seems like null reference! really??
        {
            if (this.propertyChangedEventHandler == null)
            {
                this.propertyChangedEventHandler = new PropertyChangedEventHandler(this.Child_PropertyChanged);
            }
            changed.PropertyChanged += this.propertyChangedEventHandler;
        }
    }

Where am I wrong? Or this is known bug and i need to find some workaround? Thanks!

Stomy answered 11/6, 2013 at 8:9 Comment(4)
How did you do the test? If you want to trigger the property changed event, you should subscribe the event first. If you have a GUI, it means you should bind the Data property to a specified control e.g. TextBox.Twyla
@JasonLi The idea is that the BindingList should be subscribing to the event but doesn't appear to beAisne
Potential pitfall to other people when you're inheriting BindingList<T> in your custom class: if you add any items to your list, make sure you call Add() (which is Collection<T>.Add(T item)), and not through the internal list Items.Add() (which is ICollection<T>.Add(T item)), because otherwise you will bypass BindingList's internal Add method which hooks up the PropertyChanged event of your list items. Silly mistake on my part that took a fair few hours to catch!Sanders
Older similar answer here, too.Sanders
D
6

BindingList<T> doesn't check if each particular item implements INotifyPropertyChanged. Instead, it checks it once for the Generic Type Parameter. So if your BindingList<T> is declared as follows:

private BindingList<ITest> _listTest1;

Then ITest should be inherited fromINotifyPropertyChanged in order to get BindingList raise ItemChanged events.

Decalescence answered 11/6, 2013 at 8:32 Comment(2)
+1 great catch - didn't spot that (in my code sanity check I'd declared BindingList<Test1>)Aisne
Hammered! I implement against interfaces.Pharyngology
A
1

I think we may not have the full picture from your code here, because if I take the ITest interface and Test1 class verbatim (edit Oops - not exactly - because, as Nikolay says, it's failing for you because you're using ITest as the generic type parameter for the BindingList<T> which I don't here) from your code and write this test:

[TestClass]
public class UnitTest1
{
  int counter = 0;

  [TestMethod]
  public void TestMethod1()
  {
    BindingList<Test1> list = new BindingList<Test1>();
    list.RaiseListChangedEvents = true;


    int evtCount = 0;
    list.ListChanged += (object sender, ListChangedEventArgs e) =>
    {
      Console.WriteLine("Changed, type: {0}", e.ListChangedType);
      ++evtCount;
    };

    list.Add(new Test1() { Data = "yo yo" });

    Assert.AreEqual(1, evtCount);

    list[0].Data = "ya ya";

    Assert.AreEqual(2, evtCount);

  }
}

The test passes correctly - with evtCount ending up at 2, as it should be.

Aisne answered 11/6, 2013 at 8:33 Comment(0)
S
1

The type of elements that you parameterize BindingList<> with (ITest in your case) must be inherited from INotifyPropertyChanged. Options:

  1. Change you inheritance tree ITest: INotifyPropertyChanged
  2. Pass concrete class to the generic BindingList
Struma answered 11/6, 2013 at 8:40 Comment(0)
S
1

I found in constructor some interesting things:

public BindingList()
{
    // ...
    this.Initialize();
}
private void Initialize()
{
    this.allowNew = this.ItemTypeHasDefaultConstructor;
    if (typeof(INotifyPropertyChanged).IsAssignableFrom(typeof(T))) // yes! all you're right
    {
        this.raiseItemChangedEvents = true;
        foreach (T local in base.Items)
        {
            this.HookPropertyChanged(local);
        }
    }
}

Quick fix 4 this behavior:

public class BindingListFixed<T> : BindingList<T>
{
    [NonSerialized]
    private readonly bool _fix;
    public BindingListFixed()
    {
        _fix = !typeof (INotifyPropertyChanged).IsAssignableFrom(typeof (T));
    }
    protected override void InsertItem(int index, T item)
    {
        base.InsertItem(index, item);
        if (RaiseListChangedEvents && _fix)
        {
            var c = item as INotifyPropertyChanged;
            if (null!=c)
                c.PropertyChanged += FixPropertyChanged;
        }
    }
    protected override void RemoveItem(int index)
    {
        var item = base[index] as INotifyPropertyChanged;
        base.RemoveItem(index);
        if (RaiseListChangedEvents && _fix && null!=item)
        {
            item.PropertyChanged -= FixPropertyChanged;
        }
    }
    void FixPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (!RaiseListChangedEvents) return;

        if (_itemTypeProperties == null)
        {
            _itemTypeProperties = TypeDescriptor.GetProperties(typeof(T));
        }
        var propDesc = _itemTypeProperties.Find(e.PropertyName, true);

        OnListChanged(new ListChangedEventArgs(ListChangedType.ItemChanged, IndexOf((T)sender), propDesc));
    }
    [NonSerialized]
    private PropertyDescriptorCollection _itemTypeProperties;
}

Thanks for replies!

Stomy answered 11/6, 2013 at 8:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.