Reputation: 11963
To add multi select support to the native WPF tree view I had to add a custom dependency property which stores the multi selected items. This works great until the tree's Items started to change.
For example in the initial tree there is one item A. I selected it, it gets stored in MultiSelectedItems
list. Then I removed item A and added item B. (through ViewModel ObservableCollection
binding)
I need to find a way to remove item A from MultiSelectedItems
list when this happens.
I am unable to find an event for this. The closest I get is ItemContainerGenerator.ItemsChanged
event, but this event only fires for root level nodes (does not fire for its hierarchy children).
Upvotes: 4
Views: 4071
Reputation: 11963
The key idea to solve this problem is to detect items changed event within each node instead of at the tree level.
I inherited the TreeViewItem
class
public class MultiSelectTreeViewItem : TreeViewItem
{
object _originalHeader;
protected override void OnHeaderChanged(object oldHeader, object newHeader)
{
base.OnHeaderChanged(oldHeader, newHeader);
//.NET 4.5 use BindingOperations.DisconnectedSource
if (newHeader.ToString() != "{DisconnectedItem}")
_originalHeader = newHeader;
}
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
if (Header.ToString() == "{DisconnectedItem}" && _originalHeader != null && e.Action == NotifyCollectionChangedAction.Reset)
{
//Find the parent Tree View and remove this from MultiSelectedList
}
}
protected override DependencyObject GetContainerForItemOverride()
{
return new MultiSelectTreeViewItem();
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is MultiSelectTreeViewItem;
}
}
In the inherited TreeView
protected override DependencyObject GetContainerForItemOverride()
{
return new MultiSelectTreeViewItem();
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is MultiSelectTreeViewItem;
}
When the node got removed, its header will be set to a sentinel object called DisconnectedItem, and the Items Changed event will be NotifyCollectionChangedAction.Reset
.
Note that if you did a List.Clear()
the NotifyCollectionChangedAction.Remove
event will not be fire, only NotifyCollectionChangedAction.Reset
will. So I find it the most reliable way to detect node removal.
One catch is that if the node has not been rendered (parent has never been expanded) then this event will not fire.
Upvotes: 0
Reputation: 7421
Unfortunately this is a complex problem, only made more complex by the fact that there are several ways to implement a TreeView in WPF. If you use MVVM, virtualization, and HierarchicalDataTemplates
then the selected items may not even be part of the Visual or Logical trees at any given time - not to mention that even trying to watch for an individual item's removal will not be enough since any of its ancestors might be removed instead.
My suggestion is to implement a naive MultiSelection at the control level, and implement an intelligent ViewModel hierarchy:
Allow both a 'Parent' and 'Root' node to be accessed across your ViewModel hierarchy, and allow items to remove their descendants from the Root.SelecteItems
collection when their Child collection changes.
In my MVVM framework, I have a HierarchicalRootViewModelBase
and a HierarchicalViewModelBase
that I use for all hierarchy VMs. This way all of the tree functionality (like selection and collection changed events) is implemented once and handled automatically. Each base class is constructed with a reference to its parent and the Root node (or it uses recursion to find the Root).
In this way removing an Item at any hierarchy depth can easily trigger root-level actions like checking/updating the SelectedItems collection.
Upvotes: 1