ObservableCollection ไม่สังเกตเห็นเมื่อรายการที่มีการเปลี่ยนแปลง (แม้จะมี INotifyPropertyChanged)


167

ไม่มีใครรู้ว่าทำไมรหัสนี้ใช้งานไม่ได้:

public class CollectionViewModel : ViewModelBase {  
    public ObservableCollection<EntityViewModel> ContentList
    {
        get { return _contentList; }
        set 
        { 
            _contentList = value; 
            RaisePropertyChanged("ContentList"); 
            //I want to be notified here when something changes..?
            //debugger doesn't stop here when IsRowChecked is toggled
        }
     }
}

public class EntityViewModel : ViewModelBase
{

    private bool _isRowChecked;

    public bool IsRowChecked
    {
        get { return _isRowChecked; }
        set { _isRowChecked = value; RaisePropertyChanged("IsRowChecked"); }
    }
}

ViewModelBaseกักเก็บทุกอย่างไว้สำหรับRaisePropertyChangedฯลฯ และมันก็ใช้ได้กับทุกอย่างยกเว้นปัญหานี้ ..


คำตอบ:


119

เมธอด Set ของ ContentList จะไม่ถูกเรียกเมื่อคุณเปลี่ยนค่าภายในคอลเล็กชัน แต่คุณควรมองหาการยิงเหตุการณ์CollectionChanged

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
    }
}

โอเคนั่นเป็นสองครั้งวันนี้ฉันถูกเอกสาร MSDN ถูกกัด ในลิงค์ที่ฉันให้คุณมันบอกว่า:

เกิดขึ้นเมื่อรายการถูกเพิ่มลบเปลี่ยนแปลงย้ายหรือรายการทั้งหมดจะถูกรีเฟรช

แต่จริงๆแล้วมันไม่ได้ทำงานเมื่อมีการเปลี่ยนแปลงรายการ ฉันเดาว่าคุณจะต้องใช้วิธีการดุร้ายมากขึ้นแล้ว:

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
    }
}

หากคุณต้องการสิ่งนี้มากคุณอาจต้องการคลาสย่อยของคุณเองObservableCollectionที่เรียกใช้CollectionChangedเหตุการณ์เมื่อสมาชิกทริกเกอร์PropertyChangedเหตุการณ์โดยอัตโนมัติ


ขออภัยแฮร์ริส แต่ฉันต้องใช้เหตุการณ์ใดใน EntityViewModel เพื่อให้ ContentCollectionChanged ถูกเรียก
โจเซฟจุน Melettukunnel

36
โปรดทราบว่าหากคุณไม่ต้องการใช้การจัดการเหตุการณ์ด้วยตนเองคุณสามารถใช้ BindingList <EntityViewModel> แทน ObservableCollection <EntityViewModel> จากนั้นจะส่งต่อเหตุการณ์ EntityViewModel.PropertyChanged โดยอัตโนมัติเป็นรายการที่เปลี่ยนแปลงเหตุการณ์ที่ ListChangedType == ItemChanged
mjeanes

15
ทั้งหมดนี้ไม่ได้ขึ้นอยู่กับความเข้าใจในคำศัพท์ของchangedคุณหรือไม่ นี่อาจหมายความว่าคุณสมบัติขององค์ประกอบหนึ่งในคอลเลกชันมีการเปลี่ยนแปลง (ซึ่งเป็นวิธีที่ฉันคิดว่าคุณกำลังตีความ) หรืออาจหมายถึงว่าองค์ประกอบหนึ่งของคอลเลกชันมีการเปลี่ยนแปลงโดยแทนที่ด้วยอินสแตนซ์ที่แตกต่างกัน ( นี่คือการตีความของฉัน) ไม่เชื่ออย่างเต็มที่ว่า - จะต้องตรวจสอบเพิ่มเติม
belugabob

10
จะเกิดอะไรขึ้นถ้าฉันเรียก_contentList.Clear()? ไม่มีใครจะยกเลิกการสมัครจากPropertyChanged!
เปาโล Moretti

2
@Poloolo: ถูกต้องContentCollectionChangedเพียงจัดการเพิ่ม / ลบและไม่แทนที่ / รีเซ็ต ฉันจะพยายามแก้ไขและแก้ไขโพสต์ วิธีที่ไซมอนทำในคำตอบของเขานั้นถูกต้อง
Mike Fuchs

178

นี่คือคลาสดร็อปอินที่คลาสย่อย ObservableCollection และเพิ่มการดำเนินการรีเซ็ตเมื่อคุณสมบัติในรายการเปลี่ยนแปลง INotifyPropertyChangedมันบังคับรายการทั้งหมดในการดำเนินการ

ประโยชน์ที่นี่คือคุณสามารถเชื่อมโยงข้อมูลกับคลาสนี้และการเชื่อมโยงทั้งหมดของคุณจะอัปเดตเมื่อมีการเปลี่ยนแปลงคุณสมบัติรายการของคุณ

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

4
ฉันได้ก่อให้เกิดการใช้สิ่งที่คล้ายตัวเอง แต่แทนที่จะใช้ NotifyCollectionChangedAction.Reset ฉันแทนใช้แทนที่: ใหม่ NotifyCollectionChangedEventArgs (NotifyCollectionChangedAction.Replace รายการรายการดัชนี (รายการ))
คริส

2
ทางออกที่ยอดเยี่ยมสำหรับปัญหาของฉัน - ขอบคุณ! สำหรับผู้ที่สร้าง ObservableCollection ด้วย List คุณอาจต้องการเพิ่ม Constructor ที่ทำซ้ำทั้งไอเท็มทั้งหมดและเพิ่ม PropertyChanged
Gavin

4
มีการรั่วไหลของหน่วยความจำที่เป็นไปได้ที่นี่ - เหตุการณ์การรีเซ็ตเกิดขึ้นเมื่อคอลเลกชันมีการเปลี่ยนแปลงอย่างมีนัยสำคัญเช่นเมื่อล้าง ตัวจัดการ INPC ของคุณจะไม่ถูกยกเลิกเมื่อเกิดเหตุการณ์นี้
Charles Mager

6
นี่เป็นการใช้งานที่ตกลง แต่มีหนึ่งประเด็นสำคัญ - NotifyCollectionChangedAction.Replaceไม่ใช่ความคิดที่ดีเพราะคุณไม่สามารถแยกแยะความแตกต่างระหว่างรายการที่จริง ๆ แล้วถูกแทนที่หรือเหตุการณ์ที่เกิดจากการเปลี่ยนรายการ มันจะดีขึ้นมากเมื่อคุณกำหนดpublic event PropertyChangedEventHandler CollectionItemChanged;และจากนั้นItemPropertyChangedทำthis.CollectionItemChanged?.Invoke(sender, e);
hyankov

4
มีใครบ้างที่เป็นตัวอย่างของการใช้ชั้นนี้?
Decoder94

23

ฉันได้รวบรวมสิ่งที่ฉันหวังว่าเป็นคำตอบที่ดีพอสมควรรวมถึงเทคนิคบางอย่างในคำตอบอื่น ๆ มันเป็นคลาสใหม่ที่มาจากObservableCollection<>ที่ฉันโทรFullyObservableCollection<>

มันมีคุณสมบัติดังต่อไปนี้:

  • ItemPropertyChangedมันจะเพิ่มเหตุการณ์ใหม่ ฉันตั้งใจเก็บสิ่งนี้แยกจากที่มีอยู่เดิมCollectionChanged:
    • เพื่อช่วยให้เข้ากันได้ย้อนหลัง
    • ดังนั้นสามารถให้รายละเอียดที่เกี่ยวข้องมากขึ้นในสิ่งใหม่ItemPropertyChangedEventArgsที่มาพร้อมกับมัน: ต้นฉบับPropertyChangedEventArgsและดัชนีภายในคอลเลกชัน
  • ObservableCollection<>มันซ้ำก่อสร้างทั้งหมดจาก
  • มันจัดการรายการที่ถูกรีเซ็ตอย่างถูกต้อง ( ObservableCollection<>.Clear()) หลีกเลี่ยงการรั่วไหลของหน่วยความจำที่เป็นไปได้
  • มันจะแทนที่ชั้นฐานOnCollectionChanged()มากกว่าการสมัครสมาชิกทรัพยากรมากไปยังCollectionChangedเหตุการณ์

รหัส

.csไฟล์ที่สมบูรณ์ดังต่อไปนี้ โปรดทราบว่ามีการใช้คุณลักษณะบางอย่างของ C # 6 แต่ควรง่ายพอสมควรในการย้อนกลับ:

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

ดังนั้นคุณสามารถตรวจสอบการเปลี่ยนแปลงที่คุณอาจทำ (และดูสิ่งที่ฉันทดสอบในตอนแรก!) ฉันได้รวมชั้นทดสอบของฉัน NUnit ด้วย เห็นได้ชัดว่ารหัสต่อไปนี้ไม่จำเป็นต้องใช้FullyObservableCollection<T>ในโครงการของคุณ

NBรายงานการใช้ระดับการทดสอบBindableBaseจาก PRISM INotifyPropertyChangedในการดำเนินการ ไม่มีการพึ่งพา PRISM จากรหัสหลัก

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

1
ฉันไม่รู้ว่าฉันทำอะไรผิด แต่สิ่งนี้ไม่ได้ผลสำหรับฉัน ฉันผูก ListView ของฉันกับคอลเลกชันของคุณ แต่เมื่อฉันอัปเดตคุณสมบัติของรายการภายใน ListView จะไม่อัปเดตแม้กระทั่งฉันสามารถเห็นเหตุการณ์ทั้งหมดที่เกิดขึ้น ฉันยังใช้ห้องสมุด PRISM ...
Renato Parreira

@ Renato คุณทำอะไรกับกิจกรรมใหม่หรือไม่ ListViewจะตอบสนองต่อCollectionChangedกิจกรรมเพราะรู้เกี่ยวกับพวกเขา ItemPropertyChangedเป็นส่วนเสริมที่ไม่ได้มาตรฐานดังนั้นคุณต้องสอนเกี่ยวกับสิ่งนั้น ขณะที่การแก้ไขอย่างรวดเร็วและสกปรกคุณสามารถลองเพียงแค่ยิงCollectionChangedเหตุการณ์เช่นเดียวกับ (หรือแม้กระทั่งแทน) ในItemPropertyChanged OnItemPropertyChanged()ฉันแยกพวกมันออกจากกันด้วยเหตุผลที่ระบุไว้ในคำตอบ แต่สำหรับกรณีการใช้ของคุณมันอาจจะทำในสิ่งที่คุณต้องการ
Bob Sammers

20

สิ่งนี้ใช้แนวคิดข้างต้น แต่ทำให้เป็นคอลเลกชัน 'อ่อนไหวมาก' ที่ได้รับ:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Collections;

namespace somethingelse
{
    public class ObservableCollectionEx<T> : ObservableCollection<T> where T : INotifyPropertyChanged
    {
        // this collection also reacts to changes in its components' properties

        public ObservableCollectionEx() : base()
        {
            this.CollectionChanged +=new System.Collections.Specialized.NotifyCollectionChangedEventHandler(ObservableCollectionEx_CollectionChanged);
        }

        void ObservableCollectionEx_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Remove)
            {
                foreach(T item in e.OldItems)
                {
                    //Removed items
                    item.PropertyChanged -= EntityViewModelPropertyChanged;
                }
            }
            else if (e.Action == NotifyCollectionChangedAction.Add)
            {
                foreach(T 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 - note you must make it a 'reset' - dunno why
            NotifyCollectionChangedEventArgs args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
            OnCollectionChanged(args);
        }
    }
}

12

ObservableCollection จะไม่แพร่กระจายการเปลี่ยนแปลงแต่ละรายการเป็นเหตุการณ์ CollectionChanged คุณจะต้องสมัครสมาชิกแต่ละเหตุการณ์และส่งต่อด้วยตนเองหรือคุณสามารถตรวจสอบคลาสBindingList [T]ซึ่งจะทำสิ่งนี้ให้คุณ


ทำไมคุณเป็นคนเดียวที่กล่าวถึงเรื่องนี้ +1
Atizs

7

เพิ่มในกิจกรรม TruelyObservableCollection "ItemPropertyChanged":

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; // ObservableCollection
using System.ComponentModel; // INotifyPropertyChanged
using System.Collections.Specialized; // NotifyCollectionChangedEventHandler
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ObservableCollectionTest
{
    class Program
    {
        static void Main(string[] args)
        {
            // ATTN: Please note it's a "TrulyObservableCollection" that's instantiated. Otherwise, "Trades[0].Qty = 999" will NOT trigger event handler "Trades_CollectionChanged" in main.
            // REF: http://stackoverflow.com/questions/8490533/notify-observablecollection-when-item-changes
            TrulyObservableCollection<Trade> Trades = new TrulyObservableCollection<Trade>();
            Trades.Add(new Trade { Symbol = "APPL", Qty = 123 });
            Trades.Add(new Trade { Symbol = "IBM", Qty = 456});
            Trades.Add(new Trade { Symbol = "CSCO", Qty = 789 });

            Trades.CollectionChanged += Trades_CollectionChanged;
            Trades.ItemPropertyChanged += PropertyChangedHandler;
            Trades.RemoveAt(2);

            Trades[0].Qty = 999;

            Console.WriteLine("Hit any key to exit");
            Console.ReadLine();

            return;
        }

        static void PropertyChangedHandler(object sender, PropertyChangedEventArgs e)
        {
            Console.WriteLine(DateTime.Now.ToString() + ", Property changed: " + e.PropertyName + ", Symbol: " + ((Trade) sender).Symbol + ", Qty: " + ((Trade) sender).Qty);
            return;
        }

        static void Trades_CollectionChanged(object sender, EventArgs e)
        {
            Console.WriteLine(DateTime.Now.ToString() + ", Collection changed");
            return;
        }
    }

    #region TrulyObservableCollection
    public class TrulyObservableCollection<T> : ObservableCollection<T>
        where T : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler ItemPropertyChanged;

        public TrulyObservableCollection()
            : base()
        {
            CollectionChanged += new NotifyCollectionChangedEventHandler(TrulyObservableCollection_CollectionChanged);
        }

        void TrulyObservableCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.NewItems != null)
            {
                foreach (Object item in e.NewItems)
                {
                    (item as INotifyPropertyChanged).PropertyChanged += new PropertyChangedEventHandler(item_PropertyChanged);
                }
            }
            if (e.OldItems != null)
            {
                foreach (Object item in e.OldItems)
                {
                    (item as INotifyPropertyChanged).PropertyChanged -= new PropertyChangedEventHandler(item_PropertyChanged);
                }
            }
        }

        void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            NotifyCollectionChangedEventArgs a = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
            OnCollectionChanged(a);

            if (ItemPropertyChanged != null)
            {
                ItemPropertyChanged(sender, e);
            }
        }
    }
    #endregion

    #region Sample entity
    class Trade : INotifyPropertyChanged
    {
        protected string _Symbol;
        protected int _Qty = 0;
        protected DateTime _OrderPlaced = DateTime.Now;

        public DateTime OrderPlaced
        {
            get { return _OrderPlaced; }
        }

        public string Symbol
        {
            get
            {
                return _Symbol;
            }
            set
            {
                _Symbol = value;
                NotifyPropertyChanged("Symbol");
            }
        }

        public int Qty
        {
            get
            {
                return _Qty;
            }
            set
            {
                _Qty = value;
                NotifyPropertyChanged("Qty");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void NotifyPropertyChanged(String propertyName = "")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
#endregion
}

คุณสามารถใช้ PropertyChanged จาก ObservableCollection โดยตรงเนื่องจากใช้ INotifyPropertyChanged
Dieter Meemken

6

ฉันใช้คำตอบของ Jack Kenyons เพื่อใช้ OC ของตัวเอง แต่ฉันต้องการชี้ให้เห็นการเปลี่ยนแปลงอย่างหนึ่งที่ฉันต้องทำเพื่อให้มันทำงานได้ แทน:

    if (e.Action == NotifyCollectionChangedAction.Remove)
    {
        foreach(T item in e.NewItems)
        {
            //Removed items
            item.PropertyChanged -= EntityViewModelPropertyChanged;
        }
    }

ฉันใช้สิ่งนี้:

    if (e.Action == NotifyCollectionChangedAction.Remove)
    {
        foreach(T item in e.OldItems)
        {
            //Removed items
            item.PropertyChanged -= EntityViewModelPropertyChanged;
        }
    }

ดูเหมือนว่า "e.NewItems" จะสร้างค่า NULL ถ้าการกระทำเป็นลบ


ฉันคิดว่ามันต้องมีการเปลี่ยนแปลงเพิ่มเติมเช่นกันหาก e.Action == แทนที่
jk

6

เพียงแค่เพิ่ม 2 เซ็นต์ของฉันในหัวข้อนี้ รู้สึกว่า TrulyObservableCollection ต้องการตัวสร้างอื่นสองตัวตามที่พบใน ObservableCollection:

public TrulyObservableCollection()
        : base()
    {
        HookupCollectionChangedEvent();
    }

    public TrulyObservableCollection(IEnumerable<T> collection)
        : base(collection)
    {
        foreach (T item in collection)
            item.PropertyChanged += ItemPropertyChanged;

        HookupCollectionChangedEvent();
    }

    public TrulyObservableCollection(List<T> list)
        : base(list)
    {
        list.ForEach(item => item.PropertyChanged += ItemPropertyChanged);

        HookupCollectionChangedEvent();
    }

    private void HookupCollectionChangedEvent()
    {
        CollectionChanged += new NotifyCollectionChangedEventHandler(TrulyObservableCollectionChanged);
    }

5

ฉันรู้ว่าฉันสายเกินไปสำหรับปาร์ตี้นี้ แต่อาจจะช่วยให้ใครซักคน ..

ที่นี่คุณสามารถค้นหาการใช้งาน ObservableCollectionEx ของฉัน มันมีคุณสมบัติบางอย่าง:

  • มันรองรับทุกอย่างจาก ObservableCollection
  • มันด้ายปลอดภัย
  • สนับสนุนเหตุการณ์ ItemPropertyChanged (จะเพิ่มขึ้นในแต่ละครั้งเมื่อมีการไล่รายการ Item.PropertyChanged)
  • รองรับตัวกรอง (ดังนั้นคุณสามารถสร้าง ObservableCollectionEx ส่งผ่านคอลเล็กชันอื่นเป็นซอร์สและกรองด้วยเพรดิเคตแบบง่ายมีประโยชน์มากใน WPF ฉันใช้คุณลักษณะนี้มากในแอปพลิเคชันของฉัน) มากขึ้น - กรองการติดตามการเปลี่ยนแปลงรายการผ่านทางอินเทอร์เฟซ

แน่นอนความคิดเห็นใด ๆ ที่ชื่นชม;)


1
Большоеспасибо! ขอบคุณมากสำหรับการแบ่งปัน! คุณช่วยฉันหลายชั่วโมงโดยไม่ต้องเขียนการใช้งานของฉันเอง! :)
Alexander

@ อเล็กซานเดอร์คุณสบายใจมาก :)
chopikadze

@chopikadze ฉันไม่สามารถดาวน์โหลดไฟล์ cs ของ ObservableCollectionEx ของคุณได้โปรดแก้ไขได้ ขอบคุณ
Shax

ลิงก์ตาย

5

ถ้าฉันรู้ว่า ObservableCollection สร้างกิจกรรมเฉพาะเมื่อเราเพิ่ม / ลบหรือย้ายรายการในคอลเลกชันของเรา เมื่อเราอัพเดตคุณสมบัติบางอย่างในคอลเลกชันรายการคอลเลกชันอย่าส่งสัญญาณเกี่ยวกับมันและ UI จะไม่ได้รับการอัปเดต

คุณสามารถนำINotifyPropertyChangeไปใช้ใน class Model ของคุณ และกว่าเมื่อเราอัปเดตบางอย่างในรายการคอลเลกชันมันจะอัปเดต UI โดยอัตโนมัติ

public class Model:INotifyPropertyChange
{
//...
}

และกว่า

public ObservableCollection<Model> {get; set;}

ในกรณีของฉันฉันใช้ ListView to Bind สำหรับคอลเลกชันนี้และใน ItemTemplate ตั้งค่าคุณสมบัติ Binding to Model และใช้งานได้ดี

นี่คือตัวอย่างบางส่วน

Windows XAML:

<Window.DataContext>
    <local:ViewModel/>
</Window.DataContext>
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
    </Grid.RowDefinitions>
    <ListView 
        Margin="10"
        BorderBrush="Black"
        HorizontalAlignment="Center"
        SelectedItem="{Binding SelectedPerson}"
        ItemsSource="{Binding Persons}">
        <ListView.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <Label Content="{Binding Name}"/>
                    <Label Content="-"/>
                    <Label Content="{Binding Age}"/>
                </StackPanel>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
    <Grid 
        Grid.Row="1"
        VerticalAlignment="Center"
        HorizontalAlignment="Center">
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Label 
            VerticalAlignment="Center"
            Content="Name:"/>
        <TextBox
            Text="{Binding SelectedPerson.Name,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
            Margin="10"
            Grid.Column="1" 
            Width="100"/>
        <Label 
            VerticalAlignment="Center"
            Grid.Row="1"
            Content="Age:"/>
        <TextBox
            Text="{Binding SelectedPerson.Age,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
            Margin="10"
            Grid.Row="1"
            Grid.Column="1" 
            Width="100"/>


    </Grid>
</Grid>

ตัวอย่างรหัสโมเดล:

public class PersonModel:INotifyPropertyChanged
{
    public string Name
    {
        get => _name;
        set
        {
            _name = value;
            OnPropertyChanged();
        }
    }

    public int Age
    {
        get => _age;
        set
        {
            _age = value;
            OnPropertyChanged();
        }
    }

    private string _name;
    private int _age;
    //INotifyPropertyChanged implementation
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

และการใช้งาน ViewModel:

 public class ViewModel:INotifyPropertyChanged
{
    public ViewModel()
    {
        Persons = new ObservableCollection<PersonModel>
        {
            new PersonModel
            {
                Name = "Jack",
                Age = 30
            },
            new PersonModel
            {
                Name = "Jon",
                Age = 23
            },
            new PersonModel
            {
                Name = "Max",
                Age = 23
            },
        };
    }

    public ObservableCollection<PersonModel> Persons { get;}

    public PersonModel SelectedPerson
    {
        get => _selectedPerson;
        set
        {
            _selectedPerson = value;
            OnPropertyChanged();
        }
    }

    //INotifyPropertyChanged Implementation
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    private PersonModel _selectedPerson;
}

2

วิธีแก้ปัญหาง่ายๆสำหรับการเก็บค่ามาตรฐานที่สังเกตได้ที่ฉันเคยใช้:

อย่าเพิ่มคุณสมบัติของคุณหรือเปลี่ยนเป็นไอเท็มด้านในแทนสร้างคอลเลกชันชั่วคราวบางอย่างแทน

ObservableCollection<EntityViewModel> tmpList= new ObservableCollection<EntityViewModel>();

และเพิ่มรายการหรือเปลี่ยนแปลง tmpList

tmpList.Add(new EntityViewModel(){IsRowChecked=false}); //Example
tmpList[0].IsRowChecked= true; //Example
...

จากนั้นส่งไปยังทรัพย์สินที่แท้จริงของคุณโดยมอบหมาย

ContentList=tmpList;

สิ่งนี้จะเปลี่ยนคุณสมบัติทั้งหมดซึ่งทำให้สังเกตเห็น INotifyPropertyChanged ตามที่คุณต้องการ


1

ฉันลองโซลูชันนี้ แต่ใช้งานได้กับฉันเท่านั้นเช่น RaisePropertyChange ("SourceGroupeGridView") เมื่อมีการเปลี่ยนแปลงคอลเล็กชันนั่นคือการเพิ่มหรือเปลี่ยนแปลงแต่ละรายการ

ปัญหาอยู่ใน:

public void EntityViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
     NotifyCollectionChangedEventArgs args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
    OnCollectionChanged(args);
}

NotifyCollectionChangedAction.Reset การดำเนินการนี้จะทำการเชื่อมโยงรายการทั้งหมดในกลุ่มที่มีการจัดกลุ่มใหม่ซึ่งเทียบเท่ากับ RaisePropertyChanged เมื่อคุณใช้ทุกกลุ่มของ gridview จะรีเฟรช

หากคุณต้องการรีเฟรชใน UI กลุ่มของรายการใหม่เท่านั้นคุณไม่ได้ใช้การดำเนินการรีเซ็ตคุณจะต้องจำลองการกระทำที่เพิ่มใน itemproperty ด้วยสิ่งต่อไปนี้:

void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{         
    var index = this.IndexOf((T)sender);

    this.RemoveAt(index);
    this.Insert(index, (T)sender);

    var a = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, sender);
    OnCollectionChanged(a);
}

ขอโทษด้วยภาษาอังกฤษของฉันและขอขอบคุณสำหรับรหัสฐาน :) ฉันหวังว่านี่จะช่วยให้ใครบางคน ^ _ ^

enjoi !!


1

นี่คือวิธีการขยายสำหรับโซลูชันด้านบน ...

public static TrulyObservableCollection<T> ToTrulyObservableCollection<T>(this List<T> list)
     where T : INotifyPropertyChanged
{
    var newList = new TrulyObservableCollection<T>();

    if (list != null)
    {
        list.ForEach(o => newList.Add(o));
    }

    return newList;
}  

คุณอาจต้องการคำตอบ
geedubb

1
นี่คือลิงค์ที่อธิบายวิธีการขยาย docs.microsoft.com/en-us/dotnet/csharp/programming-guide/…
LawMan

1

แทนที่จะเป็น ObservableCollection หรือ TrulyObservableCollection ให้พิจารณาใช้ BindingList และเรียกใช้เมธอด ResetBindings

ตัวอย่างเช่น:

private BindingList<TfsFile> _tfsFiles;

public BindingList<TfsFile> TfsFiles
{
    get { return _tfsFiles; }
    set
    {
        _tfsFiles = value;
        NotifyPropertyChanged();
    }
}

รับเหตุการณ์เช่นคลิกรหัสของคุณจะมีลักษณะเช่นนี้:

foreach (var file in TfsFiles)
{
    SelectedFile = file;
    file.Name = "Different Text";
    TfsFiles.ResetBindings();
}

โมเดลของฉันดูเหมือนว่า:

namespace Models
{
    public class TfsFile 
    {
        public string ImagePath { get; set; }

        public string FullPath { get; set; }

        public string Name { get; set; }

        public string Text { get; set; }

    }
}

1
ข้อมูลที่ดีเกี่ยวกับวิธีการนี้BindingListแต่มีข้อ จำกัด สำหรับวิธีการนี้ที่คำตอบอื่น ๆ เอาชนะ: เทคนิคนี้อาศัยค่าที่เปลี่ยนแปลงในโค้ดและตำแหน่งที่ResetBindings()สามารถเพิ่มการโทรได้ คำตอบอื่น ๆ ส่วนใหญ่จะทำงานหากวัตถุของรายการมีการเปลี่ยนแปลงด้วยวิธีอื่นเช่นรหัสไม่เปลี่ยนแปลงหรือจากการผูกกับตัวควบคุมที่สอง
Bob Sammers

1

เมื่อต้องการทริกเกอร์ OnChange ในรายการ ObservableCollection

  1. รับดัชนีของรายการที่เลือก
  2. ลบรายการจากผู้ปกครอง
  3. เพิ่มรายการที่ดัชนีเดียวกันในพาเรนต์

ตัวอย่าง:

int index = NotificationDetails.IndexOf(notificationDetails);
NotificationDetails.Remove(notificationDetails);
NotificationDetails.Insert(index, notificationDetails);

0

นี่คือเวอร์ชันการนำไปใช้ของฉัน มันตรวจสอบและโยนข้อผิดพลาดหากวัตถุในรายการไม่ได้ใช้ INotifyPropertyChanged ดังนั้นอย่าลืมปัญหานั้นในขณะที่กำลังพัฒนา ด้านนอกคุณใช้เหตุการณ์ ListItemChanged กำหนดว่ารายการหรือรายการในรายการมีการเปลี่ยนแปลง

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

ทางออกที่ง่ายในรหัส 2 บรรทัด เพียงใช้ตัวสร้างการคัดลอก ไม่จำเป็นต้องเขียน TrulyObservableCollection เป็นต้น

ตัวอย่าง:

        speakers.list[0].Status = "offline";
        speakers.list[0] = new Speaker(speakers.list[0]);

วิธีการอื่นที่ไม่มีตัวสร้างการคัดลอก คุณสามารถใช้การทำให้เป็นอันดับ

        speakers.list[0].Status = "offline";
        //speakers.list[0] = new Speaker(speakers.list[0]);
        var tmp  = JsonConvert.SerializeObject(speakers.list[0]);
        var tmp2 = JsonConvert.DeserializeObject<Speaker>(tmp);
        speakers.list[0] = tmp2;

0

คุณยังสามารถใช้วิธีการส่วนขยายนี้เพื่อลงทะเบียนตัวจัดการสำหรับการเปลี่ยนแปลงคุณสมบัติของรายการในคอลเลกชันที่เกี่ยวข้องได้อย่างง่ายดาย วิธีนี้จะถูกเพิ่มไปยังคอลเลกชันทั้งหมดที่ใช้ INotifyCollectionChanged โดยอัตโนมัติซึ่งเก็บรายการที่ใช้ 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;
                }
            }
        };
    }
}

วิธีใช้:

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
        });
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.