WPF 命令模式应用






4.97/5 (29投票s)
在 WPF 应用程序中应用命令模式。
介绍
WPF 命令为将 GoF 命令模式集成到应用程序中提供了基础。本文介绍了一种实现以下目标的方法
程序架构
- 在层次结构中创建功能单元
- 减少大型应用程序的复杂性
- 自动化测试
- 模式和命令的可重用性
命令描述
- 命令描述应与所使用的 GUI 无关
- 命令应易于本地化
命令运行时行为
- 静态参数(每个命令实例)
- 上下文敏感的运行时数据
- 源提供的运行时参数 (
ICommandSource.CommandParameter
)
UI 集成
- 现有命令源(
Button
和MenuItem
)的图像和带有手势的工具提示 Button
控件禁用状态的可视化- 通过 XAML 或代码隐藏选择性绑定
- 集成现有的 WPF 命令,例如
System.Windows.Documents.EditingCommands
命令存储库
- 集中管理命令
- 命令的动态注册
- 访问所有命令
- 命令执行跟踪
所附示例演示了如何在 RTF 编辑器中使用命令模式
测试项目还展示了如何对命令进行单元测试。控件 CommandComboBox
展示了如何实现自定义 ICommandSource
。
命令
类 Command
,本身派生自 RoutedUICommand
,为所有命令提供了基础。它作为开发单个命令结构的公共祖先。以下概述显示了 RTF 编辑器的一些命令类
注意:此类层次结构仅用于演示命令结构,不适用于 RTF 编辑器的生产级使用。
命令类的实现紧凑且具有描述性,并为未来扩展步骤中的分析提供了明确的入口点
// ------------------------------------------------------------------------
public class EditUndo : EditorCommand
{
// ----------------------------------------------------------------------
public EditUndo( Type ownerType, CommandDescription description ) :
base( "EditUndo", ownerType, description )
{
} // EditUndo
// ----------------------------------------------------------------------
protected override bool OnCanExecute( EditorCommandContext commandContext )
{
if ( !base.OnCanExecute( commandContext ) )
{
return false;
}
return commandContext.TextBox.CanUndo;
} // OnCanExecute
// ----------------------------------------------------------------------
protected override void OnExecute( EditorCommandContext commandContext )
{
commandContext.TextBox.Undo();
} // OnExecute
} // class EditUndo
类 WrapperCommand
允许集成现有的 WPF 命令。以下示例展示了如何重用系统命令 EditingCommands.IncreaseIndentation
// ------------------------------------------------------------------------
public class IncreaseIndentation : WrapperCommand
{
// ----------------------------------------------------------------------
public IncreaseIndentation( Type ownerType, CommandDescription description ) :
base( EditingCommands.IncreaseIndentation, ownerType, description )
{
} // IncreaseIndentation
// ----------------------------------------------------------------------
protected override bool OnCanExecute( CommandContext commandContext )
{
if ( !base.OnCanExecute( commandContext ) )
{
return false;
}
// add some additional enabling conditions
return true;
} // OnCanExecute
} // class IncreaseIndentation
命令运行时行为
在命令执行期间,会创建一个命令上下文,该上下文封装所有运行时参数。下图记录了运行时行为
CommandContext
实例的创建委托给 Command
类本身,以支持单个上下文类型。RTF 编辑器示例为此目的使用了 ApplicationCommandContext
(错误处理)和 EditorCommandContext
(访问 RichTextBox
)类。
注意:将所有运行时值组合到 CommandContext
类中增强了未来修改的灵活性:可以在不更新所有现有命令的方法签名的情况下扩展 CommandContext
。
接下来将展示如何将 CommandContext
用于单元测试。
属性 Command.Target
提供了一种为每个命令实例设置参数的方法。由于命令的生命周期是静态的,因此此 Command.Target
属性对应于一个 static
参数。
UI 集成
使用属性 Command.HasRequirements
(默认值 = True)允许指定启用/禁用是否与命令相关。如果没有要求,命令始终处于活动状态,并且 Command.CanExecute()
将永远不会执行。
按钮命令
可以使用类 ButtonCommandProvider
将 Command
附加到 Button
控件
XAML
<Button cmd:ButtonCommandProvider.Command="{x:Static rtf:RichTextEditorCommands.EditUndo}" />
代码隐藏
Button undoButton = new Button();
ButtonCommandProvider.SetCommand( undoButton, RichTextEditorCommands.EditUndo );
ButtonCommandProvider
提供了各种功能方面,可以根据需要激活/停用
Button | 功能 | 路由命令 | 命令 |
Image | 插入按钮图像(在 Visual Studio XAML 设计器中预览) | 使用 WrapperCommand |
是 |
启用 | 禁用状态的可视化 | 按钮内容 | 按钮内容 |
工具提示 | 根据命令描述的工具提示 | 使用 WrapperCommand |
是 |
工具提示手势 | 根据命令描述的工具提示手势 | 是 | 是 |
可以使用 ButtonImage
类控制图像的显示。属性 ButtonCommandProvider.DisabledOpacity
确定非活动按钮的内容将如何显示。
图像的来源由 CommandImageService
类确定。此外,属性 Command.ImageUri
提供了一种为图像设置另一个来源的方法。默认情况下,按钮图像如下
- PNG 格式
- 命令程序集中的嵌入资源
- 位于“Images”文件夹中
实用程序 CommandToolTipService
控制按钮工具提示的格式。
菜单项命令
可以使用 MenuItemCommandProvider
类将 Command
附加到 MenuItem
控件
XAML
<Button cmd:MenuItemCommandProvider.Command="{x:Static rtf:RichTextEditorCommands.EditUndo}" />
代码隐藏
MenuItem undoMenuItem = new MenuItem();
MenuItemCommandProvider.SetCommand( undoMenuItem, RichTextEditorCommands.EditUndo );
MenuItemCommandProvider
提供了各种功能方面,可以根据需要激活/停用
MenuItem | 功能 | 路由命令 | 命令 |
Image | 在菜单项旁边显示图像 | 使用 WrapperCommand |
是 |
工具提示 | 根据命令描述的工具提示 | 使用 WrapperCommand |
是 |
工具提示手势 | 根据命令描述的工具提示手势 | 是 | 是 |
可以使用 MenuItemImage
类控制图像的显示。
工具提示的格式由 CommandToolTipService
类控制。
命令存储库
CommandRepository
提供对所有命令和命令绑定的访问。事件 CommandExecuting
和 CommandExecuted
允许跟踪命令执行。RTF 编辑器“帮助”菜单中的命令“命令统计”是一个简单的实现示例,它只列出所有注册的命令。RTF 编辑器的状态栏显示有关正在运行的命令和已执行命令总数的信息。
单元测试
在 CommandContext
的基础上,我们现在有一种方法来实现单独的测试用例。所附测试项目的以下测试方法展示了如何自动化命令测试
// ----------------------------------------------------------------------
[TestMethod]
public void TestToggleBoldDefault()
{
EditorCommandContext commandContext = CreateCommandContext();
// setup selection
TextRange startContent = new TextRange( commandContext.Document.ContentStart,
commandContext.Document.ContentEnd );
commandContext.Selection.Select( startContent.Start, startContent.End );
commandContext.Selection.ApplyPropertyValue( FlowDocument.FontWeightProperty,
FontWeights.Normal );
string startText = startContent.Text;
// run the command
FontWeight defaultFontWeigth = RichTextEditorCommands.ToggleBold.BoldFontWeight;
RichTextEditorCommands.ToggleBold.Execute( commandContext );
// tests
TextRange endContent = new TextRange( commandContext.Document.ContentStart,
commandContext.Document.ContentEnd );
string endText = endContent.Text;
Assert.AreEqual( startText, endText );
object selectionFontWeight =
commandContext.Selection.GetPropertyValue( FlowDocument.FontWeightProperty );
Assert.AreEqual( defaultFontWeigth, selectionFontWeight );
} // TestToggleBoldDefault
// ----------------------------------------------------------------------
private EditorCommandContext CreateCommandContext()
{
// document
FlowDocument flowDocument = new FlowDocument();
for ( int i = 0; i < 10; i++ )
{
Paragraph paragraph = new Paragraph();
for ( int n = 0; n < 5; n++ )
{
paragraph.Inlines.Add( new Run( "Paragraph Text " +
n.ToString() + " " ) );
}
flowDocument.Blocks.Add( paragraph );
}
// editor
RichTextBox richTextBox = new RichTextBox();
richTextBox.Document = flowDocument;
// command context
return new EditorCommandContext( richTextBox );
} // CreateCommandContext
自定义命令控件
根据 MSDN 中的描述,控件 CommandComboBox
展示了如何创建一个用作命令源的控件。选择列表项将执行命令。为了识别条目是否由用户选择(而不是通过编程方式),UIElementInput
类提供了相关的事件信息。
用法
要使用命令系统,建议采用以下方法
- 设计并实现命令类层次结构(可选:抽象类和单个命令上下文)
- 设计并实现命令单元测试(可选)
- 创建
CommandCollection
:将每个命令与CommandDescription
进行静态绑定 - 嵌入按钮图像资源(可选)
- 在 XAML 中添加
Command
引用 - 在应用程序启动时配置工具提示格式(可选)
- 在应用程序启动时设置
CommandRepository
:CommandRepository.AddRange()
- 在应用程序窗口中设置
CommandBinding
:CommandBindings.AddRange()
关注点
本文带来的更多见解
- 根据
CommandComboBox
类实例的状态禁用ComboBox
- 内容驱动的启用:一旦字体大小达到 25pt,命令
IncreaseFontSize
就会被禁用 - 识别 RTF 选中内容中的不同格式:
EditFontSizeCommand.GetFontSize()
RichTextEditorCommands
中CommandDescription
的本地化- 通过
CommandWindow.CurrentCommandInfo
的DependencyProperty
从 XAML 访问类值 - 在选择菜单项时,使用 XAML
Style
EventSetter
调用CommandWindow.OnStateUpdate()
和CommandWindow.OnStatusReset()
更新状态栏 - 通过类继承进行 XAML 样式设置,如示例
ButtonImage
和MenuItemImage
历史
- 2012 年 4 月 23 日 - v1.1.0.0
- 将
ButtonCommand
重命名为ButtonCommandProvider
- 将
MenuItemCommand
重命名为MenuItemCommandProvider
- 重构了源代码
- 更新了文章并修复了格式
- 将
- 2008 年 4 月 23 日
MenuItemCommand
的图像- 新类
ImageButton
和MenuItemButton
- 新类
WrapperCommand
- 新部分:兴趣点
- 2008 年 4 月 21 日
- 首次公开发布