WPF:如何使用 MVVM 将命令绑定到 ListBoxItem?

Posted

技术标签:

【中文标题】WPF:如何使用 MVVM 将命令绑定到 ListBoxItem?【英文标题】:WPF: How to bind a command to the ListBoxItem using MVVM? 【发布时间】:2011-07-29 13:07:42 【问题描述】:

我刚刚开始学习 MVVM。我已经按照MVVM tutorial 从头开始​​制作应用程序(我强烈推荐给所有 MVVM 初学者)。基本上,到目前为止,我创建的是几个文本框,用户可以在其中添加他或她的数据,一个用于保存该数据的按钮,该按钮随后会在 ListBox 中填充所有条目。

这就是我卡住的地方:我希望能够双击 ListBoxItem 并触发我创建并添加到 ViewModel 中的命令。我不知道如何完成 XAML 方面,即我不知道如何将该命令绑定到 ListBox(Item)。

这里是 XAML:

...
<ListBox 
    Name="EntriesListBox" 
    Width="228" 
    Height="208" 
    Margin="138,12,0,0" 
    HorizontalAlignment="Left" 
    VerticalAlignment="Top" 
    ItemsSource="Binding Entries" />
...

这里是 ViewModel:

public class MainWindowViewModel : DependencyObject

    ...
    public IEntriesProvider Entries
    
        get  return entries; 
    

    private IEntriesProvider entries;
    public OpenEntryCommand OpenEntryCmd  get; set; 

    public MainWindowViewModel(IEntriesProvider source)
    
        this.entries = source;
        ...
        this.OpenEntryCmd = new OpenEntryCommand(this);
    
    ...

最后,这是我希望在用户双击 EntriesListBox 中的项目后执行的 OpenEntryCommand:

public class OpenEntryCommand : ICommand

    private MainWindowViewModel viewModel;

    public OpenEntryCommand(MainWindowViewModel viewModel)
    
        this.viewModel = viewModel;
    

    public event EventHandler CanExecuteChanged
    
        add  CommandManager.RequerySuggested += value; 
        remove  CommandManager.RequerySuggested -= value; 
    

    public bool CanExecute(object parameter)
    
        return parameter is Entry;
    

    public void Execute(object parameter)
    
        string messageFormat = "Subject: 0\nStart: 1\nEnd: 2";
        Entry entry = parameter as Entry;
        string message = string.Format(messageFormat, 
                                       entry.Subject, 
                                       entry.StartDate.ToShortDateString(), 
                                       entry.EndDate.ToShortDateString());

        MessageBox.Show(message, "Appointment");
    

请帮忙,我将不胜感激。

【问题讨论】:

【参考方案1】:

不幸的是,只有ButtonBase 派生控件可以将ICommand 对象绑定到它们的Command 属性(对于Click 事件)。

但是,您可以使用 Blend 提供的 API 将事件(例如在您的情况下为 ListBox 上的 MouseDoubleClick)映射到 ICommand 对象。

<ListBox>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="MouseDoubleClick">
            <i:InvokeCommandAction Command="Binding YourCommand"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</ListBox>

您必须定义:xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 并引用 System.Windows.Interactivity.dll

-- 编辑-- 这是 WPF4 的一部分,但如果您不使用 WPF4,您可以使用 Microsoft.Windows.Interactivity。这个 dll 来自 Blend SDK,它不需要 Blend,来自这里: http://www.microsoft.com/downloads/en/details.aspx?FamilyID=f1ae9a30-4928-411d-970b-e682ab179e17&displaylang=en

更新:我发现了一些对你有帮助的东西。检查this link on MVVM Light Toolkit,其中包含有关如何执行此操作的演练,以及所需库的link。 MVVM Light Toolkit 是一个非常有趣的框架,用于将 MVVM 与 Silverlight、WPF 和 WP7 一起应用。

希望这会有所帮助:)

【讨论】:

我找不到 System.Windows.Interactivity.dll。我的猜测是它需要有 Blend 的 API。你能指点我到合适的位置吗?谢谢。 这是 WPF4 的一部分,但如果您不使用 WPF4,您可以使用 Microsoft.Windows.Interactivity。这个 dll 来自 Blend SDK,不需要 Blend,来自这里:microsoft.com/downloads/en/… 我下载并安装了 Microsoft Expression Blend 4 SDK。当我之后运行 VS2010 时,在为 Window 命名新命名空间时,智能感知下拉列表中没有提供“schemas.microsoft.com/expression/2010/interactivity”命名空间。我应该怎么做才能把它放在那里? @Boris:您必须添加对库的引用。单击项目上的“添加引用”并查找“System.Windows.Interactivity” -1:@AbdouMoumen:虽然这个答案大部分都可以,但它有一个棘手的问题:它将 mouse2click 绑定到 ListBox,而不是 ListBoxItem。这意味着如果 ListBox 显示其内部 滚动条,则双击此类滚动条将触发此命令。通常,这是非常不受欢迎的【参考方案2】:

由于 DoubleClick 事件,这变得很棘手。有几种方法可以做到这一点:

    在后面的代码中处理双击事件,然后在 ViewModel 上手动调用命令/方法 使用附加行为路由DoubleClick event to your Command 对map the DoubleClick event to your command 使用混合行为

2 和 3 可能更纯粹,但坦率地说,1 更容易,更简单,而且不是世界上最糟糕的事情。对于一次性案例,我可能会使用方法#1。

现在,如果您将要求更改为在每个项目上使用例如超链接,那会更容易。首先命名 XAML 中的根元素 - 例如,对于 Window:

<Window .... Name="This">

现在,在 ListBox 项的 DataTemplate 中,使用如下内容:

<ListBox ...>
  <ListBox.ItemTemplate>
    <DataTemplate>
      <Hyperlink 
        Command="Binding ElementName=This, Path=DataContext.OpenEntryCmd"
        Text="Binding Path=Name" 
        />

ElementName 绑定允许您从 ViewModel 的上下文而不是特定的数据项解析 OpenEntryCmd。

【讨论】:

保罗,我同意没有 1 是最简单的解决方案,而不是世界上最糟糕的事情,正如你所说的那样。但是,由于我的问题的目的是教育,我将坚持您提供的其他两个解决方案。当谈到第二种选择时,我一无所知 - 我根本没有得到提供的答案。第三个对我来说最有意义,但我会等待 AbdouMoumen 的回复,因为他的解决方案避免创建额外的类 (ExecuteCommandAction)。感谢大家的帮助! Abdou 得分较高的答案有一个不平凡的问题。从那里复制 cmets 没有意义,我只想说将命令绑定到 LIST 与将其绑定到每个 ITEM 完全不同。这种变化具有重要的后果,从“仅处理点击”的角度来看是不可见的。数据上下文变化和“双击滚动条”可能是最容易发现的。另一方面,保罗描述的所有三种方法都没有这个问题,因为它们将命令绑定到适当的目标。简而言之,他们是正确的,Abdou 的则不然。【参考方案3】:

我发现最好的方法是为我的内容创建一个简单的用户控件包装器,其中包含命令和参数的依赖属性。

我这样做的原因是按钮没有将点击事件冒泡到我的 ListBox,这阻止了它选择 ListBoxItem。

CommandControl.xaml.cs:

public partial class CommandControl : UserControl

    public CommandControl()
    
        MouseLeftButtonDown += OnMouseLeftButtonDown;
        InitializeComponent();
    

    private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs mouseButtonEventArgs)
    
        if (Command != null)
        
            if (Command.CanExecute(CommandParameter))
            
                Command.Execute(CommandParameter);
            
        
    

    public static readonly DependencyProperty CommandProperty =
        DependencyProperty.Register("Command", typeof(ICommand),
            typeof(CommandControl),
            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.None));

    public ICommand Command
    
        get  return (ICommand)GetValue(CommandProperty); 
        set  SetValue(CommandProperty, value); 
    

    public static readonly DependencyProperty CommandParameterProperty =
        DependencyProperty.Register("CommandParameter", typeof(object),
            typeof(CommandControl),
            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.None));

    public object CommandParameter
    
        get  return (object)GetValue(CommandParameterProperty); 
        set  SetValue(CommandParameterProperty, value); 
    

CommandControl.xaml:

<UserControl x:Class="WpfApp.UserControls.CommandControl"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         mc:Ignorable="d" 
         d:DesignHeight="300" d:DesignWidth="300"
         Background="Transparent">
</UserControl>

用法:

<ListBoxItem>
    <uc:CommandControl Command="Binding LoadPageCommand"
                       CommandParameter="Binding HomePageViewModel">
        <TextBlock Text="Home" Margin="0,0,0,5" VerticalAlignment="Center"
                   Foreground="White" FontSize="24" />
    </uc:CommandControl>
</ListBoxItem>

Content可以是任意的,当控件被点击时会执行命令。

编辑:Background="Transparent" 添加到 UserControl 以启用控件整个区域的点击事件。

【讨论】:

谢谢!运行完美,似乎是 View Viewmodel 分层最干净的解决方案。 我必须将自定义事件从我的 UserControl 绑定到主视图中的命令。交互性不适用于自定义事件,我正在努力处理它。创建自定义命令而不是事件对我来说是最聪明的,实际上也是最优雅的解决方案。至少目前是这样。谢谢! 我的荣幸!我推荐使用 MVVM 框架,那么你就不必做任何这些了。例如,在 Caliburn.Micro 中,您可以使用 Actions 来处理 ViewModel 中的 XAML 事件,例如:&lt;Button cal:Message.Attach="[Event Click] = [Action OnClick]" /&gt; 其中OnClick 是 ViewModel 中的一个方法。比这更优雅:-)【参考方案4】:

这有点小技巧,但它运行良好,允许您使用命令并避免代码落后。这还有一个额外的好处,即当您在空的 ScrollView 区域中双击(或任何您的触发器)时不会触发命令,假设您的 ListBoxItems 没有填满整个容器。

基本上,只需为您的ListBox 创建一个由TextBlock 组成的DataTemplate,并将TextBlock 的宽度绑定到ListBox 的宽度,将边距和填充设置为0,并禁用水平滚动(因为TextBlock 会超出ScrollView 的可见边界,否则会触发水平滚动条)。我发现的唯一错误是,如果用户精确单击ListBoxItem 的边框,该命令将不会触发,我可以忍受。

这是一个例子:

<ListBox
    x:Name="listBox"
    Width="400"
    Height="150"
    ScrollViewer.HorizontalScrollBarVisibility="Hidden"
    ItemsSource="Binding ItemsSourceProperty"
    SelectedItem="Binding SelectedItemProperty">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Padding="0" 
                        Margin="0" 
                        Text="Binding DisplayTextProperty" 
                        Width="Binding ElementName=listBox, Path=Width">
                <TextBlock.InputBindings>
                    <MouseBinding 
                        Command="Binding RelativeSource=RelativeSource FindAncestor, AncestorType=x:Type ListBox, Path=DataContext.SelectProjectCommand" 
                                    Gesture="LeftDoubleClick" />
                </TextBlock.InputBindings>
            </TextBlock>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

【讨论】:

【参考方案5】:

如果您正在寻找一个不错的简单解决方案,该解决方案使用交互而不是使用用户控件、代码隐藏、输入绑定、自定义附加属性等。

并且您想要在 ListBoxItem 级别工作的东西,即不是按照(错误)接受的解决方案的 ListBox 级别。

然后这里是一个简单的“按钮之类”点击操作的 sn-p..

<ListBox>
  <ListBox.ItemTemplate>
    <DataTemplate>
      <Grid Background="Transparent">
        <!-- insert your visuals here -->
        
        <b:Interaction.Triggers>
          <b:EventTrigger EventName="MouseUp">
            <b:InvokeCommandAction Command="Binding YourCommand" />
          </b:EventTrigger>
        </b:Interaction.Triggers>
      </Grid>
    </DataTemplate>
  </ListBox.ItemTemplate>
</ListBox>

注意,需要 background="Transparent" 以确保整个 Grid 是可点击的,而不仅仅是里面的内容。

【讨论】:

【参考方案6】:

我最近还需要在双击 ListBoxItem 时触发 ICommand

就我个人而言,我不喜欢DataTemplate 方法,因为它绑定到ListBoxItem 容器内的内容,而不是容器本身。我选择使用附加属性在容器上分配InputBinding。它需要更多的肘部油脂,但效果很好。

首先,我们需要创建一个附加的属性类。我已经为派生自FrameworkElement 的任何类创建了一个更通用的类,以防万一我以不同的视觉效果再次遇到这个问题。

public class FrameworkElementAttachedProperties : DependencyObject

    public static readonly DependencyProperty DoubleClickProperty = DependencyProperty.RegisterAttached("DoubleClick", typeof(InputBinding),
        typeof(FrameworkElementAttachedProperties), new PropertyMetadata(null, OnDoubleClickChanged));

    public static void SetDoubleClick(FrameworkElement element, InputBinding value)
    
        element.SetValue(DoubleClickProperty, value);
    

    public static InputBinding GetDoubleClick(FrameworkElement element)
    
        return (InputBinding)element.GetValue(DoubleClickProperty);
    

    private static void OnDoubleClickChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    
        FrameworkElement element = obj as FrameworkElement;
        
        /// Potentially throw an exception if an object is not a FrameworkElement (is null).
        
        if(e.NewValue != null)
        
            element.InputBindings.Add(e.NewValue as InputBinding);
        
        if(e.OldValue != null)
        
            element.InputBindings.Remove(e.OldValue as InputBinding);
        
    

那么最后一步是覆盖ListBoxItem 的基本容器样式。

<ListBox.ItemContainerStyle>
    <Style TargetType="x:Type ListBoxItem"
        BasedOn="StaticResource ListBoxItem">
        <Setter Property="local:FrameworkElementAttachedProperties.DoubleClick">
            <Setter.Value>
                <MouseBinding Command="Binding OnListBoxItemDoubleClickCommand"
                    MouseAction="LeftDoubleClick"/>
            </Setter.Value>
        </Setter>
    </Style>
</ListBox.ItemContainerStyle>

现在,只要双击ListBoxItem,它就会触发我们的OnListBoxItemDoubleClickCommand

【讨论】:

以上是关于WPF:如何使用 MVVM 将命令绑定到 ListBoxItem?的主要内容,如果未能解决你的问题,请参考以下文章

WPF:将 ContextMenu 绑定到 MVVM 命令

WPF如何将mousedown(命令/动作)绑定到标签

在WPF使用中读取一个配置文件获得一个结构体list,然后将数据绑定到Combobox下拉列表框中,如何实现?

WPF 将 UI 事件绑定到 ViewModel 中的命令

将 WPF 快捷键绑定到 ViewModel 中的命令

WPF MVVM:如何将 GridViewColumn 绑定到 ViewModel-Collection?