65.9K
CodeProject 正在变化。 阅读更多。
Home

MapperCommandBinding - 在 WPF 中映射命令

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.88/5 (6投票s)

2008年12月14日

Ms-PL

4分钟阅读

viewsIcon

51103

downloadIcon

392

如何将 Microsoft 的 Ribbon 控件与 MVVM 一起使用。

引言

在本文中,我将向您展示如何创建 `CommandBinding`,该 `CommandBinding` 将一个命令“映射”到 WPF 中的另一个命令,这样您就可以将 MVVM 架构与 Microsoft Ribbon 控件一起使用,从而避免“代码隐藏”文件中的混乱代码。

一些关于 Ribbon 控件和 MVVM 的阅读材料

背景

RibbonControls 和 Commands

不久前,Microsoft 发布了 WPF 的 Ribbon 控件。我在一个项目中开始使用它,但我意识到命令的使用方式与我们在 WPF 中使用命令的方式不同。使用 Ribbon 控件时,`RibbonCommand` 代表 ribbon (UI) 上的一个命令,它指定了我们将如何显示它。

...
<Window.Resources>
  <r:RibbonCommand
      x:Key="FirstRCmd"
      LabelTitle="1st Cmd"
      SmallImageSource="{StaticResource BlueEllipse}"
      LargeImageSource="{StaticResource BlueEllipse}"
      CanExecute="CanExecute_First"
      Executed="Execute_First"
      ToolTipTitle="3rd Command"
      ToolTipDescription="This is the 3rd command"/>
</Window.Resources>
...

在 ribbon 的控件 (RibbonButton, ...) 中,我们无法指定控件的外观,但可以为控件提供一个 `Command` (在这种情况下是 `RibbonCommand`),因此命令的表示 (标题、图像、工具提示等) 应在 `RibbonCommand` 对象中设置。

MVVM

对于我的 WPF 项目,我使用 MVVM 来避免混乱的代码。在大多数情况下,我不需要在窗口的代码隐藏文件中编写任何“逻辑”。因此,视图在 XAML 中指定,并通过绑定与 ViewModel 连接。

使用 MVVM,我可以使我的代码尽可能简单,而且它也非常可重用。

通常,我不使用简单的 `CommandBinding`,因为那样我必须在代码隐藏文件中创建 `CanExecute` 和 `Executed` 方法。取而代之的是,我使用了 Josh Smith 的 CommandSinkBinding 技术。

问题

如果您想将 MVVM 和 Ribbon 控件一起使用,您有两种选择

1. 代码隐藏中的方法

在代码隐藏中为 `RibbonCommand` 创建 `CanExecute` 和 `Executed` 方法,并在这些方法中将调用委托给 ViewModel 的方法。代码隐藏文件将包含大量难以管理的。大量的 `CanExecute` 和 `Executed` 方法,它们只是委托代码。

2. 在 ViewModel 中注册 RibbonCommands

您可以在 `ResourceDictionary` 或静态类中创建 RibbonCommands (在这种情况下,您应该在代码中指定命令的属性 - 麻烦)。然后,您应该像这样在 ViewModel 中注册这些 RibbonCommands

RegisterCommand( MyRibbonCommands.SampleRibbonCommand,
    param => CanExecuteSampleMethod( param ),
    param => SampleMethod( param ) );

但在这种情况下,ViewModel 和 View 会被绑定在一起,ViewModel 会知道实际的 View,也就是 RibbonCommands 的 View。

解决方案

概念

  1. 在 ViewModel 中创建一个 Command 并注册 ViewModel 中实现的事件处理程序。
  2. 创建一个代表 UI 上的命令的 RibbonCommand。
  3. 告知 RibbonCommand 实际要执行的命令是在 ViewModel 中注册的命令。

所以,我们应该以某种方式将 RibbonCommand“映射”到 ViewModel 的命令。

具体解决方案 - MapperCommandBinding

我以非常通用的方式解决了这个问题,基于描述的解决方案。我创建了一个名为 `MapperCommandBinding` 的 `CommandBinding`,它只是将一个命令映射到任何其他命令。`MapperCommandBinding` 的用法如下 (重要的部分加粗显示)

<Window
  ...
  cmd:CommandSinkBinding.CommanSink="{Binding}"
  ...>

  <Window.Resources>
    <r:RibbonCommand
        x:Key="FirstRCmd"
        LabelTitle="1st Cmd"
        SmallImageSource="{StaticResource BlueEllipse}"
        LargeImageSource="{StaticResource BlueEllipse}"/>
  </Window.Resources>

  <Window.CommandBindings>
      <!-- Commands used in this window which comes from the ViewModel -->
    <cmd:CommandSinkBinding Command="{x:Static vm:ViewModel.FirstSampleCommand}"/>
  
    <!-- Command mapping -->
    <cmd:MapperCommandBinding Command="{StaticResource FirstRCmd}"
       MappedToCommand="{x:Static vm:ViewModel.FirstSampleCommand}">
  </Window.CommandBindings>

  <DockPanel>
  
      <!--Sample Ribbon for the MapperCommandBinding-->
      <r:Ribbon DockPanel.Dock="Top">
          <r:RibbonTab Label="Sample">
              <r:RibbonGroup>
                  <r:RibbonButton Command="{StaticResource FirstRCmd}" />
              </r:RibbonGroup>
          </r:RibbonTab>
      </r:Ribbon>      
      ...
  </DockPanel>
  ...
</Window>

它是如何工作的?

`MapperCommandBinding` 是 `CommandBinding` 类的子类。它有一个名为 `MappedToCommand` 的额外属性。如果设置了此属性,`MapperCommandBinding` 会订阅 `CanExecute` 和 Executed 事件。

public class MapperCommandBinding : CommandBinding
{

    private ICommand _mappedToCommand = null;

    /// <summary>
    /// The command which will executed instead of the 'Command'.
    /// </summary>
    public ICommand MappedToCommand
    {
        get { return _mappedToCommand; }
        set
        {
            //mapped command cannot be null
            if ( value == null )
                throw new ArgumentException( "value" );

            this._mappedToCommand = value;

            this.CanExecute += OnCanExecute;
            this.Executed += OnExecuted;
        }
    }
    
    ...
}

OnExecuted 事件处理程序如下所示

public class MapperCommandBinding : CommandBinding
{
  ...
    protected void OnExecuted( object sender, ExecutedRoutedEventArgs e )
    {
        if ( MappedToCommand is RoutedCommand && e.Source is IInputElement )
            ( MappedToCommand as RoutedCommand ).Execute( e.Parameter, 
                                          e.Source as IInputElement );
        else
            MappedToCommand.Execute( e.Parameter );
        e.Handled = true;
    }
    ...
}

如果命令是 `RoutedCommand` 并且源是 `IInputElement`,它将从原始源开始在逻辑树中重新搜索 `CommandBinding`,但此时它将查找绑定到 `MappedToCommand` 的 `CommandBinding`。

因此,在上面的示例中,如果我们按下第一个 `RibbonButton`,就会开始搜索一个绑定到 `FirstRCmd` 的 CommandBinding。它会在 `` 中找到它。它是一个 `MapperCommandBinding`,所以它将从第一个 `RibbonButton` 开始重新搜索,但此时它将查找绑定到 `FirstSampleCommand` 的 CommandBinding。它会在 `` 中找到它。它由 ViewModel 处理,所以我们可以将 `RibbonCommand` 绑定到我们的 ViewModel 的命令。要做到这一点,我们只需要一行代码,看起来像这样

<cmd:MapperCommandBinding Command="{StaticResource FirstRCmd}" 
   MappedToCommand="{x:Static vm:ViewModel.FirstSampleCommand}">

结论

我认为这个问题已经以一种非常简单和声明式的方式解决了。我们只需要说明哪个命令应该真正处理我们的 `RibbonCommand`。我们 couldn't have done it easier。(如果您知道更好的方法,请告诉我。)

源代码和示例可以在 WPFExtensions 找到。

历史

  • 2008 年 12 月 13 日 - 初始修订。
© . All rights reserved.