I've got something like this in a TreeView:
<DataTemplate x:Key="myTemplate">
<StackPanel MouseDown="OnItemMouseDown">
...
</StackPanel>
</DataTemplate>
Using this I get the mouse down events if I click on items in the stack panel. However... there seems to be another item behind the stack panel that is the TreeViewItem - it's very hard to hit, but not impossible, and that's when the problems start to occur.
I had a go at handling PreviewMouseDown on TreeViewItem, however that seems to require
e.Handled = false
otherwise standard tree view behaviour stops working.
Ok, Here's the source code...
MainWindow.xaml
<Window x:Class="WPFMultiSelectTree.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WPFMultiSelectTree"
Title="Multiple Selection Tree" Height="300" Width="300">
<Window.Resources>
<!-- Declare the classes that convert bool to Visibility -->
<local:VisibilityConverter x:Key="visibilityConverter"/>
<local:VisibilityInverter x:Key="visibilityInverter"/>
<!-- Set the style for any tree view item -->
<Style TargetType="TreeViewItem">
<Style.Triggers>
<DataTrigger Binding="{Binding Selected}" Value="True">
<Setter Property="Background" Value="DarkBlue"/>
<Setter Property="Foreground" Value="White"/>
</DataTrigger>
</Style.Triggers>
<EventSetter Event="PreviewMouseDown" Handler="OnTreePreviewMouseDown"/>
</Style>
<!-- Declare a hierarchical data template for the tree view items -->
<HierarchicalDataTemplate
x:Key="RecursiveTemplate"
ItemsSource="{Binding Children}">
<StackPanel Margin="2" Orientation="Horizontal" MouseDown="OnTreeMouseDown">
<Ellipse Width="12" Height="12" Fill="Green"/>
<TextBlock
Margin="2"
Text="{Binding Name}"
Visibility="{Binding Editing, Converter={StaticResource visibilityInverter}}"/>
<TextBox
Margin="2"
Text="{Binding Name}"
KeyDown="OnTextBoxKeyDown"
IsVisibleChanged="OnTextBoxIsVisibleChanged"
Visibility="{Binding Editing, Converter={StaticResource visibilityConverter}}"/>
<TextBlock
Margin="2"
Text="{Binding Index, StringFormat=({0})}"/>
</StackPanel>
</HierarchicalDataTemplate>
<!-- Declare a simple template for a list box -->
<DataTemplate x:Key="ListTemplate">
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</Window.Resources>
<Grid>
<!-- Declare the rows in this grid -->
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<!-- The first header -->
<TextBlock Grid.Row="0" Margin="5" Background="PowderBlue">Multiple selection tree view</TextBlock>
<!-- The tree view -->
<TreeView
Name="m_tree"
Margin="2"
Grid.Row="1"
ItemsSource="{Binding Children}"
ItemTemplate="{StaticResource RecursiveTemplate}"/>
<!-- The second header -->
<TextBlock Grid.Row="2" Margin="5" Background="PowderBlue">The currently selected items in the tree</TextBlock>
<!-- The list box -->
<ListBox
Name="m_list"
Margin="2"
Grid.Row="3"
ItemsSource="{Binding .}"
ItemTemplate="{StaticResource ListTemplate}"/>
</Grid>
</Window>
MainWindow.xaml.cs
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private Container m_root;
private Container m_first;
private ObservableCollection<Container> m_selection;
private string m_current;
/// <summary>
/// Constructor
/// </summary>
public MainWindow()
{
InitializeComponent();
m_selection = new ObservableCollection<Container>();
m_root = new Container("root");
for (int parents = 0; parents < 50; parents++)
{
Container parent = new Container(String.Format("parent{0}", parents + 1));
for (int children = 0; children < 1000; children++)
{
parent.Add(new Container(String.Format("child{0}", children + 1)));
}
m_root.Add(parent);
}
m_tree.DataContext = m_root;
m_list.DataContext = m_selection;
m_first = null;
}
/// <summary>
/// Has the shift key been pressed?
/// </summary>
private bool ShiftPressed
{
get
{
return Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift);
}
}
/// <summary>
/// Has the control key been pressed?
/// </summary>
private bool CtrlPressed
{
get
{
return Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl);
}
}
/// <summary>
/// Clear down the selection list
/// </summary>
private void DeselectAndClear()
{
foreach(Container container in m_selection)
{
container.Selected = false;
}
m_selection.Clear();
}
/// <summary>
/// Add the container to the list (if not already present),
/// mark as selected
/// </summary>
/// <param name="container"></param>
private void AddToSelection(Container container)
{
if (container == null)
{
return;
}
foreach (Container child in m_selection)
{
if (child == container)
{
return;
}
}
container.Selected = true;
m_selection.Add(container);
}
/// <summary>
/// Remove container from list, mark as not selected
/// </summary>
/// <param name="container"></param>
private void RemoveFromSelection(Container container)
{
m_selection.Remove(container);
container.Selected = false;
}
/// <summary>
/// Process single click on a tree item
///
/// Normally just select an item
///
/// SHIFT-Click extends selection
/// CTRL-Click toggles a selection
/// </summary>
/// <param name="sender"></param>
private void OnTreeSingleClick(object sender)
{
FrameworkElement element = sender as FrameworkElement;
if (element != null)
{
Container container = element.DataContext as Container;
if (container != null)
{
if (CtrlPressed)
{
if (container.Selected)
{
RemoveFromSelection(container);
}
else
{
AddToSelection(container);
}
}
else if (ShiftPressed)
{
if (container.Parent == m_first.Parent)
{
if (container.Index < m_first.Index)
{
Container item = container;
for (int i = container.Index; i < m_first.Index; i++)
{
AddToSelection(item);
item = item.Next;
if (item == null)
{
break;
}
}
}
else if (container.Index > m_first.Index)
{
Container item = m_first;
for (int i = m_first.Index; i <= container.Index; i++)
{
AddToSelection(item);
item = item.Next;
if (item == null)
{
break;
}
}
}
}
}
else
{
DeselectAndClear();
m_first = container;
AddToSelection(container);
}
}
}
}
/// <summary>
/// Process double click on tree item
/// </summary>
/// <param name="sender"></param>
private void OnTreeDoubleClick(object sender)
{
FrameworkElement element = sender as FrameworkElement;
if (element != null)
{
Container container = element.DataContext as Container;
if (container != null)
{
container.Editing = true;
m_current = container.Name;
}
}
}
/// <summary>
/// Clicked on the stack panel in the tree view
///
/// Double left click:
///
/// Switch to editing mode (flips visibility of textblock and textbox)
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnTreeMouseDown(object sender, MouseButtonEventArgs e)
{
Debug.WriteLine("StackPanel mouse down");
switch(e.ChangedButton)
{
case MouseButton.Left:
switch (e.ClickCount)
{
case 2:
OnTreeDoubleClick(sender);
e.Handled = true;
break;
}
break;
}
}
/// <summary>
/// Clicked on tree view item in tree
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnTreePreviewMouseDown(object sender, MouseButtonEventArgs e)
{
Debug.WriteLine("TreeViewItem preview mouse down");
switch (e.ChangedButton)
{
case MouseButton.Left:
switch (e.ClickCount)
{
case 1:
{
// We've had a single click on a tree view item
// Unfortunately this is the WHOLE tree item, including the +/-
// symbol to the left. The tree doesn't do a selection, so we
// have to filter this out...
MouseDevice device = e.Device as MouseDevice;
Debug.WriteLine(String.Format("Tree item clicked on: {0}", device.DirectlyOver.GetType().ToString()));
// This is bad. The whole point of WPF is for the code
// not to know what the UI has - yet here we are testing for
// it as a workaround. Sigh...
if (device.DirectlyOver.GetType() != typeof(Path))
{
OnTreeSingleClick(sender);
}
// Cannot say handled - if we do it stops the tree working!
//e.Handled = true;
}
break;
}
break;
}
}
/// <summary>
/// Key press in text box
///
/// Return key finishes editing
/// Escape key finishes editing, restores original value (this doesn't work!)
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnTextBoxKeyDown(object sender, KeyEventArgs e)
{
switch(e.Key)
{
case Key.Return:
{
TextBox box = sender as TextBox;
if (box != null)
{
Container container = box.DataContext as Container;
if (container != null)
{
container.Editing = false;
e.Handled = true;
}
}
}
break;
case Key.Escape:
{
TextBox box = sender as TextBox;
if (box != null)
{
Container container = box.DataContext as Container;
if (container != null)
{
container.Editing = false;
container.Name = m_current;
e.Handled = true;
}
}
}
break;
}
}
/// <summary>
/// When text box becomes visible, grab focus and select all text in it.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnTextBoxIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
bool visible = (bool)e.NewValue;
if (visible)
{
TextBox box = sender as TextBox;
if (box != null)
{
box.Focus();
box.SelectAll();
}
}
}
}
Here's the Container class
public class Container : INotifyPropertyChanged
{
private string m_name;
private ObservableCollection<Container> m_children;
private Container m_parent;
private bool m_selected;
private bool m_editing;
/// <summary>
/// Constructor
/// </summary>
/// <param name="name">name of object</param>
public Container(string name)
{
m_name = name;
m_children = new ObservableCollection<Container>();
m_parent = null;
m_selected = false;
m_editing = false;
}
/// <summary>
/// Name of object
/// </summary>
public string Name
{
get
{
return m_name;
}
set
{
if (m_name != value)
{
m_name = value;
OnPropertyChanged("Name");
}
}
}
/// <summary>
/// Index of object in parent's children
///
/// If there's no parent, the index is -1
/// </summary>
public int Index
{
get
{
if (m_parent != null)
{
return m_parent.Children.IndexOf(this);
}
return -1;
}
}
/// <summary>
/// Get the next item, assuming this is parented
///
/// Returns null if end of list reached, or no parent
/// </summary>
public Container Next
{
get
{
if (m_parent != null)
{
int index = Index + 1;
if (index < m_parent.Children.Count)
{
return m_parent.Children[index];
}
}
return null;
}
}
/// <summary>
/// List of children
/// </summary>
public ObservableCollection<Container> Children
{
get
{
return m_children;
}
}
/// <summary>
/// Selected status
/// </summary>
public bool Selected
{
get
{
return m_selected;
}
set
{
if (m_selected != value)
{
m_selected = value;
OnPropertyChanged("Selected");
}
}
}
/// <summary>
/// Editing status
/// </summary>
public bool Editing
{
get
{
return m_editing;
}
set
{
if (m_editing != value)
{
m_editing = value;
OnPropertyChanged("Editing");
}
}
}
/// <summary>
/// Parent of this object
/// </summary>
public Container Parent
{
get
{
return m_parent;
}
set
{
m_parent = value;
}
}
/// <summary>
/// WPF Property Changed event
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Handler to inform WPF that a property has changed
/// </summary>
/// <param name="name"></param>
private void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
/// <summary>
/// Add a child to this container
/// </summary>
/// <param name="child"></param>
public void Add(Container child)
{
m_children.Add(child);
child.m_parent = this;
}
/// <summary>
/// Remove a child from this container
/// </summary>
/// <param name="child"></param>
public void Remove(Container child)
{
m_children.Remove(child);
child.m_parent = null;
}
}
The two classes VisibilityConverter and VisibilityInverter are implementations of IValueConverter that translates bool to Visibility. They make sure the TextBlock is displayed when not editing, and the TextBox is displayed when editing.