ObservableCollection not noticing when Item in it changes (even with INotifyPropertyChanged)

前端 未结 18 2691
一生所求
一生所求 2020-11-22 02:06

Does anyone know why this code doesn\'t work:

public class CollectionViewModel : ViewModelBase {  
    public ObservableCollection Con         


        
相关标签:
18条回答
  • 2020-11-22 03:02

    ObservableCollection will not propagate individual item changes as CollectionChanged events. You will either need to subscribe to each event and forward it manually, or you can check out the BindingList[T] class, which will do this for you.

    0 讨论(0)
  • 2020-11-22 03:03

    Here is a drop-in class that sub-classes ObservableCollection and actually raises a Reset action when a property on a list item changes. It enforces all items to implement INotifyPropertyChanged.

    The benefit here is that you can data bind to this class and all of your bindings will update with changes to your item properties.

    public sealed class TrulyObservableCollection<T> : ObservableCollection<T>
        where T : INotifyPropertyChanged
    {
        public TrulyObservableCollection()
        {
            CollectionChanged += FullObservableCollectionCollectionChanged;
        }
    
        public TrulyObservableCollection(IEnumerable<T> pItems) : this()
        {
            foreach (var item in pItems)
            {
                this.Add(item);
            }
        }
    
        private void FullObservableCollectionCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.NewItems != null)
            {
                foreach (Object item in e.NewItems)
                {
                    ((INotifyPropertyChanged)item).PropertyChanged += ItemPropertyChanged;
                }
            }
            if (e.OldItems != null)
            {
                foreach (Object item in e.OldItems)
                {
                    ((INotifyPropertyChanged)item).PropertyChanged -= ItemPropertyChanged;
                }
            }
        }
    
        private void ItemPropertyChanged(object sender, PropertyChangedEventArgs e)
        {            
            NotifyCollectionChangedEventArgs args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, sender, sender, IndexOf((T)sender));
            OnCollectionChanged(args);
        }
    }
    
    0 讨论(0)
  • 2020-11-22 03:06

    Here is my version of the implementation. It checks and throws an error, if the objects in list doesnt implement INotifyPropertyChanged, so can't forget that issue while developing. On the outside you use the ListItemChanged Event do determine whether the list or the list item itself has changed.

    public class SpecialObservableCollection<T> : ObservableCollection<T>
    {
        public SpecialObservableCollection()
        {
            this.CollectionChanged += OnCollectionChanged;
        }
    
        void OnCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            AddOrRemoveListToPropertyChanged(e.NewItems,true); 
            AddOrRemoveListToPropertyChanged(e.OldItems,false); 
        }
    
        private void AddOrRemoveListToPropertyChanged(IList list, Boolean add)
        {
            if (list == null) { return; }
            foreach (object item in list)
            {
                INotifyPropertyChanged o = item as INotifyPropertyChanged;
                if (o != null)
                {
                    if (add)  { o.PropertyChanged += ListItemPropertyChanged; }
                    if (!add) { o.PropertyChanged -= ListItemPropertyChanged; }
                }
                else
                {
                    throw new Exception("INotifyPropertyChanged is required");
                }
            }
        }
    
        void ListItemPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            OnListItemChanged(this, e);
        }
    
        public delegate void ListItemChangedEventHandler(object sender, PropertyChangedEventArgs e);
    
        public event ListItemChangedEventHandler ListItemChanged;
    
        private void OnListItemChanged(Object sender, PropertyChangedEventArgs e)
        {
            if (ListItemChanged != null) { this.ListItemChanged(this, e); }
        }
    
    
    }
    
    0 讨论(0)
  • 2020-11-22 03:09

    The ContentList's Set method will not get called when you change a value inside the collection, instead you should be looking out for the CollectionChanged event firing.

    public class CollectionViewModel : ViewModelBase
    {          
        public ObservableCollection<EntityViewModel> ContentList
        {
            get { return _contentList; }
        }
    
        public CollectionViewModel()
        {
             _contentList = new ObservableCollection<EntityViewModel>();
             _contentList.CollectionChanged += ContentCollectionChanged;
        }
    
        public void ContentCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            //This will get called when the collection is changed
        }
    }
    

    Okay, that's twice today I've been bitten by the MSDN documentation being wrong. In the link I gave you it says:

    Occurs when an item is added, removed, changed, moved, or the entire list is refreshed.

    But it actually doesn't fire when an item is changed. I guess you'll need a more bruteforce method then:

    public class CollectionViewModel : ViewModelBase
    {          
        public ObservableCollection<EntityViewModel> ContentList
        {
            get { return _contentList; }
        }
    
        public CollectionViewModel()
        {
             _contentList = new ObservableCollection<EntityViewModel>();
             _contentList.CollectionChanged += ContentCollectionChanged;
        }
    
        public void ContentCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Remove)
            {
                foreach(EntityViewModel item in e.OldItems)
                {
                    //Removed items
                    item.PropertyChanged -= EntityViewModelPropertyChanged;
                }
            }
            else if (e.Action == NotifyCollectionChangedAction.Add)
            {
                foreach(EntityViewModel item in e.NewItems)
                {
                    //Added items
                    item.PropertyChanged += EntityViewModelPropertyChanged;
                }     
            }       
        }
    
        public void EntityViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            //This will get called when the property of an object inside the collection changes
        }
    }
    

    If you are going to need this a lot you may want to subclass your own ObservableCollection that triggers the CollectionChanged event when a member triggers its PropertyChanged event automatically (like it says it should in the documentation...)

    0 讨论(0)
  • 2020-11-22 03:09

    I've put together what I hope is a pretty robust solution, including some of the techniques in other answers. It is a new class derived from ObservableCollection<>, which I'm calling FullyObservableCollection<>

    It has the following features:

    • It adds a new event, ItemPropertyChanged. I've deliberately kept this separate from the existing CollectionChanged:
      • To aid backward compatibility.
      • So more relevant detail can be given in the new ItemPropertyChangedEventArgs that accompanies it: the original PropertyChangedEventArgs and the index within the collection.
    • It replicates all the constructors from ObservableCollection<>.
    • It correctly handles the list being reset (ObservableCollection<>.Clear()), avoiding a possible memory leak.
    • It overrides the base class's OnCollectionChanged(), rather than a more resource-intensive subscription to the CollectionChanged event.

    Code

    The complete .cs file follows. Note that a few features of C# 6 have been used, but it should be fairly simple to backport it:

    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Collections.Specialized;
    using System.ComponentModel;
    
    namespace Utilities
    {
        public class FullyObservableCollection<T> : ObservableCollection<T>
            where T : INotifyPropertyChanged
        {
            /// <summary>
            /// Occurs when a property is changed within an item.
            /// </summary>
            public event EventHandler<ItemPropertyChangedEventArgs> ItemPropertyChanged;
    
            public FullyObservableCollection() : base()
            { }
    
            public FullyObservableCollection(List<T> list) : base(list)
            {
                ObserveAll();
            }
    
            public FullyObservableCollection(IEnumerable<T> enumerable) : base(enumerable)
            {
                ObserveAll();
            }
    
            protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
            {
                if (e.Action == NotifyCollectionChangedAction.Remove ||
                    e.Action == NotifyCollectionChangedAction.Replace)
                {
                    foreach (T item in e.OldItems)
                        item.PropertyChanged -= ChildPropertyChanged;
                }
    
                if (e.Action == NotifyCollectionChangedAction.Add ||
                    e.Action == NotifyCollectionChangedAction.Replace)
                {
                    foreach (T item in e.NewItems)
                        item.PropertyChanged += ChildPropertyChanged;
                }
    
                base.OnCollectionChanged(e);
            }
    
            protected void OnItemPropertyChanged(ItemPropertyChangedEventArgs e)
            {
                ItemPropertyChanged?.Invoke(this, e);
            }
    
            protected void OnItemPropertyChanged(int index, PropertyChangedEventArgs e)
            {
                OnItemPropertyChanged(new ItemPropertyChangedEventArgs(index, e));
            }
    
            protected override void ClearItems()
            {
                foreach (T item in Items)
                    item.PropertyChanged -= ChildPropertyChanged;
    
                base.ClearItems();
            }
    
            private void ObserveAll()
            {
                foreach (T item in Items)
                    item.PropertyChanged += ChildPropertyChanged;
            }
    
            private void ChildPropertyChanged(object sender, PropertyChangedEventArgs e)
            {
                T typedSender = (T)sender;
                int i = Items.IndexOf(typedSender);
    
                if (i < 0)
                    throw new ArgumentException("Received property notification from item not in collection");
    
                OnItemPropertyChanged(i, e);
            }
        }
    
        /// <summary>
        /// Provides data for the <see cref="FullyObservableCollection{T}.ItemPropertyChanged"/> event.
        /// </summary>
        public class ItemPropertyChangedEventArgs : PropertyChangedEventArgs
        {
            /// <summary>
            /// Gets the index in the collection for which the property change has occurred.
            /// </summary>
            /// <value>
            /// Index in parent collection.
            /// </value>
            public int CollectionIndex { get; }
    
            /// <summary>
            /// Initializes a new instance of the <see cref="ItemPropertyChangedEventArgs"/> class.
            /// </summary>
            /// <param name="index">The index in the collection of changed item.</param>
            /// <param name="name">The name of the property that changed.</param>
            public ItemPropertyChangedEventArgs(int index, string name) : base(name)
            {
                CollectionIndex = index;
            }
    
            /// <summary>
            /// Initializes a new instance of the <see cref="ItemPropertyChangedEventArgs"/> class.
            /// </summary>
            /// <param name="index">The index.</param>
            /// <param name="args">The <see cref="PropertyChangedEventArgs"/> instance containing the event data.</param>
            public ItemPropertyChangedEventArgs(int index, PropertyChangedEventArgs args) : this(index, args.PropertyName)
            { }
        }
    }
    

    NUnit Tests

    So you can check changes you might make (and see what I tested in the first place!), I've also included my NUnit test class. Obviously, the following code is not necessary just to use FullyObservableCollection<T> in your project.

    NB The test class uses BindableBase from PRISM to implement INotifyPropertyChanged. There is no dependency on PRISM from the main code.

    using NUnit.Framework;
    using Utilities;
    using Microsoft.Practices.Prism.Mvvm;
    using System.Collections.Specialized;
    using System.Collections.Generic;
    
    namespace Test_Utilities
    {
        [TestFixture]
        public class Test_FullyObservableCollection : AssertionHelper
        {
            public class NotifyingTestClass : BindableBase
            {
                public int Id
                {
                    get { return _Id; }
                    set { SetProperty(ref _Id, value); }
                }
                private int _Id;
    
                public string Name
                {
                    get { return _Name; }
                    set { SetProperty(ref _Name, value); }
                }
                private string _Name;
    
            }
    
            FullyObservableCollection<NotifyingTestClass> TestCollection;
            NotifyingTestClass Fred;
            NotifyingTestClass Betty;
            List<NotifyCollectionChangedEventArgs> CollectionEventList;
            List<ItemPropertyChangedEventArgs> ItemEventList;
    
            [SetUp]
            public void Init()
            {
                Fred = new NotifyingTestClass() { Id = 1, Name = "Fred" };
                Betty = new NotifyingTestClass() { Id = 4, Name = "Betty" };
    
                TestCollection = new FullyObservableCollection<NotifyingTestClass>()
                    {
                        Fred,
                        new NotifyingTestClass() {Id = 2, Name = "Barney" },
                        new NotifyingTestClass() {Id = 3, Name = "Wilma" }
                    };
    
                CollectionEventList = new List<NotifyCollectionChangedEventArgs>();
                ItemEventList = new List<ItemPropertyChangedEventArgs>();
                TestCollection.CollectionChanged += (o, e) => CollectionEventList.Add(e);
                TestCollection.ItemPropertyChanged += (o, e) => ItemEventList.Add(e);
            }
    
            // Change existing member property: just ItemPropertyChanged(IPC) should fire
            [Test]
            public void DetectMemberPropertyChange()
            {
                TestCollection[0].Id = 7;
    
                Expect(CollectionEventList.Count, Is.EqualTo(0));
    
                Expect(ItemEventList.Count, Is.EqualTo(1), "IPC count");
                Expect(ItemEventList[0].PropertyName, Is.EqualTo(nameof(Fred.Id)), "Field Name");
                Expect(ItemEventList[0].CollectionIndex, Is.EqualTo(0), "Collection Index");
            }
    
    
            // Add new member, change property: CollectionPropertyChanged (CPC) and IPC should fire
            [Test]
            public void DetectNewMemberPropertyChange()
            {
                TestCollection.Add(Betty);
    
                Expect(TestCollection.Count, Is.EqualTo(4));
                Expect(TestCollection[3].Name, Is.EqualTo("Betty"));
    
                Expect(ItemEventList.Count, Is.EqualTo(0), "Item Event count");
    
                Expect(CollectionEventList.Count, Is.EqualTo(1), "Collection Event count");
                Expect(CollectionEventList[0].Action, Is.EqualTo(NotifyCollectionChangedAction.Add), "Action (add)");
                Expect(CollectionEventList[0].OldItems, Is.Null, "OldItems count");
                Expect(CollectionEventList[0].NewItems.Count, Is.EqualTo(1), "NewItems count");
                Expect(CollectionEventList[0].NewItems[0], Is.EqualTo(Betty), "NewItems[0] dereference");
    
                CollectionEventList.Clear();      // Empty for next operation
                ItemEventList.Clear();
    
                TestCollection[3].Id = 7;
                Expect(CollectionEventList.Count, Is.EqualTo(0), "Collection Event count");
    
                Expect(ItemEventList.Count, Is.EqualTo(1), "Item Event count");
                Expect(TestCollection[ItemEventList[0].CollectionIndex], Is.EqualTo(Betty), "Collection Index dereference");
            }
    
    
            // Remove member, change property: CPC should fire for removel, neither CPC nor IPC should fire for change
            [Test]
            public void CeaseListentingWhenMemberRemoved()
            {
                TestCollection.Remove(Fred);
    
                Expect(TestCollection.Count, Is.EqualTo(2));
                Expect(TestCollection.IndexOf(Fred), Is.Negative);
    
                Expect(ItemEventList.Count, Is.EqualTo(0), "Item Event count (pre change)");
    
                Expect(CollectionEventList.Count, Is.EqualTo(1), "Collection Event count (pre change)");
                Expect(CollectionEventList[0].Action, Is.EqualTo(NotifyCollectionChangedAction.Remove), "Action (remove)");
                Expect(CollectionEventList[0].OldItems.Count, Is.EqualTo(1), "OldItems count");
                Expect(CollectionEventList[0].NewItems, Is.Null, "NewItems count");
                Expect(CollectionEventList[0].OldItems[0], Is.EqualTo(Fred), "OldItems[0] dereference");
    
                CollectionEventList.Clear();      // Empty for next operation
                ItemEventList.Clear();
    
                Fred.Id = 7;
                Expect(CollectionEventList.Count, Is.EqualTo(0), "Collection Event count (post change)");
                Expect(ItemEventList.Count, Is.EqualTo(0), "Item Event count (post change)");
            }
    
    
            // Move member in list, change property: CPC should fire for move, IPC should fire for change
            [Test]
            public void MoveMember()
            {
                TestCollection.Move(0, 1);
    
                Expect(TestCollection.Count, Is.EqualTo(3));
                Expect(TestCollection.IndexOf(Fred), Is.GreaterThan(0));
    
                Expect(ItemEventList.Count, Is.EqualTo(0), "Item Event count (pre change)");
    
                Expect(CollectionEventList.Count, Is.EqualTo(1), "Collection Event count (pre change)");
                Expect(CollectionEventList[0].Action, Is.EqualTo(NotifyCollectionChangedAction.Move), "Action (move)");
                Expect(CollectionEventList[0].OldItems.Count, Is.EqualTo(1), "OldItems count");
                Expect(CollectionEventList[0].NewItems.Count, Is.EqualTo(1), "NewItems count");
                Expect(CollectionEventList[0].OldItems[0], Is.EqualTo(Fred), "OldItems[0] dereference");
                Expect(CollectionEventList[0].NewItems[0], Is.EqualTo(Fred), "NewItems[0] dereference");
    
                CollectionEventList.Clear();      // Empty for next operation
                ItemEventList.Clear();
    
                Fred.Id = 7;
                Expect(CollectionEventList.Count, Is.EqualTo(0), "Collection Event count (post change)");
    
                Expect(ItemEventList.Count, Is.EqualTo(1), "Item Event count (post change)");
                Expect(TestCollection[ItemEventList[0].CollectionIndex], Is.EqualTo(Fred), "Collection Index dereference");
            }
    
    
            // Clear list, chnage property: only CPC should fire for clear and neither for property change
            [Test]
            public void ClearList()
            {
                TestCollection.Clear();
    
                Expect(TestCollection.Count, Is.EqualTo(0));
    
                Expect(ItemEventList.Count, Is.EqualTo(0), "Item Event count (pre change)");
    
                Expect(CollectionEventList.Count, Is.EqualTo(1), "Collection Event count (pre change)");
                Expect(CollectionEventList[0].Action, Is.EqualTo(NotifyCollectionChangedAction.Reset), "Action (reset)");
                Expect(CollectionEventList[0].OldItems, Is.Null, "OldItems count");
                Expect(CollectionEventList[0].NewItems, Is.Null, "NewItems count");
    
                CollectionEventList.Clear();      // Empty for next operation
                ItemEventList.Clear();
    
                Fred.Id = 7;
                Expect(CollectionEventList.Count, Is.EqualTo(0), "Collection Event count (post change)");
                Expect(ItemEventList.Count, Is.EqualTo(0), "Item Event count (post change)");
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-22 03:12

    You can also use this extension method to easily register a handler for item property change in relevant collections. This method is automatically added to all the collections implementing INotifyCollectionChanged that hold items that implement INotifyPropertyChanged:

    public static class ObservableCollectionEx
    {
        public static void SetOnCollectionItemPropertyChanged<T>(this T _this, PropertyChangedEventHandler handler)
            where T : INotifyCollectionChanged, ICollection<INotifyPropertyChanged> 
        {
            _this.CollectionChanged += (sender,e)=> {
                if (e.NewItems != null)
                {
                    foreach (Object item in e.NewItems)
                    {
                        ((INotifyPropertyChanged)item).PropertyChanged += handler;
                    }
                }
                if (e.OldItems != null)
                {
                    foreach (Object item in e.OldItems)
                    {
                        ((INotifyPropertyChanged)item).PropertyChanged -= handler;
                    }
                }
            };
        }
    }
    

    How to use:

    public class Test
    {
        public static void MyExtensionTest()
        {
            ObservableCollection<INotifyPropertyChanged> c = new ObservableCollection<INotifyPropertyChanged>();
            c.SetOnCollectionItemPropertyChanged((item, e) =>
            {
                 //whatever you want to do on item change
            });
        }
    }
    
    0 讨论(0)
提交回复
热议问题