ObservableCollection ne remarque pas quand l'élément qu'il change (même avec INotifyPropertyChanged)


167

Quelqu'un sait-il pourquoi ce code ne fonctionne pas:

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

ViewModelBasecontient tout pour RaisePropertyChangedetc. et il fonctionne pour tout le reste sauf ce problème.


Réponses:


119

La méthode Set de ContentList ne sera pas appelée lorsque vous modifiez une valeur à l'intérieur de la collection, à la place, vous devriez rechercher le déclenchement de l' événement 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
    }
}

D'accord, c'est deux fois aujourd'hui que j'ai été mordu par la documentation MSDN qui était fausse. Dans le lien que je vous ai donné, il est dit:

Se produit lorsqu'un élément est ajouté, supprimé, modifié, déplacé ou que la liste entière est actualisée.

Mais en fait, il ne se déclenche pas lorsqu'un élément est modifié. Je suppose que vous aurez besoin d'une méthode plus bruteforce alors:

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

Si vous en avez beaucoup besoin, vous voudrez peut-être sous-classer le vôtre ObservableCollectionqui déclenche l' CollectionChangedévénement lorsqu'un membre déclenche PropertyChangedautomatiquement son événement (comme il le dit dans la documentation ...)


Désolé Harris, mais quel événement dois-je déclencher dans EntityViewModel pour que ContentCollectionChanged soit appelé?
Joseph jun. Melettukunnel

36
notez que si vous ne souhaitez pas implémenter la gestion des événements vous-même, vous pouvez utiliser un BindingList <EntityViewModel> à la place de ObservableCollection <EntityViewModel>. Il transmettra ensuite automatiquement les événements EntityViewModel.PropertyChanged en tant qu'événements ListChanged où ListChangedType == ItemChanged.
mjeanes

15
Tout cela ne dépend-il pas de votre compréhension du terme changed? Cela pourrait signifier qu'une propriété de l'un des éléments de la collection a changé (c'est ainsi que je pense que vous l'interprétez) ou cela pourrait signifier qu'un des éléments de la collection a été modifié en le remplaçant par une instance différente ( c'est mon interprétation). Pas totalement convaincu cependant - devra examiner cela plus en détail.
belugabob

10
Que se passe-t-il si j'invoque _contentList.Clear()? Personne ne se désabonnera PropertyChanged!
Paolo Moretti

2
@Paolo: C'est vrai, ContentCollectionChangedne gère que Ajouter / Supprimer et non Remplacer / Réinitialiser. Je vais essayer de modifier et de corriger le message. La façon dont Simon le fait dans sa réponse est correcte.
Mike Fuchs

178

Voici une classe drop-in qui sous-classe ObservableCollection et déclenche en fait une action Reset lorsqu'une propriété d'un élément de liste change. Il applique tous les éléments à mettre en œuvre INotifyPropertyChanged.

L'avantage ici est que vous pouvez lier des données à cette classe et toutes vos liaisons seront mises à jour avec les modifications apportées aux propriétés de vos éléments.

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
J'ai eu des raisons d'implémenter moi-même quelque chose de similaire, mais plutôt que d'utiliser NotifyCollectionChangedAction.Reset, j'ai plutôt utilisé .Replace: new NotifyCollectionChangedEventArgs (NotifyCollectionChangedAction.Replace, item, item, IndexOf (item)).
Chris

2
Excellente solution à mon problème - merci! Pour ceux qui ont créé leur ObservableCollection avec une liste, vous souhaiterez peut-être ajouter un constructeur qui parcourt également tous les éléments et ajoute PropertyChanged.
Gavin

4
Il y a une fuite de mémoire potentielle ici - Un événement de réinitialisation se produit lorsque la collection est considérablement modifiée, par exemple sur Clear. Aucun de vos gestionnaires INPC ne sera désabonné lorsque cela se produit.
Charles Mager

6
c'est une implémentation correcte mais elle a un problème majeur - ce NotifyCollectionChangedAction.Replacen'est pas une bonne idée, car alors vous ne pouvez pas faire la distinction entre un élément en fait remplacé ou un événement causé par un changement d'élément. Cela va beaucoup mieux quand vous définissez public event PropertyChangedEventHandler CollectionItemChanged;et puis ItemPropertyChangedfaitesthis.CollectionItemChanged?.Invoke(sender, e);
hyankov

4
Quelqu'un at-il un exemple de l'utilisation de cette classe?
Decoder94

23

J'ai mis au point ce que j'espère être une solution assez robuste, y compris certaines des techniques dans d'autres réponses. C'est une nouvelle classe dérivée de ObservableCollection<>, que j'appelleFullyObservableCollection<>

Il présente les caractéristiques suivantes:

  • Il ajoute un nouvel événement, ItemPropertyChanged. J'ai délibérément gardé cela séparé de l'existant CollectionChanged:
    • Pour faciliter la rétrocompatibilité.
    • Ainsi, des détails plus pertinents peuvent être donnés dans le nouveau ItemPropertyChangedEventArgsqui l'accompagne: l'original PropertyChangedEventArgset l'index dans la collection.
  • Il réplique tous les constructeurs de ObservableCollection<>.
  • Il gère correctement la liste en cours de réinitialisation ( ObservableCollection<>.Clear()), évitant ainsi une éventuelle fuite de mémoire.
  • Il remplace la classe de base OnCollectionChanged(), plutôt qu'un abonnement plus gourmand en ressources à l' CollectionChangedévénement.

Code

Le .csdossier complet suit. Notez que quelques fonctionnalités de C # 6 ont été utilisées, mais il devrait être assez simple de le rétroporter:

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

Tests NUnit

Pour que vous puissiez vérifier les modifications que vous pourriez apporter (et voir ce que j'ai testé en premier lieu!), J'ai également inclus ma classe de test NUnit. De toute évidence, le code suivant n'est pas nécessaire uniquement pour l'utiliser FullyObservableCollection<T>dans votre projet.

NB La classe de test utilise BindableBasede PRISM pour l'implémentation INotifyPropertyChanged. Il n'y a aucune dépendance sur PRISM du code principal.

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
Je ne sais pas ce que je fais de mal, mais cela ne fonctionne pas pour moi. Je lie mon ListView à votre collection mais lorsque je mets à jour les propriétés des éléments à l'intérieur, le ListView ne se met pas à jour, même si je peux voir tous les événements se déclencher. J'utilise aussi la bibliothèque PRISM ...
Renato Parreira

@Renato, avez-vous fait quelque chose avec le nouvel événement? ListViewrépondra aux CollectionChangedévénements parce qu'il les connaît. ItemPropertyChangedest un ajout non standard, vous devez donc lui apprendre à ce sujet. En tant que solution rapide et sale, vous pouvez essayer de tirer tout l' CollectionChangedévénement ainsi que (ou même au lieu de) ItemPropertyChangeddans OnItemPropertyChanged(). Je les ai gardés séparés pour les raisons indiquées dans la réponse, mais pour votre cas d'utilisation, cela pourrait simplement faire ce dont vous avez besoin.
Bob Sammers

20

Cela utilise les idées ci-dessus mais en fait une collection dérivée `` plus sensible '':

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 ne propagera pas les modifications d'éléments individuels en tant qu'événements CollectionChanged. Vous devrez soit vous abonner à chaque événement et le transférer manuellement, soit consulter la classe BindingList [T] , qui le fera pour vous.


Pourquoi êtes-vous le seul à mentionner cela? +1
Atizs

7

Ajouté à l'événement 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
}

Vous pouvez utiliser PropertyChanged depuis ObservableCollection directement, car il implémente INotifyPropertyChanged.
Dieter Meemken

6

J'ai utilisé la réponse de Jack Kenyons pour implémenter mon propre OC, mais j'aimerais souligner un changement que j'ai dû faire pour que cela fonctionne. Au lieu de:

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

J'ai utilisé ceci:

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

Il semble que le "e.NewItems" produit null si l'action est .Remove.


Je pense qu'il a également besoin de modifications supplémentaires, que faire si e.Action == replace
jk.

6

Je viens d'ajouter mes 2 cents sur ce sujet. Senti que TrulyObservableCollection nécessite les deux autres constructeurs trouvés avec 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

Je sais que je suis trop tard pour cette fête, mais peut-être - cela aidera quelqu'un ...

Ici vous pouvez trouver mon implémentation d'ObservableCollectionEx. Il a quelques caractéristiques:

  • il prend en charge tout de ObservableCollection
  • c'est thread safe
  • il prend en charge l'événement ItemPropertyChanged (il se déclenche à chaque fois que l'élément Item.PropertyChanged est déclenché)
  • il prend en charge les filtres (vous pouvez donc créer ObservableCollectionEx, lui passer une autre collection en tant que source, et filtrer avec un simple prédicat. Très utile dans WPF, j'utilise beaucoup cette fonctionnalité dans mes applications). Encore plus - le filtre suit les modifications des éléments via l'interface INotifyPropertyChanged.

Bien sûr, tous les commentaires sont appréciés;)


1
Большое спасибо! Merci beaucoup de partager ça! Vous m'avez fait gagner de nombreuses heures en n'ayant pas à écrire ma propre implémentation! :)
Alexander

@Alexander vous êtes très bienvenu :)
chopikadze

@chopikadze, je ne peux pas télécharger le fichier cs de votre ObservableCollectionEx pouvez-vous le réparer. Merci
Shax

Le lien est mort.

5

Si je connais ObservableCollection, créez un événement uniquement lorsque nous ajoutons / supprimons ou déplaçons des éléments dans notre collection. Lorsque nous mettons simplement à jour certaines propriétés de la collection d'éléments de collection, ne le signalez pas et l'interface utilisateur ne sera pas mise à jour.

Vous pouvez simplement implémenter INotifyPropertyChange dans votre classe Model. Et que lorsque nous mettons à jour certaines propriétés dans l'élément de collection, l'interface utilisateur sera automatiquement mise à jour.

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

et que

public ObservableCollection<Model> {get; set;}

Dans mon cas, j'ai utilisé ListView to Bind pour cette collection et dans ItemTemplate, définissez Binding to Model et cela fonctionne bien.

Voici un extrait

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>

Exemple de code de modèle:

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

Et l'implémentation de 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

Solution simple pour la collecte d'observation standard que j'ai utilisée:

NE PAS AJOUTER à votre propriété OU CHANGER DIRECTEMENT ses éléments internes, mais plutôt créer une collection temporaire comme celle-ci

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

et ajouter des éléments ou apporter des modifications à tmpList,

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

puis transmettez-le à votre propriété réelle par cession.

ContentList=tmpList;

cela modifiera toute la propriété, ce qui entraînera la notification de INotifyPropertyChanged selon vos besoins.


1

J'essaie cette solution, mais ne fonctionne que pour moi comme un RaisePropertyChange ("SourceGroupeGridView") lorsque la collection a changé, qui a déclenché pour chaque élément ajouté ou modifié.

Le problème est dans:

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

NotifyCollectionChangedAction.Réinitialiser cette action pour effectuer une nouvelle liaison complète de tous les éléments dans groupedgrid, est équivalent à RaisePropertyChanged. Lorsque vous l'utilisez, tous les groupes de gridview sont actualisés.

Si vous souhaitez uniquement actualiser dans l'interface utilisateur le groupe du nouvel élément, vous n'utilisez pas l'action Réinitialiser, vous devrez simuler une action Ajouter dans la propriété de l'élément avec quelque chose comme ceci:

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

Désolé pour mon anglais, et merci pour le code de base :), j'espère que cela aidera quelqu'un ^ _ ^

Enjoi !!


1

Voici une méthode d'extension pour la solution ci-dessus ...

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

Vous voudrez peut-être expliquer la réponse
geedubb

1
Voici un lien qui décrit les méthodes d'extension. docs.microsoft.com/en-us/dotnet/csharp/programming-guide/…
LawMan

1

Au lieu d'un ObservableCollection ou TrulyObservableCollection, envisagez d'utiliser un BindingList et d'appeler la méthode ResetBindings.

Par exemple:

private BindingList<TfsFile> _tfsFiles;

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

Étant donné un événement, tel qu'un clic, votre code ressemblerait à ceci:

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

Mon modèle ressemblait à ceci:

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
Bonnes informations sur cette méthode de BindingList, mais il y a une limitation à cette approche que les autres réponses surmontent: cette technique repose sur la valeur en cours de modification dans le code et où un appel à ResetBindings()peut être ajouté. La plupart des autres réponses fonctionneront si les objets de la liste sont modifiés par d'autres moyens, tels que du code inaltérable ou d'une liaison à un second contrôle.
Bob Sammers

1

Pour déclencher OnChange dans la liste ObservableCollection

  1. Obtenir l'index de l'élément sélectionné
  2. Supprimer l'élément du parent
  3. Ajouter l'élément au même index dans le parent

Exemple:

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

0

Voici ma version de l'implémentation. Il vérifie et renvoie une erreur, si les objets de la liste n'implémentent pas INotifyPropertyChanged, vous ne pouvez donc pas oublier ce problème lors du développement. À l'extérieur, vous utilisez l'événement ListItemChanged pour déterminer si la liste ou l'élément de liste lui-même a changé.

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

Solution simple en 2 lignes de code. Utilisez simplement le constructeur de copie. Pas besoin d'écrire TrulyObservableCollection etc.

Exemple:

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

Une autre méthode sans constructeur de copie. Vous pouvez utiliser la sérialisation.

        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

Vous pouvez également utiliser cette méthode d'extension pour enregistrer facilement un gestionnaire pour le changement de propriété d'élément dans les collections pertinentes. Cette méthode est automatiquement ajoutée à toutes les collections implémentant INotifyCollectionChanged qui contiennent des éléments qui implémentent 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;
                }
            }
        };
    }
}

Comment utiliser:

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
        });
    }
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.