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:
- Any
FamilyItemVM
observes itsItems
collection. - An item is added, which triggers the
CollectionChanged
event. - A handler for the
DescendantsChanged
event is added to the item. - The
DescendantsChanged
event on the current instance (parent of the new item,this
) is raised. - The parent of the parent which listens for the
DescendantsChanged
event (added in 2.) gets its handler invoked, reacts to it and raises itsDescendantsChanged
event the same way to notify its parent (see 4.). - This goes on, until there is no parent anymore,
_myParent
isnull
.
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.
}