tags:

views:

3885

answers:

4

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

+15  A: 

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.

soren.enemaerke
Marking this as the answer as nothing better (in my opinion) has surfaced
soren.enemaerke
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().
Josh Einstein
Nicely caught, Josh. I've updated the code to reflect this.
soren.enemaerke
See Mark Whitfeld's comment for a cleaner (and more correct) way to handle the subscription and unsubscription: http://stackoverflow.com/questions/269073/observablecollection-that-also-monitors-changes-on-the-elements-in-collection/3316435#3316435
kpozin
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 answer
Thomas Levesque
Updated the sample to include the unsubscribe stuff, thanks Mark and Thomas
soren.enemaerke
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?
kpozin
Hi KP, added an edit to answer you question of why 'hiding' with new is a bit complicated. Hope that helps
soren.enemaerke
A: 

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.

Marcus Griep
+3  A: 

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)
 {
 }
}
Todd White
You should also note that FreezableCollections are constrained to hold only items that inherit from DependencyObject.
Samuel Jack
+3  A: 

@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 :)

Mark Whitfeld
Thanks Mark, updated my answer to reflect your insights...
soren.enemaerke