八,WPF 命令

  1. WPF命令模型
    ICommand接口
    WPF命令模型的核心是System.Windows.Input.ICommand接口,该接口定义了命令的工作原理,它包含了两个方法和一个事件:
    public interface ICommand
    {
    void Execute(object parameter); //定义在调用此命令时调用的方法。
    bool CanExecute(object parameter); //此方法返回命令的状态,如果命令可用则返回true,否则返回false.
    event EventHandler CanExecuteChanged; //当命令状态改变时,引发该事件。
    }

    RoutedCommand类
    当创建自己的命令时,不会直接实现ICommand接口,而是使用System.Windows.Input.RoutedCommand类。它是WPF中唯一实例了ICommand接口的类,它为事件冒泡和隧道添加了一些额外的基础结构。为了支持路由事件,RoutedCommand类私有地实现了ICommand接口,并且添加了ICommand接口方法的一些不同的版本,最明显的变化是,Execute()和CanExecute()方法使用了一个额外参数。代码示例如下:

    public void Execute(object parameter, IInputElement target)
    {
    }
    public bool CanExecute(object parameter, IInputElement target)
    {
    }

    参数target是开始处理事件的元素,事件从target元素开始,然后冒泡至高层的容器,直到应用程序为了执行合适的任务而处理了事件。
    RoutedCommand类还引入了三个属性:Name(命令名称)、OwnerType(包含命令的类)及InputGestures集合(可以被用于触发命令的按键或鼠标操作)。

    RoutedUICommand类

    RoutedUICommand类只增加了一个属性 Text,它是命令显示的文本。在程序中处理的大部分命令不是RoutedCommand对象,而是RoutedUICommand类的实例,RoutedUICommand类继承自RoutedCommand类。而WPF提供的所有预先构建好的命令都是RoutedUICommand对象。RoutedUICommand类用于具有文本的命令,这些文本显示在用户界面中的某些地方(如菜单项文本,工具栏按钮的工具提示)。

    命令库

    因为每个应用程序可能都有大量的命令,且对于许多不同的应用程序,很多命令是通用的,为了减少创建这些命令所需要的工作,WPF提供了一个基本命令库,这些命令通过以下5个专门的静态类的静态属性提供:

    八,WPF 命令

    许多命令对象都是有一个额外的特征:默认输入绑定,例如,ApplicationCommands.Open命令被映射到Ctrl+O组合键,只要将命令绑定到一个命令源,并为窗口添加该命令源,这个组合键就会被激活,即使没有在用户界面的任何地方显示该命令也同样如此。

  2. 命令源

    命令源是一个实现了ICommandSource接口的控件,它定义了三个属性:

    八,WPF 命令

    例如,下面的按钮使用Command属性连接到ApplicationCommands.New命令:

    <Button Command="New">New</Button>

    此时,会看到按钮是被禁用的状态,这是因为按钮查询到命令还没有进行操作绑定,命令的状态为不可用,所以按钮也设置为不可用。

  3. 为命令进行操作绑定
    下在的代码片段为New命令创建绑定,可将这些代码添加到窗口的构造函数中:

    CommandBinding binding = new CommandBinding(ApplicationCommands.New);
    binding.Executed += new ExecutedRoutedEventHandler(binding_Executed);
    this.CommandBindings.Add(binding); void binding_Executed(object sender, ExecutedRoutedEventArgs e)
    {
    MessageBox.Show("New command triggered by " + e.Source.ToString());
    }

    尽管习惯上为窗口创建所有绑定,但CommandBindings属性实际上是在UIElement基类中定义的,所以任何元素都支持该属性,但为了得到最大的灵活性,命令绑定通常被添加到*窗口,如果希望在多个窗口中使用相同的命令,就需要在这些窗口中分别创建命令绑定。

    以上的命令绑定是使用代码生成的,但,如果希望精简代码隐藏文件,使用XAML以声明方式关联命令也很容易,如下所示:

    <Window.CommandBindings>
    <CommandBinding Command="ApplicationCommands.New" Executed="binding_Executed"></CommandBinding>
    </Window.CommandBindings>
    <Button Command="ApplicationCommands.New">New</Button>
  4. 使用多命令源,如下是为命令New创建了一个MenuItem的命令源:

    <Menu>
    <MenuItem Header="File">
    <MenuItem Command="New"></MenuItem>
    </MenuItem>
    </Menu>

    注意,没有为New命令的MenuItem对象设置Header属性,这是因为MenuItem类足够智能,如果没有设置Header属性,它将从命令中提取文本(Button不具有此特性)。虽然该特性带来的便利看起来很小,但是如果计划使用不同的语言本地化应用程序,这一特性就很重要了。MunuItem类还有另一个功能,它能够自动提取Command.InputBindings集合中的第一个快捷键,对于以上的New命令,在菜单旁边会显示快捷键:Ctrl+N。

  5. 使Button这种不能自动提取命令文本的控件来提取命令文本,有两种技术来重用命令文本,一种是直接从静态的命令对象中提取文本,XAML可以使用Static标记扩展完成这一任务,该方法的问题在于它只是调用命令对象的ToString()方法,因此,得到的是命令的名称,而不是命令的文本。最好的方法是使用数据绑定表达式,以下第二条代码示例绑定表达式绑定到当前元素,获取正在使用的Command对象,并且提取其Text属性:

    <Button Command="ApplicationCommands.New" Content="{x:Static ApplicationCommands.New}"></Button>
    <Button Command="ApplicationCommands.New" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"></Button>
  6. 直接调用命令
    不是只有实现了ICommandSource接口的类才能触发命令的执行,也可以使用Execute()方法直接调用来自任何事件处理程序的方法:

    ApplicationCommands.New.Execute(null,targetElement);

    targetElement是WPF开始查找命令绑定的地方。可以使用包含窗口(具有命令绑定)或嵌套的元素(实际引发事件的元素)。也可以在关联的CommandBinding对象中调用Execute()方法,对于这种情况,不需要提供目标元素,因为会自动公开正在使用的CommandBindings集合的元素设置为目标元素:this.CommandBindings[0].Command.Execute(null);

  7. 禁用命令
    例如有一个由菜单、工具栏及一个大的文本框构成的文本编辑器的应用程序,该应用程序可以打开文件,创建新的文档以及保存所进行的操作。在应用程序中,只有文本框中的内容发生了变化才启用Save命令,我们可以在代码中使用一个Boolean变量isUpdate来跟踪是否发生了变化。当文本发生了变化时设置标志。

    private bool isUpdate = false;
    private void txt_TextChanged(object sender, TextChangedEventArgs e)
    {
    isUpdate = true;
    }

    现在需要从窗口向命令绑定传递信息,从而使连接的控件可以根据需要进行更新,技巧是处理命令绑定的CanExecute事件,代码如下:

    CommandBinding binding = new CommandBinding(ApplicationCommands.Save);
    binding.Executed += new ExecutedRoutedEventHandler(binding_Executed);
    binding.CanExecute += new CanExecuteRoutedEventHandler(binding_CanExecute);
    this.CommandBindings.Add(binding);

    或者使用声明方式:

    <CommandBinding Command="Save" Executed="CommandBinding_Executed_1" CanExecute="binding_CanExecute"></CommandBinding>

    在事件处理程序中,只需要检查isUpdate变量,并设置CanExecuteRoutedEventArgs.CanExecute属性:

    void binding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
    {
    e.CanExecute = isUpdate;
    }

    如果isUpdate的值为false,就会禁用Save命令,否则会启用Save命令。
    当使用CanExecute事件时,是由WPF负责调用RoutedCommand.CanExecute()方法触发事件处理程序,并且确定命令的状态。当WPF命令管理器探测到一个确信是重要的变化时,例如,当焦点从一个控件移动到另一个控件,或者执行了一个命令之后,WPF命令管理器就会完成该工作。控件还能引发CanExecuteChanged事件以通知WPF重新评估命令,例如,当用户在文本框中按下一个键时就会发生该事件,总之,CanExecute事件会被频繁的触发,所以不应当在该事件的处理程序中使用耗时的代码。 然而,其化因素有可能会影响命令的状态,在以上的示例中,为了响应其它操作,isUpdate标志可能会被修改,如果注意到命令状态没有在正确的时间更新,可以强制WPF为所有正在使用的命令调用CanExecute()方法,通过调用静态的CommandManager.InvalidateRequerySuggested()方法完成该工作。然后命令管理器触发RequerySuggested事件,通知窗口中的命令源。然后命令源会查询它们连接的命令并相应地更新它们的状态。

  8. 具有内置命令的控件

    一些输入控件自身可以处理命令事件,如TextBox类的Cut、Copy及Paste命令,以及一些来自EditingCommand类的用于选择文本以及将光标移到不同位置的命令,把此类命令绑定到命令源会自动获取对应命令的功能,而不需要再为命令绑定操作。如:

    <ToolBar>
    <Button Command="Cut" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"></Button>
    <Button Command="Copy" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"></Button>
    <Button Command="Paste" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"></Button>
    </ToolBar>

    此外,文本框还处理了CanExecute事件,如果在文本框中当前没有选中任何内容,剪切和复制命令就会被禁用,当焦点改变到其他不支持这些命令的控件时,这些命令都会被禁用。
    在以上代码中使用了ToolBar控件,它提供了一些内置逻辑,可以将它的子元素的CommandTarget属性自动设置为具有焦点的控件。但如果在不同的容器(不是ToolBar或Menu控件)中放置按钮,就不会得到这一优点而按钮不能正常工作,此时就需要手动设置CommandTarget属性,为此,必须使用命名目标元素的绑定表达式。如:

    <StackPanel Grid.Row="1">
    <Button Command="Cut" CommandTarget="{Binding ElementName=txt}" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"/>
    <Button Command="Copy" CommandTarget="{Binding ElementName=txt}" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"/>
    <Button Command="Paste" CommandTarget="{Binding ElementName=txt}" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"/>
    </StackPanel>

    另一个较简单的选择是使用FocusManager.IsFocusScope附加属性创建新的焦点范围,当命令触发时,该焦点范围会通知WPF在父元素的焦点范围中查找元素:

    <StackPanel FocusManager.IsFocusScope="True" Grid.Row="1">
    <Button Command="Cut" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"></Button>
    <Button Command="Copy" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"></Button>
    <Button Command="Paste" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"></Button>
    </StackPanel>

    在有些情况下,可能发现控件支持内置命令,但不想启用它,此时有三种方法可以禁用命令:

    1. 理想情况下,控件会提供用于关闭命令支持的属性,例如TextBox控件的IsUndoEnabled属性。

    2. 如果控件没有提供关闭命令支持的属性,还可以为希望禁用的命令添加一个新的命令绑定,然后该命令绑定可以提供新的事件处理程序。且总是将CanExecute属性设置为false,下面是一个使用该技术删除文本框Cut特性支持的示例:

      CommandBinding binding = new CommandBinding(ApplicationCommands.Cut, null, SuppressCommand);
      this.CommandBindings.Add(binding);
      private void SuppressCommand(object sender, CanExecuteRoutedEventArgs e)
      {
      e.CanExecute= false;
      e.Handled= true;
      }

      上面的代码设置了Handled标志,以阻止文本框自我执行计算,而文本框可能将CanExecute属性设置为true.

    3. 使用InputBinding集合删除触发命令的输入,例如,可以使用代码禁用触发TextBox控件中Cut命令的Ctrl+X组合键,如下所示:

      KeyBinding keyBinding = new KeyBinding(ApplicationCommands.NotACommand, Key.X, ModifierKeys.Control);
      txt.InputBindings.Add(keyBinding);

      ApplicationCommands.NotACommand命令不做任何事件,它专门用于禁用输入绑定。
      文本框默认显示上下文菜单,可以通过将ContextMenu属性设置为null删除上下文本菜单

      <TextBoxGrid.Row="3" Name="txt" ContextMenu="{x:Null}" TextWrapping="Wrap" TextChanged="txt_TextChanged" />
  9. 自定义命令
    下面的示例定义了一个Requery的命令:

    public class DataCommands
    {
    private static RoutedUICommand requery;
    static DataCommands()
    {
    InputGestureCollection inputs= new InputGestureCollection();
    inputs.Add(new KeyGesture(Key.R, ModifierKeys.Control, "Ctrl+R"));
    requery= new RoutedUICommand("查询", "Requery", typeof(DataCommands), inputs);
    }
    public static RoutedUICommand Requery //通过静态属性提供自定义的命令
    {
    get { return requery; }
    }
    }

    使用Requery命令时需要将它的.Net名称空间映射为一个XML名称空间,XAML代码如下:

    <Window x:Class="WpfApplication1.Test4"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfApplication1"
    Title="Test4" Height="300" Width="300">
    <Window.CommandBindings>
    <CommandBinding Command="local:DataCommands.Requery" Executed="Requery_Executed"></CommandBinding>
    </Window.CommandBindings>
    <Grid>
    <Button Command="local:DataCommands.Requery" CommandParameter="ai" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}" />
    </Grid>
    </Window>

    在以上代码中使用CommandParameter为命令传递了参数,命令的事件处理方法中就可以使用Parameter属性获取该参数:

    private void Requery_Executed(object sender, ExecutedRoutedEventArgs e)
    {
    string parameters = e.Parameter.ToString();
    }  
  10. 在不同的位置使用相同的命令
    在WPF命令模型中,一个重要的思想是Scope。尽管每个命令只有一个副本,但是使用命令的效果却会根据触发命令位置而不同,例如,如果有两个文本框,它们都支持Cut、Copy、Paste命令,操作只会在当前具有焦点的文本框中发生。但是对于自定实现的命令如New、Open、Requery及Save命令就区分不出是哪一个文本框触发的命令,尽管ExecuteRoutedEventArgs对象提供了Source属性,但是该属性反映的是具有命令绑定的元素,也就是容器窗口。此问题的解决方法是使用文本框的CommandBindings集合为每个文本框分别绑定命令。

  11. 跟踪和翻转命令
    创建自己的用于支持命令翻转的数据结构,示例中定义一个名为CommandHistoryItem的类用于存储命令状态:

    public class CommandHistoryItem
    {
    public string CommandName { get; set; } //命令名称
    public UIElement ElementActedOn { get; set; } //执行命令的元素
    public string PropertyActedOn { get; set; } //在目标元素中被改变了的属性
    public object PreviousState { get; set; } //用于保存受影响元素以前状态的对象
    public CommandHistoryItem(string commandName)
    : this(commandName, null, "", null)
    {
    } public CommandHistoryItem(string commandName, UIElement elementActedOn, string propertyActed, object previousState)
    {
    this.CommandName = commandName;
    this.ElementActedOn = elementActedOn;
    this.PropertyActedOn = propertyActed;
    this.PreviousState = previousState;
    } public bool CanUndo
    {
    get { return (ElementActedOn != null && PropertyActedOn != ""); }
    } /// <summary>
    /// 使用反射为修改过的属性应用以前的值
    /// </summary>
    public void Undo()
    {
    Type elementType = ElementActedOn.GetType();
    PropertyInfo property = elementType.GetProperty(PropertyActedOn);
    property.SetValue(ElementActedOn, PreviousState, null);
    }
    }

    需要自定义一个执行应用程序范围内翻转操作的命令,如下所示:

    private static RoutedUICommand applicationUndo;
    public static RoutedUICommand ApplicationUndo
    {
    get { return applicationUndo; }
    } static ApplicationUndoDemo()
    {
    applicationUndo = new RoutedUICommand("Applicaion Undo", "ApplicationUndo", typeof(ApplicationUndoDemo));
    }

    可以使用CommandManager类来跟踪任何命令的执行情况,它提供了几个静态事件:Executed及PreviewExecuted,无论何时,当执行任何一个命令时都会触发它们。 尽管CommandManager类挂起了Executed事件,但是仍然可以使用UIElement.AddHandler()方法关联事件处理程序,并且为可选的第三个参数传递true值,从而允许接收事件。下面的代码在窗口的构造函数中关联PreviewExecuted事件处理程序,且在关闭窗口时解除关联:

    public ApplicationUndoDemo()
    {
    InitializeComponent();
    this.AddHandler(CommandManager.PreviewExecutedEvent, new ExecutedRoutedEventHandler(CommandPreviewExecute),true);
    } private void Window_Unloaded(object sender, RoutedEventArgs e)
    {
    this.RemoveHandler(CommandManager.PreviewExecutedEvent, new ExecutedRoutedEventHandler(CommandPreviewExecute));
    }

    当触发PreviewExecute事件时,需要确定准备执行的命令是否是我们所关心的,如果是就创建CommandHistoryItem对象,且将其添加到历史命令集合中。

    private void CommandExecuted(object sender, ExecutedRoutedEventArgs e)
    {
    // Ignore menu button source.
    if (e.Source is ICommandSource) return; // Ignore the ApplicationUndo command.
    if (e.Command == MonitorCommands.ApplicationUndo) return; // Could filter for commands you want to add to the stack
    // (for example, not selection events). TextBox txt = e.Source as TextBox;
    if (txt != null)
    {
    RoutedCommand cmd = (RoutedCommand)e.Command; CommandHistoryItem historyItem = new CommandHistoryItem(
    cmd.Name, txt, "Text", txt.Text); ListBoxItem item = new ListBoxItem();
    item.Content = historyItem;
    lstHistory.Items.Add(historyItem); // CommandManager.InvalidateRequerySuggested();
    }
    }

    使用CanExecute事件处理程序,确保只有当Undo历史中有一项时,才能执行翻转操作:

    <Window.CommandBindings>
    <CommandBinding Command="local:ApplicationUndoDemo.ApplicationUndo" Executed="CommandBinding_Executed" CanExecute="CommandBinding_CanExecute"></CommandBinding>
    </Window.CommandBindings>
    private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
    {
    //不知lstHistory.Items[lstHistory.Items.Count - 1]为什么强制转化不成CommandHistoryItem,有待解决
    CommandHistoryItem historyItem = (CommandHistoryItem)lstHistory.Items[lstHistory.Items.Count - ];
    if (historyItem.CanUndo)
    historyItem.Undo();
    lstHistory.Items.Remove(historyItem);
    } private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
    {
    if (lstHistory == null || lstHistory.Items.Count == )
    e.CanExecute = false;
    else
    e.CanExecute = true;
    }
上一篇:JS表格分页组件:fupage的设计思路和具体用法(未来考虑开源,争取在2015年)


下一篇:Vue.js 2.x:组件的定义和注册(详细的图文教程)