MapperCommandBinding - 在 WPF 中映射命令
如何将 Microsoft 的 Ribbon 控件与 MVVM 一起使用。
引言
在本文中,我将向您展示如何创建 `CommandBinding`,该 `CommandBinding` 将一个命令“映射”到 WPF 中的另一个命令,这样您就可以将 MVVM 架构与 Microsoft Ribbon 控件一起使用,从而避免“代码隐藏”文件中的混乱代码。
一些关于 Ribbon 控件和 MVVM 的阅读材料
- Ribbon 控件
- 模型/视图/视图模型
背景
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。
解决方案
概念
- 在 ViewModel 中创建一个 Command 并注册 ViewModel 中实现的事件处理程序。
- 创建一个代表 UI 上的命令的 RibbonCommand。
- 告知 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。它会在 `
<cmd:MapperCommandBinding Command="{StaticResource FirstRCmd}"
MappedToCommand="{x:Static vm:ViewModel.FirstSampleCommand}">
结论
我认为这个问题已经以一种非常简单和声明式的方式解决了。我们只需要说明哪个命令应该真正处理我们的 `RibbonCommand`。我们 couldn't have done it easier。(如果您知道更好的方法,请告诉我。)
源代码和示例可以在 WPFExtensions 找到。
历史
- 2008 年 12 月 13 日 - 初始修订。