ObservableCollection bemerkt nicht, wenn sich das Element darin ändert (auch bei INotifyPropertyChanged)

167

Weiß jemand, warum dieser Code nicht funktioniert:

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

ViewModelBaseenthält alles für RaisePropertyChangedetc. und es funktioniert für alles andere außer diesem Problem ..

Joseph jun. Melettukunnel
quelle

Antworten:

119

Die Set-Methode von ContentList wird nicht aufgerufen, wenn Sie einen Wert in der Auflistung ändern. Stattdessen sollten Sie auf das Auslösen des CollectionChanged- Ereignisses achten.

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

Okay, das ist heute zweimal so. Ich wurde von der falschen MSDN-Dokumentation gebissen. In dem Link, den ich dir gegeben habe, heißt es:

Tritt auf, wenn ein Element hinzugefügt, entfernt, geändert, verschoben oder die gesamte Liste aktualisiert wird.

Aber es wird tatsächlich nicht ausgelöst, wenn ein Gegenstand geändert wird. Dann brauchen Sie wohl eine Bruteforce-Methode:

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

Wenn Sie dies häufig benötigen, möchten Sie möglicherweise eine eigene Unterklasse erstellen ObservableCollection, die das CollectionChangedEreignis auslöst , wenn ein Mitglied sein PropertyChangedEreignis automatisch auslöst (wie in der Dokumentation angegeben ...).

Martin Harris
quelle
Sorry Harris, aber welches Ereignis muss ich in EntityViewModel auslösen, damit ContentCollectionChanged aufgerufen wird?
Joseph jun. Melettukunnel
36
Beachten Sie, dass Sie anstelle von ObservableCollection <EntityViewModel> eine BindingList <EntityViewModel> verwenden können, wenn Sie die Ereignisverwaltung nicht selbst implementieren möchten. Anschließend werden EntityViewModel.PropertyChanged-Ereignisse automatisch als ListChanged-Ereignisse weitergeleitet, wobei ListChangedType == ItemChanged.
Mjeanes
15
Hängt das nicht alles von Ihrem Verständnis des Begriffs ab changed? Dies kann bedeuten, dass sich eine Eigenschaft eines der Elemente in der Sammlung geändert hat (wie Sie es meiner Meinung nach interpretieren), oder dass eines der Elemente der Sammlung geändert wurde, indem es durch eine andere Instanz ersetzt wurde ( das ist meine Interpretation). Nicht ganz überzeugt - muss weiter untersucht werden.
Belugabob
10
Was passiert, wenn ich aufrufe _contentList.Clear()? Niemand wird sich abmelden PropertyChanged!
Paolo Moretti
2
@Paolo: Richtig, behandelt ContentCollectionChangednur Hinzufügen / Entfernen und nicht Ersetzen / Zurücksetzen. Ich werde versuchen, den Beitrag zu bearbeiten und zu reparieren. Die Art und Weise, wie Simon es in seiner Antwort tut, ist richtig.
Mike Fuchs
178

Hier ist eine Drop-In-Klasse, die ObservableCollection unterordnet und tatsächlich eine Reset-Aktion auslöst, wenn sich eine Eigenschaft für ein Listenelement ändert. Es erzwingt die Implementierung aller Elemente INotifyPropertyChanged.

Der Vorteil hierbei ist, dass Sie Daten an diese Klasse binden können und alle Ihre Bindungen mit Änderungen an Ihren Elementeigenschaften aktualisiert werden.

public sealed class TrulyObservableCollection<T> : ObservableCollection<T>
    where T : INotifyPropertyChanged
{
    public TrulyObservableCollection()
    {
        CollectionChanged += FullObservableCollectionCollectionChanged;
    }

    public TrulyObservableCollection(IEnumerable<T> pItems) : this()
    {
        foreach (var item in pItems)
        {
            this.Add(item);
        }
    }

    private void FullObservableCollectionCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.NewItems != null)
        {
            foreach (Object item in e.NewItems)
            {
                ((INotifyPropertyChanged)item).PropertyChanged += ItemPropertyChanged;
            }
        }
        if (e.OldItems != null)
        {
            foreach (Object item in e.OldItems)
            {
                ((INotifyPropertyChanged)item).PropertyChanged -= ItemPropertyChanged;
            }
        }
    }

    private void ItemPropertyChanged(object sender, PropertyChangedEventArgs e)
    {            
        NotifyCollectionChangedEventArgs args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, sender, sender, IndexOf((T)sender));
        OnCollectionChanged(args);
    }
}
Simon
quelle
4
Ich hatte Grund, selbst etwas Ähnliches zu implementieren, anstatt NotifyCollectionChangedAction.Reset zu verwenden. Stattdessen habe ich .Replace: new NotifyCollectionChangedEventArgs (NotifyCollectionChangedAction.Replace, item, item, IndexOf (item)) verwendet.
Chris
2
Tolle Lösung für mein Problem - danke! Für diejenigen, die ihre ObservableCollection mit einer Liste erstellt haben, möchten Sie möglicherweise einen Konstruktor hinzufügen, der auch alle Elemente durchläuft und PropertyChanged hinzufügt.
Gavin
4
Hier liegt ein potenzieller Speicherverlust vor. Ein Reset-Ereignis tritt auf, wenn die Sammlung erheblich geändert wird, z. B. beim Löschen. In diesem Fall wird keiner Ihrer INPC-Handler abgemeldet.
Charles Mager
6
Dies ist eine OK-Implementierung, hat jedoch ein großes Problem: Das NotifyCollectionChangedAction.Replaceist keine gute Idee, da Sie dann nicht zwischen einem tatsächlich zu ersetzenden Element oder einem durch eine Elementänderung verursachten Ereignis unterscheiden können. Es wird viel besser, wenn Sie definieren public event PropertyChangedEventHandler CollectionItemChanged;und dann in ItemPropertyChangeddothis.CollectionItemChanged?.Invoke(sender, e);
hyankov
4
Hat jemand ein Beispiel für die Verwendung dieser Klasse?
Decoder94
23

Ich habe eine hoffentlich ziemlich robuste Lösung zusammengestellt, einschließlich einiger Techniken in anderen Antworten. Es ist eine neue Klasse, von ObservableCollection<>der ich rufeFullyObservableCollection<>

Es hat die folgenden Funktionen:

  • Es wird ein neues Ereignis hinzugefügt ItemPropertyChanged. Ich habe dies bewusst von den bestehenden getrennt CollectionChanged:
    • Zur Unterstützung der Abwärtskompatibilität.
    • So können im neuen ItemPropertyChangedEventArgsBegleitmaterial relevantere Details angegeben werden : das Original PropertyChangedEventArgsund der Index innerhalb der Sammlung.
  • Es repliziert alle Konstruktoren von ObservableCollection<>.
  • Die zurückgesetzte Liste wird korrekt behandelt ( ObservableCollection<>.Clear()), wodurch ein möglicher Speicherverlust vermieden wird.
  • Es überschreibt die Basisklasse OnCollectionChanged()und nicht ein ressourcenintensiveres Abonnement für das CollectionChangedEreignis.

Code

Die vollständige .csDatei folgt. Beachten Sie, dass einige Funktionen von C # 6 verwendet wurden, die Rückportierung jedoch recht einfach sein sollte:

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

Damit Sie überprüfen können, welche Änderungen Sie möglicherweise vornehmen (und sehen, was ich zuerst getestet habe!), Habe ich auch meine NUnit-Testklasse aufgenommen. Offensichtlich ist der folgende Code nicht nur für die Verwendung FullyObservableCollection<T>in Ihrem Projekt erforderlich .

NB Die Testklasse verwendet BindableBasePRISM zur Implementierung INotifyPropertyChanged. Es besteht keine Abhängigkeit von PRISM vom Hauptcode.

using NUnit.Framework;
using Utilities;
using Microsoft.Practices.Prism.Mvvm;
using System.Collections.Specialized;
using System.Collections.Generic;

namespace Test_Utilities
{
    [TestFixture]
    public class Test_FullyObservableCollection : AssertionHelper
    {
        public class NotifyingTestClass : BindableBase
        {
            public int Id
            {
                get { return _Id; }
                set { SetProperty(ref _Id, value); }
            }
            private int _Id;

            public string Name
            {
                get { return _Name; }
                set { SetProperty(ref _Name, value); }
            }
            private string _Name;

        }

        FullyObservableCollection<NotifyingTestClass> TestCollection;
        NotifyingTestClass Fred;
        NotifyingTestClass Betty;
        List<NotifyCollectionChangedEventArgs> CollectionEventList;
        List<ItemPropertyChangedEventArgs> ItemEventList;

        [SetUp]
        public void Init()
        {
            Fred = new NotifyingTestClass() { Id = 1, Name = "Fred" };
            Betty = new NotifyingTestClass() { Id = 4, Name = "Betty" };

            TestCollection = new FullyObservableCollection<NotifyingTestClass>()
                {
                    Fred,
                    new NotifyingTestClass() {Id = 2, Name = "Barney" },
                    new NotifyingTestClass() {Id = 3, Name = "Wilma" }
                };

            CollectionEventList = new List<NotifyCollectionChangedEventArgs>();
            ItemEventList = new List<ItemPropertyChangedEventArgs>();
            TestCollection.CollectionChanged += (o, e) => CollectionEventList.Add(e);
            TestCollection.ItemPropertyChanged += (o, e) => ItemEventList.Add(e);
        }

        // Change existing member property: just ItemPropertyChanged(IPC) should fire
        [Test]
        public void DetectMemberPropertyChange()
        {
            TestCollection[0].Id = 7;

            Expect(CollectionEventList.Count, Is.EqualTo(0));

            Expect(ItemEventList.Count, Is.EqualTo(1), "IPC count");
            Expect(ItemEventList[0].PropertyName, Is.EqualTo(nameof(Fred.Id)), "Field Name");
            Expect(ItemEventList[0].CollectionIndex, Is.EqualTo(0), "Collection Index");
        }


        // Add new member, change property: CollectionPropertyChanged (CPC) and IPC should fire
        [Test]
        public void DetectNewMemberPropertyChange()
        {
            TestCollection.Add(Betty);

            Expect(TestCollection.Count, Is.EqualTo(4));
            Expect(TestCollection[3].Name, Is.EqualTo("Betty"));

            Expect(ItemEventList.Count, Is.EqualTo(0), "Item Event count");

            Expect(CollectionEventList.Count, Is.EqualTo(1), "Collection Event count");
            Expect(CollectionEventList[0].Action, Is.EqualTo(NotifyCollectionChangedAction.Add), "Action (add)");
            Expect(CollectionEventList[0].OldItems, Is.Null, "OldItems count");
            Expect(CollectionEventList[0].NewItems.Count, Is.EqualTo(1), "NewItems count");
            Expect(CollectionEventList[0].NewItems[0], Is.EqualTo(Betty), "NewItems[0] dereference");

            CollectionEventList.Clear();      // Empty for next operation
            ItemEventList.Clear();

            TestCollection[3].Id = 7;
            Expect(CollectionEventList.Count, Is.EqualTo(0), "Collection Event count");

            Expect(ItemEventList.Count, Is.EqualTo(1), "Item Event count");
            Expect(TestCollection[ItemEventList[0].CollectionIndex], Is.EqualTo(Betty), "Collection Index dereference");
        }


        // Remove member, change property: CPC should fire for removel, neither CPC nor IPC should fire for change
        [Test]
        public void CeaseListentingWhenMemberRemoved()
        {
            TestCollection.Remove(Fred);

            Expect(TestCollection.Count, Is.EqualTo(2));
            Expect(TestCollection.IndexOf(Fred), Is.Negative);

            Expect(ItemEventList.Count, Is.EqualTo(0), "Item Event count (pre change)");

            Expect(CollectionEventList.Count, Is.EqualTo(1), "Collection Event count (pre change)");
            Expect(CollectionEventList[0].Action, Is.EqualTo(NotifyCollectionChangedAction.Remove), "Action (remove)");
            Expect(CollectionEventList[0].OldItems.Count, Is.EqualTo(1), "OldItems count");
            Expect(CollectionEventList[0].NewItems, Is.Null, "NewItems count");
            Expect(CollectionEventList[0].OldItems[0], Is.EqualTo(Fred), "OldItems[0] dereference");

            CollectionEventList.Clear();      // Empty for next operation
            ItemEventList.Clear();

            Fred.Id = 7;
            Expect(CollectionEventList.Count, Is.EqualTo(0), "Collection Event count (post change)");
            Expect(ItemEventList.Count, Is.EqualTo(0), "Item Event count (post change)");
        }


        // Move member in list, change property: CPC should fire for move, IPC should fire for change
        [Test]
        public void MoveMember()
        {
            TestCollection.Move(0, 1);

            Expect(TestCollection.Count, Is.EqualTo(3));
            Expect(TestCollection.IndexOf(Fred), Is.GreaterThan(0));

            Expect(ItemEventList.Count, Is.EqualTo(0), "Item Event count (pre change)");

            Expect(CollectionEventList.Count, Is.EqualTo(1), "Collection Event count (pre change)");
            Expect(CollectionEventList[0].Action, Is.EqualTo(NotifyCollectionChangedAction.Move), "Action (move)");
            Expect(CollectionEventList[0].OldItems.Count, Is.EqualTo(1), "OldItems count");
            Expect(CollectionEventList[0].NewItems.Count, Is.EqualTo(1), "NewItems count");
            Expect(CollectionEventList[0].OldItems[0], Is.EqualTo(Fred), "OldItems[0] dereference");
            Expect(CollectionEventList[0].NewItems[0], Is.EqualTo(Fred), "NewItems[0] dereference");

            CollectionEventList.Clear();      // Empty for next operation
            ItemEventList.Clear();

            Fred.Id = 7;
            Expect(CollectionEventList.Count, Is.EqualTo(0), "Collection Event count (post change)");

            Expect(ItemEventList.Count, Is.EqualTo(1), "Item Event count (post change)");
            Expect(TestCollection[ItemEventList[0].CollectionIndex], Is.EqualTo(Fred), "Collection Index dereference");
        }


        // Clear list, chnage property: only CPC should fire for clear and neither for property change
        [Test]
        public void ClearList()
        {
            TestCollection.Clear();

            Expect(TestCollection.Count, Is.EqualTo(0));

            Expect(ItemEventList.Count, Is.EqualTo(0), "Item Event count (pre change)");

            Expect(CollectionEventList.Count, Is.EqualTo(1), "Collection Event count (pre change)");
            Expect(CollectionEventList[0].Action, Is.EqualTo(NotifyCollectionChangedAction.Reset), "Action (reset)");
            Expect(CollectionEventList[0].OldItems, Is.Null, "OldItems count");
            Expect(CollectionEventList[0].NewItems, Is.Null, "NewItems count");

            CollectionEventList.Clear();      // Empty for next operation
            ItemEventList.Clear();

            Fred.Id = 7;
            Expect(CollectionEventList.Count, Is.EqualTo(0), "Collection Event count (post change)");
            Expect(ItemEventList.Count, Is.EqualTo(0), "Item Event count (post change)");
        }
    }
}
Bob Sammers
quelle
1
Ich weiß nicht, was ich falsch mache, aber das funktioniert bei mir nicht. Ich binde meine ListView an Ihre Sammlung, aber wenn ich die Eigenschaften der darin enthaltenen Elemente aktualisiere, wird die ListView nicht aktualisiert, obwohl ich sehe, dass alle Ereignisse ausgelöst werden. Ich benutze auch die PRISM-Bibliothek ...
Renato Parreira
@ Renato, hast du etwas mit dem neuen Event gemacht? ListViewwird auf CollectionChangedEreignisse reagieren, weil es über sie weiß. ItemPropertyChangedist eine nicht standardmäßige Ergänzung, daher müssen Sie sie darüber unterrichten. Als schnelle und schmutzige Lösung können Sie versuchen, das CollectionChangedEreignis sowohl als auch (oder sogar anstelle von) ItemPropertyChangedauszulösen OnItemPropertyChanged(). Ich habe sie aus den in der Antwort angegebenen Gründen getrennt gehalten, aber für Ihren Anwendungsfall könnte es genau das tun, was Sie brauchen.
Bob Sammers
20

Dies verwendet die oben genannten Ideen, macht es jedoch zu einer abgeleiteten "sensibleren" Sammlung:

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

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

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

        void ObservableCollectionEx_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Remove)
            {
                foreach(T item in e.OldItems)
                {
                    //Removed items
                    item.PropertyChanged -= EntityViewModelPropertyChanged;
                }
            }
            else if (e.Action == NotifyCollectionChangedAction.Add)
            {
                foreach(T item in e.NewItems)
                {
                    //Added items
                    item.PropertyChanged += EntityViewModelPropertyChanged;
                }     
            }       
        }

        public void EntityViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            //This will get called when the property of an object inside the collection changes - note you must make it a 'reset' - dunno why
            NotifyCollectionChangedEventArgs args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
            OnCollectionChanged(args);
        }
    }
}
Jack Kenyon
quelle
12

ObservableCollection gibt einzelne Elementänderungen nicht als CollectionChanged-Ereignisse weiter. Sie müssen entweder jedes Ereignis abonnieren und manuell weiterleiten, oder Sie können die BindingList [T] -Klasse auschecken , die dies für Sie erledigt .

mjeanes
quelle
Warum bist du der einzige, der dies erwähnt? +1
Atizs
7

Zum TruelyObservableCollection-Ereignis "ItemPropertyChanged" hinzugefügt:

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

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

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

            Trades[0].Qty = 999;

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

            return;
        }

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

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

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

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

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

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

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

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

        public DateTime OrderPlaced
        {
            get { return _OrderPlaced; }
        }

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

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

        public event PropertyChangedEventHandler PropertyChanged;

        private void NotifyPropertyChanged(String propertyName = "")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
#endregion
}
Swab.Jat
quelle
Sie können PropertyChanged direkt aus ObservableCollection verwenden, da es INotifyPropertyChanged implementiert.
Dieter Meemken
6

Ich habe Jack Kenyons Antwort verwendet, um mein eigenes OK zu implementieren, aber ich möchte auf eine Änderung hinweisen, die ich vornehmen musste, damit es funktioniert. Anstatt:

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

Ich habe das benutzt:

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

Es scheint, dass "e.NewItems" null erzeugt, wenn die Aktion .Remove ist.

Triazotan
quelle
Ich denke, es braucht auch weitere Änderungen, was ist, wenn e.Action == replace
jk.
6

Ich füge nur meine 2 Cent zu diesem Thema hinzu. Für die TrulyObservableCollection waren die beiden anderen Konstruktoren erforderlich, die mit ObservableCollection gefunden wurden:

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

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

        HookupCollectionChangedEvent();
    }

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

        HookupCollectionChangedEvent();
    }

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

Ich weiß, dass ich für diese Party zu spät bin, aber vielleicht - es wird jemandem helfen.

Hier finden Sie meine Implementierung von ObservableCollectionEx. Es hat einige Funktionen:

  • Es unterstützt alles von ObservableCollection
  • Es ist threadsicher
  • Es unterstützt das ItemPropertyChanged-Ereignis (es wird jedes Mal ausgelöst, wenn das Item.PropertyChanged-Element ausgelöst wird.)
  • Es unterstützt Filter (Sie können also ObservableCollectionEx erstellen, eine andere Sammlung als Quelle übergeben und mit einem einfachen Prädikat filtern. Sehr nützlich in WPF, ich verwende diese Funktion häufig in meinen Anwendungen). Noch mehr - Filter verfolgt Änderungen von Elementen über die INotifyPropertyChanged-Schnittstelle.

Kommentare sind natürlich willkommen;)

Chopikadze
quelle
1
Большое спасибо! Vielen Dank für das Teilen! Sie haben mir viele Stunden gespart, weil Sie meine eigene Implementierung nicht schreiben mussten! :)
Alexander
@ Alexander Sie sind sehr
willkommen
@chopikadze, ich kann die cs-Datei Ihres ObservableCollectionEx nicht herunterladen. Können Sie das bitte beheben? Danke
Shax
Der Link ist tot.
5

Wenn ich ObservableCollection kenne, mache ich nur dann ein Ereignis, wenn wir Elemente in unserer Sammlung hinzufügen / löschen oder verschieben. Wenn wir einige Eigenschaften in der Sammlung von Sammlungselementen einfach aktualisieren, signalisieren wir dies nicht und die Benutzeroberfläche wird nicht aktualisiert.

Sie können INotifyPropertyChange einfach in Ihrer Model-Klasse implementieren . Und dann, wenn wir einige Eigenschaften im Sammlungselement aktualisieren, wird die Benutzeroberfläche automatisch aktualisiert.

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

und dann

public ObservableCollection<Model> {get; set;}

In meinem Fall habe ich ListView to Bind für diese Sammlung verwendet und in ItemTemplate die Eigenschaft Binding to Model festgelegt, und es funktioniert gut.

Hier ist ein Ausschnitt

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>

Modellcodebeispiel:

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

Und ViewModel-Implementierung:

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

    public ObservableCollection<PersonModel> Persons { get;}

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

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

    private PersonModel _selectedPerson;
}
Sviatoslav Kindrat
quelle
2

Einfache Lösung für die standardmäßige beobachtbare Sammlung, die ich verwendet habe:

NICHT ZU DEM EIGENTUM HINZUFÜGEN ODER DIE inneren Elemente DIREKT ÄNDERN, sondern eine temporäre Sammlung wie diese erstellen

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

und Elemente hinzufügen oder Änderungen an tmpList vornehmen,

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

Übergeben Sie es dann durch Abtretung an Ihr eigentliches Eigentum.

ContentList=tmpList;

Dadurch wird die gesamte Eigenschaft geändert, wodurch INotifyPropertyChanged nach Bedarf angezeigt wird.

denktomid
quelle
1

Ich versuche diese Lösung, arbeite aber nur für mich wie eine RaisePropertyChange ("SourceGroupeGridView"), wenn die Sammlung geändert wurde, die für jedes hinzugefügte oder geänderte Element ausgelöst wurde.

Das Problem liegt in:

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

NotifyCollectionChangedAction.Reset Diese Aktion führt eine vollständige Neubindung aller Elemente in groupedgrid durch. Dies entspricht RaisePropertyChanged. Wenn Sie es verwenden, werden alle Gruppen der Rasteransicht aktualisiert.

Wenn Sie nur die Gruppe des neuen Elements in der Benutzeroberfläche aktualisieren möchten und die Aktion "Zurücksetzen" nicht verwenden, müssen Sie eine Aktion "Hinzufügen" in itemproperty mit folgendem Ergebnis simulieren:

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

Entschuldigung für mein Englisch und danke für den Basiscode :), ich hoffe das hilft jemandem ^ _ ^

Enjoi !!

alberto sainz
quelle
1

Hier ist eine Erweiterungsmethode für die obige Lösung ...

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

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

    return newList;
}  
LawMan
quelle
Vielleicht möchten Sie die Antwort erklären
geedubb
1
Hier ist ein Link, der Erweiterungsmethoden beschreibt. docs.microsoft.com/en-us/dotnet/csharp/programming-guide/…
LawMan
1

Verwenden Sie anstelle einer ObservableCollection oder TrulyObservableCollection eine BindingList und rufen Sie die ResetBindings-Methode auf.

Beispielsweise:

private BindingList<TfsFile> _tfsFiles;

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

Bei einem Ereignis wie einem Klick würde Ihr Code folgendermaßen aussehen:

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

Mein Modell sah so aus:

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

        public string FullPath { get; set; }

        public string Name { get; set; }

        public string Text { get; set; }

    }
}
ClubbieTim
quelle
1
Gute Informationen zu dieser Methode BindingList, aber es gibt eine Einschränkung für diesen Ansatz, die die anderen Antworten überwinden: Diese Technik hängt davon ab, ResetBindings()welcher Wert im Code geändert wird und wo ein Aufruf hinzugefügt werden kann. Die meisten anderen Antworten funktionieren, wenn die Objekte der Liste auf andere Weise geändert werden, z. B. durch unveränderlichen Code oder von einer Bindung an ein zweites Steuerelement.
Bob Sammers
1

So lösen Sie OnChange in der ObservableCollection-Liste aus

  1. Index des ausgewählten Elements abrufen
  2. Entfernen Sie das Element aus dem übergeordneten Element
  3. Fügen Sie das Element am selben Index im übergeordneten Element hinzu

Beispiel:

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

Hier ist meine Version der Implementierung. Es prüft und löst einen Fehler aus, wenn die Objekte in der Liste INotifyPropertyChanged nicht implementieren, sodass dieses Problem während der Entwicklung nicht vergessen werden kann. Außen verwenden Sie das ListItemChanged-Ereignis, um festzustellen, ob sich die Liste oder das Listenelement selbst geändert hat.

public class SpecialObservableCollection<T> : ObservableCollection<T>
{
    public SpecialObservableCollection()
    {
        this.CollectionChanged += OnCollectionChanged;
    }

    void OnCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        AddOrRemoveListToPropertyChanged(e.NewItems,true); 
        AddOrRemoveListToPropertyChanged(e.OldItems,false); 
    }

    private void AddOrRemoveListToPropertyChanged(IList list, Boolean add)
    {
        if (list == null) { return; }
        foreach (object item in list)
        {
            INotifyPropertyChanged o = item as INotifyPropertyChanged;
            if (o != null)
            {
                if (add)  { o.PropertyChanged += ListItemPropertyChanged; }
                if (!add) { o.PropertyChanged -= ListItemPropertyChanged; }
            }
            else
            {
                throw new Exception("INotifyPropertyChanged is required");
            }
        }
    }

    void ListItemPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        OnListItemChanged(this, e);
    }

    public delegate void ListItemChangedEventHandler(object sender, PropertyChangedEventArgs e);

    public event ListItemChangedEventHandler ListItemChanged;

    private void OnListItemChanged(Object sender, PropertyChangedEventArgs e)
    {
        if (ListItemChanged != null) { this.ListItemChanged(this, e); }
    }


}
Michael
quelle
0

Einfache Lösung in 2 Codezeilen. Verwenden Sie einfach den Kopierkonstruktor. Sie müssen TrulyObservableCollection usw. nicht schreiben.

Beispiel:

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

Eine andere Methode ohne Kopierkonstruktor. Sie können die Serialisierung verwenden.

        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;
Singapur Saravanan
quelle
0

Mit dieser Erweiterungsmethode können Sie auch einfach einen Handler für die Änderung von Elementeigenschaften in relevanten Sammlungen registrieren. Diese Methode wird automatisch allen Sammlungen hinzugefügt, die INotifyCollectionChanged implementieren und Elemente enthalten, die INotifyPropertyChanged implementieren:

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

Wie benutzt man:

public class Test
{
    public static void MyExtensionTest()
    {
        ObservableCollection<INotifyPropertyChanged> c = new ObservableCollection<INotifyPropertyChanged>();
        c.SetOnCollectionItemPropertyChanged((item, e) =>
        {
             //whatever you want to do on item change
        });
    }
}
ed22
quelle