【问题标题】:Firing a double click event from a WPF ListView item using MVVM使用 MVVM 从 WPF ListView 项触发双击事件
【发布时间】:2010-11-05 07:43:45
【问题描述】:

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

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

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

【问题讨论】:

    标签: wpf mvvm


    【解决方案1】:

    我喜欢使用Attached Command Behaviors 和命令。 Marlon Grech 对附加命令行为有很好的实现。使用这些,我们可以为 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 在使用 WPF 复合应用程序指南 (Prism) 时,我发现这是我的首选解决方案。
    • 在上面的代码示例中,命名空间 'acb:' 代表什么?
    • @NamGiVU acb: = AttachedCommandBehavior。代码可以在答案的第一个链接中找到
    • 我试过了,并从类 CommandBehaviorBinding 第 99 行获取空指针异常。变量“策略”为空。怎么了?
    【解决方案2】:

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

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

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

    【讨论】:

      【解决方案3】:

      拜托,代码落后根本不是一件坏事。不幸的是,WPF 社区中的很多人都搞错了。

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

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

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

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

      【讨论】:

      • 说得好,我拒绝使用所有代码和额外的 DLL 来进行双击!
      • 这个只使用Binding的东西真让我头疼。这就像被要求用 1 只手臂、1 只眼睛在眼罩上并用 1 条腿站立来编码。双击应该很简单,我看不出所有这些额外的代码是多么值得。
      • 恐怕我并不完全同意你的看法。如果您说“代码落后”,那么我有一个问题:为什么我们不委托按钮的单击事件,而是经常使用绑定(使用 Command 属性)?
      • @Nam Gi VU:当 WPF 控件支持命令绑定时,我总是更喜欢它。命令绑定不仅仅是将“点击”事件传递给 ViewModel(例如 CanExecute)。但命令仅适用于最常见的场景。对于其他场景,我们可以使用代码隐藏文件,并将与 UI 无关的问题委托给 ViewModel 或 Model。
      • 现在我更了解你了!与您讨论愉快!
      【解决方案4】:

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

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

      【讨论】:

      • 您的视图模型应该具有代表您可以在域中执行的操作的名称。什么是您域中的“ButtonClick”操作? ViewModel 在视图友好的上下文中表示域的逻辑,它不仅仅是视图的助手。所以:ButtonClick 永远不应该在视图模型上,使用 viewModel.DeleteSelectedCustomer 或者这个动作实际代表的任何东西。
      【解决方案5】:

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

      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 中工作。
      【解决方案6】:

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

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

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

      现在为 ListViewItem 添加一个 ControlTemplate,如果你还没有使用的话:

      <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 派生一个类并查找 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 上,可能会出现问题。列之间的空白空间可能根本不接收鼠标事件(可能一种解决方法是使用对齐拉伸设置它们的样式)。
      • 在这种情况下,最好在 GridViewRowPresenter 周围放置一个空网格并将触发器放在那里。这似乎有效。
      • 请注意,如果您像这样替换模板,则会丢失 ListViewItem 的默认样式。这对我正在开发的应用程序来说并不重要,因为它使用的是高度定制的样式。
      【解决方案7】:

      我能够让它与 .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>
      

      【讨论】:

      • 似乎不适用于整个区域,例如我在停靠面板上执行此操作,它仅适用于停靠面板中的某些内容(例如文本块、图像)而不是空白区域。
      • OK - 这个老栗子又来了...需要将背景设置为透明以接收鼠标事件,按照stackoverflow.com/questions/7991314/…
      • 我一直在摸索,想弄清楚为什么它对你们所有人都有效,而不是对我有用。我突然意识到,在项目模板的上下文中,数据上下文是来自 itemssource 的当前项目,而不是主窗口的视图模型。因此,我使用以下方法使其工作 在我的情况下,EditBandCommand 是页面视图模型上的命令不在绑定实体上。
      • naskew 有我在 MVVM Light 中需要的秘诀,在双击的 listboxitem 中获得一个命令参数作为模型对象,并且窗口的数据上下文设置为暴露命令:
      • 只想补充一点,InputBindings 在 .NET 3.0 中可用,而在 Silverlight 中可用。
      【解决方案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 及其文本绑定,我不知道我是否可以在一个位置声明它。

      我希望这对某人有帮助!

      【讨论】:

      • 这是一个很好的解决方案,我使用了类似的解决方案,但您实际上只需要一个控制模板。如果用户要双击listviewitem,他们可能不在乎它是否已经被选中。同样重要的是要注意突出显示效果可能还需要调整以匹配listview 样式。投票赞成。
      【解决方案9】:

      这是在 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>
      

      【讨论】:

        【解决方案10】:

        我通过使用交互库成功地在 .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 和 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 类。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2021-04-21
          • 2010-10-18
          • 2016-02-17
          • 2017-06-28
          • 1970-01-01
          • 2021-04-23
          • 2011-08-22
          • 2018-03-31
          相关资源
          最近更新 更多