With the
WPF TreeView it is not possible to select multiple items. But it can be easily extended. Therefore an
Attached Property can be used. First we are adding the used namespace to the control.
xmlns:e="clr-namespace:MultipleSelectionTreeView;
assembly=MultipleSelectionTreeView"
Then we can add the
Attached Property to the
TreeView. With IsMultipleSelection="True" we activate the multiple selection in the
TreeView. With the property SelectedItems we can
bind the multiple selected items to a property of the
DataContext. We can define the style of a multiple selected
TreeViewItem within a
Style Trigger. Therefore the property IsItemSelected is used.
<TreeView ItemsSource="{Binding Elements}"
MinHeight="20"
e:TreeViewMultipleSelectionAttached.IsMultipleSelection="True"
e:TreeViewMultipleSelectionAttached.SelectedItems=
"{Binding SelectedElements}">
<TreeView.Resources>
<Style x:Key="{x:Type TreeViewItem}" BasedOn="{StaticResource
{x:Type TreeViewItem}}"
TargetType=
"{x:Type TreeViewItem}">
<Style.Triggers>
<Trigger
Property="e:TreeViewMultipleSelectionAttached.IsItemSelected"
Value="True">
<Setter Property="Background"
Value="LightGreen" />
</Trigger>
</Style.Triggers>
</Style>
<HierarchicalDataTemplate DataType="{x:Type e:Element}"
ItemsSource="{Binding ChildElements}">
<StackPanel Orientation="Horizontal"
VerticalAlignment="Stretch"
Margin="0,2,0,2">
<TextBlock Text="{Binding ElementName}"
Margin="5,0,0,0"
VerticalAlignment="Center" />
</StackPanel>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type e:ChildElement}">
<StackPanel Orientation="Horizontal"
VerticalAlignment="Stretch"
Margin="0,2,0,2">
<TextBlock Text="{Binding ElementName}"
VerticalAlignment="Center"
Margin="5,0,0,0" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
Now have a look at the implementation of the
Attached Property. To create a custom
Attached Property a static
DependencyProperty is defined by using the RegisterAttached method. This method has the possibility to set a
PropertyMetadata as parameter in that a PropertyChangedCallback can be defined by using
Delegates. In the callback method an
event handler is added or removed to
DependencyObject. First we define the IsMultipleSelection property. With that a
TreeView is marked in
XAML to have multiple selection abilities.
public static readonly DependencyProperty IsMultipleSelectionProperty =
DependencyProperty.RegisterAttached(
"IsMultipleSelection",
typeof(Boolean),
typeof(TreeViewMultipleSelectionAttached),
new PropertyMetadata(false, OnMultipleSelectionPropertyChanged));
public static bool GetIsMultipleSelection(TreeView element)
{
return (bool)element.GetValue(IsMultipleSelectionProperty);
}
public static void SetIsMultipleSelection(TreeView element, Boolean value)
{
element.SetValue(IsMultipleSelectionProperty, value);
}
In the RegisterAttached method we have defined a
PropertyMetadata object that defines a call back method (OnMultipleSelectionPropertyChanged) that is called if the property has changed. In that method a handler for the MouseLeftButtonDownEvent of
TreeViewItem is added or removed to the
TreeView. Remember to mark the handledEventsToo parameter in the AddHandler method as true to allow calling the handler OnTreeViewItemClicked correctly.
private static void OnMultipleSelectionPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
TreeView treeView = d as TreeView;
if (treeView != null)
{
if (e.NewValue is bool)
{
if ((bool)e.NewValue)
{
treeView.AddHandler(TreeViewItem.MouseLeftButtonDownEvent,
new MouseButtonEventHandler(OnTreeViewItemClicked), true);
}
else
{
treeView.RemoveHandler(TreeViewItem.MouseLeftButtonDownEvent,
new MouseButtonEventHandler(OnTreeViewItemClicked));
}
}
}
}
The OnTreeViewItemClicked method is now called on each click at a
TreeViewItem if IsMultipleSelection for a
TreeView is set to true.
private static void OnTreeViewItemClicked(object sender, MouseButtonEventArgs e)
{
TreeViewItem treeViewItem = FindTreeViewItem(
e.OriginalSource as DependencyObject);
TreeView treeView = sender as TreeView;
if (treeViewItem != null && treeView != null)
{
if (Keyboard.Modifiers == ModifierKeys.Control)
{
SelectMultipleItemsRandomly(treeView, treeViewItem);
}
else if (Keyboard.Modifiers == ModifierKeys.Shift)
{
SelectMultipleItemsContinuously(treeView, treeViewItem);
}
else
{
SelectSingleItem(treeView, treeViewItem);
}
}
}
First we need to find the
TreeViewItem that is invoked with the click. This happens with the recursive FindTreeViewItem method.
private static TreeViewItem FindTreeViewItem(DependencyObject dependencyObject)
{
if (dependencyObject == null)
{
return null;
}
TreeViewItem treeViewItem = dependencyObject as TreeViewItem;
if (treeViewItem != null)
{
return treeViewItem;
}
return FindTreeViewItem(VisualTreeHelper.GetParent(dependencyObject));
}
Then the OnTreeViewItemClicked method checks what kind of click is happening. There are
Left-Mouse-Button (LMB) + Ctrl, LMB + Shift, and all other cases with
LMB. In the third case a single click is performed.
private static void SelectSingleItem(TreeView treeView,
TreeViewItem treeViewItem)
{
// first deselect all items
DeSelectAllItems(treeView, null);
SetIsItemSelected(treeViewItem, true);
SetStartItem(treeView, treeViewItem);
}
Therefore at first all items are deselected by calling the recursive method DeSelectAllItems.
private static void DeSelectAllItems(TreeView treeView,
TreeViewItem treeViewItem)
{
if (treeView != null)
{
for (int i = 0; i < treeView.Items.Count; i++)
{
TreeViewItem item = treeView.ItemContainerGenerator.
ContainerFromIndex(i) as TreeViewItem;
if (item != null)
{
SetIsItemSelected(item, false);
DeSelectAllItems(null, item);
}
}
}
else
{
for (int i = 0; i < treeViewItem.Items.Count; i++)
{
TreeViewItem item = treeViewItem.ItemContainerGenerator.
ContainerFromIndex(i) as TreeViewItem;
if (item != null)
{
SetIsItemSelected(item, false);
DeSelectAllItems(null, item);
}
}
}
}
Selection and deselection is happening by the
DependencyProperty IsItemSelected that we have also defined. This property is used in
XAML to apply a certain style on the selected TreeViewItems.
public static readonly DependencyProperty IsItemSelectedProperty =
DependencyProperty.RegisterAttached(
"IsItemSelected",
typeof(Boolean),
typeof(TreeViewMultipleSelectionAttached),
new PropertyMetadata(false, OnIsItemSelectedPropertyChanged));
public static bool GetIsItemSelected(TreeViewItem element)
{
return (bool)element.GetValue(IsItemSelectedProperty);
}
public static void SetIsItemSelected(TreeViewItem element, Boolean value)
{
element.SetValue(IsItemSelectedProperty, value);
}
In the RegisterAttached method we have defined a
PropertyMetadata object
that defines a call back method (OnIsItemSelectedPropertyChanged)
that is called if the property (selection of the
TreeViewItem) has changed. In that method the Header of
TreeViewItem will be added to or removed to a List of all selected TreeViewItems. This List is associated with the corresponding
TreeView.
private static void OnIsItemSelectedPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
TreeViewItem treeViewItem = d as TreeViewItem;
TreeView treeView = FindTreeView(treeViewItem);
if (treeViewItem != null && treeView != null)
{
var selectedItems = GetSelectedItems(treeView);
if (selectedItems != null)
{
if (GetIsItemSelected(treeViewItem))
{
selectedItems.Add(treeViewItem.Header);
}
else
{
selectedItems.Remove(treeViewItem.Header);
}
}
}
}
To find the
TreeView corresponding to the
TreeViewItem the recursive method FindTreeView is used.
private static TreeView FindTreeView(DependencyObject dependencyObject)
{
if (dependencyObject == null)
{
return null;
}
TreeView treeView = dependencyObject as TreeView;
if (treeView != null)
{
return treeView;
}
return FindTreeView(VisualTreeHelper.GetParent(dependencyObject));
}
To associate the List of all selected TreeViewItems with the corresponding
TreeView the
DependencyProperty SelectedItems is used. This property can be used in
XAML to bind the multiple selected items to a property of the
DataContext.
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.RegisterAttached(
"SelectedItems",
typeof(IList),
typeof(TreeViewMultipleSelectionAttached),
new PropertyMetadata());
public static IList GetSelectedItems(TreeView element)
{
return (IList)element.GetValue(SelectedItemsProperty);
}
public static void SetSelectedItems(TreeView element, IList value)
{
element.SetValue(SelectedItemsProperty, value);
}
Now let's go back to the SelectSingleItem method. After deselecting all TreeViewItems by setting the IsItemSelected
DependencyProperty to false, the clicked
TreeViewItem is marked as selected by setting the IsItemSelected
DependencyProperty to true. After that the
TreeViewItem is marked as StartItem within the corresponding
TreeView. Therefore the private StartItem
DependencyProperty is used. The StartItem
DependencyProperty is used as starting point for a subsequent multiple selection where a continuous range is selected.
private static readonly DependencyProperty StartItemProperty =
DependencyProperty.RegisterAttached(
"StartItem",
typeof(TreeViewItem),
typeof(TreeViewMultipleSelectionAttached),
new PropertyMetadata());
private static TreeViewItem GetStartItem(TreeView element)
{
return (TreeViewItem)element.GetValue(StartItemProperty);
}
private static void SetStartItem(TreeView element, TreeViewItem value)
{
element.SetValue(StartItemProperty, value);
}
If a LMB + Ctrl is performed the method SelectMultipleItemsRandomly is called. In this method the IsItemSelected
DependencyProperty of the clicked
TreeViewItem is toggled. Furthermore the StartItem
DependencyProperty will be set of not already set or unset if no
TreeViewItem is selected anymore.
private static void SelectMultipleItemsRandomly(TreeView treeView,
TreeViewItem treeViewItem)
{
SetIsItemSelected(treeViewItem, !GetIsItemSelected(treeViewItem));
if (GetStartItem(treeView) == null)
{
if (GetIsItemSelected(treeViewItem))
{
SetStartItem(treeView, treeViewItem);
}
}
else
{
if (GetSelectedItems(treeView).Count == 0)
{
SetStartItem(treeView, null);
}
}
}
If a LMB + Shift is performed the method SelectMutlipleItemsContinuously is called. If no StartItem is set, no action takes place. If the selected
TreeViewItem is equal to the startItem then the SelectSingleItem method is called and then the method is returned. If both conditions not met multiple continuously TreeViewItems selection takes place.
private static void SelectMultipleItemsContinuously(TreeView treeView,
TreeViewItem treeViewItem)
{
TreeViewItem startItem = GetStartItem(treeView);
if (startItem != null)
{
if (startItem == treeViewItem)
{
SelectSingleItem(treeView, treeViewItem);
return;
}
ICollection<TreeViewItem> allItems = new List<TreeViewItem>();
GetAllItems(treeView, null, allItems);
DeSelectAllItems(treeView, null);
bool isBetween = false;
foreach (var item in allItems)
{
if (item == treeViewItem || item == startItem)
{
// toggle to true if first element is found and
// back to false if last element is found
isBetween = !isBetween;
// set boundary element
SetIsItemSelected(item, true);
continue;
}
if (isBetween)
{
SetIsItemSelected(item, true);
}
}
}
}
With the GetAllItems method all TreeViewItems of a
TreeView are recursively collected into a collection. Furthermore all items are deselected by the DeSelectAllItems method.
private static void GetAllItems(TreeView treeView, TreeViewItem treeViewItem,
ICollection<TreeViewItem> allItems)
{
if (treeView != null)
{
for (int i = 0; i < treeView.Items.Count; i++)
{
TreeViewItem item = treeView.ItemContainerGenerator.
ContainerFromIndex(i) as TreeViewItem;
if (item != null)
{
allItems.Add(item);
GetAllItems(null, item, allItems);
}
}
}
else
{
for (int i = 0; i < treeViewItem.Items.Count; i++)
{
TreeViewItem item = treeViewItem.ItemContainerGenerator.
ContainerFromIndex(i) as TreeViewItem;
if (item != null)
{
allItems.Add(item);
GetAllItems(null, item, allItems);
}
}
}
}
Then each element of the list will be checked, if it is in the range of StartItem and selected
TreeViewItem. If this is true the
TreeViewItem will be marked as selected.