Home > Software engineering >  Notify ObservableCollection changed with nested objects
Notify ObservableCollection changed with nested objects

Time:10-13

I have a class FamilyItemVM which is used to bind to a TreeView source. This class is used in a main view model. I would like to know when the FamilyItemVM has changed (i.e. add or remove children in the UI).

Main VM :

public class FamilyVM : ObservableRecipient
{
    private ObservableCollection<FamilyItemVM> myFamilies;
    
    public FamilyVM()
    {
        myFamilies = new ObservableCollection<FamilyItemVM>();
        Families.CollectionChanged  = FamilyCollectionChanged;
        BuildTree();
    }

    public ObservableCollection<FamilyItemVM> Families // the property binded to the Treeview
    {
        get { return myFamilies; }
    }

    private void BuildTree()
    {
        //... the method which populate myFamilies property recursively
    }

    private void FamilyCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        //Here I want to know when something has changed (only add and remove items) in the whole object 
    }
}

Items Class:

public class FamilyItemVM : ObservableObject
{
    FamilyItemVM myParent;

    public FamilyItemVM(FamilyItemVM parent)
    {
        Items = new ObservableCollection<FamilyItemVM>();
        myParent = parent;
    }

    public ObservableCollection<FamilyItemVM> Items { get; set; }
}

Now, when an item is added or removed in the UI, how to notify each parent to finally notify the main VM in FamilyCollectionChanged method.

CodePudding user response:

In your current design, each FamilyItemVM only knows its parent FamilyItemVM. Therefore changes need to be notified upwards your family item tree. This can be done with an event that is raised whenever an Items collection changes. In fact, this is exactly what an ObservableCollection does with its CollectionChanged event. You can reuse the same parameter type, too, which is NotifyCollectionChangedEventArgs. The difference is that we need to propagate the event all the way up to the root item.

The following apprach exposes an event DescendantsChanged, which notifies that any Items collection anywhere down the hierarchy changed. Each FamilyItemVM listens for its own Items collection changes by handling the CollectionChanged event. If any FamilyItemVM is added, a handler for its own DescendantsChanged event is added. This handler simply reacts to any descendant changes and raises the DescendantsChanged of its own instance, the parent of the current FamilyItemVM, to notify its own parent.

Let us review what happens at this point using this mechnism:

  1. Any FamilyItemVM observes its Items collection.
  2. An item is added, which triggers the CollectionChanged event.
  3. A handler for the DescendantsChanged event is added to the item.
  4. The DescendantsChanged event on the current instance (parent of the new item, this) is raised.
  5. The parent of the parent which listens for the DescendantsChanged event (added in 2.) gets its handler invoked, reacts to it and raises its DescendantsChanged event the same way to notify its parent (see 4.).
  6. This goes on, until there is no parent anymore, _myParent is null.

The last point with null is an assumption from the code you provided, I will explain later.

public class FamilyItemVM : ObservableObject
{
   private readonly FamilyItemVM _myParent;

   public FamilyItemVM(FamilyItemVM parent)
   {
      _myParent = parent;
      Items = new ObservableCollection<FamilyItemVM>();
      Items.CollectionChanged  = OnItemsChanged;

      // If the Items collection can be reassigned (has a setter), remove this and the child handlers again.
      // Otherwise there may be a memory leak. This must be triggered from the setter.
   }

   public ObservableCollection<FamilyItemVM> Items { get; set; }

   // An event to notify that ANY of the descendent Items collections changed.
   public event NotifyCollectionChangedEventHandler DescendantsChanged;

   private void OnItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
   {
      // Add the handler to any new VM that is added.
      if (e.Action == NotifyCollectionChangedAction.Add)
      {
         foreach (var newItem in e.NewItems)
         {
            var descendantFamilyItemVm = (FamilyItemVM)newItem;
            descendantFamilyItemVm.DescendantsChanged  = OnDescendantsChanged;
         }
      }

      // Remove the handler if a child is removed to prevent a memory leak.
      if (e.Action == NotifyCollectionChangedAction.Remove)
      {
         foreach (var oldItem in e.OldItems)
         {
            var descendantFamilyItemVm = (FamilyItemVM)oldItem;
            descendantFamilyItemVm.DescendantsChanged -= OnDescendantsChanged;
         }
      }

      // You should handle other collection changed actions, too, e.g. Reset.
      // ...

      // React to changes of this Items collection.
      // ...

      // Notify that descendants have changed.
      DescendantsChanged?.Invoke(this, e);
   }

   private void OnDescendantsChanged(object sender, NotifyCollectionChangedEventArgs e)
   {
      // React to any of the descendants having their Items collection changed.
      // ...
      var familyItemVm = (FamilyItemVM)sender;

      // Notify parent that any of its descendants changed its Items collection.
      // The sender here is the descendant that originally raised the event.
      DescendantsChanged?.Invoke(sender, e);
   }
}

Now, from your code, I assume that the topmost items that are added to myFamilies in the FamilyVM have null passed as parent in the constructor, because you cannot pass FamilyVM. This means, that the FamilyVM is not notified.

To solve this issue, you can simply add your FamilyCollectionChanged method as handler to the DescendantsChanged event when building the tree in FamilyVM. But be aware that you have to add the handlers to the root items before adding further children, otherwise you will not get notified at this point.

private void BuildTree()
{
   // ...code to build the root items.

   foreach (var familyItemVm in myFamilies)
      familyItemVm.DescendantsChanged  = FamilyCollectionChanged;
      
   // ...even more code to add the the children.
}
  • Related