ObservableCollection that also monitors changes on the elements in collection
Asked Answered
V

8

33

Is there a collection (BCL or other) that has the following characteristics:

Sends event if collection is changed AND sends event if any of the elements in the collection sends a PropertyChanged event. Sort of an ObservableCollection<T> where T: INotifyPropertyChanged and the collection is also monitoring the elements for changes.

I could wrap an observable collection my self and do the event subscribe/unsubscribe when elements in the collection are added/removed but I was just wondering if any existing collections did this already?

Vin answered 6/11, 2008 at 15:20 Comment(0)
V
43

Made a quick implementation myself:

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

Admitted, it would be kind of confusing and misleading to have the PropertyChanged fire on the collection when the property that actually changed is on a contained element, but it would fit my specific purpose. It could be extended with a new event that is fired instead inside ContainerElementChanged

Thoughts?

EDIT: Should note that the BCL ObservableCollection only exposes the INotifyPropertyChanged interface through an explicit implementation so you would need to provide a cast in order to attach to the event like so:

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

EDIT2: Added handling of ClearItems, thanks Josh

EDIT3: Added a correct unsubscribe for PropertyChanged, thanks Mark

EDIT4: Wow, this is really learn-as-you-go :). KP noted that the event was fired with the collection as sender and not with the element when the a contained element changes. He suggested declaring a PropertyChanged event on the class marked with new. This would have a few issues which I'll try to illustrate with the sample below:

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

You can see from the sample that 'overriding' the event has the side effect that you need to be extremely careful of which type of variable you use when subscribing to the event since that dictates which events you receive.

Vin answered 6/11, 2008 at 15:28 Comment(10)
Marking this as the answer as nothing better (in my opinion) has surfacedVin
There is an issue with this class though. If you call Clear() the OnCollectionChanged event will get a Reset notification and you won't have access to the items that were cleared from the collection. This can be mitigated by overriding ClearItems and unsubscribing the handlers before calling base.ClearItems().Bearce
See Mark Whitfeld's comment for a cleaner (and more correct) way to handle the subscription and unsubscription: #269573Malachy
In your implementation, the handlers for PropertyChanged are never removed, because each occurrence of your lambda expression creates a new delegate instance. You should use a method instead, as explained in Mark's answerMonoclinous
Updated the sample to include the unsubscribe stuff, thanks Mark and ThomasVin
With this implementation, subscribers to the PropertyChanged event will never know which item had a property change, as sender will always be the collection itself. Would hiding the PropertyChanged event with a new event and raising it with the changed element as the sender parameter be a reasonable way to fix this?Malachy
Hi KP, added an edit to answer you question of why 'hiding' with new is a bit complicated. Hope that helpsVin
I added constructor compatibility: public ObservableCollectionEx() : base() { } public ObservableCollectionEx(IEnumerable<T> ie) : base(ie) { Subscribe(ie.ToList<T>()); }Madelle
It would be helpful to pass forward also the sender argument from ContainedElementChanged. By inheriting from PropertyChangedEventArgs and adding the sender to PropertyChangedEventArgs, you got the element in which the change occurredGaea
Thanks a lot for the solution. The only problem I had with it is that the actual element that triggers the call is lost. I've added another reply to this thread to show the code changes I've made to fix this.Planography
M
7

@soren.enemaerke: I would have made this comment on your answer post, but I can't (I don't know why, maybe because I don't have many rep points). Anyway, I just thought that I'd mention that in your code you posted I don't think that the Unsubscribe would work correctly because it is creating a new lambda inline and then trying to remove the event handler for it.

I would change the add/remove event handler lines to something like:

element.PropertyChanged += ContainedElementChanged;

and

element.PropertyChanged -= ContainedElementChanged;

And then change the ContainedElementChanged method signature to:

private void ContainedElementChanged(object sender, PropertyChangedEventArgs e)

This would recognise that the remove is for the same handler as the add and then remove it correctly. Hope this helps somebody :)

Marquis answered 23/7, 2010 at 8:14 Comment(0)
J
3

If you want to use something built into the framework you can use FreezableCollection. Then you will want to listen to the Changed event.

Occurs when the Freezable or an object it contains is modified.

Here is a small sample. The collection_Changed method will get called twice.

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)
    {
    }
}
Jem answered 6/11, 2008 at 16:17 Comment(1)
You should also note that FreezableCollections are constrained to hold only items that inherit from DependencyObject.Roslyn
A
1

I would use ReactiveUI's ReactiveCollection:

reactiveCollection.Changed.Subscribe(_ => ...);
Aseptic answered 6/11, 2011 at 0:10 Comment(0)
O
1

The simplest way to do it is just do

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

}

The BindingList class as been in .net sence 2.0. It will fire it's ListChanged event any time a item in the collection fires INotifyPropertyChanged.

Orvas answered 7/2, 2018 at 22:10 Comment(1)
So the BCL already contains a built-in class solving this exact problem. Why is this answer at the bottom of the list since almost 5 years with no upvotes?Fluellen
S
0

Check out the C5 Generic Collection Library. All of its collections contain events that you can use to attach callbacks for when items are added, removed, inserted, cleared, or when the collection changes.

I am working for some extensions to that libary here that in the near future should allow for "preview" events that could allow you to cancel an add or change.

Samford answered 6/11, 2008 at 16:4 Comment(0)
P
0

@soren.enemaerke Made this a reply in order to post proper code as the comments section on your answer would render it unreadable. The only issue I've had with the solution is that the particular element which triggers the PropertyChanged event is lost and you have no way of knowing in the PropertyChanged call.

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

To fix this I've created a new class PropertyChangedEventArgsEx and changed the ContainedElementChanged method within your class.

new class

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

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

changes to your class

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

After this you can get the actual Sender element in col.PropertyChanged += (s, e) by casting e to PropertyChangedEventArgsEx

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

Again, you should note the the s here is the collection of elements, not the actual element that triggered the event. Hence the new Sender property in the PropertyChangedEventArgsEx class.

Planography answered 2/3, 2013 at 0:34 Comment(0)
P
0

Rxx 2.0 contains operators that along with this conversion operator for ObservableCollection<T> makes it easy to achieve your goal.

ObservableCollection<MyClass> collection = ...;

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

The following property changed notification patterns are supported for MyClass and MyChildClass:

  • INotifyPropertyChanged
  • [Property]Changed event pattern (legacy, for use by Component Model)
  • WPF dependency properties
Paco answered 4/9, 2014 at 21:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.