使用 MVVM 从 WPF ListView 项目触发双击事件

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1035023/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-08 20:36:55  来源:igfitidea点击:

Firing a double click event from a WPF ListView item using MVVM

wpfmvvm

提问by Emad Gabriel

In a WPF application using MVVM, I have a usercontrol with a listview item. In run time, it will use databinding to fill the listview with a collection of objects.

在使用 MVVM 的 WPF 应用程序中,我有一个带有列表视图项的用户控件。在运行时,它将使用数据绑定用对象集合填充列表视图。

What is the correct way to attach a double click event to the items in the listview so that when an item in the list view is doubleclicked, A corresponding event in the view model is fired and has a reference to the item clicked?

将双击事件附加到列表视图中的项目的正确方法是什么,以便在双击列表视图中的项目时,会触发视图模型中的相应事件并引用单击的项目?

How can it be done in a clean MVVM way i.e. no code behind in the View?

如何以干净的 MVVM 方式完成,即视图中没有代码?

回答by jbe

Please, code behind is not a bad thing at all. Unfortunately, quite a lot people in the WPF community got this wrong.

拜托,代码隐藏并不是一件坏事。不幸的是,WPF 社区中的很多人都弄错了。

MVVM is not a pattern to eliminate the code behind. It is to separate the view part (appearance, animations, etc.) from the logic part (workflow). Furthermore, you are able to unit test the logic part.

MVVM 不是消除背后代码的模式。就是将视图部分(外观、动画等)与逻辑部分(工作流)分离。此外,您可以对逻辑部分进行单元测试。

I know enough scenarios where you have to write code behind because data binding is not a solution to everything. In your scenario I would handle the DoubleClick event in the code behind file and delegate this call to the ViewModel.

我知道足够多的场景,您必须在后面编写代码,因为数据绑定不是所有问题的解决方案。在您的场景中,我将在文件隐藏代码中处理 DoubleClick 事件并将此调用委托给 ViewModel。

Sample applications that use code behind and still fulfill the MVVM separation can be found here:

可以在此处找到使用隐藏代码并仍然实现 MVVM 分离的示例应用程序:

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

WPF 应用程序框架 (WAF) - http://waf.codeplex.com

回答by Rushui Guan

I am able to get this to work with .NET 4.5. Seems straight forward and no third party or code behind needed.

我能够让它与 .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.png" Width="48" Height="48"/>
                    <TextBlock Grid.Row="1" Text="{Binding Name}" />
                </Grid>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>

回答by rmoore

I like to use Attached Command Behaviorsand Commands. Marlon Grechhas a very good implementation of the Attached Command Behaviors. Using these, we could then assign a style to the ListView's ItemContainerStyleproperty that will set the command for each ListViewItem.

我喜欢使用附加命令行为和命令。Marlon Grech有一个非常好的附加命令行为的实现。使用这些,我们可以为 ListView 的ItemContainerStyle属性分配一个样式,该属性将为每个 ListViewItem 设置命令。

Here we set the command to be fired on the MouseDoubleClick event, and the CommandParameter, will be the data object that we click on. Here I'm traveling up the visual tree to get the command that I'm using, but you could just as easily create application wide commands.

这里我们设置了在 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>

For the commands, you can either implement an ICommanddirectly, or use some of the helpers like those that come in the MVVM Toolkit.

对于命令,您可以直接实现ICommand,也可以使用一些帮助程序,例如MVVM Toolkit 中的帮助程序

回答by Gunter

I have found a very easy and clean way to do this with the Blend SDK Event triggers. Clean MVVM, reusable and no code-behind.

我找到了一种非常简单和干净的方法来使用 Blend SDK 事件触发器来做到这一点。干净的 MVVM,可重用且无代码隐藏。

You probably already have something like this:

你可能已经有了这样的东西:

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

Now include a ControlTemplate for the ListViewItem like this if you don't already use one:

如果您还没有使用过,现在为 ListViewItem 包含一个 ControlTemplate:

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

The GridViewRowPresenter will be the visual root of all elements "inside" making up a list row element. Now we could insert a trigger there to look for MouseDoubleClick routed events and call a command via InvokeCommandAction like this:

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>

If you have visual elements "above" the GridRowPresenter (probalby starting with a grid) you can also put the Trigger there.

如果您在 GridRowPresenter 的“上方”有视觉元素(可能从网格开始),您也可以将触发器放在那里。

Unfortunately MouseDoubleClick events are not generated from every visual element (they are from Controls, but not from FrameworkElements for example). A workaround is to derive a class from EventTrigger and look for MouseButtonEventArgs with a ClickCount of 2. This effectively filters out all non-MouseButtonEvents and all MoseButtonEvents with a ClickCount != 2.

不幸的是,MouseDoubleClick 事件不是从每个视觉元素生成的(例如,它们来自控件,但不是来自 FrameworkElements)。一种解决方法是从 EventTrigger 派生一个类并查找 ClickCount 为 2 的 MouseButtonEventArgs。这有效地过滤掉所有非 MouseButtonEvents 和所有 ClickCount != 2 的 MoseButtonEvents。

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

Now we can write this ('h' is the Namespace of the helper class above):

现在我们可以这样写('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>

回答by Aaron

I realize that this discussion is a year old, but with .NET 4, are there any thoughts on this solution? I absolutely agree that the point of MVVM is NOT to eliminate a code behind file. I also feel very strongly that just because something is complicated, doesn't mean it's better. Here is what I put in the code behind:

我意识到这个讨论已经有一年了,但是对于 .NET 4,对这个解决方案有什么想法吗?我绝对同意 MVVM 的重点不是消除文件背后的代码。我也非常强烈地感觉到,仅仅因为事情复杂,并不意味着它更好。这是我在后面的代码中添加的内容:

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

回答by Timothy Pratley

I am finding it simpler to link the command when the view is created:

我发现在创建视图时链接命令更简单:

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

In my case BindAndShowlooks like this (updatecontrols+avalondock):

在我的情况下BindAndShow看起来像这样(updatecontrols+avalondock):

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

Though the approach should work with whatever method you have of opening new views.

尽管该方法应该适用于您打开新视图的任何方法。

回答by idursun

You can use Caliburn's Action feature to map events to methods on your ViewModel. Assuming you have an ItemActivatedmethod on your ViewModel, then corresponding XAML would look like:

您可以使用Caliburn的 Action 功能将事件映射到 ViewModel 上的方法。假设您的 上有一个ItemActivated方法ViewModel,那么相应的 XAML 将如下所示:

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

For further details you can examine Caliburn's documentation and samples.

有关更多详细信息,您可以查看 Caliburn 的文档和示例。

回答by user3235445

I saw the solution from rushuiwith the InuptBindings but I was still unable to hit the area of the ListViewItem where there was no text - even after setting the background to transparent, so I solved it by using different templates.

我用 InuptBindings看到了rushui的解决方案,但我仍然无法点击 ListViewItem 没有文本的区域 - 即使将背景设置为透明,所以我通过使用不同的模板解决了它。

This template is for when the ListViewItem has been selected and is active:

此模板适用于已选择 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>

This template is for when the ListViewItem has been selected and is inactive:

此模板适用于已选择 ListViewItem 且处于非活动状态的情况:

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

This is the Default style used for the ListViewItem:

这是用于 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>

What I don't like is the repetition of the TextBlock and its text binding, I don't know I I can get around declaring that in just the one location.

我不喜欢 TextBlock 及其文本绑定的重复,我不知道 II 可以绕过在一个位置声明它。

I hope this helps someone!

我希望这可以帮助别人!

回答by Prince Owen

Here's a behavior that gets that done on both ListBoxand ListView.

这是一个在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
}

Here's the extension class used to find the parent.

这是用于查找父级的扩展类。

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

Usage:

用法:

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>

回答by luis_laurent

I succeed to make this functionality with .Net 4.7 framework by using the interactivity library, first of all make sure of declaring the namespace in the XAML file

我通过使用交互库成功地使用 .Net 4.7 框架实现了此功能,首先确保在 XAML 文件中声明命名空间

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

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

Then set the Event Trigger with his respective InvokeCommandAction inside the ListView like below.

然后在 ListView 中使用他各自的 InvokeCommandAction 设置事件触发器,如下所示。

View:

看法:

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

Adapting the code above should be enough to make the double click event work on your ViewModel, however I added you the Model and View Model class from my example so you can have the full idea.

修改上面的代码应该足以使双击事件在您的 ViewModel 上工作,但是我从我的示例中添加了 Model 和 View Model 类,以便您可以有完整的想法。

Model:

模型:

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

    public string DevelopedBy { get; set; }
}

View Model:

查看型号:

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

In case you need the implementation of the DelegateCommandclass.

如果您需要DelegateCommand类的实现。