简体   繁体   中英

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

(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 - eg 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>

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);
    }
}

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.

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);
    }
}

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("*****");
    }
}   

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);
    }

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.

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);
    }
}

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;
        }

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM