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

使用CommandGroup聚合WPF命令

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (22投票s)

2008年5月4日

CPOL

4分钟阅读

viewsIcon

129851

downloadIcon

1283

介绍一种将命令链接在一起的通用技术。

引言

本文讨论了一种将多个命令分组并按顺序执行的技术。我通过创建一个实现了WPF的ICommand接口的类CommandGroup来实现这一点。该类除了分组一组命令并将它们视为一个原子单元外,没有固有的行为或含义。这种设计类似于我在2006年8月创建的ValueConverterGroup类。

背景

我最近与一位WPF Disciple——Karl Shifflett——就他在一个WPF应用程序中遇到的问题进行了交流。假设你有一个窗口中的TextBox,以及一个带有“保存”按钮的ToolBar。假设TextBoxText属性绑定到一个业务对象上的属性,并且该绑定的UpdateSourceTrigger属性设置为默认值LostFocus,这意味着当TextBox失去输入焦点时,绑定值会推回到业务对象的属性。此外,假设ToolBar的“保存”按钮的Command属性设置为ApplicationCommands.Save命令。

在这种情况下,如果你编辑TextBox并单击鼠标保存按钮,就会出现问题。当单击ToolBar中的Button时,TextBox不会失去焦点。由于TextBoxLostFocus事件不会触发,Text属性绑定也不会将更改更新到业务对象的源属性。

显然,如果不将UI中最近编辑的值推送到对象中,就不应该进行验证和保存对象。这正是Karl试图解决的问题,他通过编写代码在窗口中手动查找具有焦点的TextBox并更新数据绑定的源。他的解决方案工作得很好,但这让我考虑一种通用的解决方案,这种解决方案在特定场景之外也很有用。CommandGroup应运而生……

介绍 CommandGroup

我为上述问题提出的解决方案是,给ToolBar的“保存”按钮一个依次执行两个命令的命令。首先,我需要一个执行命令来更新TextBoxText属性绑定,以便底层业务对象具有用户输入的新值。在此之后,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接口,但方式相当不寻常。它只是将CanExecuteExecute方法的所有调用委托给它的子命令。以下是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方法返回falseCommandGroup也会返回false。当被告知执行时,CommandGroup只是逐个执行其所有子命令。此外,当它的任何子命令引发CanExecuteChanged事件时,CommandGroup会通过引发自己的CanExecuteChanged事件将其冒泡到WPF的命令系统中。

演示应用程序

当您运行与本文相关的演示应用程序时,您会看到一个带有“保存”按钮的ToolBar,以及它下面的两个TextBox控件。这些TextBox绑定到一个Foo对象,该对象具有NameAge属性。编辑其中一个TextBox后,单击“保存”按钮将看到一个MessageBox,其中显示Foo对象中当前的值。

commandgroup-screenshot.png

在上面的屏幕截图中,我在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日 – 创建文章。
© . All rights reserved.