Wpf/Mvvm - How to Handle Double-Click on Treeviewitems in the Viewmodel

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

Sample Image

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



Leave a reply



Submit