WPF/MVVM - how to handle double-click on TreeViewItems in the ViewModel?
Asked Answered
A

9

35

(Note - this is a re-post as my first question got posted under wrong headline: Here Sorry!)

I have a standard WPF treeview and have bound items to view model classes.

I now wish to handle behaviour when items are double-clicked (opening documents visual-studio-style).

I can get event-handler to fire in the control housing the treeview (xaml shown), but how do I bind to specific behaviour on the view model classes - e.g. ProjectViewModel?

Preferable bound to ICommand-implementer, as this is used elsewhere...

<TreeView ItemsSource="{Binding Projects}" MouseDoubleClick="TreeView_MouseDoubleClick">
    <TreeView.ItemContainerStyle>
        <!-- 
This Style binds a TreeViewItem to a TreeViewItemViewModel. 
-->
        <Style TargetType="{x:Type TreeViewItem}">
            <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
            <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
            <Setter Property="FontWeight" Value="Normal" />
            <Style.Triggers>
                <Trigger Property="IsSelected" Value="True">
                    <Setter Property="FontWeight" Value="Bold" />
                </Trigger>
            </Style.Triggers>
        </Style>
    </TreeView.ItemContainerStyle>

    <TreeView.Resources>
        <HierarchicalDataTemplate DataType="{x:Type Implementations:ProjectViewModel}" ItemsSource="{Binding Children}">
            <StackPanel Orientation="Horizontal">
                <Image Width="16" Height="16" Margin="3,0" Source="Images\Region.png" />
                <TextBlock Text="{Binding DisplayName}" />
            </StackPanel>
        </HierarchicalDataTemplate>

        <HierarchicalDataTemplate DataType="{x:Type Implementations:PumpViewModel}" ItemsSource="{Binding Children}">
            <StackPanel Orientation="Horizontal">
                <Image Width="16" Height="16" Margin="3,0" Source="Images\State.png" />
                <TextBlock Text="{Binding Name}" />
            </StackPanel>
        </HierarchicalDataTemplate>

        <DataTemplate DataType="{x:Type Implementations:PumpDesignViewModel}">
            <StackPanel Orientation="Horizontal">
                <Image Width="16" Height="16" Margin="3,0" Source="Images\City.png" />
                <TextBlock Text="{Binding Name}" />
            </StackPanel>
        </DataTemplate>
    </TreeView.Resources>
</TreeView>
Anhinga answered 21/12, 2010 at 9:30 Comment(3)
I have viewmodel-objects handling the individual treeitems with regards to display, lazy load etc. However, when I want to 'activate'/double-click an item in the treeview, I'd like this action to be handled by the same viewmodel objects that handle the display - but how do I do that?Anhinga
Have you tried looking at Attached Behaviors?Clique
Similar question using ListView, but answers are control-agnostic #1035523Malissa
C
65

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

enter image description here

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);
    }
}
Clique answered 21/12, 2010 at 9:56 Comment(6)
@Anders Juul: Do you get any binding errors in the output window? See this question: #4052213 for how to set Data Binding Output to All in case you don't have this turned onClique
One can also create an attached event called ItemActivated, which handles also the Enter key: serialseb.blogspot.com/2007/01/…. You would bind that event to a command in the same way as suggested in this answer.Malissa
I've used your approach for a TreeView container style, it works OK in app, but designer shows an error: "The property "Command" is not a DependencyProperty. To be used in markup, non-attached properties must be exposed on the target type with an accessible instance property "Command". For attached properties, the declaring type must provide static "GetCommand" and "SetCommand" methods." But your class contains SetCommand and GetCommand methods, so I don't know why this error. How to fix it? Without fixing this error, all my XAML styles are not loaded in entire solution.Grimona
I've tried both of your examples now. "Set it on Tree View" works. "Set it on TreeViewItem" doesn't. I've even added a test Setter <Setter Property="Background" Value="Red"></Setter> to see, whether the style is applied. It is, but somehow doubleclicking on the items won't fire the command.Smile
For those, who is going to use this attached behavior - need to add additional method: public static ICommand GetCommand(DependencyObject target) { return (ICommand)target.GetValue(CommandProperty); }Recreation
If using in TreeView, there could be an issue with DoubleClick event, since it is triggered for every node in a chain. To avoid this, add following code to OnMouseDoubleClick: if (sender is TreeViewItem treeViewItem && !treeViewItem.IsSelected) return;Recreation
O
10

I am late for this, but I just used a different solution. Once again, it might not be the best, but here is how I did that.

First of all, the previous answer from Meleak is cool, but I feel like it is very heavy to be forced to add AttachedBehaviors just for something as basic as a MouseDoubleClick. This would force me to use a new pattern in my app and would even more complicate everything.

My aim is to stay as simple as possible. Therefore I did something very basic (my example is for a DataGrid, but you can use that on a lot of different controls):

<DataGrid MouseDoubleClick="DataGrid_MouseDoubleClick">
   <!-- ... -->
</DataGrid>

In the code-behind:

private void DataGrid_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
    //Execute the command related to the doubleclick, in my case Edit
    (this.DataContext as VmHome).EditAppCommand.Execute(null);
}

Why do I feel like it doesn't break the MVVM-pattern? Because in my opinion, the only things you should put in the code-behind are bridges to your viewModel, things very specific to your UI. In this case it just says that if you double click, fire the related command. It's almost the same than a Command="{Binding EditAppCommand}", I just simulated this behavior.

Feel free to give me your opinion on this, I'd be glad to hear some critics to this way of thinking, but for now I believe it's the easiest way to implement it without breaking MVVM.

Olga answered 23/3, 2011 at 10:4 Comment(2)
@Demascus: Thanks for an alternative solution. In my opinion, tt really depends on how often you would need to do that. Notice that an attachedBehaviour solution is generic - control indepentent so it promotes reusability and maintainability, because you would have all your Commands bound declaratively in XAML. With all WPF has to offer, writing codebehind to map events to commands feels like too much overhead to me. I wish that all events supported Command binding out of the box.Malissa
In the case of a TreeView, you are really interested on the TreeViewItem's DataContext on which the double click has been done, but your solution isn't giving such information to the ViewModel.Latticework
L
6

Both Meleak and ígor's recommendations are great, but when the double click event handler is bound to TreeViewItem then this event handler is called for all of the item's parent elements (not just the clicked element). If it is not desired, here is another addition:

private static void OnMouseDoubleClick(object sender, RoutedEventArgs e)
{
    Control control = sender as Control;
    ICommand command = (ICommand)control.GetValue(CommandProperty);
    object commandParameter = control.GetValue(CommandParameterProperty);

    if (sender is TreeViewItem)
    {
        if (!((TreeViewItem)sender).IsSelected)
            return;
    }

    if (command.CanExecute(commandParameter))
    {
        command.Execute(commandParameter);
    }
}
Lorie answered 17/9, 2012 at 12:38 Comment(1)
This was very useful for me indeed. The only slight problem is that it assumed that the sender is actually selected, which is always true for a double click, but not when you change the event type to something else (i.e. MouseHover or things like that).Browning
R
6

It is really simple and this is how I handled double click at the TreeView:

<Window x:Class="TreeViewWpfApplication.MainWindow"
    ...
    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
    ...>

      <TreeView ItemsSource="{Binding Departments}" >
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="MouseDoubleClick">
                <ei:CallMethodAction MethodName="SomeMethod" TargetObject="{Binding}"/>
            </i:EventTrigger>
        </i:Interaction.Triggers>
      </TreeView>
</Window>

System.Windows.Interactivity.dll is taken from C:\Program Files (x86)\Microsoft SDKs\Expression\Blend.NETFramework\v4.0\Libraries\System.Windows.Interactivity.dll or by NuGet

My view model:

public class TreeViewModel : INotifyPropertyChanged
{   
    private List<Department> departments;
    public TreeViewModel()
    {
        Departments = new List<Department>()
        {
            new Department("Department1"),
            new Department("Department2"),
            new Department("Department3")
        };
    }

    public List<Department> Departments
    {
        get
        {
            return departments;
        }
        set
        {
            departments = value;
            OnPropertyChanged("Departments");
        }
    }

    public void SomeMethod()
    {
        MessageBox.Show("*****");
    }
}   
Revolting answered 16/10, 2015 at 12:9 Comment(1)
+1. This works for me. I always prefer the Interactivity-based approach. And this works as you say - install the package via NuGet, insert the i:Interaction.Triggers section bit, add the test method to the ViewModel class and you're away...Miyamoto
T
2

Meleak solution is great!, but i added check

    private static void OnMouseDoubleClick(object sender, RoutedEventArgs e)
    {
        Control control = sender as Control;
        ICommand command = (ICommand)control.GetValue(CommandProperty);
        object commandParameter = control.GetValue(CommandParameterProperty);
       //Check command can execute!!  
      if(command.CanExecute(commandParameter ))
         command.Execute(commandParameter);
    }
Tractate answered 26/7, 2012 at 9:28 Comment(0)
C
1

Mouse Binding on the TextBlock

In the TreeView.Resources of the View:

   <HierarchicalDataTemplate 
      DataType="{x:Type treeview:DiscoveryUrlViewModel}" 
      ItemsSource="{Binding Children}">

      <StackPanel Orientation="Horizontal">
           <Image Width="16" Height="16" Margin="3,0" Source="../Images/ic_search.png" />

           <TextBlock Text="{Binding DisplayText}" >
               <TextBlock.InputBindings>
                     <MouseBinding Gesture="LeftDoubleClick"
                                   Command="{Binding DoubleClickCopyCommand}"
                                   CommandParameter="{Binding }" />
               </TextBlock.InputBindings>
            </TextBlock>
       </StackPanel>
 </HierarchicalDataTemplate>

In the ViewModel of that View (DiscoveryUrlViewModel.cs):

private RelayCommand _doubleClickCommand;   
public ICommand DoubleClickCopyCommand
        {
            get
            {
                if (_doubleClickCommand == null)
                    _doubleClickCommand = new RelayCommand(OnDoubleClick);
                return _doubleClickCommand;
            }
        }

        private void OnDoubleClick(object obj)
        {
            var clickedViewModel = (DiscoveryUrlViewModel)obj;
        }
Curtal answered 7/2, 2018 at 9:28 Comment(0)
L
0

The best approach I've reached is just binding the IsSelected property from the TreeViewItem to the ViewModel in a Two-way mode and implement the logic in the property setter. Then you can define what to do if the value is true or false, because this property will change whenever the user click an item.

class MyVM
{
  private bool _isSelected;
  public bool IsSelected
  {
    get { return _isSelected; }
    set
    {
      if (_isSelected == null)
       return;

      _isSelected = vale;

      if (_isSelected)
      {
        // Your logic goes here.
      }
      else
      {
        // Your other logic goes here.
      }
   }
}

This avoids a lot of code.

Also, this technique allows you to implement the "onclick" behaviour only in the ViewModels that really need it.

Latticework answered 12/4, 2012 at 16:28 Comment(1)
But you missed the question, as the OP wants to handle the "double click" event. Your code is great, but only works for the "click" event.Craniology
P
0

Just for curiosity: what if I take Frederiks part, but implement it directly as behavior?

public class MouseDoubleClickBehavior : Behavior<Control>
{
    public static readonly DependencyProperty CommandProperty =
        DependencyProperty.Register("Command", typeof (ICommand), typeof (MouseDoubleClickBehavior), new PropertyMetadata(default(ICommand)));

    public ICommand Command
    {
        get { return (ICommand) GetValue(CommandProperty); }
        set { SetValue(CommandProperty, value); }
    }

    public static readonly DependencyProperty CommandParameterProperty =
        DependencyProperty.Register("CommandParameter", typeof (object), typeof (MouseDoubleClickBehavior), new PropertyMetadata(default(object)));

    public object CommandParameter
    {
        get { return GetValue(CommandParameterProperty); }
        set { SetValue(CommandParameterProperty, value); }
    }

    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.MouseDoubleClick += OnMouseDoubleClick;
    }

    protected override void OnDetaching()
    {
        AssociatedObject.MouseDoubleClick -= OnMouseDoubleClick;
        base.OnDetaching();
    }

    void OnMouseDoubleClick(object sender, RoutedEventArgs e)
    {
        if (Command == null) return;
        Command.Execute(/*commandParameter*/null);
    }
}
Phinney answered 17/4, 2014 at 21:19 Comment(0)
B
0

11 Years passed. I just did it in my treeview based on solution from @Damascus.

In the Xaml, there is a UserControl with a TreeView in it. The DataType=FileResultBrief is what I want to double click.

<resultTrees:ResultTreeView x:Class="ChiSharedFormsWpf.ResultTrees.ChiTreeView"
                        MouseDoubleClick="ChiTreeView_OnMouseDoubleClick"
                        d:DesignHeight="450" d:DesignWidth="3800">
<Grid>
    <DockPanel HorizontalAlignment="Stretch">
        <TreeView Name="Tree" HorizontalAlignment="Stretch">
            <TreeView.Resources>
                <HierarchicalDataTemplate DataType="{x:Type dirTrees:FileResultBrief}"
                                          ItemsSource="{Binding BadSmells}">
                    <StackPanel Orientation="Horizontal" Background="{Binding  Index.SoftColorHex}"
                                Tag="{Binding  FileName}">
                        <fa5:FontAwesome Icon="Regular_FileCode" Margin="0,2,5,0" />
                        <TextBlock Text="{Binding Index.Brief}" />
                        <TextBlock Text="{Binding FileName}" Margin="10 0"/>
                    </StackPanel>
                </HierarchicalDataTemplate>

here is what happened in code behind:

private void ChiTreeView_OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
    if (sender is not ChiTreeView {Tree: TreeView {SelectedItem: FileResultBrief brief}})
        return;
    FileUtility.OpenFileWithDefaultApp(ScanTask.FullNameOf(brief.FileName));
}

I began to use Wpf and Xaml 2 weeks ago. This was done by putting a break point at the entry of the method and watch into "sender" deeper and deeper.

Bashibazouk answered 24/4, 2022 at 16:1 Comment(1)
WPF never sleeps ;)Anhinga

© 2022 - 2024 — McMap. All rights reserved.