wsnzone
wsnzone

Reputation: 51

BindingList<T> INotifyPropertyChanged unexpected behavior

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!

Upvotes: 4

Views: 3614

Answers (4)

SergeyS
SergeyS

Reputation: 4520

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

Upvotes: 1

wsnzone
wsnzone

Reputation: 51

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!

Upvotes: 1

Nick Hill
Nick Hill

Reputation: 4917

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.

Upvotes: 6

Andras Zoltan
Andras Zoltan

Reputation: 42353

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.

Upvotes: 1

Related Questions