WPF/MVVM - how to handle double-click on TreeViewItems in the ViewModel?
Updating my answer a bit.
I've tried alot of different approaches for this and I still feel like Attached Behaviors is the best solution. Although it might look like alot of overhead in the begining it really isn't. I keep all of my behaviors for ICommands
in the same place and whenever I need support for another event it is just a matter of copy/paste and change the event in the PropertyChangedCallback
.
I also added the optional support for CommandParameter
.
In the designer it is just a matter of selecting the desired event
You can set this either on TreeView
, TreeViewItem
or any other place that you like.
Example. Set it on the TreeView
<TreeView commandBehaviors:MouseDoubleClick.Command="{Binding YourCommand}"
commandBehaviors:MouseDoubleClick.CommandParameter="{Binding}"
.../>
Example. Set it on TreeViewItem
<TreeView ItemsSource="{Binding Projects}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="commandBehaviors:MouseDoubleClick.Command"
Value="{Binding YourCommand}"/>
<Setter Property="commandBehaviors:MouseDoubleClick.CommandParameter"
Value="{Binding}"/>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
And here is the Attached Behavior MouseDoubleClick
public class MouseDoubleClick
{
public static DependencyProperty CommandProperty =
DependencyProperty.RegisterAttached("Command",
typeof(ICommand),
typeof(MouseDoubleClick),
new UIPropertyMetadata(CommandChanged));
public static DependencyProperty CommandParameterProperty =
DependencyProperty.RegisterAttached("CommandParameter",
typeof(object),
typeof(MouseDoubleClick),
new UIPropertyMetadata(null));
public static void SetCommand(DependencyObject target, ICommand value)
{
target.SetValue(CommandProperty, value);
}
public static void SetCommandParameter(DependencyObject target, object value)
{
target.SetValue(CommandParameterProperty, value);
}
public static object GetCommandParameter(DependencyObject target)
{
return target.GetValue(CommandParameterProperty);
}
private static void CommandChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
Control control = target as Control;
if (control != null)
{
if ((e.NewValue != null) && (e.OldValue == null))
{
control.MouseDoubleClick += OnMouseDoubleClick;
}
else if ((e.NewValue == null) && (e.OldValue != null))
{
control.MouseDoubleClick -= OnMouseDoubleClick;
}
}
}
private static void OnMouseDoubleClick(object sender, RoutedEventArgs e)
{
Control control = sender as Control;
ICommand command = (ICommand)control.GetValue(CommandProperty);
object commandParameter = control.GetValue(CommandParameterProperty);
command.Execute(commandParameter);
}
}
Custom Treeview User Control MVVM Double Click Bubbling Event WPF
You don't need to publish double-click event outside from the user control at all.
You need to add some InputBinding
(MouseBinding
in this particular case) into InputBindings
collection of the TreeView.SelectedItem
.
The problem is that you can't do that in normal, obvious way - set InputBindings
via TreeView.ItemContainerStyle
, because InputBindings
collection is read-only. Sad, but true.
Good news is that you can use attached property to accomplish that.
The sample:
View models.
a) this is what will be displayed as items in tree view:
public class Node : ViewModelBase
{
public String Text
{
get { return text; }
set
{
if (text != value)
{
text = value;
OnPropertyChanged("Text");
}
}
}
private String text;
public ObservableCollection<Node> Nodes { get; set; }
}
b) this is "main" view model:
public class ViewModel : ViewModelBase
{
public ViewModel()
{
this.selectedNodeDoubleClickedCommand = new RelayCommand<Node>(node =>
{
Debug.WriteLine(String.Format("{0} clicked!", node.Text));
});
}
public ObservableCollection<Node> Nodes { get; set; }
public RelayCommand<Node> SelectedNodeDoubleClickedCommand
{
get { return selectedNodeDoubleClickedCommand; }
}
private readonly RelayCommand<Node> selectedNodeDoubleClickedCommand;
}
User control code-behind. Basic idea - we're adding one attached property to set input binding though it in XAML, and another one - to allow external world bind command, when input binding fires:
public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
}
public ICommand SelectedItemDoubleClickedCommand
{
get { return (ICommand)GetValue(SelectedItemDoubleClickedCommandProperty); }
set { SetValue(SelectedItemDoubleClickedCommandProperty, value); }
}
public static readonly DependencyProperty SelectedItemDoubleClickedCommandProperty = DependencyProperty.Register(
"SelectedItemDoubleClickedCommand", typeof(ICommand),
typeof(UserControl1),
new UIPropertyMetadata(null));
public static ICommand GetSelectedItemDoubleClickedCommandAttached(DependencyObject obj)
{
return (ICommand)obj.GetValue(SelectedItemDoubleClickedCommandAttachedProperty);
}
public static void SetSelectedItemDoubleClickedCommandAttached(DependencyObject obj, ICommand value)
{
obj.SetValue(SelectedItemDoubleClickedCommandAttachedProperty, value);
}
public static readonly DependencyProperty SelectedItemDoubleClickedCommandAttachedProperty = DependencyProperty.RegisterAttached(
"SelectedItemDoubleClickedCommandAttached",
typeof(ICommand), typeof(UserControl1),
new UIPropertyMetadata(null, SelectedItemDoubleClickedCommandAttachedChanged));
private static void SelectedItemDoubleClickedCommandAttachedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var item = d as TreeViewItem;
if (item != null)
{
if (e.NewValue != null)
{
var binding = new MouseBinding((ICommand)e.NewValue, new MouseGesture(MouseAction.LeftDoubleClick));
BindingOperations.SetBinding(binding, InputBinding.CommandParameterProperty, new Binding("SelectedItem")
{
RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(TreeView), 1)
});
item.InputBindings.Add(binding);
}
}
}
}
User control XAML:
<Grid>
<TreeView ItemsSource="{Binding Nodes}">
<TreeView.Resources>
<HierarchicalDataTemplate ItemsSource="{Binding Nodes}" DataType="{x:Type local:Node}">
<TextBlock Text="{Binding Text}"/>
</HierarchicalDataTemplate>
</TreeView.Resources>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="local:UserControl1.SelectedItemDoubleClickedCommandAttached"
Value="{Binding SelectedItemDoubleClickedCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
</Grid>
Main window XAML:
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication2"
Title="MainWindow" Height="350" Width="525">
<Grid>
<local:UserControl1 SelectedItemDoubleClickedCommand="{Binding SelectedNodeDoubleClickedCommand}"/>
</Grid>
</Window>
Main window code-behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel
{
Nodes = new ObservableCollection<Node>
{
new Node
{
Text = "Parent 1",
Nodes = new ObservableCollection<Node>
{
new Node { Text = "Child 1.1"},
new Node { Text = "Child 1.2"},
}
},
new Node
{
Text = "Parent 2",
Nodes = new ObservableCollection<Node>
{
new Node { Text = "Child 2.1"},
new Node { Text = "Child 2.2"},
}
},
}
};
}
}
How to add a MouseDoubleClick event on C# to a TreeViewItem
You could get a reference to the parent TreeViewItem
of the clicked element (e.OriginalSource
) using the VisualTreeHelper
class:
private void TreeViewItem_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
var hijo = FindParent<TreeViewItem>(e.OriginalSource as DependencyObject);
//...
}
private static T FindParent<T>(DependencyObject dependencyObject) where T : DependencyObject
{
var parent = VisualTreeHelper.GetParent(dependencyObject);
if (parent == null) return null;
var parentT = parent as T;
return parentT ?? FindParent<T>(parent);
}
wpf treeviewitem mouse double click
The issue I believe is that ItemContainerStyle
only applies to the first level of nodes (this I believe is due to the way TreeView
inherits from ItemsControl
and ItemContainerStyle
is inherited from there - where is just applies to the basic Items
of the ItemsControl
. Anyway, I'm going off on one...)
You can fix it by instead of assigning the style to ItemContainerStyle
, just move it to Resources
, like so:
<TreeView.Resources>
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource {x:Type TreeViewItem}}">
<EventSetter Event="MouseDoubleClick" Handler="OnItemMouseDoubleClick"/>
</Style>
</TreeView.Resources>
Depending on what you want, this might not work as you intend...
Because this adds a trigger to every TreeViewItem - and, unfortunately, they nest like so:
<TreeViewItem Header="Module 1">
<TreeViewItem Header="Sub Module 1"/>
</TreeViewItem>
<TreeViewItem Header="Module 2">
<TreeViewItem Header="Sub Module 1"/>
<TreeViewItem Header="Sub Module 2"/>
</TreeViewItem>
So, depending on what you're actually going to use the code for, it might be a problem that the event fires on both the outer and inner module.
The usual way of dealing with this in a ClickEvent
is to set the args.Handled
to be True
in the code, which stops the event 'bubbling' up to the higher levels. But unfortunately this doesn't work on MouseDoubleClick
events, due to the way they're triggered. (Nice one Microsoft... xD)
A possible answer instead is here: https://stackoverflow.com/a/6326181/3940783
Basically, we dispense with the MouseDoubleClick
event, and instead use the PreviewMouseLeftButtonDown
event (which Microsoft uses to trigger the MouseDoubleClick
event apparently). Anyway, we can get it to fire once like so:
(Please excuse the C#, I tried to answer this from a wpf perspective, but I don't know the equivalent VB code)
OnItemPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
if (e.ClickCount == 2) {
e.Handled = true;
var treeViewItem = sender as TreeViewItem;
MessageBox.Show(treeViewItem.Header.ToString());
}
}
HOWEVER, because a Preview event tunnels down, it unfortunately fires once on the containing item, not the contained item, so we're back to square 1. We could do some looking into the args.OriginalSource
here but we're basically getting to the stage where a different solution is advisable:
Separate Alternative
A separate alternative is to just add an event listener to the whole TreeView
on its double click event, which then simply finds in which TreeViewItem
its OriginalSource
is located. (If you want to handle it and stop it propogating / causing other effects, you will need to instead hook onto the PreviewMouseLeftButtonDown
event as above instead).
To do so, you can take the MouseButtonEventArgs args
and consider args.OriginalSource
(this is the element top-most on the visual tree that you clicked on) and then you have a few cases: either it's not inside any TreeViewItem
at all, or it is itself the TreeViewItem
you require, or the third possibility is that the OriginalSource is an element somewhere inside the TreeViewItem
you require (this is the most likely case - from testing, you usually click on a TextBlock
contained within a TreeViewItem
), in which case you can recursively find parents till you hit the first parent of type TreeViewItem
. (You can do this using the VisualTreeHelper
GetParent
method.)
Basically, the above algorithm can be summed up as just recursively check the current item to see if it's of type TreeViewItem
or TreeView
else you make the current item its parent and recurse... If you end on TreeViewItem
, you have the required item, else if you clicked outside an item, you will end up on TreeView
.
PS: As I can't write VB, I think explaining the above algorithm's probably better than attempting to write it - but I hope the above explanations will still be useful to you :)
TreeViewItem MouseDoubleClick event and MvvmLight
You'll need to use a RelativeSource Binding
in order to reach the TreeView.SelectedItem
property from within the Trigger
. Try this Binding
for your CommandParameter
instead:
CommandParameter="{Binding SelectedItem,
RelativeSource={RelativeSource AncestorType={x:Type TreeView}}}"
Related Topics
How to Center Your Main Window in Wpf
How to Split a Number into Individual Digits in C#
Expose and Raise Event of a Child Control in a Usercontrol in C#
Wpf How to Access Control from Datatemplate
Net Core: Execute All Dependency Injection in Xunit Test for Appservice, Repository, etc
System.Valuetype Understanding
How to Write to a Onenote 2013 Page Using C# and the Onenote Interop
Why Do I Get "System.Data.Datarowview" Instead of Real Values in My Winforms Listbox
How to Generically Format a Boolean to a Yes/No String
Gracefully Handling Corrupted State Exceptions
Resharper Complains When Method Can Be Static, But Isn'T
Will Using Linq to SQL Help Prevent SQL Injection
How to Clear Browser Cache on Browser Back Button Click in MVC4
How to Add Http Header to Soap Client
Encrypting/Decrypting Large Files (.Net)
Exceptions That Can't Be Caught by Try-Catch Block in Application Code