WPF MVVM:命令很简单。如何使用 RoutedEvent 连接 View 和 ViewModel

Posted

技术标签:

【中文标题】WPF MVVM:命令很简单。如何使用 RoutedEvent 连接 View 和 ViewModel【英文标题】:WPF MVVM : Commands are easy. How to Connect View and ViewModel with RoutedEvent 【发布时间】:2010-10-25 03:09:42 【问题描述】:

假设我有一个在资源字典中实现为 DataTempate 的视图。 我有一个相应的 ViewModel。 绑定命令很容易。但是,如果我的 View 包含一个 ListBox 之类的控件,并且我需要根据 List 上正在更改的项目发布一个应用程序范围的事件(使用 Prism 的事件聚合器)。

如果 ListBox 支持命令,我可以将它绑定到 ViewModel 中的命令并发布事件。但是 Listbox 不允许这样的选项。 我该如何解决这个问题?

编辑: 很多很棒的答案。

看看这个链接http://blogs.microsoft.co.il/blogs/tomershamam/archive/2009/04/14/wpf-commands-everywhere.aspx

谢谢

爱丽儿

【问题讨论】:

【参考方案1】:

我没有尝试将命令绑定到项目更改时,而是以另一种方式看待问题。

如果您将 ListBox 的选定项绑定到 ViewModel 中的属性,那么当该属性发生更改时,您可以发布事件。这样,ViewModel 仍然是事件的来源,它是由项目更改触发的,这正是您想要的。

<ListBox ItemsSource="Binding Items" SelectedItem="Binding SelectedItem" />

...

public class ViewModel

    public IEnumerable<Item> Items  get; set;  

    private Item selectedItem;
    public Item SelectedItem
    
        get  return selectedItem; 
        set
        
            if (selectedItem == value)
                return;
            selectedItem = value;
            // Publish event when the selected item changes
        

【讨论】:

绑定不应该是实现这一点的两种方式吗? SelectedItem 上的默认绑定是双向的。见msdn.microsoft.com/en-us/library/…【参考方案2】:

扩展控件以支持 ICommandSource 并决定哪个动作应该触发命令。

我使用组合框执行此操作,并使用 OnSelectionChanged 作为命令的触发器。首先,我将在 XAML 中展示如何将命令绑定到我称为 CommandComboBox 的扩展 Control ComboBox,然后我将展示 CommandComboBox 的代码,它将对 ICommandSource 的支持添加到 ComboBox。

1) 在 XAML 代码中使用 CommandComboBox:

在您的 XAML 命名空间声明中包含

   xmlns:custom="clr-namespace:WpfCommandControlsLibrary;assembly=WpfCommandControlsLibrary">

使用 CommandComboBox 代替 ComboBox 并将命令绑​​定到它,如下所示:请注意,在此示例中,我在我的 ViewModel 中定义了一个名为 SetLanguageCommand 的命令,并且我将此 ComboBox 的选定值作为参数传递给命令。

 <custom:CommandComboBox 
    x:Name="ux_cbSelectLanguage"
    ItemsSource="Binding Path = ImagesAndCultures"
    ItemTemplate="DynamicResource LanguageComboBoxTemplate"           
    Command="Binding Path=SetLanguageCommand, Mode=Default"
    CommandParameter="Binding RelativeSource=x:Static RelativeSource.Self, Path=SelectedValue, Mode=Default"
    IsSynchronizedWithCurrentItem="True" 
    HorizontalAlignment="Right" 
    VerticalAlignment="Center" 
    Grid.Column="1" Margin="0,0,20,0" Style="DynamicResource GlassyComboBox" ScrollViewer.IsDeferredScrollingEnabled="True"
 />

2) CommandComboBox 的代码

CommandComboBox.cs 文件的代码包含在下面。我将此文件添加到名为 WpfCommandControlsLibrary 的类库中,并使其成为一个单独的项目,因此我可以轻松地将任何扩展命令添加到使用它们所需的任何解决方案中,因此我可以轻松添加其他 WPF 控件并扩展它们以支持 ICommandSource 接口。

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace WpfCommandControlsLibrary

   /// <summary>
   /// Follow steps 1a or 1b and then 2 to use this custom control in a XAML file.
   ///
   /// Step 1a) Using this custom control in a XAML file that exists in the current project.
   /// Add this XmlNamespace attribute to the root element of the markup file where it is 
   /// to be used:
   ///
   ///     xmlns:MyNamespace="clr-namespace:WpfCommandControlsLibrary"
   ///
   ///
   /// Step 1b) Using this custom control in a XAML file that exists in a different project.
   /// Add this XmlNamespace attribute to the root element of the markup file where it is 
   /// to be used:
   ///
   ///     xmlns:MyNamespace="clr-namespace:WpfCommandControlsLibrary;assembly=WpfCommandControlsLibrary"
   ///
   /// You will also need to add a project reference from the project where the XAML file lives
   /// to this project and Rebuild to avoid compilation errors:
   ///
   ///     Right click on the target project in the Solution Explorer and
   ///     "Add Reference"->"Projects"->[Select this project]
   ///
   ///
   /// Step 2)
   /// Go ahead and use your control in the XAML file.
   ///
   ///     <MyNamespace:CustomControl1/>
   ///
   /// </summary>

   public class CommandComboBox : ComboBox, ICommandSource
   
      public CommandComboBox() : base()
      
      

  #region Dependency Properties
  // Make Command a dependency property so it can use databinding.
  public static readonly DependencyProperty CommandProperty =
      DependencyProperty.Register(
          "Command",
          typeof(ICommand),
          typeof(CommandComboBox),
          new PropertyMetadata((ICommand)null,
          new PropertyChangedCallback(CommandChanged)));

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

  // Make CommandTarget a dependency property so it can use databinding.
  public static readonly DependencyProperty CommandTargetProperty =
      DependencyProperty.Register(
          "CommandTarget",
          typeof(IInputElement),
          typeof(CommandComboBox),
          new PropertyMetadata((IInputElement)null));

  public IInputElement CommandTarget
  
     get
     
        return (IInputElement)GetValue(CommandTargetProperty);
     
     set
     
        SetValue(CommandTargetProperty, value);
     
  

  // Make CommandParameter a dependency property so it can use databinding.
  public static readonly DependencyProperty CommandParameterProperty =
      DependencyProperty.Register(
          "CommandParameter",
          typeof(object),
          typeof(CommandComboBox),
          new PropertyMetadata((object)null));

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

  #endregion

  // Command dependency property change callback.
  private static void CommandChanged(DependencyObject d,
      DependencyPropertyChangedEventArgs e)
  
     CommandComboBox cb = (CommandComboBox)d;
     cb.HookUpCommand((ICommand)e.OldValue, (ICommand)e.NewValue);
  

  // Add a new command to the Command Property.
  private void HookUpCommand(ICommand oldCommand, ICommand newCommand)
  
     // If oldCommand is not null, then we need to remove the handlers.
     if (oldCommand != null)
     
        RemoveCommand(oldCommand, newCommand);
     
     AddCommand(oldCommand, newCommand);
  

  // Remove an old command from the Command Property.
  private void RemoveCommand(ICommand oldCommand, ICommand newCommand)
  
     EventHandler handler = CanExecuteChanged;
     oldCommand.CanExecuteChanged -= handler;
  

  // Add the command.
  private void AddCommand(ICommand oldCommand, ICommand newCommand)
  
     EventHandler handler = new EventHandler(CanExecuteChanged);
     canExecuteChangedHandler = handler;
     if (newCommand != null)
     
        newCommand.CanExecuteChanged += canExecuteChangedHandler;
     
  
  private void CanExecuteChanged(object sender, EventArgs e)
  

     if (this.Command != null)
     
        RoutedCommand command = this.Command as RoutedCommand;

        // If a RoutedCommand.
        if (command != null)
        
           if (command.CanExecute(CommandParameter, CommandTarget))
           
              this.IsEnabled = true;
           
           else
           
              this.IsEnabled = false;
           
        
        // If a not RoutedCommand.
        else
        
           if (Command.CanExecute(CommandParameter))
           
              this.IsEnabled = true;
           
           else
           
              this.IsEnabled = false;
           
        
     
  

  // If Command is defined, selecting a combo box item will invoke the command;
  // Otherwise, combo box will behave normally.
  protected override void OnSelectionChanged(SelectionChangedEventArgs e)
  
     base.OnSelectionChanged(e);

     if (this.Command != null)
     
        RoutedCommand command = Command as RoutedCommand;

        if (command != null)
        
           command.Execute(CommandParameter, CommandTarget);
        
        else
        
           ((ICommand)Command).Execute(CommandParameter);
        
     
  

  // Keep a copy of the handler so it doesn't get garbage collected.
  private static EventHandler canExecuteChangedHandler;

  

【讨论】:

谢谢,这是一个很好的答案,对不起,我也不能给你积分。问候。【参考方案3】:

一种选择是扩展相关控件并添加对您需要的特定命令的支持。比如我有modified ListView before支持ItemActivated事件和相关命令。

【讨论】:

我使用同样的技术来扩展组合框以支持命令。由于我没有可链接的博客,因此我将在单独的答案中包含我的解决方案。 @kent-boogaart 毫不奇怪,博客链接已失效,如果您将相关信息粘贴到 SO 帖子中将会很有用。【参考方案4】:

嗯,没有人回答。 所以我放弃了,将字典之外的 View 的实现移到了一个常规的 UserControl 中,我给他注入了一个对 ViewModel 的引用。

现在,当 ListBox 触发 Event 时,它会调用 ViewModel,然后一切皆有可能再次发生。

爱丽儿

【讨论】:

【参考方案5】:

此类问题的一个很好的解决方案来自附加属性的使用。 Marlon Grech 通过创建Attached Command Behaviors 将附加属性的使用提升到了一个新的水平。使用这些可以将 ViewModel 中存在的任何命令绑定到视图中存在的任何事件。

这是我在处理 ListBoxes 的类似问题时经常使用的东西,我希望它们在双击时打开、编辑或执行一些操作。

在这个例子中,我使用的是旧版本的附加命令行为,但效果是一样的。我有一种用于我明确键入的 ListBoxItems 的样式。 但是,创建一个应用程序或窗口范围的样式将很容易,该样式应用于所有将命令设置在更高级别的 ListBoxItem。然后,只要附加到 CommandBehavior.Event 属性的 ListBoxItem 的事件触发,它就会触发附加的 Command。

<!-- acb is the namespace reference to the Attached Command Behaviors -->
<Style x:Key="Local_OpenListItemCommandStyle">
    <Setter Property="acb:CommandBehavior.Event"
            Value="MouseDoubleClick" />
    <Setter Property="acb:CommandBehavior.Command"
            Value="Binding ElementName=uiMyListBorder, Path=DataContext.OpenListItemCommand" />
    <Setter Property="acb:CommandBehavior.CommandParameter"
            Value="Binding" />
</Style>

<DataTemplate x:Key="MyView">
<Border x:Name="uiMyListBorder">
<ListBox  ItemsSource="Binding MyItems"
          ItemContainerStyle="StaticResource local_OpenListItemCommandStyle" />
</Border>
</DataTemplate>

【讨论】:

【参考方案6】:

我一直在编写行为(附加属性)来执行此操作,但在某些情况下我仍然需要它们。

然而,对于通常情况,只需将事件绑定到命令,如果您安装了 Blend SDK 4,您可以在 Xaml 中执行所有操作。请注意,您必须添加对 System.Windows.Interactivity.dll 的引用,并重新分发此程序集。

Expression Blend SDK for .NET 4 Microsoft SDKs(供将来参考)

当触发 Grid 的 DragEnter 事件时,此示例在 ViewModel 上调用 ICommand DragEnterCommand:

<UserControl xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" >
    <Grid>
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="DragEnter">
                <i:InvokeCommandAction Command="Binding DragEnterCommand" CommandParameter="Binding ..." />
            </i:EventTrigger>
        </i:Interaction.Triggers>
    </Grid>
</UserControl>

【讨论】:

是否适用于非路由事件,例如 CollectionVeiwSource.Filter【参考方案7】:

尝试使用Prism 2。

它对命令进行了很好的扩展,并打开了许多新的可能性(比如绑定到可视树的命令)。

【讨论】:

当然我用的是prism2,它和问题有什么关系? Prism 命令仅支持可点击控件(继承自 ButtonBase),但您可以通过创建自定义命令行为来扩展命令。您可以通过从 CommandBhaviorBase 继承来做到这一点,如下面的代码所示: public class ListBoxItemChangedCommandBehavior : CommandBehaviorBase public ListBoxItemChangedCommandBehavior(ListBox control) : base(control) control.SelectedIndexChanged = OnSelectedIndexChanged; private void OnSelectedIndexChanged(object sender, EventArgs e) ExecuteCommand();

以上是关于WPF MVVM:命令很简单。如何使用 RoutedEvent 连接 View 和 ViewModel的主要内容,如果未能解决你的问题,请参考以下文章

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

WPF自学入门WPF MVVM模式Command命令

使用 Microsoft.Toolkit.Mvvm 和 Microsoft.Xaml.Behaviors.Wpf 将事件参数传递给命令

WPF MVVM:如何根据事件更新 UI 控制器

WPF:将 ContextMenu 绑定到 MVVM 命令

wpf mvvm下viewmodel中对view进行操作