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
.