WPF/MVVM - как обрабатывать двойной щелчок на TreeViewItems в ViewModel?

(Примечание - это повторная публикация, поскольку мой первый вопрос был опубликован под неправильным заголовком: здесь Извините!)

У меня есть стандартный вид дерева WPF и связанные элементы для просмотра классов моделей.

Теперь я хочу обработать поведение при двойном щелчке элементов (открытие документов в визуальном стиле студии).

Я могу получить обработчик событий для запуска в элементе управления, содержащем древовидное представление (показан xaml), но как мне привязать к определенному поведению на классах модели представления - например, ProjectViewModel?

Предпочтительно привязывать к ICommand-Implementer, так как это используется в других местах...

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

9 ответов

Решение

Обновление моего ответа немного.

Я пробовал много разных подходов для этого, и я все еще чувствую, что Attached Behaviors - лучшее решение. Хотя вначале это может показаться много накладных расходов, на самом деле это не так. Я сохраняю все свое поведение для ICommands в том же месте, и когда мне нужна поддержка для другого события, это просто вопрос копирования / вставки и изменения события в PropertyChangedCallback,

Я также добавил дополнительную поддержку для CommandParameter ,

В дизайнере это просто вопрос выбора нужного события

Вы можете установить это либо на TreeView, TreeViewItem или любое другое место, которое вам нравится.

Пример. Установите его на TreeView

<TreeView commandBehaviors:MouseDoubleClick.Command="{Binding YourCommand}"
          commandBehaviors:MouseDoubleClick.CommandParameter="{Binding}"
          .../>

Пример. Установите его на 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>

А вот и прикрепленное поведение 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);
    }
}

Я опоздал на это, но я просто использовал другое решение. Еще раз, это может быть не лучшим, но вот как я это сделал.

Прежде всего, предыдущий ответ от Meleak - это круто, но я чувствую, что очень тяжело быть вынужденным добавлять AttachedBehaviors только для чего-то столь же простого, как MouseDoubleClick. Это заставило бы меня использовать новый шаблон в моем приложении и еще более усложнило бы все.

Моя цель - оставаться максимально простым. Поэтому я сделал кое-что очень простое (мой пример для DataGrid, но вы можете использовать это на множестве различных элементов управления):

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

В коде позади:

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

Почему я чувствую, что это не нарушает MVVM-паттерн? Потому что, по моему мнению, единственные вещи, которые вы должны поместить в код позади, - это мосты к вашей viewModel, вещи, очень специфичные для вашего пользовательского интерфейса. В этом случае это просто говорит о том, что если вы дважды щелкнете, запустите соответствующую команду. Это почти то же самое, что Command="{Binding EditAppCommand}", я просто симулировал это поведение.

Не стесняйтесь высказать мне свое мнение по этому поводу, я был бы рад услышать некоторых критиков этого мышления, но на данный момент я считаю, что это самый простой способ реализовать его, не ломая MVVM.

Рекомендации Meleak и igor хороши, но когда обработчик события двойного щелчка привязан к TreeViewItem затем этот обработчик события вызывается для всех родительских элементов элемента (а не только для элемента, по которому щелкнули). Если это не желательно, вот еще одно дополнение:

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

Это действительно просто, и вот как я обработал двойной щелчок на 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 берется из C:\Program Files (x86)\Microsoft SDKs\Expression\Blend.NETFramework\v4.0\Libraries\System.Windows.Interactivity.dll или из NuGet.

На мой взгляд модель:

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 решение отличное!, но я добавил проверить

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

Привязка мыши к текстовому блоку

В TreeView. Ресурсы представления:

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

В ViewModel этого представления (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;
        }

Просто для любопытства: что, если я приму участие Фредерикса, но реализую это непосредственно как поведение?

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

Прошло 11 лет. Я только что сделал это в своем дереве на основе решения @Damascus.

В Xaml есть UserControl с TreeView. DataType=FileResultBrief - это то, что я хочу дважды щелкнуть.

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

вот что произошло в коде позади:

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

Я начал использовать Wpf и Xaml 2 недели назад. Это было сделано путем установки точки останова на входе метода и наблюдения за «отправителем» все глубже и глубже.

Лучший подход, который я достиг, - это просто связать IsSelected свойство от TreeViewItem до ViewModel в двустороннем режиме и реализовать логику в установщике свойств. Затем вы можете определить, что делать, если значение равно true или false, потому что это свойство будет меняться всякий раз, когда пользователь щелкает элемент.

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

Это позволяет избежать большого количества кода.

Кроме того, этот метод позволяет вам реализовать поведение "onclick" только в моделях представления, которые действительно нуждаются в этом.

Другие вопросы по тегам