ObservableCollection non nota quando l'oggetto in esso cambia (anche con INotifyPropertyChanged)


167

Qualcuno sa perché questo codice non funziona:

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

ViewModelBasecontiene tutto per RaisePropertyChangedecc. e funziona per tutto il resto tranne questo problema.


Risposte:


119

Il metodo Set di ContentList non verrà chiamato quando si modifica un valore all'interno della raccolta, invece si dovrebbe cercare l' attivazione dell'evento 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
    }
}

Va bene, oggi due volte sono stato morso dalla documentazione di MSDN che era sbagliata. Nel link che ti ho dato dice:

Si verifica quando un elemento viene aggiunto, rimosso, modificato, spostato o l'intero elenco viene aggiornato.

Ma in realtà non si attiva quando un oggetto viene cambiato. Immagino che avrai bisogno di un metodo più bruteforce quindi:

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

Se ne avrai bisogno molto, potresti voler sottoclassare il tuo ObservableCollectionche innesca l' CollectionChangedevento quando un membro attiva PropertyChangedautomaticamente il suo evento (come dice che dovrebbe nella documentazione ...)


Mi dispiace Harris, ma quale evento devo attivare in EntityViewModel in modo che venga chiamato ContentCollectionChanged?
Joseph jun. Melettukunnel,

36
notare che se non si desidera implementare da soli la gestione degli eventi, è possibile utilizzare un BindingList <EntityViewModel> al posto di ObservableCollection <EntityViewModel>. Quindi inoltrerà automaticamente gli eventi EntityViewModel.PropertyChanged come eventi ListChanged in cui ListChangedType == ItemChanged.
mjeanes,

15
Tutto ciò non dipende dalla tua comprensione del termine changed? Ciò potrebbe significare che una proprietà di uno degli elementi della raccolta è cambiata (che è come penso che tu la stia interpretando) o potrebbe significare che uno degli elementi della raccolta è stato cambiato sostituendolo con un'istanza diversa ( questa è la mia interpretazione). Non del tutto convinto però - dovrà approfondire ulteriormente.
Belugabob,

10
Cosa succede se invoco _contentList.Clear()? Nessuno annullerà l'iscrizione PropertyChanged!
Paolo Moretti,

2
@Paolo: Esatto, ContentCollectionChangedgestisce solo Aggiungi / Rimuovi e non Sostituisci / Ripristina. Proverò a modificare e correggere il post. Il modo in cui Simon lo fa nella sua risposta è corretto.
Mike Fuchs,

178

Ecco una classe drop-in che subclasse ObservableCollection e in realtà genera un'azione di reset quando cambia una proprietà su una voce di elenco. Fa rispettare tutti gli elementi da implementareINotifyPropertyChanged .

Il vantaggio qui è che puoi associare i dati a questa classe e tutti i tuoi collegamenti verranno aggiornati con modifiche alle proprietà dell'articolo.

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
Ho avuto motivo di implementare qualcosa di simile da solo, tuttavia piuttosto che usare NotifyCollectionChangedAction.Reset che ho usato invece. Sostituisci: nuovo NotifyCollectionChangedEventArgs (NotifyCollectionChangedAction.Replace, item, item, IndexOf (item)).
Chris,

2
Ottima soluzione al mio problema - grazie! Per coloro che hanno creato la loro ObservableCollection con un elenco, potresti voler aggiungere un costruttore che alterna anche tutti gli elementi e aggiunge PropertyChanged.
Gavin,

4
C'è una potenziale perdita di memoria qui - Un evento di ripristino si verifica quando la raccolta viene modificata in modo significativo, ad esempio su Cancella. Nessuno dei tuoi gestori INPC verrà annullato quando ciò accade.
Charles Mager,

6
questa è un'implementazione OK ma ha un grosso problema: NotifyCollectionChangedAction.Replacenon è una buona idea, perché non è possibile distinguere tra un articolo effettivamente sostituito o un evento causato da una modifica dell'articolo. Diventa molto meglio quando lo definisci public event PropertyChangedEventHandler CollectionItemChanged;e poi nel ItemPropertyChangedfarethis.CollectionItemChanged?.Invoke(sender, e);
hyankov il

4
Qualcuno ha avuto un esempio dell'uso di questa classe?
Decoder94

23

Ho messo insieme quella che spero sia una soluzione abbastanza solida, tra cui alcune delle tecniche in altre risposte. È una nuova classe derivata ObservableCollection<>, che sto chiamandoFullyObservableCollection<>

Ha le seguenti caratteristiche:

  • Si aggiunge un nuovo evento, ItemPropertyChanged. Ho deliberatamente tenuto questo separato dall'esistente CollectionChanged:
    • Per favorire la retrocompatibilità.
    • Quindi dettagli più rilevanti possono essere forniti nel nuovo ItemPropertyChangedEventArgsche lo accompagna: l'originale PropertyChangedEventArgse l'indice all'interno della collezione.
  • Replica tutti i costruttori da ObservableCollection<>.
  • Gestisce correttamente la reimpostazione dell'elenco ( ObservableCollection<>.Clear()), evitando una possibile perdita di memoria.
  • Sovrascrive la classe di base OnCollectionChanged(), piuttosto che una sottoscrizione ad alta intensità di risorse CollectionChangedall'evento.

Codice

.csSegue il file completo . Si noti che sono state utilizzate alcune funzionalità di C # 6, ma dovrebbe essere abbastanza semplice eseguirne il 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)
        { }
    }
}

Test NUnit

Quindi puoi controllare le modifiche che potresti apportare (e vedere cosa ho testato in primo luogo!), Ho anche incluso la mia classe di test NUnit. Ovviamente, il seguente codice non è necessario solo per l'uso FullyObservableCollection<T>nel progetto.

NB La classe di test utilizza BindableBaseda PRISM per implementare INotifyPropertyChanged. Non vi è alcuna dipendenza da PRISM dal codice principale.

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
Non so cosa sto facendo di sbagliato, ma questo non funziona per me. Sto associando il mio ListView alla tua raccolta ma quando aggiorno le proprietà degli oggetti all'interno, il ListView non si aggiorna, anche se riesco a vedere tutti gli eventi che si accendono. Sto anche usando la libreria PRISM ...
Renato Parreira,

@Renato, hai fatto qualcosa con il nuovo evento? ListViewrisponderà agli CollectionChangedeventi perché li conosce. ItemPropertyChangedè un'aggiunta non standard, quindi è necessario insegnarlo al riguardo. Come una soluzione rapida e sporca, si potrebbe provare semplicemente a sparare la CollectionChangedmanifestazione, così come (o anche al posto di) ItemPropertyChangedin OnItemPropertyChanged(). Li ho tenuti separati per i motivi indicati nella risposta, ma per il tuo caso d'uso potrebbe fare proprio quello di cui hai bisogno.
Bob Sammers

20

Questo utilizza le idee di cui sopra ma lo rende una raccolta "più sensibile" derivata:

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 non propaga le modifiche ai singoli articoli come eventi CollectionChanged. Dovrai iscriverti a ciascun evento e inoltrarlo manualmente, oppure puoi controllare la classe BindingList [T] , che lo farà per te.


Perché sei l'unico a menzionarlo? +1
Atiz

7

Aggiunto all'evento 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
}

È possibile utilizzare PropertyChanged da ObservableCollection direttamente, poiché implementa INotifyPropertyChanged.
Dieter Meemken,

6

Ho usato la risposta di Jack Kenyons per implementare il mio OC, ma vorrei sottolineare un cambiamento che ho dovuto fare per farlo funzionare. Invece di:

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

Ho usato questo:

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

Sembra che "e.NewItems" produca null se l'azione è .Remove.


Penso che occorrano ulteriori cambiamenti e se e.Action == sostituisca
jk.

6

Sto solo aggiungendo i miei 2 centesimi su questo argomento. Sentito il TrulyObservableCollection richiesto gli altri due costruttori come trovato con 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

So che sono troppo tardi per questa festa, ma forse - aiuterà qualcuno ...

Qui puoi trovare la mia implementazione di ObservableCollectionEx. Ha alcune caratteristiche:

  • supporta tutto da ObservableCollection
  • è sicuro per i thread
  • supporta l'evento ItemPropertyChanged (aumenta ogni volta che viene generato l'oggetto Item.PropertyChanged)
  • supporta i filtri (quindi, è possibile creare ObservableCollectionEx, passare un'altra raccolta come sorgente e filtrare con predicato semplice. Molto utile in WPF, utilizzo molto questa funzione nelle mie applicazioni). Ancora di più: il filtro tiene traccia delle modifiche degli elementi tramite l'interfaccia INotifyPropertyChanged.

Ovviamente, ogni commento è apprezzato;)


1
Большое спасибо! Mille grazie per averlo condiviso! Mi hai risparmiato molte ore non dover scrivere la mia implementazione! :)
Alexander

@Alexander sei molto ben
accolto

@chopikadze, non riesco a scaricare il file cs di ObservableCollectionEx, puoi gentilmente risolverlo. Grazie
Shax,

Il link è morto.

5

Se conosco ObservableCollection rende l'evento solo quando aggiungiamo / eliminiamo o spostiamo elementi nella nostra raccolta. Quando aggiorniamo semplicemente alcune proprietà nella raccolta degli elementi di raccolta, non segnalarlo e l'interfaccia utente non verrà aggiornata.

Puoi semplicemente implementare INotifyPropertyChange nella tua classe Model. E rispetto a quando aggiorniamo un po 'di proprietà nell'elemento di raccolta, aggiornerà automaticamente l'interfaccia utente.

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

e poi

public ObservableCollection<Model> {get; set;}

Nel mio caso ho usato ListView to Bind per questa raccolta e in ItemTemplate ho impostato la proprietà Binding to Model e funziona bene.

Ecco qualche frammento

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>

Esempio di codice modello:

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

E implementazione di 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

Soluzione semplice per la raccolta standard osservabile che ho usato:

NON AGGIUNGERE alla tua proprietà O MODIFICARE DIRETTAMENTE gli oggetti interni, invece, crea una raccolta temporanea come questa

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

e aggiungere elementi o apportare modifiche a tmpList,

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

quindi passalo alla tua proprietà reale per incarico.

ContentList=tmpList;

questo cambierà l'intera proprietà che causa l'avviso INotifyPropertyChanged di cui hai bisogno.


1

Provo questa soluzione, ma funziona solo per me come RaisePropertyChange ("SourceGroupeGridView") quando la raccolta viene modificata, attivata per ogni aggiunta o modifica di elementi.

Il problema è in:

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

NotifyCollectionChangedAction. Reimpostare questa azione per effettuare un rebind completo di tutti gli elementi in groupedgrid, equivale a RaisePropertyChanged. Quando lo usi, vengono aggiornati tutti i gruppi di gridview.

Se si desidera aggiornare nell'interfaccia utente solo il gruppo del nuovo elemento, non si utilizza l'azione Ripristina, è necessario simulare un'azione Aggiungi in esso errata con qualcosa del genere:

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

Scusate il mio inglese e grazie per il codice di base :), spero che questo aiuti qualcuno ^ _ ^

Enjoi !!


1

Ecco un metodo di estensione per la soluzione sopra ...

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

Potresti voler spiegare la risposta
geedubb,

1
Ecco un link che descrive i metodi di estensione. docs.microsoft.com/en-us/dotnet/csharp/programming-guide/…
LawMan

1

Invece di un ObservableCollection o TrulyObservableCollection, considerare l'utilizzo di un BindingList e la chiamata del metodo ResetBindings.

Per esempio:

private BindingList<TfsFile> _tfsFiles;

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

Dato un evento, come un clic, il tuo codice sarebbe simile al seguente:

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

Il mio modello sembrava così:

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
Buone informazioni su questo metodo di BindingList, ma c'è un limite a questo approccio che le altre risposte superano: questa tecnica si basa sul valore che viene modificato nel codice e su cui è ResetBindings()possibile aggiungere una chiamata . La maggior parte delle altre risposte funzionerà se gli oggetti dell'elenco vengono modificati con altri mezzi, come un codice inalterabile o da un'associazione a un secondo controllo.
Bob Sammers

1

Per attivare OnChange nell'elenco ObservableCollection

  1. Ottieni indice dell'articolo selezionato
  2. Rimuovi l'elemento dal genitore
  3. Aggiungi l'elemento allo stesso indice tra parent

Esempio:

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

0

Ecco la mia versione dell'implementazione. Controlla e genera un errore, se gli oggetti nell'elenco non implementano INotifyPropertyChanged, quindi non possono dimenticare quel problema durante lo sviluppo. All'esterno si utilizza l'evento ListItemChanged per determinare se l'elenco o l'elemento dell'elenco stesso sono stati modificati.

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

Soluzione semplice in 2 righe di codice. Basta usare il costruttore di copie. Non c'è bisogno di scrivere TrulyObservableCollection ecc.

Esempio:

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

Un altro metodo senza costruttore di copie. È possibile utilizzare la serializzazione.

        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

È inoltre possibile utilizzare questo metodo di estensione per registrare facilmente un gestore per la modifica delle proprietà degli articoli nelle raccolte pertinenti. Questo metodo viene aggiunto automaticamente a tutte le raccolte che implementano INotifyCollectionChanged che contengono elementi che implementano 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;
                }
            }
        };
    }
}

Come usare:

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
        });
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.