ObservableCollection not noticing when Item in it changes (even with INotifyPropertyChanged)

Viewed 215656

Does anyone know why this code doesn't work:

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 containts everything for RaisePropertyChanged etc. and it's working for everything else except this problem..

21 Answers

I used Jack Kenyons answer to implement my own OC, but I'd like to point out one change i had to make to make it work. Instead of:

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

I used this:

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

It seems that the "e.NewItems" produces null if action is .Remove.

To Trigger OnChange in ObservableCollection List

  1. Get index of selected Item
  2. Remove the item from the Parent
  3. Add the item at same index in parent

Example:

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

I see most examples here placing INotifyPropertyChanged constraint on the generic type which forces the model to Implement INotifyPropertyChanged.

If you follow the examples that place INotifyPropertyChanged constraints on the model, it's as good as implementing INotifyPropertyChanged in your model and allow ObservableCollection to handle the Update property change.

But if you do not want your model to implement INotifyPropertyChanged, you can try this.

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

Model

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

You can also use this extension method to easily register a handler for item property change in relevant collections. This method is automatically added to all the collections implementing INotifyCollectionChanged that hold items that implement 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;
                }
            }
        };
    }
}

How to use:

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

For me helps this trick - removeAt and insert to replace item, during this "Change" - events rise properly.

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

Having had the same problem, I thought I'd post my solution using a class derived from ObservableCollection. It doesn't add a great deal to the similar implementations above, but it does use the PropertyChangedEventManager, which has two advantages: 1. it uses weak events so the problems of not unhooking the events and any resulting memory leaks are removed. 2. it allows the specific properties that, when changed, will trigger the CollectionChangedEvent to be specified.

In addition, for those like @Martin Harris who are confused about the behaviour of ObservableCollection, may I recommend this excellent article.

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