【问题标题】:WPF: How to bind a command to the ListBoxItem using MVVM?WPF:如何使用 MVVM 将命令绑定到 ListBoxItem?
【发布时间】: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");
    }
}

请帮忙,我将不胜感激。

【问题讨论】:

    标签: c# wpf mvvm binding commandbinding


    【解决方案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 事件,这变得很棘手。有几种方法可以做到这一点:

    1. 在后面的代码中处理双击事件,然后在 ViewModel 上手动调用命令/方法
    2. 使用附加行为路由DoubleClick event to your Command
    3. 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

        【讨论】:

          猜你喜欢
          • 2011-04-04
          • 2014-11-06
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-04-25
          • 1970-01-01
          • 2020-03-10
          相关资源
          最近更新 更多