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


102

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

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

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

Answers:


76

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

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

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

可以在以下位置找到示例应用程序,这些示例应用程序使用背后的代码并仍实现MVVM分离:

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


5
好吧,我拒绝使用所有这些代码和一个额外的DLL来进行双击!
Eduardo Molteni,2009年

4
这种只用Binding的东西真让我头疼。就像被要求用一只手臂,一只眼睛放在眼罩上并站在一只腿上进行编码。双击应该很简单,我看不出所有这些额外的代码是值得的。
Echiban

1
恐怕我并不完全同意你的看法。如果您说“后面的代码还不错”,那么我有一个问题:为什么我们不为按钮委派click事件,而是经常使用绑定(使用Command属性)?
Nam G VU 2010年

21
@Nam Gi VU:当WPF控件支持命令绑定时,我总是喜欢使用命令绑定。命令绑定不仅仅将“ Click”事件中继到ViewModel(例如CanExecute)。但是命令仅适用于最常见的情况。对于其他情况,我们可以使用代码隐藏文件,然后在其中将与UI无关的问题委托给ViewModel或Model。
jbe

2
现在我更加了解您!很高兴与您讨论!
Nam G VU 2010年

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
似乎不适用于整个区域,例如,我在停靠面板上执行此操作,并且仅在停靠面板内有某物(例如文本块,图像)但空白处才起作用。
Stephen Drew 2014年

3
好的-这个古老的栗子再次...需要根据stackoverflow.com/questions/7991314/…
斯蒂芬·德鲁

6
我挠头想弄清楚为什么它对你们所有人而不对我有用。我突然意识到,在项目模板的上下文中,数据上下文是来自itemssource的当前项目,而不是主窗口的视图模型。因此,我使用以下命令使它正常工作<MouseBinding MouseAction =“ LeftDoubleClick” Command =“ {Binding Path = DataContext.EditBandCommand,RelativeSource = {RelativeSource AncestorType = {x:Type Window}}}” /> />页面视图模型上的命令而不是绑定实体上的命令。
naskew,2015年

naskew拥有了我对MVVM Light所需要的秘密,在双击的listboxitem中将命令参数作为模型对象,并将窗口的数据上下文设置为可显示命令的视图模型:<MouseBinding Gesture =“ LeftDoubleClick “ Command =” {Binding Path = DataContext.OpenSnapshotCommand,RelativeSource = {RelativeSource AncestorType = {x:Type Window}}}“ CommandParameter =” {Binding}“ />
MC5,2015年

只想添加InputBindings.NET 3.0中可用的内容,而Silverlight 中不可用。
马丁

44

我喜欢使用“ 附加命令行为和命令”。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
+1我发现这是与WPF(Prism)的复合应用程序指南一起使用时的首选解决方案。
特拉维斯·赫斯曼

1
上面的代码示例中,名称空间“ acb:”代表什么?
Nam G VU 2010年

@NamGiVU acb:= AttachedCommandBehavior。该代码可以在回答第一个链接中找到
雷切尔

我只是尝试这样做,并从类CommandBehaviorBinding第99行获取了空指针异常。变量“ strategy”为空。怎么了?
etwas77

13

我发现使用Blend SDK Event触发器可以非常简便地完成此任务。干净的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上方(以网格开头可能是probalby),则也可以将Trigger放在此处。

不幸的是,并非从每个可视元素生成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上,可能会出现问题。列之间的空白可能根本不会收到鼠标事件(可能的解决方法是使用对齐方式拉伸它们的样式)。
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

您可以使用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

我看到了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及其文本绑定,我不知道II只能在一个位置声明它。

我希望这可以帮助别人!


这是一个很好的解决方案,我使用了类似的解决方案,但实际上您只需要一个控件模板。如果用户要双击a listviewitem,则他们可能不在乎是否已选择它。同样重要的是要注意,可能还需要调整高光效果以匹配listview样式。投票赞成。
David Bentley

1

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


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>
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.