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

103
在使用MVVM的WPF应用程序中,我有一个带有ListView项的用户控件。在运行时,它将使用数据绑定来填充ListView中的对象集合。
正确的方法是如何将双击事件附加到ListView中的项目,以便在双击列表视图中的项目时,在视图模型中触发相应的事件并具有对单击的项目的引用?
如何以纯净的MVVM方式完成,即在View中没有后台代码?
10个回答

85

请注意,在IT技术中,代码后台并不是什么坏事。不幸的是,WPF社区中有很多人对此存在误解。

MVVM模式并不是为了消除代码后台,而是为了将视图部分(外观、动画等)与逻辑部分(工作流程)分开。此外,您可以对逻辑部分进行单元测试。

我知道很多场景下需要编写代码后台,因为数据绑定并不是万能的解决方案。在您的情况下,我会在代码后台文件中处理DoubleClick事件,并将其委托给ViewModel。

这里可以找到使用代码后台但仍符合MVVM分离的示例应用程序:

Win应用程序框架(WAF) - https://github.com/jbe2277/waf


5
说得好,我不愿意为了双击而使用那么多代码和额外的动态链接库! - Eduardo Molteni
4
这个只使用绑定的东西真让我头疼。这就像被要求只用一只手臂、一个眼罩和单腿站立来编码一样。双击应该很简单,我不明白为什么需要所有这些额外的代码。 - Echiban
3
很抱歉,我不完全同意你的观点。如果你说“代码后台不错”,那么我有一个问题:为什么我们不委托按钮的点击事件,而经常使用绑定(使用Command属性)呢? - Nam G VU
21
@Nam Gi VU: 当WPF控件支持命令绑定时,我总是更喜欢使用命令绑定。命令绑定不仅可以将“Click”事件转发到ViewModel(例如CanExecute),还可以执行其他操作。但是命令仅适用于最常见的情况。对于其他情况,我们可以使用代码后置文件,将非UI相关的问题委托给ViewModel或Model处理。 - jbe
3
现在我更了解你了!与你的讨论很愉快! - Nam G VU
显示剩余3条评论

79

我能够在 .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
似乎不适用于整个区域,例如我在Dock面板上执行此操作,它仅适用于Dock面板内有内容的位置(例如TextBlock、Image),而不适用于空白处。 - Stephen Drew
3
好的——又是这个老问题……需要将背景设置为透明才能接收鼠标事件,参见https://dev59.com/nGsz5IYBdhLWcg3wTl4T。 - Stephen Drew
9
我正在思考为什么这个方法适用于你们所有人,但对我不起作用。突然间我意识到,在项目模板的上下文中,数据上下文是来自项源的当前项,而不是主窗口的视图模型。因此,我使用了以下内容使其工作:<MouseBinding MouseAction="LeftDoubleClick" Command="{Binding Path=DataContext.EditBandCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"/> 在我的情况下,EditBandCommand 是页面视图模型上的命令,而不是绑定实体上的命令。 - naskew
只是想补充一下,InputBindings 从 .NET 3.0 开始提供,在 Silverlight 中 _不可用_。 - Martin
谢谢,我刚刚在我的网格容器中添加了明确设置的Background="Transparent",它完美地工作了。 - luis_laurent
显示剩余2条评论

43

我喜欢使用 附加命令行为 和命令。 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的工具。


1
+1 我发现这是在使用 WPF(Prism)复合应用程序指南时我首选的解决方案。 - Travis Heseman
1
您的代码示例中,名称空间“acb:”代表什么意思? - Nam G VU
@NamGiVU acb: = AttachedCommandBehavior。代码可以在答案的第一个链接中找到。 - Rachel
我尝试了这个,但是在CommandBehaviorBinding类的第99行出现了空指针异常。变量“strategy”为空。有什么问题吗? - etwas77

14

我发现使用Blend SDK事件触发器可以非常简单和清晰地完成这个操作。干净的MVVM,可重用且没有代码后台。

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

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

如果您还没有使用ControlTemplate,现在可以为ListViewItem添加一个像这样的ControlTemplate:

<Setter Property="Template">
  <Setter.Value>
    <ControlTemplate TargetType="{x:Type ListViewItem}">
      <GridViewRowPresenter Content="{TemplateBinding Content}"
                            Columns="{TemplateBinding GridView.ColumnCollection}" />
    </ControlTemplate>
  </Setter.Value>
 </Setter>
< p >GridViewRowPresenter将是所有元素“内部”的视觉根,构成列表行元素。现在我们可以在那里插入一个触发器来寻找MouseDoubleClick路由事件,并通过InvokeCommandAction调用命令,就像这样:< /p >
<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之上的可视元素(可能是网格),您也可以在那里放置触发器。

遗憾的是,双击鼠标事件并不会从每个可视元素生成(例如,它们来自控件,但不来自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);
        }
    }
}

现在我们可以这样编写代码('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中,而应该使用viewModel.DeleteSelectedCustomer或者实际代表该操作的其他名称。 - Marius

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

4
你可以使用Caliburn的Action功能将事件映射到ViewModel上的方法。假设你在ViewModel上有一个方法,那么相应的XAML代码如下:
<ListView x:Name="list" 
   Message.Attach="[Event MouseDoubleClick] = [Action ItemActivated(list.SelectedItem)]" >

您可以查看Caliburn的文档和示例以获取更多详细信息。


2

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

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

然后在 ListView 中设置带有其相应 InvokeCommandAction 的事件触发器,如下所示。

视图:

<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和ViewModel类,以便您可以完全理解。
模型:
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类,请参考以下内容。


1
我看到了rushui使用InputBindings的解决方案,但即使将背景设置为透明,我仍然无法点击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的重复和文本绑定,我不知道如何在一个位置声明来避免这种情况。
希望这能对某些人有所帮助!

这是一个很好的解决方案,我使用类似的方法,但实际上你只需要一个控件模板。如果用户要双击 listviewitem,他们可能并不关心它是否已经被选中。此外,需要注意的是,高亮效果可能还需要进行调整以匹配 listview 样式。点赞。 - David Bentley

1
这里有一个行为,可以在ListBoxListView上实现。
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>

对我来说,这个问题的最佳解决方案是: - BitKFu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接