ObservableCollection, который также отслеживает изменения элементов в коллекции

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

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

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

Ответ 1

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

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

Мысли?

EDIT: следует отметить, что BCL ObservableCollection предоставляет только интерфейс INotifyPropertyChanged через явную реализацию, поэтому вам необходимо предоставить листинг, чтобы прикрепить к событию так:

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

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

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

EDIT4: Вау, это действительно учиться-как-ты-го!:). КП отметил, что событие было запущено с коллекцией в качестве отправителя, а не с элементом, когда элемент, содержащий элемент, изменяется. Он предложил объявить событие PropertyChanged в классе, отмеченном новым. У этого было бы несколько проблем, которые я попытаюсь проиллюстрировать с помощью примера ниже:

  // 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

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

Ответ 2

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

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

element.PropertyChanged += ContainedElementChanged;

и

element.PropertyChanged -= ContainedElementChanged;

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

private void ContainedElementChanged(object sender, PropertyChangedEventArgs e)

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

Ответ 3

Если вы хотите использовать что-то встроенное в фреймворк, вы можете использовать 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)
    {
    }
}

Ответ 4

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

reactiveCollection.Changed.Subscribe(_ => ...);

Ответ 5

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

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

Ответ 6

@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.

Ответ 7

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

Ответ 8

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

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.