BindingList‹T› INotifyPropertyChanged неожиданное поведение

Предположим, у меня есть объекты:

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;
}

и его держатель:

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

Также я подписываюсь на ListChangedEvent

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

И 2 обработчика тестов: для добавления объекта

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

и для изменения

    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 происходит, а ItemChanged — нет. Внутри, увидев свойство «Данные», я обнаружил, что нет подписчиков на мое событие 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;

Копнув глубже, я взял отражатель и обнаружил 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;
        }
    }

Где я не прав? Или это известная ошибка, и мне нужно найти обходной путь? Спасибо!


person wsnzone    schedule 11.06.2013    source источник
comment
Как вы делали тест? Если вы хотите инициировать событие изменения свойства, вы должны сначала подписаться на событие. Если у вас есть графический интерфейс, это означает, что вы должны привязать свойство Data к указанному элементу управления, например. Текстовое окно.   -  person Jason Li    schedule 11.06.2013
comment
@JasonLi Идея состоит в том, что BindingList должен подписываться на событие, но не выглядит   -  person Andras Zoltan    schedule 11.06.2013


Ответы (4)


BindingList<T> не проверяет, реализует ли каждый конкретный элемент INotifyPropertyChanged. Вместо этого он проверяет его один раз на наличие параметра универсального типа. Итак, если ваш BindingList<T> объявлен следующим образом:

private BindingList<ITest> _listTest1;

Тогда ITest должно быть унаследовано от INotifyPropertyChanged, чтобы получить BindingList поднять ItemChanged событий.

person Nikolay Khil    schedule 11.06.2013
comment
+1 отличный улов - не заметил этого (в моей проверке работоспособности кода я объявил BindingList<Test1>) - person Andras Zoltan; 11.06.2013

Я думаю, что у нас может не быть полной картины из вашего кода здесь, потому что, если я возьму интерфейс ITest и класс Test1 дословно (edit Упс - не совсем - потому что, как говорит Николай, это не работает для вас, потому что вы используете ITest в качестве параметра универсального типа для BindingList<T>, которого здесь нет) из вашего кода, и напишите этот тест:

[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);

  }
}

Тест проходит правильно - evtCount заканчивается на 2, как и должно быть.

person Andras Zoltan    schedule 11.06.2013

Я нашел в конструкторе несколько интересных вещей:

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);
        }
    }
}

Быстрое исправление 4 этого поведения:

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;
}

Спасибо за ответы!

person wsnzone    schedule 11.06.2013

Тип элементов, которые вы параметризуете BindingList<> с помощью (ITest в вашем случае), должен быть унаследован от INotifyPropertyChanged. Параметры:

  1. Измените дерево наследования ITest: INotifyPropertyChanged
  2. Передать конкретный класс в общий BindingList
person SergeyS    schedule 11.06.2013