ObservableCollection and Item PropertyChanged
Solution 1
Here is how you would attach/detach to each item's PropertyChanged event.
ObservableCollection<INotifyPropertyChanged> items = new ObservableCollection<INotifyPropertyChanged>();
items.CollectionChanged += items_CollectionChanged;
static void items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.OldItems != null)
{
foreach (INotifyPropertyChanged item in e.OldItems)
item.PropertyChanged -= item_PropertyChanged;
}
if (e.NewItems != null)
{
foreach (INotifyPropertyChanged item in e.NewItems)
item.PropertyChanged += item_PropertyChanged;
}
}
static void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
throw new NotImplementedException();
}
Solution 2
We wrote this in the WPF-chat:
public class OcPropertyChangedListener<T> : INotifyPropertyChanged where T : INotifyPropertyChanged
{
private readonly ObservableCollection<T> _collection;
private readonly string _propertyName;
private readonly Dictionary<T, int> _items = new Dictionary<T, int>(new ObjectIdentityComparer());
public OcPropertyChangedListener(ObservableCollection<T> collection, string propertyName = "")
{
_collection = collection;
_propertyName = propertyName ?? "";
AddRange(collection);
CollectionChangedEventManager.AddHandler(collection, CollectionChanged);
}
private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
AddRange(e.NewItems.Cast<T>());
break;
case NotifyCollectionChangedAction.Remove:
RemoveRange(e.OldItems.Cast<T>());
break;
case NotifyCollectionChangedAction.Replace:
AddRange(e.NewItems.Cast<T>());
RemoveRange(e.OldItems.Cast<T>());
break;
case NotifyCollectionChangedAction.Move:
break;
case NotifyCollectionChangedAction.Reset:
Reset();
break;
default:
throw new ArgumentOutOfRangeException();
}
}
private void AddRange(IEnumerable<T> newItems)
{
foreach (T item in newItems)
{
if (_items.ContainsKey(item))
{
_items[item]++;
}
else
{
_items.Add(item, 1);
PropertyChangedEventManager.AddHandler(item, ChildPropertyChanged, _propertyName);
}
}
}
private void RemoveRange(IEnumerable<T> oldItems)
{
foreach (T item in oldItems)
{
_items[item]--;
if (_items[item] == 0)
{
_items.Remove(item);
PropertyChangedEventManager.RemoveHandler(item, ChildPropertyChanged, _propertyName);
}
}
}
private void Reset()
{
foreach (T item in _items.Keys.ToList())
{
PropertyChangedEventManager.RemoveHandler(item, ChildPropertyChanged, _propertyName);
_items.Remove(item);
}
AddRange(_collection);
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void ChildPropertyChanged(object sender, PropertyChangedEventArgs e)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(sender, e);
}
private class ObjectIdentityComparer : IEqualityComparer<T>
{
public bool Equals(T x, T y)
{
return object.ReferenceEquals(x, y);
}
public int GetHashCode(T obj)
{
return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj);
}
}
}
public static class OcPropertyChangedListener
{
public static OcPropertyChangedListener<T> Create<T>(ObservableCollection<T> collection, string propertyName = "") where T : INotifyPropertyChanged
{
return new OcPropertyChangedListener<T>(collection, propertyName);
}
}
- Weak events
- Keeps track of the same item being added multiple times to the collection
- It ~bubbles~ up the property changed events of the children.
- The static class is just for convenience.
Use it like this:
var listener = OcPropertyChangedListener.Create(yourCollection);
listener.PropertyChanged += (sender, args) => { //do you stuff}
Solution 3
Bill,
I'm sure that you have found a workaround or solution to your issue by now, but I posted this for anyone with this common issue. You can substitute this class for ObservableCollections that are collections of objects that implement INotifyPropertyChanged. It is kind of draconian, because it says that the list needs to Reset rather than find the one property/item that has changed, but for small lists the performance hit should be unoticable.
Marc
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
namespace WCIOPublishing.Helpers
{
public class ObservableCollectionWithItemNotify<T> : ObservableCollection<T> where T: INotifyPropertyChanged
{
public ObservableCollectionWithItemNotify()
{
this.CollectionChanged += items_CollectionChanged;
}
public ObservableCollectionWithItemNotify(IEnumerable<T> collection) :base( collection)
{
this.CollectionChanged += items_CollectionChanged;
foreach (INotifyPropertyChanged item in collection)
item.PropertyChanged += item_PropertyChanged;
}
private void items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if(e != null)
{
if(e.OldItems!=null)
foreach (INotifyPropertyChanged item in e.OldItems)
item.PropertyChanged -= item_PropertyChanged;
if(e.NewItems!=null)
foreach (INotifyPropertyChanged item in e.NewItems)
item.PropertyChanged += item_PropertyChanged;
}
}
private void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
var reset = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
this.OnCollectionChanged(reset);
}
}
}
Solution 4
As you found out, there is no collection-level event that indicates that a property of an item in the collection has changed. Generally, the code responsible for displaying the data adds a PropertyChanged event handler to each object currently displayed onscreen.
Solution 5
Instead of ObservableCollection simply use the BindingList<T>.
The following code shows a DataGrid binding to a List and to item's properties.
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<DataGrid ItemsSource="{Binding}" AutoGenerateColumns="False" >
<DataGrid.Columns>
<DataGridTextColumn Header="Values" Binding="{Binding Value}" />
</DataGrid.Columns>
</DataGrid>
</Window>
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Threading;
namespace WpfApplication1 {
public partial class MainWindow : Window {
public MainWindow() {
var c = new BindingList<Data>();
this.DataContext = c;
// add new item to list on each timer tick
var t = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(1) };
t.Tick += (s, e) => {
if (c.Count >= 10) t.Stop();
c.Add(new Data());
};
t.Start();
}
}
public class Data : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged = delegate { };
System.Timers.Timer t;
static Random r = new Random();
public Data() {
// update value on each timer tick
t = new System.Timers.Timer() { Interval = r.Next(500, 1000) };
t.Elapsed += (s, e) => {
Value = DateTime.Now.Ticks;
this.PropertyChanged(this, new PropertyChangedEventArgs("Value"));
};
t.Start();
}
public long Value { get; private set; }
}
}
Bill Campbell
Updated on July 09, 2022Comments
-
Bill Campbell almost 2 years
I've seen lots of talk about this question but maybe I'm just too much of a newbie to get it. If I have an observable collection that is a collection of "PersonNames" as in the msdn example (http: //msdn.microsoft.com/en-us/library/ms748365.aspx), I get updates to my View if a
PersonName
is added or removed, etc. I want to get an update to my View when I change a property in thePersonName
as well. Like if I change the first name. I can implementOnPropertyChanged
for each property and have this class derive fromINotifyPropertyChanged
and that seems to get called as expected.My question is, how does the View get the updated data from the
ObservableCollection
as the property changed does not cause any event for theObservableCollection
?This is probably something really simple but why I can't seem to find an example surprises me. Can anyone shed any light on this for me or have any pointers to examples I would greatly appreciate it. We have this scenario in multiple places in our current WPF app and are struggling with figuring it out.
"Generally, the code responsible for displaying the data adds a
PropertyChanged
event handler to each object currently displayed onscreen."Could someone please give me an example of what this means? My View binds to my
ViewModel
which has aObservableCollection
. This collection is made up of aRowViewModel
which has properties that support thePropertiesChanged
event. But I can't figure out how to make the collection update itself so my view will be updated. -
Bill Campbell about 15 yearsThanks. I am using WPF and have a DataGrid whose ItemsSource is binding in XAML to the ObservableCollection. So, I need to add code somewhere in my ViewModel to handle the PropertyChanged event in order for the View to know to update the DataGrid? And then do I have to remove and add the item to the collection to get it the View to update it? It's seems counter intuitive (but that doesn't mean it's not right :)
-
Goblin almost 14 yearsThe DataGrid does this automatically if the elements in the ObservableCollection implement INotifyPropertyChanged (or are DependencyObjects).
-
pqsk almost 13 yearsThis is beautiful. I've been looking for this for a while and it really helped me. Thanks a lot.
-
Michael Goldshteyn over 12 yearsWhy are the two changed functions static?
-
chilltemp about 12 yearsIt's been a while, and I can't find the source that I abstracted for this sample. I think I was using WPF dependency properties that were static. I see no reason why these functions can't be per-instance.
-
infografnet over 11 yearsThis is good. But fortunately, you don't have to Reset the collection, you can Replace also by: var replace = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, sender, sender, this.Items.IndexOf((T)sender)); this.OnCollectionChanged(replace);
-
likebobby over 11 years@Marc-Ziss That's a nice solution.
-
likebobby over 11 years@infografnet I can't get it to work with Replace instead. Could it be cause the oldItem and newItem are both sender? I've made sure that the property changing does not check if the value is the same as before.
-
infografnet over 11 years@BobbyJ, Yes, you're right. In this case oldItem will be the same like newItem. But this should not disturb. Just try skip checking oldItem in your callback function. if (e.Action == NotifyCollectionChangedAction.Replace) { do something with e.NewItems and don't check e.OldItems; }
-
Johan Larsson almost 11 yearsNo need to check that e.OldItems != null and e.NewItems != null?
-
Reed Copsey over 10 yearsVery nice - I particularly like that it's using weak events to track the items, since that eliminates a lot of the complexity of unsubscribing and makes it more useful.
-
Maverik about 10 years@JohanLarsson no, foreach won't iterate over null source
-
Maverik about 10 yearsGiven all the different solutions, I think this one is the best implementation so far. Well done Johan.
-
CodeHulk over 9 yearsI don't think this works when Reset events. As they do not populate the old items.
-
chilltemp over 9 years@CodeHulk: Yes that would defiantly be a problem. This would only be safe to use if the collection never had anything removed, or you had another way of tracking deletions.
-
Mark Richman about 9 years
e.NewItems
is null if you remove all objects from the collection. Best way to test? Before theforeach
? -
Alex Telon almost 7 years@Maverik The foreach will throw a NullReferenceException so we do in fact need to check for null.
-
Alex Telon almost 7 years@MarkRichman One before each
foreach
is a good idea. Or you could use null coalescing operator. You can see examples of this here -
drojf over 5 yearsIf you want to properly handle Reset events, see this example which handles different types of Actions: stackoverflow.com/a/8168913/848627 - that example only handles 3 actions, but you should handle all 5 possible actions as per: docs.microsoft.com/en-us/dotnet/api/… . Would also be a good idea to add a default case.
-
Aaron. S about 4 yearscoming in late here. works fine for the parent model but not navigation properties, such as a model property that is a List that implements INotifyPropertyChanged
-
SANDEEP MACHIRAJU over 3 yearsIt's not working for me. Same as Observable Collection. Am I missing anything?
-
Apfelkuacha about 3 yearsI replaced the CollectionChangedEventManager and PropertyChangedEventManager as it didn't work when using DependencyInjection (Unity) Instead using the regular
+= ChildPropertyChanged;
did work -
Alberto Rechy about 3 yearsI had no idea there was a DispatcherTimer that allows you to update the UI thread. Thanks