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

Метод "Набір вмісту списку" не буде викликаний, коли ви зміните значення всередині колекції, замість цього слід шукати запуск події 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 як ListChanged події, де ListChangedType == ItemChanged.
mjeanes

15
Чи не все це залежить від вашого розуміння цього терміна changed? Це може означати, що властивість одного з елементів колекції змінилася (так я думаю, що ви її інтерпретуєте) або може означати, що один із елементів колекції був змінений, замінивши його іншим екземпляром ( це моє тлумачення). Не зовсім переконаний - доведеться вивчити це далі.
belugabob

10
Що станеться, якщо я подзвоню _contentList.Clear()? Ніхто не відмовиться від підписки PropertyChanged!
Паоло Моретті

2
@Paolo: Правильно, ContentCollectionChangedлише обробляє «Додати / видалити», а не замінити / скинути. Я спробую відредагувати та виправити публікацію. Те, як це робить Сімон у своїй відповіді, є правильним.
Майк Фукс

178

Ось клас, що випадає, що підкласи ObservableCollection і фактично викликає дію Reset, коли властивість у списку змінюється. Він примушує реалізувати всі елементи 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, я замість цього використав .Replace: new NotifyCollectionChangedEventArgs (NotifyCollectionChangedAction.Replace, item, item, IndexOf (item)).
Кріс

2
Дивовижне рішення моєї проблеми - дякую! Для тих, хто створив ObservableCollection зі списком, ви можете додати конструктор, який також змінює всі елементи та додає PropertyChanged.
Гавін

4
Тут можливий витік пам'яті - подія Скидання відбувається, коли колекція істотно змінюється, наприклад, на Очистити. Жоден з ваших обробників INPC не буде скасовано підписки, коли це станеться.
Чарльз Магер

6
це нормальна реалізація, але у неї є одне головне питання - NotifyCollectionChangedAction.Replaceце не дуже гарна ідея, оскільки тоді ви не можете розрізнити предмет, який насправді замінюється, або подія, спричинена зміною елемента. Це стає набагато краще, коли ви визначаєте, public event PropertyChangedEventHandler CollectionItemChanged;а потім ItemPropertyChangedробитиthis.CollectionItemChanged?.Invoke(sender, e);
hyankov

4
Хтось отримав приклад використання цього класу?
Декодер94

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>у вашому проекті.

Примітка . Тестовий клас використовує BindableBasePRISM для впровадження 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, ви щось робили з новою подією? ListViewбуде реагувати на CollectionChangedподії, бо знає про них. ItemPropertyChangedє нестандартним доповненням, тому вам цього потрібно навчити. Як швидко і брудний виправити, ви можете спробувати тільки випал CollectionChangedподія, а також (або навіть замість) ItemPropertyChangedв OnItemPropertyChanged(). Я тримав їх окремо з причин, зазначених у відповіді, але для вашого використання він може просто зробити все, що вам потрібно.
Боб Саммерс

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.
Дітер Мемкен

6

Я використав відповідь Джека Кеніонса, щоб реалізувати свій власний 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" видає нуль, якщо дія.


Я думаю, що також потрібні подальші зміни, що робити, якщо 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, я дуже часто використовую цю функцію у своїх програмах). Ще більше - фільтруйте відстеження змін елементів через інтерфейс INotifyPropertyChanged.

Звичайно, будь-які коментарі цінуються;)


1
Большое спасибо! Дякую за те, що поділилися цим! Ви врятували мене численні години, не потрібно писати власну реалізацію! :)
Олександр

@Alexander, ти дуже привітний :)
привітніли

@chopikadze, я не можу завантажити файл cs вашого ObservableCollectionEx, чи зможете ви його виправити. Спасибі
Шакс

Посилання мертва.

5

Якщо я знаю, що ObservableCollection робить події лише тоді, коли ми додаємо / видаляємо або переміщуємо предмети в нашій колекції. Коли ми просто оновимо деякі властивості в колекції предметів колекції, не сигналізуйте про це, і інтерфейс користувача не буде оновлений.

Ви можете просто реалізувати INotifyPropertyChange у своєму класі Model. І ніж коли ми оновлюємо деякий товар у колекційному предметі, він автоматично оновить інтерфейс користувача.

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

і ніж

public ObservableCollection<Model> {get; set;}

У моєму випадку я використовував ListView to Bind для цієї колекції та в ItemTemplate встановив Прив'язка до властивості 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. Скиньте цю дію, щоб зробити повне відновлення всіх елементів у згрупованій сітці, еквівалентно RaisePropertyChanged. Коли ви використовуєте його, усі групи gridview оновлюються.

Якщо ви хочете лише оновити в інтерфейсі групу нового елемента, ви не використовуєте дію Скидання, вам буде потрібно моделювати дію Add у властивості item таким чином:

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

Вибачте за мою англійську, і дякую за базовий код :), я сподіваюся, що це комусь допоможе ^ _ ^

Насолоджуйтесь !!


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()можна додавати виклик . Більшість інших відповідей спрацює, якщо об'єкти списку будуть змінені за допомогою інших засобів, наприклад, незмінного коду або від прив'язки до другого елемента управління.
Боб Саммерс

1

Для запуску OnChange у списку ObservableCollection

  1. Отримайте індекс вибраного елемента
  2. Вийміть елемент із батьківського
  3. Додайте елемент з тим самим індексом у батьківському

Приклад:

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

0

Ось моя версія реалізації. Він перевіряє та видаляє помилку, якщо об’єкти у списку не реалізують INotifyPropertyChanged, тому не можуть забути цю проблему під час розробки. Зовнішнє використання ви використовуєте ListItemChanged Event, визначаючи, чи змінився список або сам елемент списку.

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
        });
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.