ObservableCollection, которая также отслеживает изменения элементов коллекции - PullRequest
32 голосов
/ 06 ноября 2008

Существует ли коллекция (BCL или другая), которая имеет следующие характеристики:

Отправляет событие, если коллекция изменена, и отправляет событие, если какой-либо из элементов в коллекции отправляет событие PropertyChanged. Сортировка по ObservableCollection<T>, где T: INotifyPropertyChanged, и коллекция также отслеживает элементы на предмет изменений.

Я мог бы обернуть наблюдаемую коллекцию самостоятельно и сделать событие подписаться / отписаться, когда элементы в коллекции были добавлены / удалены, но мне было просто интересно, если какие-либо существующие коллекции уже сделали это?

Ответы [ 8 ]

43 голосов
/ 06 ноября 2008

Сделал быструю реализацию сам:

public class ObservableCollectionEx<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        Unsubscribe(e.OldItems);
        Subscribe(e.NewItems);
        base.OnCollectionChanged(e);
    }

    protected override void ClearItems()
    {
        foreach(T element in this)
            element.PropertyChanged -= ContainedElementChanged;

        base.ClearItems();
    }

    private void Subscribe(IList iList)
    {
        if (iList != null)
        {
            foreach (T element in iList)
                element.PropertyChanged += ContainedElementChanged;
        }
    }

    private void Unsubscribe(IList iList)
    {
        if (iList != null)
        {
            foreach (T element in iList)
                element.PropertyChanged -= ContainedElementChanged;
        }
    }

    private void ContainedElementChanged(object sender, PropertyChangedEventArgs e)
    {
        OnPropertyChanged(e);
    }
}

Признано, было бы немного запутанно и вводить в заблуждение запуск PropertyChanged в коллекции, когда свойство, которое фактически изменилось, находится в отдельном элементе, но это будет соответствовать моей конкретной цели. Это может быть расширено новым событием, которое запускается вместо внутри ContainerElementChanged

Мысли

РЕДАКТИРОВАТЬ: Следует отметить, что BCL ObservableCollection предоставляет интерфейс INotifyPropertyChanged только через явную реализацию, поэтому вам необходимо предоставить приведение для присоединения к событию следующим образом:

ObservableCollectionEx<Element> collection = new ObservableCollectionEx<Element>();
((INotifyPropertyChanged)collection).PropertyChanged += (x,y) => ReactToChange();

EDIT2: добавлена ​​обработка ClearItems, спасибо, Джош

EDIT3: добавлена ​​правильная отмена подписки на PropertyChanged, спасибо Mark

РЕДАКТИРОВАТЬ4: Wow это действительно учиться, как вы идете :). KP отметил, что событие было инициировано с коллекцией в качестве отправителя, а не с элементом, когда изменяется содержащийся элемент. Он предложил объявить событие PropertyChanged для класса, помеченного new . Это будет иметь несколько проблем, которые я попытаюсь проиллюстрировать на следующем примере:

  // work on original instance
  ObservableCollection<TestObject> col = new ObservableCollectionEx<TestObject>();
  ((INotifyPropertyChanged)col).PropertyChanged += (s, e) => { Trace.WriteLine("Changed " + e.PropertyName); };

  var test = new TestObject();
  col.Add(test); // no event raised
  test.Info = "NewValue"; //Info property changed raised

  // working on explicit instance
  ObservableCollectionEx<TestObject> col = new ObservableCollectionEx<TestObject>();
  col.PropertyChanged += (s, e) => { Trace.WriteLine("Changed " + e.PropertyName); };

  var test = new TestObject();
  col.Add(test); // Count and Item [] property changed raised
  test.Info = "NewValue"; //no event raised

Вы можете видеть из примера, что «переопределение» события имеет побочный эффект, который вы должны быть чрезвычайно осторожны с тем, какой тип переменной вы используете при подписке на событие, так как это диктует, какие события вы получаете.

7 голосов
/ 23 июля 2010

@soren.enemaerke: Я бы сделал этот комментарий к вашему посту с ответом, но не могу (не знаю почему, может, потому что у меня не так много точек повторений). В любом случае, я просто подумал, что упомяну, что в вашем коде, который вы опубликовали, я не думаю, что Unsubscribe будет работать правильно, потому что он создает новую лямбда-строку и затем пытается удалить для нее обработчик событий.

Я бы изменил строки обработчика добавления / удаления на что-то вроде:

element.PropertyChanged += ContainedElementChanged;

и

element.PropertyChanged -= ContainedElementChanged;

А затем измените сигнатуру метода ContainedElementChanged на:

private void ContainedElementChanged(object sender, PropertyChangedEventArgs e)

Это распознает, что удаление выполняется для того же обработчика, что и добавление, а затем удалит его правильно. Надеюсь, это кому-нибудь поможет:)

3 голосов
/ 06 ноября 2008

Если вы хотите использовать что-то встроенное в платформу, вы можете использовать FreezableCollection . Тогда вы захотите прослушать Измененное событие .

Происходит, когда Freezable или объект он содержит измененный.

Вот небольшой образец. Метод collection_Changed будет вызван дважды.

public partial class Window1 : Window
{
    public Window1()
    {
        InitializeComponent();

        FreezableCollection<SolidColorBrush> collection = new FreezableCollection<SolidColorBrush>();
        collection.Changed += collection_Changed;
        SolidColorBrush brush = new SolidColorBrush(Colors.Red);
        collection.Add(brush);
        brush.Color = Colors.Blue;
    }

    private void collection_Changed(object sender, EventArgs e)
    {
    }
}
1 голос
/ 06 ноября 2011

Я бы использовал ReactiveUI ReactiveCollection:

reactiveCollection.Changed.Subscribe(_ => ...);
0 голосов
/ 08 февраля 2018

Самый простой способ сделать это просто сделать

using System.ComponentModel;
public class Example
{
    BindingList<Foo> _collection;

    public Example()
    {
        _collection = new BindingList<Foo>();
        _collection.ListChanged += Collection_ListChanged;
    }

    void Collection_ListChanged(object sender, ListChangedEventArgs e)
    {
        MessageBox.Show(e.ListChangedType.ToString());
    }

}

Класс BindingList , как в .net sence 2.0. Он будет вызывать событие ListChanged каждый раз, когда предмет в коллекции срабатывает INotifyPropertyChanged.

0 голосов
/ 05 сентября 2014

Rxx 2.0 содержит операторов , которые вместе с этим оператором преобразования для ObservableCollection<T> облегчают достижение вашей цели.

ObservableCollection<MyClass> collection = ...;

var changes = collection.AsCollectionNotifications<MyClass>();
var itemChanges = changes.PropertyChanges();
var deepItemChanges = changes.PropertyChanges(
  item => item.ChildItems.AsCollectionNotifications<MyChildClass>());

Следующие свойства измененных шаблонов уведомлений поддерживаются для MyClass и MyChildClass:

  • INotifyPropertyChanged
  • [Свойство] Изменен шаблон событий (устаревший, для использования в компонентной модели)
  • Свойства зависимостей WPF
0 голосов
/ 02 марта 2013

@soren.enemaerke Сделал это ответом, чтобы опубликовать правильный код, так как раздел комментариев к вашему ответу сделает его нечитаемым. Единственная проблема, с которой я столкнулся при решении этой проблемы, заключается в том, что конкретный элемент, вызывающий событие PropertyChanged, теряется, и вы не можете узнать об этом при вызове PropertyChanged.

col.PropertyChanged += (s, e) => { Trace.WriteLine("Changed " + e.PropertyName)

Чтобы исправить это, я создал новый класс PropertyChangedEventArgsEx и изменил метод ContainedElementChanged в вашем классе.

новый класс

public class PropertyChangedEventArgsEx : PropertyChangedEventArgs
{
    public object Sender { get; private set; }

    public PropertyChangedEventArgsEx(string propertyName, object sender) 
        : base(propertyName)
    {
        this.Sender = sender;
    }
}

изменения в вашем классе

 private void ContainedElementChanged(object sender, PropertyChangedEventArgs e)
    {
        var ex = new PropertyChangedEventArgsEx(e.PropertyName, sender);
        OnPropertyChanged(ex);
    }

После этого вы можете получить фактический элемент Sender в col.PropertyChanged += (s, e), применив e к PropertyChangedEventArgsEx

((INotifyPropertyChanged)col).PropertyChanged += (s, e) =>
        {
            var argsEx = (PropertyChangedEventArgsEx)e;
            Trace.WriteLine(argsEx.Sender.ToString());
        };

Опять же, вы должны заметить, что s - это набор элементов, а не фактический элемент, вызвавший событие. Отсюда новое свойство Sender в классе PropertyChangedEventArgsEx.

0 голосов
/ 06 ноября 2008

Ознакомьтесь с C5 Generic Collection Library . Все его коллекции содержат события, которые можно использовать для прикрепления обратных вызовов, когда элементы добавляются, удаляются, вставляются, очищаются или когда коллекция изменяется.

Я работаю над некоторыми расширениями этой библиотеки здесь , которые в ближайшем будущем должны допускать события «предварительного просмотра», которые могут позволить вам отменить добавление или изменение.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...