53

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.

StayOnTarget
  • 7,829
  • 8
  • 42
  • 59
Bill Campbell
  • 2,243
  • 6
  • 24
  • 32

6 Answers6

73

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();
}
Alex Telon
  • 867
  • 10
  • 27
chilltemp
  • 8,348
  • 8
  • 38
  • 45
  • 2
    This is beautiful. I've been looking for this for a while and it really helped me. Thanks a lot. – pqsk Jul 27 '11 at 21:40
  • 1
    Why are the two changed functions static? – Michael Goldshteyn Mar 20 '12 at 19:27
  • 2
    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. – chilltemp Apr 12 '12 at 15:54
  • 9
    No need to check that e.OldItems != null and e.NewItems != null? – Johan Larsson Jul 25 '13 at 11:52
  • @JohanLarsson no, foreach won't iterate over null source – Maverik May 09 '14 at 11:10
  • 4
    I don't think this works when Reset events. As they do not populate the old items. – CodeHulk Jan 30 '15 at 12:57
  • @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. – chilltemp Feb 09 '15 at 18:10
  • `e.NewItems` is null if you remove all objects from the collection. Best way to test? Before the `foreach`? – Mark Richman Jun 19 '15 at 18:02
  • 1
    @Maverik The foreach will throw a NullReferenceException so we do in fact need to check for null. – Alex Telon Jul 14 '17 at 11:30
  • @MarkRichman One before each `foreach` is a good idea. Or you could use *null coalescing* operator. You can see examples of this [here](https://stackoverflow.com/questions/6455311/is-ifitems-null-superfluous-before-foreacht-item-in-items) – Alex Telon Jul 14 '17 at 11:33
  • If you want to properly handle Reset events, see this example which handles different types of Actions: https://stackoverflow.com/a/8168913/848627 - that example only handles 3 actions, but you should handle all 5 possible actions as per: https://docs.microsoft.com/en-us/dotnet/api/system.collections.specialized.notifycollectionchangedaction?view=netframework-4.7.2 . Would also be a good idea to add a default case. – drojf Oct 09 '18 at 00:21
25

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}
Johan Larsson
  • 15,603
  • 9
  • 65
  • 81
  • 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. – Reed Copsey Nov 12 '13 at 21:20
  • 1
    Given all the different solutions, I think this one is the best implementation so far. Well done Johan. – Maverik May 09 '14 at 11:13
  • 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 – Aaron. S Apr 10 '20 at 09:02
  • I replaced the CollectionChangedEventManager and PropertyChangedEventManager as it didn't work when using DependencyInjection (Unity) Instead using the regular `+= ChildPropertyChanged;` did work – Apfelkuacha Apr 15 '21 at 06:39
13

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

        }

    }
}
Marc Ziss
  • 550
  • 5
  • 9
  • This 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); – infografnet Oct 12 '12 at 10:46
  • @Marc-Ziss That's a nice solution. – likebobby Nov 13 '12 at 13:00
  • @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. – likebobby Nov 13 '12 at 13:13
  • 1
    @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; } – infografnet Nov 14 '12 at 10:44
4

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; }
    }
}
Stack
  • 195
  • 1
  • 11
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.

Jason Kresowaty
  • 15,147
  • 9
  • 54
  • 79
  • 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 :) – Bill Campbell May 24 '09 at 11:25
  • 1
    The DataGrid does this automatically if the elements in the ObservableCollection implement INotifyPropertyChanged (or are DependencyObjects). – Goblin Aug 16 '10 at 19:45
2

Following is the code giving a simple explanation of answer by @Stack and showing how BindingList is observing if it has a item changed and shows ObservableCollection will not observe the change inside an item.

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;

namespace BindingListExample
{
    class Program
    {
        public ObservableCollection<MyStruct> oc = new ObservableCollection<MyStruct>();
        public System.ComponentModel.BindingList<MyStruct> bl = new BindingList<MyStruct>();

        public Program()
        {
            oc.Add(new MyStruct());
            oc.CollectionChanged += CollectionChanged;

            bl.Add(new MyStruct());
            bl.ListChanged += ListChanged;
        }

        void ListChanged(object sender, ListChangedEventArgs e)
        {
            //Observe when the IsActive value is changed this event is triggered.
            Console.WriteLine(e.ListChangedType.ToString());
        }

        void CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            //Observe when the IsActive value is changed this event is not triggered.
            Console.WriteLine(e.Action.ToString());
        }

        static void Main(string[] args)
        {
            Program pm = new Program();
            pm.bl[0].IsActive = false;
        }
    }

    public class MyStruct : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private bool isactive;
        public bool IsActive
        {
            get { return isactive; }
            set
            {
                isactive = value;
                NotifyPropertyChanged("IsActive");
            }
        }

        private void NotifyPropertyChanged(String PropertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
            }
        }
    }
}
Community
  • 1
  • 1
Abbas
  • 3,264
  • 1
  • 24
  • 47