33

(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>
Community
  • 1
  • 1
Anders Juul
  • 2,407
  • 3
  • 34
  • 56
  • 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? – Anders Juul Dec 21 '10 at 09:33
  • Have you tried looking at Attached Behaviors? – Fredrik Hedblad Dec 21 '10 at 09:36
  • Similar question using ListView, but answers are control-agnostic http://stackoverflow.com/questions/1035023/firing-a-double-click-event-from-a-wpf-listview-item-using-mvvm/1510592#1510592 – surfen Dec 03 '11 at 17:15

9 Answers9

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);
    }
}
Fredrik Hedblad
  • 83,499
  • 23
  • 264
  • 266
  • @Anders Juul: Do you get any binding errors in the output window? See this question: http://stackoverflow.com/questions/4051713/what-are-the-tools-available-for-troubleshooting-wpf-and-silverlight-databinding/4051953#4051953 for how to set Data Binding Output to All in case you don't have this turned on – Fredrik Hedblad Dec 21 '10 at 21:56
  • One can also create an attached event called ItemActivated, which handles also the Enter key: http://serialseb.blogspot.com/2007/01/attached-events-by-example-adding.html. You would bind that event to a command in the same way as suggested in this answer. – surfen Nov 27 '11 at 00:37
  • 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. – Dominik Palo Jun 14 '16 at 12:26
  • 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 `` to see, whether the style is applied. It is, but somehow doubleclicking on the items won't fire the command. – LuckyLikey Oct 03 '19 at 08:54
  • 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); } – uzrgm Sep 13 '21 at 10:43
  • 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; – uzrgm Nov 26 '21 at 11:17
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.

Damascus
  • 6,553
  • 5
  • 39
  • 53
  • 1
    @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. – surfen Nov 27 '11 at 01:06
  • 1
    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. – JoanComasFdz Jul 08 '15 at 13:31
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);
    }
}
Scott
  • 21,211
  • 8
  • 65
  • 72
hightower70
  • 61
  • 1
  • 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). – Yellow Nov 05 '13 at 11:49
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("*****");
    }
}   
StepUp
  • 36,391
  • 15
  • 88
  • 148
  • 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... – AndyUK May 03 '17 at 10:37
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);
    }
ígor
  • 1,144
  • 7
  • 16
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;
        }
Pinfi
  • 11
  • 3
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.

Noctis
  • 11,507
  • 3
  • 43
  • 82
JoanComasFdz
  • 2,911
  • 5
  • 34
  • 50
  • 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. – Julien N Aug 16 '12 at 17:21
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);
    }
}
Slesa
  • 245
  • 2
  • 11
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.

cheny
  • 2,545
  • 1
  • 24
  • 30