Reputation: 4830
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?
Upvotes: 33
Views: 28553
Reputation: 127543
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
.
Upvotes: 1
Reputation: 2652
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
:
Upvotes: 0
Reputation: 2750
@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.
Upvotes: 0
Reputation: 5650
I would use ReactiveUI's ReactiveCollection
:
reactiveCollection.Changed.Subscribe(_ => ...);
Upvotes: 1
Reputation: 4830
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.
Upvotes: 43
Reputation: 6848
@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 :)
Upvotes: 7
Reputation: 7880
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)
{
}
}
Upvotes: 3
Reputation: 8404
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.
Upvotes: 0