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

31
我刚开始学习MVVM。我通过遵循这个MVVM教程(我强烈推荐给所有的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}" />
...

这里是视图模型:

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

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

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

请帮忙,我会很感激。

6个回答

70

很遗憾,只有从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 更新:我找到了一些可能会帮助您的东西。请查看MVVM Light Toolkit上的此链接,其中包含有关如何执行此操作的演练,以及所需库的链接。 MVVM Light Toolkit是一个非常有趣的框架,可在Silverlight,WPF和WP7中应用MVVM。
希望这可以帮到你 :)

我找不到 System.Windows.Interactivity.dll。我猜这是需要具备 Blend 的 API 才能使用。您能否指点一下合适的位置?谢谢。 - Boris
@AbdouMoumen,我不能只是将<i:EventTrigger>放在<ListBox>标签内。为了使它起作用,我应该用什么包装<i:EventTrigger>?我尝试过<ListBox.Triggers><i:EventTrigger>,但它不起作用。请帮帮我? - Boris
太好了,我现在能编译了,但命令没有被执行。可能是绑定或类似的问题。无论如何,AbdouMoumen,谢谢你帮助我!我真的很感激。(也感谢Elad) - Boris
16
虽然这个回答大部分都可以,但有一个棘手的问题:它将 mouse2click 绑定到了 ListBox 而不是 ListBoxItem。这意味着如果 ListBox 显示其内部滚动条,则双击此类滚动条将触发此命令。通常,这是非常不希望的。 - quetzalcoatl
不幸的是,添加对System.Windiows.Interaction.dll的引用是不够的。只有使用以下后期构建操作才能解决问题:copy /Y "%ProgramFiles%\Microsoft SDKs\Expression\Blend\.NETFramework\v4.0\Libraries\System.Windows.Interactivity.*" "$(TargetDir)" - Bernhard Hiller
显示剩余7条评论

10

由于DoubleClick事件的存在,这变得有些棘手。有几种方法可以解决:

  1. 在代码后台处理双击事件,然后在ViewModel上手动调用命令/方法
  2. 使用附加行为将DoubleClick事件路由到您的Command
  3. 使用Blend Behavior将DoubleClick事件映射到您的命令

2和3可能更纯粹,但老实说,1更容易、不太复杂,而且并不是世界上最糟糕的事情。对于单个案例,我可能会使用第一种方法。

现在,如果您改变了需求,比如说,要在每个项目上使用超链接,那么就更容易了。首先,在XAML中为根元素命名 - 例如,对于窗口:

<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
Paul,我同意1号是最简单的解决方案,而且并不像你所说的那样糟糕。然而,由于我的问题是为了教育目的,我将坚持你提供的另外两个解决方案。当涉及到第二种选择时,我一无所知 - 我根本不理解所提供的答案。第三个解决方案对我来说是最有意义的,但我会等待AbdouMoumen的回复一段时间,因为他的解决方案避免了创建额外的类(ExecuteCommandAction)。感谢所有的帮助! - Boris
6
Abdou的得分更高的答案有一个非平凡的问题。没有必要从那里复制评论,我只想说绑定LIST命令与将其绑定到每个ITEM是完全不同的。这种改变具有重要后果,从“处理点击”角度来看并不容易看到。数据上下文的更改和“双击滚动条”可能是最容易发现的问题。另一方面,保罗描述的所有三种方法都没有这些问题,因为它们将命令绑定到正确的目标上。简而言之,他们是正确的,Abdou的不正确。 - quetzalcoatl

5

编辑:我写这篇文章时是一个缺乏经验的WPF开发者,如今我会使用提供事件绑定功能的框架,或者简单地使用一个按钮并重新设计样式。当然,为了最大灵活性,也许这种做法更好。

我认为做到这一点的最佳方式是为我的内容创建一个简单的用户控件包装器,具有命令和参数的依赖属性。

我之所以这样做,是因为按钮不会将点击事件冒泡到我的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>

内容可以是任何东西,当控件被点击时,它会执行命令。

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


非常感谢!运行完美,似乎是使用View Viewmodel分层的最干净的解决方案。 - Nelly
我必须将自定义UserControl的事件绑定到主视图中的一个命令。交互性无法与自定义事件一起工作,我很难处理它。创建自定义命令而不是事件对我来说是最聪明且最优雅的解决方案。至少目前是这样。谢谢! - Efthymios
非常愉快!我建议使用MVVM框架,这样你就不必做任何事情了。例如在Caliburn.Micro中,您可以使用Actions来处理ViewModel中的XAML事件,例如:<Button cal:Message.Attach="[Event Click] = [Action OnClick]" />其中OnClick是您的ViewModel中的一个方法。比这更优雅得多 :-) - Shahin Dohan

3
这是一种有点取巧的方法,但它可以很好地使用命令并避免编写代码。这还有一个额外的好处,就是不会在双击(或者您的触发方式)空的ScrollView区域时触发命令,假设您的ListBoxItems没有填满整个容器。
基本上,只需为ListBox创建一个DataTemplate,由一个TextBlock组成,并将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>

1

最近我需要在双击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

0
如果你正在寻找一个漂亮简单的解决方案,它使用交互而不是搞乱用户控件、代码后台、输入绑定、自定义附加属性等等。
并且你想要的是一些在 ListBoxItem 级别工作的东西,即不像(错误地)被接受的解决方案那样在 ListBox 级别上操作。
那么这里有一段简单的“按钮式”点击操作的片段。
<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" 以确保整个网格可点击,而不仅仅是内部内容。

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