ObservableCollection tidak memperhatikan ketika Item di dalamnya berubah (bahkan dengan INotifyPropertyChanged)

167

Adakah yang tahu mengapa kode ini tidak berfungsi:

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

ViewModelBaseMelawan segala sesuatu untuk RaisePropertyChangeddll. dan berfungsi untuk yang lainnya kecuali masalah ini ..

Joseph Jun. Melettukunnel
sumber

Jawaban:

119

Metode Set ContentList tidak akan dipanggil ketika Anda mengubah nilai di dalam koleksi, sebaliknya Anda harus melihat keluar untuk penembakan acara 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
    }
}

Oke, itu dua kali hari ini saya salah karena dokumentasi MSDN salah. Di tautan yang saya berikan itu tertulis:

Terjadi ketika suatu item ditambahkan, dihapus, diubah, dipindahkan, atau seluruh daftar di-refresh.

Tapi itu sebenarnya tidak menyala ketika item diubah. Saya kira Anda akan membutuhkan metode yang lebih brutal:

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

Jika Anda akan sangat membutuhkan ini, Anda mungkin ingin mensubklasifikasikan acara Anda ObservableCollectionyang memicu CollectionChangedacara ketika seorang anggota memicu PropertyChangedacara secara otomatis (seperti yang tertulis di dokumentasi ...)

Martin Harris
sumber
Maaf Harris, tapi Peristiwa apa yang harus saya aktifkan di EntityViewModel sehingga ContentCollectionChanged dipanggil?
Joseph Juni. Melettukunnel
36
perhatikan bahwa jika Anda tidak ingin menerapkan manajemen acara sendiri, Anda dapat menggunakan BindingList <EntityViewModel> sebagai ganti dari ObservableCollection <EntityViewModel>. Kemudian akan secara otomatis meneruskan peristiwa EntityViewModel.PropertyChanged sebagai peristiwa ListChanged di mana ListChangedType == ItemChanged.
mjeanes
15
Bukankah ini semua tergantung pada pemahaman Anda tentang istilah ini changed? Ini bisa berarti bahwa properti dari salah satu elemen dalam koleksi telah berubah (yang menurut saya Anda menafsirkannya) atau itu bisa berarti bahwa salah satu elemen koleksi telah diubah dengan menggantinya dengan contoh yang berbeda ( ini interpretasi saya). Namun tidak sepenuhnya yakin - harus melihat lebih jauh.
belugabob
10
Apa yang terjadi jika saya memohon _contentList.Clear()? Tidak ada yang akan berhenti berlangganan PropertyChanged!
Paolo Moretti
2
@ Paolo: Benar, ContentCollectionChangedhanya menangani Tambah / Hapus, dan tidak Ganti / Atur Ulang. Saya akan mencoba mengedit dan memperbaiki posting. Cara simon melakukannya dalam jawabannya sudah benar.
Mike Fuchs
178

Berikut adalah kelas drop-in yang sub-kelas ObservableCollection dan benar-benar menimbulkan tindakan Reset ketika properti pada item daftar berubah. Itu memberlakukan semua item untuk diimplementasikanINotifyPropertyChanged .

Manfaatnya di sini adalah Anda dapat mengikat data ke kelas ini dan semua ikatan Anda akan diperbarui dengan perubahan pada properti item Anda.

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);
    }
}
simon
sumber
4
Saya memiliki alasan untuk mengimplementasikan sesuatu yang serupa dengan diri saya sendiri, namun daripada menggunakan NotifyCollectionChangedAction.Reset saya malah menggunakan. Ganti: notifyCollectionChangedEventArgs baru (NotifyCollectionChangedAction.Replace, item, item, IndexOf (item)).
Chris
2
Solusi luar biasa untuk masalah saya - terima kasih! Bagi mereka yang membuat ObservableCollection mereka dengan Daftar, Anda mungkin ingin menambahkan konstruktor yang juga mengulangi semua item dan menambahkan PropertyChanged.
Gavin
4
Ada potensi kebocoran memori di sini - Peristiwa Reset terjadi ketika koleksi berubah secara signifikan, misalnya pada Bersihkan. Tak satu pun dari penangan INPC Anda akan berhenti berlangganan ketika ini terjadi.
Charles Mager
6
ini adalah implementasi yang OK tetapi memiliki satu masalah besar - NotifyCollectionChangedAction.Replaceini bukan ide yang baik, karena Anda tidak dapat membedakan antara item yang sebenarnya diganti atau peristiwa yang disebabkan oleh perubahan item. Ini akan menjadi jauh lebih baik ketika Anda mendefinisikan public event PropertyChangedEventHandler CollectionItemChanged;dan kemudian ItemPropertyChangedmelakukanthis.CollectionItemChanged?.Invoke(sender, e);
hyankov
4
Adakah yang punya contoh penggunaan kelas ini?
Decoder94
23

Saya telah mengumpulkan apa yang saya harap merupakan solusi yang cukup kuat, termasuk beberapa teknik dalam jawaban lain. Ini adalah kelas baru yang berasal dari ObservableCollection<>, yang saya panggilFullyObservableCollection<>

Ini memiliki beberapa fitur berikut:

  • Itu menambahkan acara baru ItemPropertyChanged,. Saya sengaja memisahkan ini dari yang ada CollectionChanged:
    • Untuk membantu kompatibilitas ke belakang.
    • Jadi detail yang lebih relevan dapat diberikan pada yang baru ItemPropertyChangedEventArgsyang menyertainya: yang asli PropertyChangedEventArgsdan indeks dalam koleksi.
  • Ini mereplikasi semua konstruktor dari ObservableCollection<>.
  • Itu benar menangani daftar yang diatur ulang ( ObservableCollection<>.Clear()), menghindari kemungkinan kebocoran memori.
  • Ini mengesampingkan kelas dasar OnCollectionChanged(), daripada langganan yang lebih banyak sumber daya untuk CollectionChangedacara tersebut.

Kode

File lengkap .csberikut. Perhatikan bahwa beberapa fitur C # 6 telah digunakan, tetapi harus cukup sederhana untuk membuat backport:

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

Tes NUnit

Jadi Anda dapat memeriksa perubahan yang mungkin Anda buat (dan lihat apa yang saya uji sejak awal!), Saya juga sudah memasukkan kelas tes NUnit saya. Jelas, kode berikut ini tidak perlu hanya digunakanFullyObservableCollection<T> di proyek Anda.

NB Kelas tes menggunakan BindableBasedari PRISM untuk mengimplementasikan INotifyPropertyChanged. Tidak ada ketergantungan pada PRISM dari kode utama.

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)");
        }
    }
}
Bob Sammers
sumber
1
Saya tidak tahu apa yang saya lakukan salah, tetapi ini tidak berhasil untuk saya. Saya mengikat ListView saya ke koleksi Anda tetapi ketika saya memperbarui properti dari item di dalam, ListView tidak memperbarui, bahkan karena saya bisa melihat semua acara menyala. Saya juga menggunakan perpustakaan PRISM ...
Renato Parreira
@Renato, apakah Anda melakukan sesuatu dengan acara baru? ListViewakan menanggapi CollectionChangedacara karena tahu tentang mereka. ItemPropertyChangedadalah tambahan yang tidak standar, jadi Anda perlu mengajarkannya tentang itu. Sebagai perbaikan cepat dan kotor, Anda dapat mencoba hanya menembakkan CollectionChangedacara serta (atau bahkan bukannya) ItemPropertyChangeddi OnItemPropertyChanged(). Saya memisahkan mereka untuk alasan yang disebutkan dalam jawaban, tetapi untuk kasus penggunaan Anda mungkin hanya melakukan apa yang Anda butuhkan.
Bob Sammers
20

Ini menggunakan ide-ide di atas tetapi menjadikannya koleksi 'yang lebih sensitif':

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);
        }
    }
}
Jack Kenyon
sumber
12

ObservableCollection tidak akan menyebarkan perubahan item individual sebagai acara CollectionChanged. Anda harus berlangganan ke setiap acara dan meneruskannya secara manual, atau Anda dapat melihat kelas BindingList [T] , yang akan melakukan ini untuk Anda.

mjeanes
sumber
Kenapa hanya kamu yang menyebutkan ini? +1
Atizs
7

Ditambahkan ke acara 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
}
Swab
sumber
Anda bisa menggunakan PropertyChanged dari ObservableCollection secara langsung, karena mengimplementasikan INotifyPropertyChanged.
Dieter Meemken
6

Saya menggunakan jawaban Jack Kenyons untuk mengimplementasikan OC saya sendiri, tetapi saya ingin menunjukkan satu perubahan yang harus saya lakukan untuk membuatnya bekerja. Dari pada:

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

Saya menggunakan ini:

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

Tampaknya "e.NewItems" menghasilkan nol jika tindakan .Hapus.

triazotan
sumber
Saya pikir perlu perubahan lebih lanjut juga bagaimana jika e.Action == ganti
jk.
6

Hanya menambahkan 2 sen saya pada topik ini. Felt the TrulyObservableCollection memerlukan dua konstruktor lain seperti yang ditemukan dengan 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);
    }
pjdupreez
sumber
5

Saya tahu bahwa saya terlambat untuk pesta ini, tapi mungkin - ini akan membantu seseorang ..

Di sini Anda dapat menemukan implementasi saya dari ObservableCollectionEx. Ini memiliki beberapa fitur:

  • itu mendukung semuanya dari ObservableCollection
  • aman untuknya
  • itu mendukung acara ItemPropertyChanged (itu memunculkan setiap kali ketika item.PropertyChanged item dipecat)
  • ini mendukung filter (jadi, Anda dapat membuat ObservableCollectionEx, melewati koleksi lain sebagai Sumber untuk itu, dan Filter dengan predikat sederhana. Sangat berguna dalam WPF, saya banyak menggunakan fitur ini di aplikasi saya). Terlebih lagi - memfilter melacak perubahan item melalui antarmuka INotifyPropertyChanged.

Tentu saja, komentar apa pun dihargai;)

chopikadze
sumber
1
Большое спасибо! Terima kasih banyak telah berbagi itu! Anda menyelamatkan saya beberapa jam dengan tidak harus menulis implementasi saya sendiri! :)
Alexander
@Alexander Anda sangat baik-baik saja :)
chopikadze
@ chopikadze, saya tidak dapat mengunduh file cs dari ObservableCollectionEx Anda, bisakah Anda memperbaikinya. Terima kasih
Shax
Tautannya sudah mati.
5

Jika saya tahu ObservableCollection membuat acara hanya ketika kami menambah / menghapus atau memindahkan item dalam koleksi kami. Ketika kami simly memperbarui beberapa properti dalam koleksi item koleksi, jangan menandakannya dan UI tidak akan diperbarui.

Anda dapat secara simultan mengimplementasikan INotifyPropertyChange di kelas Model Anda. Dan daripada saat kami memperbarui beberapa item koleksi yang layak secara otomatis akan memperbarui UI.

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

dan dari

public ObservableCollection<Model> {get; set;}

Dalam kasus saya, saya menggunakan ListView untuk Mengikat untuk koleksi ini dan di ItemTemplate mengatur properti Binding ke Model dan itu berfungsi dengan baik.

Ini beberapa cuplikan

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>

Contoh kode model:

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

Dan implementasi 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;
}
Sviatoslav Kindrat
sumber
2

Solusi sederhana untuk pengumpulan observasi standar yang telah saya gunakan:

JANGAN TAMBAHKAN ke properti Anda ATAU MENGUBAHnya item batin LANGSUNG, sebagai gantinya, buat beberapa koleksi temp seperti ini

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

dan tambahkan item atau buat perubahan ke tmpList,

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

kemudian kirimkan ke properti Anda yang sebenarnya dengan penugasan.

ContentList=tmpList;

ini akan mengubah seluruh properti yang menyebabkan pemberitahuan INotifyPropertyChanged sesuai kebutuhan.

berfikiromid
sumber
1

Saya mencoba solusi ini, tetapi hanya berfungsi untuk saya seperti RaisePropertyChange ("SourceGroupeGridView") ketika koleksi berubah, yang dipecat untuk setiap item ditambahkan atau diubah.

Masalahnya ada di:

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

NotifyCollectionChangedAction.Reset tindakan ini membuat rebind lengkap dari semua item di groupedgrid, setara di RaisePropertyChanged. Saat Anda menggunakannya semua grup gridview disegarkan.

JIKA Anda, hanya ingin menyegarkan di UI grup item baru, Anda tidak menggunakan tindakan Reset, Anda perlu mensimulasikan tindakan Tambahkan di itemproperty dengan sesuatu seperti ini:

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

Maaf oleh bahasa Inggris saya, dan terima kasih untuk kode dasar :), saya harap ini membantu seseorang ^ _ ^

Enjoi !!

alberto sainz
sumber
1

Inilah metode ekstensi untuk solusi di atas ...

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;
}  
Lawman
sumber
Anda mungkin ingin menjelaskan jawabannya
geedubb
1
Berikut ini tautan yang menjelaskan metode ekstensi. docs.microsoft.com/en-us/dotnet/csharp/programming-guide/…
LawMan
1

Daripada ObservableCollection atau TrulyObservableCollection, pertimbangkan untuk menggunakan BindingList dan memanggil metode ResetBindings.

Sebagai contoh:

private BindingList<TfsFile> _tfsFiles;

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

Diberikan acara, seperti klik kode Anda akan terlihat seperti ini:

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

Model saya terlihat seperti ini:

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

    }
}
ClubbieTim
sumber
1
Info bagus tentang metode ini BindingList, tetapi ada batasan untuk pendekatan ini yang diatasi oleh jawaban lain: teknik ini bergantung pada nilai yang diubah dalam kode dan di mana panggilan untuk ResetBindings()dapat ditambahkan. Sebagian besar jawaban lain akan berfungsi jika objek daftar diubah melalui cara lain, seperti kode yang tidak dapat diubah atau dari pengikatan ke kontrol kedua.
Bob Sammers
1

Untuk Memicu OnChange dalam Daftar ObservableCollection

  1. Dapatkan indeks dari Item yang dipilih
  2. Hapus item dari Induk
  3. Tambahkan item pada indeks yang sama di induk

Contoh:

int index = NotificationDetails.IndexOf(notificationDetails);
NotificationDetails.Remove(notificationDetails);
NotificationDetails.Insert(index, notificationDetails);
Aravindakumar.S
sumber
0

Ini versi implementasi saya. Ini memeriksa dan melempar kesalahan, jika objek dalam daftar tidak mengimplementasikan INotifyPropertyChanged, jadi tidak bisa melupakan masalah itu saat berkembang. Di luar Anda menggunakan Acara ListItemChanged menentukan apakah daftar atau item daftar itu sendiri telah berubah.

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


}
Michael
sumber
0

Solusi sederhana dalam 2 baris kode. Cukup gunakan copy constructor. Tidak perlu menulis TrulyObservableCollection dll.

Contoh:

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

Metode lain tanpa copy constructor. Anda dapat menggunakan serialisasi.

        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;
saravanan singapura
sumber
0

Anda juga dapat menggunakan metode ekstensi ini untuk mendaftarkan handler dengan mudah untuk perubahan properti item dalam koleksi yang relevan. Metode ini secara otomatis ditambahkan ke semua koleksi yang menerapkan INotifyCollectionChanged yang menyimpan item yang menerapkan 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;
                }
            }
        };
    }
}

Cara Penggunaan:

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