ObservableCollection не замечает, когда элемент в нем изменяется (даже с INotifyPropertyChanged)

Кто-нибудь знает, почему этот код не работает:

public class CollectionViewModel : ViewModelBase {  
    public ObservableCollection<EntityViewModel> ContentList
    {
        get { return _contentList; }
        set 
        { 
            _contentList = value; 
            RaisePropertyChanged("ContentList"); 
            //I want to be notified here when something changes..?
            //debugger doesn't stop here when IsRowChecked is toggled
        }
     }
}

public class EntityViewModel : ViewModelBase
{

    private bool _isRowChecked;

    public bool IsRowChecked
    {
        get { return _isRowChecked; }
        set { _isRowChecked = value; RaisePropertyChanged("IsRowChecked"); }
    }
}

ViewModelBase содержит все для RaisePropertyChanged и т.д., и это работает для всего остального, кроме этой проблемы..

21 ответ

Решение

Метод Set ContentList не будет вызываться при изменении значения внутри коллекции, вместо этого вы должны искать срабатывание события CollectionChanged.

public class CollectionViewModel : ViewModelBase
{          
    public ObservableCollection<EntityViewModel> ContentList
    {
        get { return _contentList; }
    }

    public CollectionViewModel()
    {
         _contentList = new ObservableCollection<EntityViewModel>();
         _contentList.CollectionChanged += ContentCollectionChanged;
    }

    public void ContentCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        //This will get called when the collection is changed
    }
}

Хорошо, сегодня уже дважды я был укушен ошибкой документации MSDN. В приведенной мной ссылке сказано:

Происходит, когда элемент добавляется, удаляется, изменяется, перемещается или обновляется весь список.

Но на самом деле он не срабатывает при смене предмета. Я думаю, вам понадобится более грубый метод, чем:

public class CollectionViewModel : ViewModelBase
{          
    public ObservableCollection<EntityViewModel> ContentList
    {
        get { return _contentList; }
    }

    public CollectionViewModel()
    {
         _contentList = new ObservableCollection<EntityViewModel>();
         _contentList.CollectionChanged += ContentCollectionChanged;
    }

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

    public void EntityViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        //This will get called when the property of an object inside the collection changes
    }
}

Если вам это понадобится, вы можете захотеть создать свой собственный подкласс ObservableCollection что вызывает CollectionChanged событие, когда член запускает его PropertyChanged событие автоматически (как сказано в документации...)

Вот раскрывающийся класс, который является подклассом ObservableCollection и фактически вызывает действие Reset при изменении свойства элемента списка. Он обеспечивает все элементы для реализации INotifyPropertyChanged,

Преимущество заключается в том, что вы можете привязывать данные к этому классу, и все ваши привязки будут обновляться с изменениями свойств вашего элемента.

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

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

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

    private void ItemPropertyChanged(object sender, PropertyChangedEventArgs e)
    {            
        NotifyCollectionChangedEventArgs args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, sender, sender, IndexOf((T)sender));
        OnCollectionChanged(args);
    }
}

Я собрал, как я надеюсь, довольно надежное решение, включая некоторые приемы из других ответов. Это новый класс, полученный из ObservableCollection<>, которому я звоню FullyObservableCollection<>

Он имеет следующие особенности:

  • Это добавляет новое событие, ItemPropertyChanged, Я сознательно держал это отдельно от существующих CollectionChanged:
    • Чтобы помочь обратной совместимости.
    • Так что более актуальные подробности можно дать в новом ItemPropertyChangedEventArgs что сопровождает это: оригинал PropertyChangedEventArgs и индекс в коллекции.
  • Он копирует все конструкторы из ObservableCollection<>,
  • Это правильно обрабатывает список, который будет сброшен (ObservableCollection<>.Clear()), избегая возможной утечки памяти.
  • Это переопределяет базовый класс OnCollectionChanged()вместо более ресурсоемкой подписки на CollectionChanged событие.

Код

Полный .cs файл следует. Обратите внимание, что было использовано несколько функций C# 6, но это должно быть довольно просто:

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

namespace Utilities
{
    public class FullyObservableCollection<T> : ObservableCollection<T>
        where T : INotifyPropertyChanged
    {
        /// <summary>
        /// Occurs when a property is changed within an item.
        /// </summary>
        public event EventHandler<ItemPropertyChangedEventArgs> ItemPropertyChanged;

        public FullyObservableCollection() : base()
        { }

        public FullyObservableCollection(List<T> list) : base(list)
        {
            ObserveAll();
        }

        public FullyObservableCollection(IEnumerable<T> enumerable) : base(enumerable)
        {
            ObserveAll();
        }

        protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Remove ||
                e.Action == NotifyCollectionChangedAction.Replace)
            {
                foreach (T item in e.OldItems)
                    item.PropertyChanged -= ChildPropertyChanged;
            }

            if (e.Action == NotifyCollectionChangedAction.Add ||
                e.Action == NotifyCollectionChangedAction.Replace)
            {
                foreach (T item in e.NewItems)
                    item.PropertyChanged += ChildPropertyChanged;
            }

            base.OnCollectionChanged(e);
        }

        protected void OnItemPropertyChanged(ItemPropertyChangedEventArgs e)
        {
            ItemPropertyChanged?.Invoke(this, e);
        }

        protected void OnItemPropertyChanged(int index, PropertyChangedEventArgs e)
        {
            OnItemPropertyChanged(new ItemPropertyChangedEventArgs(index, e));
        }

        protected override void ClearItems()
        {
            foreach (T item in Items)
                item.PropertyChanged -= ChildPropertyChanged;

            base.ClearItems();
        }

        private void ObserveAll()
        {
            foreach (T item in Items)
                item.PropertyChanged += ChildPropertyChanged;
        }

        private void ChildPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            T typedSender = (T)sender;
            int i = Items.IndexOf(typedSender);

            if (i < 0)
                throw new ArgumentException("Received property notification from item not in collection");

            OnItemPropertyChanged(i, e);
        }
    }

    /// <summary>
    /// Provides data for the <see cref="FullyObservableCollection{T}.ItemPropertyChanged"/> event.
    /// </summary>
    public class ItemPropertyChangedEventArgs : PropertyChangedEventArgs
    {
        /// <summary>
        /// Gets the index in the collection for which the property change has occurred.
        /// </summary>
        /// <value>
        /// Index in parent collection.
        /// </value>
        public int CollectionIndex { get; }

        /// <summary>
        /// Initializes a new instance of the <see cref="ItemPropertyChangedEventArgs"/> class.
        /// </summary>
        /// <param name="index">The index in the collection of changed item.</param>
        /// <param name="name">The name of the property that changed.</param>
        public ItemPropertyChangedEventArgs(int index, string name) : base(name)
        {
            CollectionIndex = index;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ItemPropertyChangedEventArgs"/> class.
        /// </summary>
        /// <param name="index">The index.</param>
        /// <param name="args">The <see cref="PropertyChangedEventArgs"/> instance containing the event data.</param>
        public ItemPropertyChangedEventArgs(int index, PropertyChangedEventArgs args) : this(index, args.PropertyName)
        { }
    }
}

Тесты NUnit

Таким образом, вы можете проверить изменения, которые вы можете внести (и посмотреть, что я тестировал в первую очередь!), Я также включил мой тестовый класс NUnit. Очевидно, что следующий код не нужен просто для использования FullyObservableCollection<T> в вашем проекте.

NB Тестовый класс использует BindableBase от ПРИЗМЫ реализовать INotifyPropertyChanged, Нет зависимости от ПРИЗМЫ от основного кода.

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

Это использует вышеупомянутые идеи, но делает его производной "более чувствительной" коллекцией:

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

ObservableCollection не будет распространять изменения отдельных элементов как события CollectionChanged. Вам нужно будет либо подписаться на каждое событие и переслать его вручную, либо вы можете проверить класс BindingList[T], который сделает это за вас.

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

Вы можете просто реализовать INotifyPropertyChange в своем классе Model. И тогда, когда мы обновим некоторые свойства в элементе коллекции, он автоматически обновит пользовательский интерфейс.

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

и тогда

public ObservableCollection<Model> {get; set;}

В моем случае я использовал ListView для Bind для этой коллекции и в ItemTemplate установил свойство Binding to Model, и оно работало хорошо.

Вот небольшой фрагмент

Windows XAML:

<Window.DataContext>
    <local:ViewModel/>
</Window.DataContext>
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
    </Grid.RowDefinitions>
    <ListView 
        Margin="10"
        BorderBrush="Black"
        HorizontalAlignment="Center"
        SelectedItem="{Binding SelectedPerson}"
        ItemsSource="{Binding Persons}">
        <ListView.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <Label Content="{Binding Name}"/>
                    <Label Content="-"/>
                    <Label Content="{Binding Age}"/>
                </StackPanel>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
    <Grid 
        Grid.Row="1"
        VerticalAlignment="Center"
        HorizontalAlignment="Center">
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Label 
            VerticalAlignment="Center"
            Content="Name:"/>
        <TextBox
            Text="{Binding SelectedPerson.Name,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
            Margin="10"
            Grid.Column="1" 
            Width="100"/>
        <Label 
            VerticalAlignment="Center"
            Grid.Row="1"
            Content="Age:"/>
        <TextBox
            Text="{Binding SelectedPerson.Age,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
            Margin="10"
            Grid.Row="1"
            Grid.Column="1" 
            Width="100"/>


    </Grid>
</Grid>

Пример кода модели:

public class PersonModel:INotifyPropertyChanged
{
    public string Name
    {
        get => _name;
        set
        {
            _name = value;
            OnPropertyChanged();
        }
    }

    public int Age
    {
        get => _age;
        set
        {
            _age = value;
            OnPropertyChanged();
        }
    }

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

И реализация ViewModel:

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

    public ObservableCollection<PersonModel> Persons { get;}

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

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

    private PersonModel _selectedPerson;
}

Добавлено в 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://stackru.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
}

Просто добавляю мои 2 цента по этой теме. Чувствовал, что TrulyObservableCollection требует двух других конструкторов, как найдено с ObservableCollection:

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

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

        HookupCollectionChangedEvent();
    }

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

        HookupCollectionChangedEvent();
    }

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

Я использовал ответ Джека Кениона для реализации своего собственного OC, но я хотел бы указать на одно изменение, которое я должен был сделать, чтобы заставить его работать. Вместо:

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

Я использовал это:

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

Кажется, что "e.NewItems" выдает ноль, если действие.Remove.

Я знаю, что я опоздал на эту вечеринку, но, может быть, это кому-то поможет..

Здесь вы можете найти мою реализацию ObservableCollectionEx. У него есть некоторые особенности:

  • он поддерживает все из ObservableCollection
  • это потокобезопасный
  • он поддерживает событие ItemPropertyChanged (возникает каждый раз, когда срабатывает элемент Item.PropertyChanged)
  • он поддерживает фильтры (так что вы можете создать ObservableCollectionEx, передать в него другую коллекцию в качестве источника и фильтр с простым предикатом. Очень полезен в WPF, я часто использую эту функцию в своих приложениях). Более того - фильтр отслеживает изменения элементов через интерфейс INotifyPropertyChanged.

Конечно, любые комментарии приветствуются;)

Простое решение для стандартной наблюдаемой коллекции, которое я использовал:

НЕ ДОБАВЛЯЙТЕ в свою собственность ИЛИ ИЗМЕНЯЙТЕ ее внутренние предметы ПРЯМО, вместо этого создайте некоторую временную коллекцию, подобную этой

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

и добавлять элементы или вносить изменения в tmpList,

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

затем передайте его вашей фактической собственности по назначению.

ContentList=tmpList;

это изменит целое свойство, которое вызывает уведомление INotifyPropertyChanged, как вам нужно.

Для запуска OnChange в списке наблюдаемых коллекций

  1. Получить индекс выбранного элемента
  2. Удалить элемент из родительского
  3. Добавить элемент с тем же индексом в родительский

Пример:

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

Вот моя версия реализации. Он проверяет и выдает ошибку, если объекты в списке не реализуют INotifyPropertyChanged, поэтому не могут забыть эту проблему при разработке. Снаружи вы используете событие ListItemChanged, чтобы определить, изменился ли список или сам элемент списка.

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


}

Я вижу здесь большинство примеров, налагающих ограничение INotifyPropertyChanged на общий тип, которое заставляет модель реализовывать INotifyPropertyChanged.

Если вы следуете примерам, которые накладывают ограничения INotifyPropertyChanged на модель, это так же хорошо, как реализация INotifyPropertyChanged в вашей модели и позволяет ObservableCollection обрабатывать изменение свойства Update.

Но если вы не хотите, чтобы ваша модель реализовывала INotifyPropertyChanged, вы можете попробовать это.

CustomObservableCollection

       public class CustomObservableCollection<T> : ObservableCollection<T>
 {

      public void Refresh(T item)
      {
          var index = IndexOf(item);

          RemoveAt(index);
          Insert(index, item);

          OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, IndexOf(item)));
      }
 }

Модель

      public class Person
{
    public string FirstName { get; set; }   
    public string LastName { get; set; }   
    public int Age { get; set; }   
}

ViewModel

       public class PersonViewModel
 {       

     public PersonViewModel(){

          People=new CustomObservableCollection<Person>();

     }
    
     private void AddPerson(){

        People.Add(new Person(){
            FirstName="John",
            LastName="Doe",
            Age=20
        });

      }
    
      private void UpdatePerson(){

         var person=People.Where(...).FirstOrDefault();
         person.Age=25;

         People.Refresh(person);
      }

      public CustomObservableCollection<Person> People { get; set; } 
    
    }

Я пробую это решение, но оно работает только для меня, как RaisePropertyChange("SourceGroupeGridView"), когда коллекция изменена, которая запускается для каждого добавления или изменения элемента.

Проблема в:

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

NotifyCollectionChangedAction.Reset это действие делает полное повторное связывание всех элементов в groupedgrid, эквивалентно в RaisePropertyChanged. При его использовании все группы gridview обновляются.

Если вам нужно только обновить в пользовательском интерфейсе группу нового элемента, вы не используете действие "Сброс", вам нужно будет смоделировать действие "Добавить" в itemproperty примерно так:

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

Извините за мой английский, и спасибо за базовый код:), я надеюсь, что это поможет кому-то ^ _ ^

Enjoi!!

Вместо ObservableCollection или TrulyObservableCollection рассмотрите возможность использования BindingList и вызова метода ResetBindings.

Например:

private BindingList<TfsFile> _tfsFiles;

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

Учитывая событие, такое как щелчок, ваш код будет выглядеть так:

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

Моя модель выглядела так:

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

        public string FullPath { get; set; }

        public string Name { get; set; }

        public string Text { get; set; }

    }
}

Вот метод расширения для вышеуказанного решения...

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

Вы также можете использовать этот метод расширения, чтобы легко зарегистрировать обработчик для изменения свойства элемента в соответствующих коллекциях. Этот метод автоматически добавляется во все коллекции, реализующие INotifyCollectionChanged, которые содержат элементы, реализующие INotifyPropertyChanged:

public static class ObservableCollectionEx
{
    public static void SetOnCollectionItemPropertyChanged<T>(this T _this, PropertyChangedEventHandler handler)
        where T : INotifyCollectionChanged, ICollection<INotifyPropertyChanged> 
    {
        _this.CollectionChanged += (sender,e)=> {
            if (e.NewItems != null)
            {
                foreach (Object item in e.NewItems)
                {
                    ((INotifyPropertyChanged)item).PropertyChanged += handler;
                }
            }
            if (e.OldItems != null)
            {
                foreach (Object item in e.OldItems)
                {
                    ((INotifyPropertyChanged)item).PropertyChanged -= handler;
                }
            }
        };
    }
}

Как пользоваться:

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

Имея ту же проблему, я решил опубликовать свое решение, используя класс, производный от ObservableCollection. Он не особо добавляет ничего к аналогичным реализациям, описанным выше, но использует PropertyChangedEventManager, который имеет два преимущества: 1. Он использует слабые события, поэтому устраняются проблемы, связанные с отсутствием отсоединения событий и возникающими в результате утечками памяти. 2. он позволяет указать определенные свойства, которые при изменении будут вызывать событие CollectionChangedEvent.

Кроме того, для таких, как @Martin Harris, которых смущает поведение ObservableCollection, могу ли я порекомендовать эту отличную статью .

      /// <summary>
/// Implements an ObservableCollection that raises a CollectionChanged (Reset) event if an item in the collection raises PropertyChanged
/// The property name or names mey be optionally specified.
/// Note, could have performance issues if used on a large collection.
/// </summary>
/// <typeparam name="T"></typeparam>
public sealed class ObservableCollectionResetOnItemChange<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
    public IEnumerable<string> PropertyNames { get; private set; }

    public ObservableCollectionResetOnItemChange(IEnumerable<string> propertyNames = null)
    {
        PropertyNames = propertyNames?? new List<string>();
        CollectionChanged += OnCollectionChanged;
    }

    public ObservableCollectionResetOnItemChange(string propertyName = null) :
        this(propertyName is null ? null : new List<string>() { propertyName } )
    {
    }

    public ObservableCollectionResetOnItemChange(IEnumerable<T> items, IEnumerable<string> propertyNames = null) :
        this(propertyNames)
    {
        foreach (T item in items)
        {
            {
                Add(item);
            }
        }
    }

    public ObservableCollectionResetOnItemChange(IEnumerable<T> items, string propertyName = null) :
        this(items, propertyName is null ? null : new List<string>() { propertyName })
    {
    }

    private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.NewItems != null)
        {
            foreach (T item in e.NewItems)
            {
                if (PropertyNames.Any())
                {
                    foreach (string name in PropertyNames)
                    {
                        PropertyChangedEventManager.AddHandler(item, ItemPropertyChanged, name);
                    }
                }
                else
                {
                    PropertyChangedEventManager.AddHandler(item, ItemPropertyChanged, string.Empty);
                }
            }
        }
    }

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

Мне помогает такой трюк - удалить и вставить вместо элемента, при этом "Изменить" - события встают нормально.

      private ObservableCollection<CollectionItem> collection = new ObservableCollection<CollectionItem>();

public void Update(CollectionItem newItem, CollectionItem old ) {
            int j = collection.IndexOf(old);
            collection.RemoveAt(j); 
            collection.Insert(j, newComplexCondition);
        }

Простое решение в 2 строки кода. Просто используйте конструктор копирования. Нет необходимости писать TrulyObservableCollection и т. Д.

Пример:

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

Еще один метод без конструктора копирования. Вы можете использовать сериализацию.

        speakers.list[0].Status = "offline";
        //speakers.list[0] = new Speaker(speakers.list[0]);
        var tmp  = JsonConvert.SerializeObject(speakers.list[0]);
        var tmp2 = JsonConvert.DeserializeObject<Speaker>(tmp);
        speakers.list[0] = tmp2;
Другие вопросы по тегам