使用CommandGroup聚合WPF命令






4.95/5 (22投票s)
介绍一种将命令链接在一起的通用技术。
引言
本文讨论了一种将多个命令分组并按顺序执行的技术。我通过创建一个实现了WPF的ICommand
接口的类CommandGroup
来实现这一点。该类除了分组一组命令并将它们视为一个原子单元外,没有固有的行为或含义。这种设计类似于我在2006年8月创建的ValueConverterGroup
类。
背景
我最近与一位WPF Disciple——Karl Shifflett——就他在一个WPF应用程序中遇到的问题进行了交流。假设你有一个窗口中的TextBox
,以及一个带有“保存”按钮的ToolBar
。假设TextBox
的Text
属性绑定到一个业务对象上的属性,并且该绑定的UpdateSourceTrigger
属性设置为默认值LostFocus
,这意味着当TextBox
失去输入焦点时,绑定值会推回到业务对象的属性。此外,假设ToolBar
的“保存”按钮的Command
属性设置为ApplicationCommands.Save
命令。
在这种情况下,如果你编辑TextBox
并单击鼠标保存按钮,就会出现问题。当单击ToolBar
中的Button
时,TextBox
不会失去焦点。由于TextBox
的LostFocus
事件不会触发,Text
属性绑定也不会将更改更新到业务对象的源属性。
显然,如果不将UI中最近编辑的值推送到对象中,就不应该进行验证和保存对象。这正是Karl试图解决的问题,他通过编写代码在窗口中手动查找具有焦点的TextBox
并更新数据绑定的源。他的解决方案工作得很好,但这让我考虑一种通用的解决方案,这种解决方案在特定场景之外也很有用。CommandGroup
应运而生……
介绍 CommandGroup
我为上述问题提出的解决方案是,给ToolBar
的“保存”按钮一个依次执行两个命令的命令。首先,我需要一个执行命令来更新TextBox
的Text
属性绑定,以便底层业务对象具有用户输入的新值。在此之后,Save命令可以执行,并确保要保存的业务对象具有正确的值。以下是演示应用程序中创建此解决方案的XAML:
<ToolBar DockPanel.Dock="Top">
<Button Content="Save">
<Button.Command>
<!--
Chain together a set of commands that will sequentially
execute in the order they appear below. The first command
ensures that the focused TextBox's Text is pushed into the
source property before the Save is executed.
-->
<local:CommandGroup>
<x:StaticExtension Member="local:FlushFocusedTextBoxBindingCommand.Instance" />
<x:StaticExtension Member="ApplicationCommands.Save" />
</local:CommandGroup>
</Button.Command>
</Button>
</ToolBar>
当单击“保存”按钮时,首先会执行我的自定义FlushFocusedTextBoxBindingCommand
,强制具有焦点的TextBox
更新其数据源。如果具有键盘焦点的控件不是TextBox
,该命令会立即完成,一切正常。接下来,将执行标准的Save
路由命令,允许应用程序执行任何需要执行的操作来验证和保存业务对象。
CommandGroup的工作原理
CommandGroup
实现了ICommand
接口,但方式相当不寻常。它只是将CanExecute
和Execute
方法的所有调用委托给它的子命令。以下是CommandGroup
中为它提供子命令列表的代码:
private ObservableCollection<ICommand> _commands;
/// <summary>
/// Returns the collection of child commands. They are executed
/// in the order that they exist in this collection.
/// </summary>
public ObservableCollection<ICommand> Commands
{
get
{
if (_commands == null)
{
_commands = new ObservableCollection<ICommand>();
_commands.CollectionChanged += this.OnCommandsCollectionChanged;
}
return _commands;
}
}
void OnCommandsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// We have a new child command so our ability to execute may have changed.
this.OnCanExecuteChanged();
if (e.NewItems != null && 0 < e.NewItems.Count)
{
foreach (ICommand cmd in e.NewItems)
cmd.CanExecuteChanged += this.OnChildCommandCanExecuteChanged;
}
if (e.OldItems != null && 0 < e.OldItems.Count)
{
foreach (ICommand cmd in e.OldItems)
cmd.CanExecuteChanged -= this.OnChildCommandCanExecuteChanged;
}
}
void OnChildCommandCanExecuteChanged(object sender, EventArgs e)
{
// Bubble up the child commands CanExecuteChanged event so that
// it will be observed by WPF.
this.OnCanExecuteChanged();
}
有了这些基础设施,ICommand
的实现实际上相当简单。您可以在下面看到我是如何实现的:
public bool CanExecute(object parameter)
{
foreach (ICommand cmd in this.Commands)
if (!cmd.CanExecute(parameter))
return false;
return true;
}
public event EventHandler CanExecuteChanged;
protected virtual void OnCanExecuteChanged()
{
if (this.CanExecuteChanged != null)
this.CanExecuteChanged(this, EventArgs.Empty);
}
public void Execute(object parameter)
{
foreach (ICommand cmd in this.Commands)
cmd.Execute(parameter);
}
如上代码所示,CommandGroup
类只是一个命令容器。如果它的任何子命令的CanExecute
方法返回false
,CommandGroup
也会返回false
。当被告知执行时,CommandGroup
只是逐个执行其所有子命令。此外,当它的任何子命令引发CanExecuteChanged
事件时,CommandGroup
会通过引发自己的CanExecuteChanged
事件将其冒泡到WPF的命令系统中。
演示应用程序
当您运行与本文相关的演示应用程序时,您会看到一个带有“保存”按钮的ToolBar
,以及它下面的两个TextBox
控件。这些TextBox
绑定到一个Foo
对象,该对象具有Name
和Age
属性。编辑其中一个TextBox
后,单击“保存”按钮将看到一个MessageBox
,其中显示Foo
对象中当前的值。
在上面的屏幕截图中,我在Name
字段中添加了一个感叹号,然后单击了“保存”。如MessageBox
所示,在执行Save
命令之前,Foo
对象的Name
属性已更新。以下是Save
命令的执行逻辑:
private void Save_Executed(object sender, ExecutedRoutedEventArgs e)
{
Foo f = this.DataContext as Foo;
string msg = String.Format(
"Foo values:\r\nName={0}\r\nAge={1}",
f.Name,
f.Age);
MessageBox.Show(msg);
}
这是我编写的命令类,它查找具有焦点的TextBox
并更新已绑定Text
属性的源。如果需要,此类可以扩展以支持查找各种类型的焦点输入控件,例如ComboBox
。
public class FlushFocusedTextBoxBindingCommand : ICommand
{
#region Creation
public static readonly FlushFocusedTextBoxBindingCommand Instance;
static FlushFocusedTextBoxBindingCommand()
{
Instance = new FlushFocusedTextBoxBindingCommand();
// We need to know when any element gains keyboard focus
// so that we can raise the CanExecuteChanged event.
EventManager.RegisterClassHandler(
typeof(UIElement),
Keyboard.PreviewGotKeyboardFocusEvent,
(KeyboardFocusChangedEventHandler)OnPreviewGotKeyboardFocus);
}
static void OnPreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
Instance.OnCanExecuteChanged();
}
protected FlushFocusedTextBoxBindingCommand()
{
}
#endregion // Creation
#region ICommand Members
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
protected virtual void OnCanExecuteChanged()
{
if (this.CanExecuteChanged != null)
this.CanExecuteChanged(this, EventArgs.Empty);
}
public void Execute(object parameter)
{
TextBox focusedTextBox = Keyboard.FocusedElement as TextBox;
if (focusedTextBox == null)
return;
BindingExpression textBindingExpr =
focusedTextBox.GetBindingExpression(TextBox.TextProperty);
if (textBindingExpr == null)
return;
textBindingExpr.UpdateSource();
}
#endregion // ICommand Members
}
可能的改进
您可以通过多种方式增强CommandGroup
,但我将留给读者作为练习。我想到的一项很酷的功能是添加一个IsAsync
属性,当设置为true
时,它会使CommandGroup
在工作线程上并发执行所有子命令。当运行一个命令的副作用与其他命令无关时,这将非常有用。
另一件有用的事情是继承CommandGroup
并为它提供默认的内置子命令。假设您希望在任何命令执行之前执行一个自定义日志记录命令,您可以创建一个LoggingCommandGroup
,它始终首先执行一个日志记录命令。
可能性是无限的!:)
修订历史
- 2008年5月4日 – 创建文章。