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

前端 未结 18 2673
一生所求
一生所求 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: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 : ObservableCollection
            where T : INotifyPropertyChanged
        {
            /// 
            /// Occurs when a property is changed within an item.
            /// 
            public event EventHandler ItemPropertyChanged;
    
            public FullyObservableCollection() : base()
            { }
    
            public FullyObservableCollection(List list) : base(list)
            {
                ObserveAll();
            }
    
            public FullyObservableCollection(IEnumerable 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);
            }
        }
    
        /// 
        /// Provides data for the  event.
        /// 
        public class ItemPropertyChangedEventArgs : PropertyChangedEventArgs
        {
            /// 
            /// Gets the index in the collection for which the property change has occurred.
            /// 
            /// 
            /// Index in parent collection.
            /// 
            public int CollectionIndex { get; }
    
            /// 
            /// Initializes a new instance of the  class.
            /// 
            /// The index in the collection of changed item.
            /// The name of the property that changed.
            public ItemPropertyChangedEventArgs(int index, string name) : base(name)
            {
                CollectionIndex = index;
            }
    
            /// 
            /// Initializes a new instance of the  class.
            /// 
            /// The index.
            /// The  instance containing the event data.
            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 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 TestCollection;
            NotifyingTestClass Fred;
            NotifyingTestClass Betty;
            List CollectionEventList;
            List ItemEventList;
    
            [SetUp]
            public void Init()
            {
                Fred = new NotifyingTestClass() { Id = 1, Name = "Fred" };
                Betty = new NotifyingTestClass() { Id = 4, Name = "Betty" };
    
                TestCollection = new FullyObservableCollection()
                    {
                        Fred,
                        new NotifyingTestClass() {Id = 2, Name = "Barney" },
                        new NotifyingTestClass() {Id = 3, Name = "Wilma" }
                    };
    
                CollectionEventList = new List();
                ItemEventList = new List();
                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)");
            }
        }
    }
    

提交回复
热议问题