ObservableCollection,它还监视集合中元素的更改

本文关键字:元素 集合 监视 ObservableCollection | 更新日期: 2023-09-27 17:48:49

是否存在具有以下特征的集合(BCL或其他):

如果集合发生更改,则发送事件;如果集合中的任何元素发送PropertyChanged事件,则发送消息。有点像ObservableCollection<T>,其中T: INotifyPropertyChanged和集合也在监视元素的更改。

我可以自己包装一个可观察的集合,并在添加/删除集合中的元素时进行事件订阅/取消订阅,但我只是想知道是否有现有的集合已经这样做了?

ObservableCollection,它还监视集合中元素的更改

我自己做了一个快速实现:

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

编辑2:增加了对ClearItems的处理,感谢Josh

第3版:添加了PropertyChanged的正确取消订阅,感谢Mark

第四版:哇,这真是边走边学:)。KP指出,当包含的元素发生更改时,事件是以集合作为发送方而不是以元素触发的。他建议在标有new的类上声明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

您可以从示例中看到,"覆盖"事件会产生副作用,即您在订阅事件时需要非常小心使用哪种类型的变量,因为这决定了您接收哪些事件。

@soren.enemaerke:我本想在你的回答帖上发表这样的评论,但我不能(我不知道为什么,也许是因为我没有太多的代表点)。无论如何,我只是想在你发布的代码中提到,我认为取消订阅不会正确工作,因为它正在创建一个新的lambda内联,然后试图删除它的事件处理程序。

我会将添加/删除事件处理程序行更改为以下内容:

element.PropertyChanged += ContainedElementChanged;

element.PropertyChanged -= ContainedElementChanged;

然后将ContainerdElementChanged方法签名更改为:

private void ContainedElementChanged(object sender, PropertyChangedEventArgs e)

这将识别出remove与add是针对同一个处理程序的,然后正确地将其移除。希望这能帮助到一些人:)

如果你想使用框架中内置的东西,你可以使用FreezableCollection。然后您将想要收听Changed事件。

当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)
    {
    }
}

我会使用ReactiveUI的ReactiveCollection:

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

最简单的方法就是进行

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中。每当集合中的某个项激发INotifyPropertyChanged时,它都会激发它的ListChanged事件。

查看C5通用收藏库。它的所有集合都包含事件,您可以使用这些事件在添加、删除、插入、清除项时或集合更改时附加回调。

我正在为这个库做一些扩展,在不久的将来,它应该允许"预览"事件,允许您取消添加或更改。

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

之后,您可以通过将e转换为PropertyChangedEventArgsEx 来获得col.PropertyChanged += (s, e)中的实际Sender元素

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

同样,您应该注意这里的s是元素的集合,而不是触发事件的实际元素。因此,PropertyChangedEventArgsEx类中出现了新的Sender属性。

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

MyClassMyChildClass支持以下属性更改通知模式:

  • INotifyPropertyChanged
  • [Property]更改的事件模式(遗留,供组件模型使用)
  • WPF依赖项属性