ObservableCollection and Item PropertyChanged

74,816

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; }
    }
}
Share:
74,816
Bill Campbell
Author by

Bill Campbell

Updated on July 09, 2022

Comments

  • Bill Campbell
    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 the PersonName as well. Like if I change the first name. I can implement OnPropertyChanged for each property and have this class derive from INotifyPropertyChanged 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 the ObservableCollection?

    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 a ObservableCollection. This collection is made up of a RowViewModel which has properties that support the PropertiesChanged event. But I can't figure out how to make the collection update itself so my view will be updated.

  • Bill Campbell
    Bill Campbell about 15 years
    Thanks. 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
    Goblin almost 14 years
    The DataGrid does this automatically if the elements in the ObservableCollection implement INotifyPropertyChanged (or are DependencyObjects).
  • pqsk
    pqsk almost 13 years
    This is beautiful. I've been looking for this for a while and it really helped me. Thanks a lot.
  • Michael Goldshteyn
    Michael Goldshteyn over 12 years
    Why are the two changed functions static?
  • chilltemp
    chilltemp about 12 years
    It'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
    infografnet over 11 years
    This is good. But fortunately, you don't have to Reset the collection, you can Replace also by: var replace = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedActi‌​on.Replace, sender, sender, this.Items.IndexOf((T)sender)); this.OnCollectionChanged(replace);
  • likebobby
    likebobby over 11 years
    @Marc-Ziss That's a nice solution.
  • likebobby
    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
    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
    Johan Larsson almost 11 years
    No need to check that e.OldItems != null and e.NewItems != null?
  • Reed Copsey
    Reed Copsey over 10 years
    Very 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
    Maverik about 10 years
    @JohanLarsson no, foreach won't iterate over null source
  • Maverik
    Maverik about 10 years
    Given all the different solutions, I think this one is the best implementation so far. Well done Johan.
  • CodeHulk
    CodeHulk over 9 years
    I don't think this works when Reset events. As they do not populate the old items.
  • chilltemp
    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
    Mark Richman about 9 years
    e.NewItems is null if you remove all objects from the collection. Best way to test? Before the foreach?
  • Alex Telon
    Alex Telon almost 7 years
    @Maverik The foreach will throw a NullReferenceException so we do in fact need to check for null.
  • Alex Telon
    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
    drojf over 5 years
    If 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
    Aaron. S about 4 years
    coming 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
    SANDEEP MACHIRAJU over 3 years
    It's not working for me. Same as Observable Collection. Am I missing anything?
  • Apfelkuacha
    Apfelkuacha about 3 years
    I replaced the CollectionChangedEventManager and PropertyChangedEventManager as it didn't work when using DependencyInjection (Unity) Instead using the regular += ChildPropertyChanged; did work
  • Alberto Rechy
    Alberto Rechy about 3 years
    I had no idea there was a DispatcherTimer that allows you to update the UI thread. Thanks