Вызов события двойного щелчка из элемента списка элементов WPF с использованием MVVM

В приложении WPF с использованием MVVM у меня есть usercontrol с элементом listview. Во время выполнения он будет использовать привязку данных для заполнения списка с помощью коллекции объектов.

Каков правильный способ привязки события двойного щелчка к элементам в списке, так что когда элемент в представлении списка дважды кликнут, соответствующее событие в модели представления увольняется и имеет ссылку на элемент, который нажал?

Как это можно сделать с помощью чистого MVVM-способа, то есть никакого кода в представлении?

Ответ 1

Пожалуйста, код позади - это совсем не плохо. К сожалению, довольно много людей в сообществе WPF ошибались.

MVVM не является шаблоном для устранения кода. Это отделить часть представления (внешний вид, анимацию и т.д.) От логической части (рабочий процесс). Кроме того, вы можете unit test логическую часть.

Я знаю достаточно сценариев, где вам приходится писать код, потому что привязка данных не является решением всего. В вашем сценарии я бы обработал событие DoubleClick в коде за файлом и делегировал этот вызов ViewModel.

Примеры приложений, которые используют код позади и все еще выполняют разделение MVVM, можно найти здесь:

WPF Application Framework (WAF) - http://waf.codeplex.com

Ответ 2

Я могу заставить это работать с .NET 4.5. Кажется прямолинейным, и никакой третьей стороне или коду не нужно.

<ListView ItemsSource="{Binding Data}">
        <ListView.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel Orientation="Horizontal"/>
            </ItemsPanelTemplate>
        </ListView.ItemsPanel>
        <ListView.ItemTemplate>
            <DataTemplate>
                <Grid Margin="2">
                    <Grid.InputBindings>
                        <MouseBinding Gesture="LeftDoubleClick" Command="{Binding ShowDetailCommand}"/>
                    </Grid.InputBindings>
                    <Grid.RowDefinitions>
                        <RowDefinition/>
                        <RowDefinition/>
                    </Grid.RowDefinitions>
                    <Image Source="..\images\48.png" Width="48" Height="48"/>
                    <TextBlock Grid.Row="1" Text="{Binding Name}" />
                </Grid>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>

Ответ 3

Мне нравится использовать "Привязанные команды" и "Команды" . Marlon Grech имеет очень хорошую реализацию команд Attached Command Behaviors. Используя их, мы могли бы затем присвоить стиль атрибуту ListView ItemContainerStyle, который установит команду для каждого ListViewItem.

Здесь мы устанавливаем команду для запуска команды MouseDoubleClick, а CommandParameter - объект данных, на который мы нажимаем. Здесь я просматриваю визуальное дерево, чтобы получить команду, которую я использую, но вы могли бы так же легко создавать команды с широким диапазоном приложений.

<Style x:Key="Local_OpenEntityStyle"
       TargetType="{x:Type ListViewItem}">
    <Setter Property="acb:CommandBehavior.Event"
            Value="MouseDoubleClick" />
    <Setter Property="acb:CommandBehavior.Command"
            Value="{Binding ElementName=uiEntityListDisplay, Path=DataContext.OpenEntityCommand}" />
    <Setter Property="acb:CommandBehavior.CommandParameter"
            Value="{Binding}" />
</Style>

Для команд вы можете непосредственно реализовать ICommand или использовать некоторые из помощников, например те, которые входят в MVVM Toolkit.

Ответ 4

Я нашел очень простой и чистый способ сделать это с помощью триггеров события Blend SDK. Очистить MVVM, повторно использовать и не использовать код.

У вас, вероятно, уже есть что-то вроде этого:

<Style x:Key="MyListStyle" TargetType="{x:Type ListViewItem}">

Теперь включите ControlTemplate для ListViewItem, если вы еще не используете его:

<Setter Property="Template">
  <Setter.Value>
    <ControlTemplate TargetType="{x:Type ListViewItem}">
      <GridViewRowPresenter Content="{TemplateBinding Content}"
                            Columns="{TemplateBinding GridView.ColumnCollection}" />
    </ControlTemplate>
  </Setter.Value>
 </Setter>

GridViewRowPresenter будет визуальным корнем из всех элементов "внутри", составляющих элемент строки списка. Теперь мы можем вставить триггер, чтобы искать перенаправленные события MouseDoubleClick и вызывать команду через InvokeCommandAction следующим образом:

<Setter Property="Template">
  <Setter.Value>
    <ControlTemplate TargetType="{x:Type ListViewItem}">
      <GridViewRowPresenter Content="{TemplateBinding Content}"
                            Columns="{TemplateBinding GridView.ColumnCollection}">
        <i:Interaction.Triggers>
          <i:EventTrigger EventName="MouseDoubleClick">
            <i:InvokeCommandAction Command="{Binding DoubleClickCommand}" />
          </i:EventTrigger>
        </i:Interaction.Triggers>
      </GridViewRowPresenter>
    </ControlTemplate>
  </Setter.Value>
 </Setter>

Если у вас есть визуальные элементы "выше" GridRowPresenter (вероятно, начиная с сетки), вы также можете установить триггер.

К сожалению, события MouseDoubleClick не генерируются из каждого визуального элемента (они из элементов управления, но не из FrameworkElements, например). Обходным путем является получение класса из EventTrigger и поиск MouseButtonEventArgs с помощью ClickCount of 2. Это эффективно отфильтровывает все не-MouseButtonEvents и все MoseButtonEvents с помощью ClickCount!= 2.

class DoubleClickEventTrigger : EventTrigger
{
    protected override void OnEvent(EventArgs eventArgs)
    {
        var e = eventArgs as MouseButtonEventArgs;
        if (e == null)
        {
            return;
        }
        if (e.ClickCount == 2)
        {
            base.OnEvent(eventArgs);
        }
    }
}

Теперь мы можем написать это ('h' - пространство имен вспомогательного класса выше):

<Setter Property="Template">
  <Setter.Value>
    <ControlTemplate TargetType="{x:Type ListViewItem}">
      <GridViewRowPresenter Content="{TemplateBinding Content}"
                            Columns="{TemplateBinding GridView.ColumnCollection}">
        <i:Interaction.Triggers>
          <h:DoubleClickEventTrigger EventName="MouseDown">
            <i:InvokeCommandAction Command="{Binding DoubleClickCommand}" />
          </h:DoubleClickEventTrigger>
        </i:Interaction.Triggers>
      </GridViewRowPresenter>
    </ControlTemplate>
  </Setter.Value>
 </Setter>

Ответ 5

Я понимаю, что эта дискуссия - год, но с .NET 4, есть ли мысли по этому решению? Я абсолютно согласен с тем, что точкой MVVM является не устранение кода за файлом. Я также очень сильно чувствую, что только потому, что что-то сложно, это не значит, что это лучше. Вот что я добавил в код:

    private void ButtonClick(object sender, RoutedEventArgs e)
    {
        dynamic viewModel = DataContext;
        viewModel.ButtonClick(sender, e);
    }

Ответ 6

Вы можете использовать Caliburn Действие для сопоставления событий методам на ViewModel. Предполагая, что у вас есть метод ItemActivated на вашем ViewModel, тогда соответствующий XAML будет выглядеть так:

<ListView x:Name="list" 
   Message.Attach="[Event MouseDoubleClick] = [Action ItemActivated(list.SelectedItem)]" >

Для получения дополнительной информации вы можете ознакомиться с документацией и образцами Caliburn.

Ответ 7

Мне проще упростить ссылку на команду при создании представления:

var r = new MyView();
r.MouseDoubleClick += (s, ev) => ViewModel.MyCommand.Execute(null);
BindAndShow(r, ViewModel);

В моем случае BindAndShow выглядит так (updatecontrols + avalondock):

private void BindAndShow(DockableContent view, object viewModel)
{
    view.DataContext = ForView.Wrap(viewModel);
    view.ShowAsDocument(dockManager);
    view.Focus();
}

Хотя подход должен работать с любым методом, который вы открываете для новых просмотров.

Ответ 8

Я видел решение от rushui с InuptBindings, но я все еще не мог попасть в область ListViewItem, где не было текста - даже после того, как фон был прозрачным, поэтому я решил его с помощью разных шаблонов.

Этот шаблон предназначен, когда ListViewItem был выбран и активен:

<ControlTemplate x:Key="SelectedActiveTemplate" TargetType="{x:Type ListViewItem}">
   <Border Background="LightBlue" HorizontalAlignment="Stretch">
   <!-- Bind the double click to a command in the parent view model -->
      <Border.InputBindings>
         <MouseBinding Gesture="LeftDoubleClick" 
                       Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.ItemSelectedCommand}"
                       CommandParameter="{Binding}" />
      </Border.InputBindings>
      <TextBlock Text="{Binding TextToShow}" />
   </Border>
</ControlTemplate>

Этот шаблон предназначен, когда ListViewItem был выбран и неактивен:

<ControlTemplate x:Key="SelectedInactiveTemplate" TargetType="{x:Type ListViewItem}">
   <Border Background="Lavender" HorizontalAlignment="Stretch">
      <TextBlock Text="{Binding TextToShow}" />
   </Border>
</ControlTemplate>

Это стиль по умолчанию, используемый для ListViewItem:

<Style TargetType="{x:Type ListViewItem}">
   <Setter Property="Template">
      <Setter.Value>
         <ControlTemplate>
            <Border HorizontalAlignment="Stretch">
               <TextBlock Text="{Binding TextToShow}" />
            </Border>
         </ControlTemplate>
      </Setter.Value>
   </Setter>
   <Style.Triggers>
      <MultiTrigger>
         <MultiTrigger.Conditions>
            <Condition Property="IsSelected" Value="True" />
            <Condition Property="Selector.IsSelectionActive" Value="True" />
         </MultiTrigger.Conditions>
         <Setter Property="Template" Value="{StaticResource SelectedActiveTemplate}" />
      </MultiTrigger>
      <MultiTrigger>
         <MultiTrigger.Conditions>
            <Condition Property="IsSelected" Value="True" />
            <Condition Property="Selector.IsSelectionActive" Value="False" />
         </MultiTrigger.Conditions>
         <Setter Property="Template" Value="{StaticResource SelectedInactiveTemplate}" />
      </MultiTrigger>
   </Style.Triggers>
</Style>

То, что мне не нравится, это повторение TextBlock и его привязка текста, я не знаю, могу ли я обойти объявить это только в одном месте.

Надеюсь, это поможет кому-то!

Ответ 9

Здесь поведение, которое делает это как на ListBox и на ListView.

public class ItemDoubleClickBehavior : Behavior<ListBox>
{
    #region Properties
    MouseButtonEventHandler Handler;
    #endregion

    #region Methods

    protected override void OnAttached()
    {
        base.OnAttached();

        AssociatedObject.PreviewMouseDoubleClick += Handler = (s, e) =>
        {
            e.Handled = true;
            if (!(e.OriginalSource is DependencyObject source)) return;

            ListBoxItem sourceItem = source is ListBoxItem ? (ListBoxItem)source : 
                source.FindParent<ListBoxItem>();

            if (sourceItem == null) return;

            foreach (var binding in AssociatedObject.InputBindings.OfType<MouseBinding>())
            {
                if (binding.MouseAction != MouseAction.LeftDoubleClick) continue;

                ICommand command = binding.Command;
                object parameter = binding.CommandParameter;

                if (command.CanExecute(parameter))
                    command.Execute(parameter);
            }
        };
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.PreviewMouseDoubleClick -= Handler;
    }

    #endregion
}

Здесь класс расширения используется для поиска родителя.

public static class UIHelper
{
    public static T FindParent<T>(this DependencyObject child, bool debug = false) where T : DependencyObject
    {
        DependencyObject parentObject = VisualTreeHelper.GetParent(child);

        //we've reached the end of the tree
        if (parentObject == null) return null;

        //check if the parent matches the type we're looking for
        if (parentObject is T parent)
            return parent;
        else
            return FindParent<T>(parentObject);
    }
}

Использование:

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
xmlns:coreBehaviors="{{Your Behavior Namespace}}"


<ListView AllowDrop="True" ItemsSource="{Binding Data}">
    <i:Interaction.Behaviors>
       <coreBehaviors:ItemDoubleClickBehavior/>
    </i:Interaction.Behaviors>

    <ListBox.InputBindings>
       <MouseBinding MouseAction="LeftDoubleClick" Command="{Binding YourCommand}"/>
    </ListBox.InputBindings>
</ListView>