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

102

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

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

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

Эмад Габриэль
источник

Ответы:

76

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

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

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

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

Платформа приложений WPF (WAF) - http://waf.codeplex.com

jbe
источник
5
Хорошо сказано, я отказываюсь использовать весь этот код и дополнительную DLL только для двойного щелчка!
Эдуардо Молтени,
4
Это единственное использование привязки вызывает у меня настоящую головную боль. Это как если бы вас попросили написать код с одной рукой, одним глазом на повязке и стоя на одной ноге. Двойной щелчок должен быть простым, и я не понимаю, чего стоит весь этот дополнительный код.
Эчибан
1
Боюсь, я не совсем согласен с вами. Если вы скажете, что «код позади - неплохой», тогда у меня возникнет вопрос: почему бы нам не делегировать событие щелчка для кнопки, а вместо этого часто использовать привязку (используя свойство Command)?
Nam G VU
21
@Nam Gi VU: я всегда предпочитаю привязку команд, если она поддерживается элементом управления WPF. Привязка команд делает больше, чем просто ретранслирует событие «Click» в ViewModel (например, CanExecute). Но команды доступны только для наиболее распространенных сценариев. Для других сценариев мы можем использовать файл кода программной части, и там мы делегируем проблемы, не связанные с пользовательским интерфейсом, ViewModel или Model.
jbe
2
Теперь я понимаю вас больше! Приятного обсуждения с вами!
Nam G VU
73

Я могу заставить это работать с .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>
Рушуи Гуань
источник
2
Кажется, не работает для всей области, например, я делаю это на панели док-станции, и это работает только там, где есть что-то внутри панели док-станции (например, текстовый блок, изображение), но не пустое пространство.
Стивен Дрю
3
Хорошо - снова этот старый каштан ... нужно установить прозрачный фон для получения событий мыши, согласно stackoverflow.com/questions/7991314/…
Стивен Дрю
6
Я чесал в затылке, пытаясь понять, почему это сработало для всех вас, а не для меня. Я внезапно понял, что в контексте шаблона элемента контекст данных - это текущий элемент из источника элементов, а не модель представления главного окна. Поэтому я использовал следующее, чтобы заставить его работать <MouseBinding MouseAction = "LeftDoubleClick" Command = "{Binding Path = DataContext.EditBandCommand, RelativeSource = {RelativeSource AncestorType = {x: Type Window}}}" /> В моем случае EditBandCommand команда в модели просмотра страницы, а не в привязанном объекте.
naskew 06
naskew имел секретный соус, который мне был нужен с MVVM Light, получая параметр команды, являющийся объектом модели в элементе списка, дважды щелкнувшем его, а контекст данных окна установлен на модель представления, которая предоставляет команду: <MouseBinding Gesture = "LeftDoubleClick "Command =" {Binding Path = DataContext.OpenSnapshotCommand, RelativeSource = {RelativeSource AncestorType = {x: Type Window}}} "CommandParameter =" {Binding} "/>
MC5,
Просто хочу добавить , что InputBindingsдоступны с .NET 3.0 и не доступны в Silverlight.
Мартин
44

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

Рмур
источник
1
+1 Я считаю, что это мое предпочтительное решение при работе с руководством по составным приложениям для WPF (Prism).
Трэвис Хесеман, 01
1
Что означает пространство имен acb: в приведенном выше примере кода?
Nam G VU
@NamGiVU acb:= AttachedCommandBehavior. Код можно найти в первой ссылке в ответе
Рэйчел
Я попробовал именно это и получил исключение нулевого указателя из класса CommandBehaviorBinding, строка 99. переменная «стратегия» имеет значение null. в чем дело?
etwas77
13

Я нашел очень простой и понятный способ сделать это с помощью триггеров событий 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 не генерируются для каждого визуального элемента (например, они происходят из Controls, но не из 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>
Гюнтер
источник
Как я выяснил, если вы поместите триггер непосредственно в GridViewRowPresenter, может возникнуть проблема. Пустые пространства между столбцами, вероятно, вообще не получают событий мыши (возможно, обходным путем было бы стилизовать их с помощью растягивания выравнивания).
Gunter
В этом случае, вероятно, лучше поместить пустую сетку вокруг GridViewRowPresenter и поместить туда триггер. Кажется, это работает.
Gunter
1
Обратите внимание, что вы потеряете стиль по умолчанию для ListViewItem, если замените шаблон таким образом. Это не имело значения для приложения, над которым я работал, поскольку оно все равно использовало сильно индивидуализированный стиль.
Gunter
6

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

    private void ButtonClick(object sender, RoutedEventArgs e)
    {
        dynamic viewModel = DataContext;
        viewModel.ButtonClick(sender, e);
    }
Аарон
источник
12
У модели viewmodel должны быть имена, представляющие действия, которые вы можете выполнять в своем домене. Что такое действие «ButtonClick» в вашем домене? ViewModel представляет логику домена в удобном для просмотра контексте, а не просто помощником для представления. Итак: ButtonClick никогда не должен быть в модели просмотра, вместо этого используйте viewModel.DeleteSelectedCustomer или что-то еще, что это действие фактически представляет.
Мариус
4

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

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

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

Идурсун
источник
4

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

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

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

Тимоти Пратли
источник
Мне кажется, что это самое простое решение, а не пытаться заставить его работать только в XAML.
Mas
1

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

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

user3235445
источник
Это отличное решение, я использую похожее, но вам действительно нужен только один шаблон управления. Если пользователь собирается дважды щелкнуть по значку, его listviewitem, вероятно, не волнует, выбран он уже или нет. Также важно отметить, что эффект выделения может также нуждаться в корректировке, чтобы соответствовать listviewстилю. Проголосовали "за".
Дэвид Бентли
1

Мне удалось реализовать эту функциональность с помощью .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 .

luis_laurent
источник
0

Вот поведение, которое выполняется на обоих 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>
Принц Оуэн
источник