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

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

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

Как это можно сделать чистым способом MVVM, т.е. без кода в представлении?

8 ответов

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

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

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

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

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

Я могу заставить это работать с.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>

Мне нравится использовать Прикрепленные Командные Поведения и Команды. У Марлона Греча очень хорошая реализация поведения Attached Command. Используя их, мы могли бы затем назначить стиль свойству ItemContainerStyle объекта ListView, который будет устанавливать команду для каждого объекта 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.

Я нашел очень простой и понятный способ сделать это с помощью триггеров событий 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 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>

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

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

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

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

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

Я считаю, что проще связать команду при создании представления:

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

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

Мне удалось реализовать эту функциональность с помощью.Net 4.7 framework с помощью библиотеки интерактивности, прежде всего убедитесь, что объявили пространство имен в файле XAML

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"

Затем установите Event Trigger с его соответствующим InvokeCommandAction внутри ListView, как показано ниже.

Посмотреть:

<ListView x:Name="lv" IsSynchronizedWithCurrentItem="True" 
          ItemsSource="{Binding Path=AppsSource}"  >
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="MouseDoubleClick">
            <i:InvokeCommandAction CommandParameter="{Binding ElementName=lv, Path=SelectedItem}"
                                   Command="{Binding OnOpenLinkCommand}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <ListView.View>
        <GridView>
            <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}" />
            <GridViewColumn Header="Developed By" DisplayMemberBinding="{Binding DevelopedBy}" />
        </GridView>
    </ListView.View>
</ListView>

Адаптации приведенного выше кода должно быть достаточно, чтобы событие двойного щелчка работало на вашей ViewModel, однако я добавил вам класс Model и View Model из моего примера, чтобы вы могли иметь полное представление.

Модель:

public class ApplicationModel
{
    public string Name { get; set; }

    public string DevelopedBy { get; set; }
}

Просмотреть модель:

public class AppListVM : BaseVM
{
        public AppListVM()
        {
            _onOpenLinkCommand = new DelegateCommand(OnOpenLink);
            _appsSource = new ObservableCollection<ApplicationModel>();
            _appsSource.Add(new ApplicationModel("TEST", "Luis"));
            _appsSource.Add(new ApplicationModel("PROD", "Laurent"));
        }

        private ObservableCollection<ApplicationModel> _appsSource = null;

        public ObservableCollection<ApplicationModel> AppsSource
        {
            get => _appsSource;
            set => SetProperty(ref _appsSource, value, nameof(AppsSource));
        }

        private readonly DelegateCommand _onOpenLinkCommand = null;

        public ICommand OnOpenLinkCommand => _onOpenLinkCommand;

        private void OnOpenLink(object commandParameter)
        {
            ApplicationModel app = commandParameter as ApplicationModel;

            if (app != null)
            {
                //Your code here
            }
        }
}

Если вам нужна реализация класса DelegateCommand.

Вот поведение, которое делает это на обоих 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>

Я видел решение от 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 и его привязки к тексту, я не знаю, я могу обойтись, объявив об этом только в одном месте.

Я надеюсь, что это помогает кому-то!

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