【笔记】【WPF编程宝典】 第09章 命令


  使用路由事件可相应广泛的鼠标和键盘动作。但是,事件是非常低级的元素。在实际应用程序种,功能被划分成一些高级任务。这些任务可通过各种不同的工作和用户界面元素触发,包括主菜单、上下文菜单、键盘快捷键以及工具栏。
  可在WPF中定义这些任务——所谓的命令——并将控件连接到命令,从而不需要重复编写事件处理代码。更重要的是,当连接的命令不可用时,命令特性通过自动禁用控件来管理用户界面的状态。命令模型还为存储和本地化命令的文本标题提供了一个中心场所。

9.1 理解命令

  在设计良好的Windows应用程序中,应用程序逻辑不应位于事件处理程序中,而应在更高层的方法中编写代码。其中,每个方法都代表单独的应用程序任务。
将事件处理程序映射到任务
  使用这种设计最明显的方式是在需要的地方添加事件处理程序,并使用各个事件处理程序调用恰当的应用程序方法。本质上,窗口代码编程一个精简的交换台,可以响应输入,并将请求转发到应用程序的核心。
尽管这种设计非常合理,但却没有减少任何工作。许多应用程序任务可通过各种不同的路由触发,所以经常需要编写多个事件处理程序来调用相同的应用程序方法。就其本身而言,这并不是什么问题(因为交换台代码非常简单),但当需要处理用户界面的状态时,问题就变得复杂了。
  下面通过一个简单的例子说明该问题。设想有一个主程序,该程序包含应用程序方法PrintDocument()。可使用4种方式触发该方法:通过主菜单、上下文菜单、键盘快捷键以及工具栏。在应用程序声明周期的特定时刻,需要暂时近用PrintDocument()任务。这意味着需要近用两个菜单命令和一个工具栏按钮,以使它们不能被击中,并且需要忽略快捷键。编写代码完成这些工作使很麻烦的。更糟糕的是,如果没有 正确地完成这项工作,可能会使不同状态的代码块不正确地相互重叠,从而导致某个控件在不应该可用时而被启用。
  幸运的是,WPF使用新的命令模型帮助您解决这些问题。它增加了两个重要特性:

  • 将事件委托到合适的命令
  • 使控件的启用状态和相应命令的状态保持同步
    WPF命令模型不像您所期望的那样直观。为了嵌入路由事件模型,需要几个单独元素。下图显示了基于命令的应用程序的逻辑,现在每个启动打印的动作都映射到同一个命令。在代码种使用命令绑定将链接到单个事件处理程序。
    将事件映射到命令
      虽然WPF命令系统使一款简化应用程序设计的优秀工具,但仍然有一些很重要的问题没有解决。特别是,WPF对以下方面没有提供任何支持:
  • 命令跟踪(例如,保留最近命令的历史)
  • “可撤销的”命令
  • 具有状态并可处于不同“模式”的命令(例如,可被打开或关闭的命令)

9.2 WPF 命令模型

  WPF 命令模型由许多可变的部分组成。总之,它们都具有如下4个重要元素:

  • 命令:命令标识应用程序任务,并且跟踪任务是否能够被执行。然而,命令实际上不包含执行应用程序任务的代码。
  • 命令绑定:每个命令绑定针对用户界面的具体区域,将命令连接到相关的应用程序逻辑。这种分解的设计是非常重要的,因为单个命令可用于应用程序种的多个地方,并且在每个地方有不同的意义。为处理这一问题,需要将同一命令与不同的命令绑定。
  • 命令源:命令源触发命令。例如,MenuItem和Button都是命令源。单击它们都会执行绑定命令。
  • 命令目标:命令目标是在其中执行命令的元素。例如,Paste命令可在TextBox控件种插入文本,而OpenFile命令可在DocumentViewer中打开文档。根据命令的本质,目标可能和重要,也可能不重要。

9.2.1 ICommand

  WPF命令模型的核心是System.Windows.Input.ICommand接口,该接口定义了命令的工作原理。该接口包含两个方法和一个事件:

public interface ICommand
{
	void Execute(object parameter);
	bool CanExecute(object parameter);
	
	event EventHandler CanExecuteChanged;
}

  在一个简单实现中,Execute()方法将包含应用程序任务逻辑。然而,WPF的实现更加复杂。它使用Execute()方法引发一个更复杂的过程,该过程最终触发在应用程序其他地方处理的事件。通过这种方式可以使用预先准备好的命令类,并插入自己的逻辑。还可以灵活地在几个不同的地方使用同一个命令。
  CanExecute()方法返回命令的状态——如果命令可用,就返回true;如果不可用,就返回false。Execute()和CanExecute()方法都接受一个附加的参数对象,可使用该对象传递所需的任何附加信息。
  最后,当命令状态改变时引发CanExecuteChanged事件。对于使用命令的任何控件,这是指示信号,标识它们应当调用CanExecute()方法检查命令状态。通过使用该事件,命令可用时,命令源可自动启用自身;当命令不可用时,禁用自身。

9.2.2 RoutedCommand

  当创建自己的命令时,不会直接实现ICommand接口;而是使用System.Windows.Input.RoutedCommand类,该类自动实现了ICommand接口。RoutedCommand类是WPF中唯一实现了ICommand接口的类。换句话说,所有WPF命令都是RoutedCommand类及其派生类的实例。
  在WPF命令模型背后的一个重要该你那是,RoutedCommand类不包含任何应用程序逻辑,而只是代表命令,这意味着各个RoutedCommand对象具有相同的功能。
RoutedCommand类为事件冒泡和隧道添加了一些额外的基础结构。鉴于ICommand接口封装了命令的思想——可被触发的动作并可被启用或禁用——RoutedCommand类对命令进行修改,使命令可在WPF元素层次接口中冒泡,以便获得正确的处理程序。
  为支持路由事件,RoutedCommand类私有地实现了ICommand接口方法的一些不同版本。最明显的变化使,Execute()和CanExecute()方法使用了一个额外参数。下面使新的签名:

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

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

9.2.3 RoutedUICommand

  在程序中处理的发部分命令不是RoutedCommand对象,而是RoutedUICommand类的实例,RoutedUICommand类继承自RoutedCommand类(实际上,WPF提供的所有预先构建好的命令都是RoutedUICommand对象)。
  RoutedUICommand类用于具有文本的命令,这些文本显示在用户界面中某些地方(例如,菜单项文本、工具栏按钮的工具提示)。RoutedUICommand类只增加了Text属性,该属性是为命令显示的文本。
为命令定义文本(而不是直接在控件上定义文本)的优点是可在某个位置执行本地化。但如果命令文本永远不会在用户界面的任何地方显示,那么RoutedUICommand类和RoutedCommand类是等效的。

9.2.3 命令库

  WPF提供了基本命令库,这些命令通过以下五个专门的静态类的静态属性提供:

  • ApplicationCommands:该类提供了通用命令,包括剪贴板命令(Copy、Cut、Paste)以及文档命令(New、Open、Save、SaveAs、Print)
  • NavigationCommands:该类提供了用于导航的命令,包括为基准页面的应用程序设计的一些命令(BrowseBack、BrowseForward、NextPage),以及其他合适基于文档的应用程序的命令(IncreaseZoom、Refresh)
  • EditingCommands:该类提供了许多中的文档编辑命令,包括用于移动的命令(MoveToLineEnd、MoveLeftByWord、MoveUpByPage),选择内容的命令(SelectToLineEnd、SelectLeftByWord),以及改变格式的命令(ToggleBold、ToggleUnderline)
  • ComponentEndCommands:该类提供了由用户界面组件使用的命令,包括用于移动和选择内容的命令,这些命令和EditingCommans类中的一些命令相似(甚至完全相同)
  • MediaCommands:该类提供了一组用于处理多媒体的命令(Play、Pause、NextTrack、IncreaseVolumn)

9.3 执行命令

  RoutedUICommand类没有任何硬编码的功能,而是只标识命令。为了触发命令,需要由命令源(也可使用代码)。为了响应命令,需要有命令绑定,命令绑定键根治性转发给普通的事件处理程序。

9.3.1 命令源

  命令库中的命令始终可用。触发它们的最简单方法是将它们关联到实现了ICommandSource接口的控件,其中包括集成自BuutonBase类的控件、单独的ListBoxItem对象、Hyperlink以及MenuItem。
ICommandSource接口定义了三个属性:

名称说明
Command指向连接的命令,这是唯一必须的细节
CommandParameter提供其他希望随命令发送的数据
CommandTarget确定将在其中执行命令的元素

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

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

  WPF的智能成都足够高,它能查找自带命令库中的5个命令容器类,这意味着可使用下面的缩写形式:

<Button Commond="New">New</Button>

  然而,由于没有 指明包含命令的类,这种语法不够明确、不够清晰,不建议使用。

9.3.2 命令绑定

  当将命令关联到命令源时,会看到一些有趣的现象。命令源会被自动禁用。例如,如果创建上面提到的New按钮,该按钮 的颜色就会变浅且不能被单击,就像将IsEnabled属性设置为false那样。这是因为按钮已经查询了命令的状态,而且由于命令还没有与其关联的绑定,所以按钮被认为是禁用的。为了改变这种状态,需要为命令创建绑定以明确三件事情:

  • 当命令被触发时执行什么操作
  • 如何确定命令是否能够被执行
  • 命令在何处起作用
    下面的代码片段为New命令创建绑定。
CommandBinding binding = new CommandBinding(ApplicationCommands.New);
binding.Executed += NewCommand_Executed;
this.CommandBindings.Add(binding);

private void NewCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
	MessageBox.Show("New command triggered by " + e.Source.ToString());
}

  现在,如果运行应用程序,按钮将处于启用状态。如果单击按钮,就会触发Executed事件,该事件冒泡至窗口,并被上给出的NewCommand()事件处理程序处理。这时WPF会告知事件源。
  注意,上面创建的CommandBinding对象被添加到包含窗口的CommandBindings集合中,这通过事件冒泡进行工作。实际上,当单击按钮时,CommandBinding.Executed事件从按钮冒泡到包含元素。
  尽管习惯上为窗口添加所有绑定,但CommandBindings属性实际上是在UIElement基类中定义的。这意味着任何元素都支持该属性。例如,如果将命令绑定直接添加到使用它的按钮中,这个示例仍能工作的很好(尽管不能再将该绑定冲用与其他高级元素)。为得到最大的灵活性,命令绑定通常被添加到顶级窗口。如果希望在多个窗口中使用相同的命令,需要在这些窗口中分别创建命令绑定。

9.3.3 使用多个命令源

  下面在上例中添加菜单项的New命令,但不需要再次创建命令绑定。如下所示,边能正常工作:

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

  注意,New命令的这个MenuItem对象没设设置Header属性。这是因为MenuItem类足够智能,如果没有设置Header属性,它将从命令中提取文本。虽然该特性带来的便利看起来不大 ,但如果计划使用不同语言本地化应用程序,这一特性就很重要了。在这种情况下,只需要在一个地方修改文本即可,这比在整个窗口中进行跟踪更容易。
MenuItem类还有一项功能:能自动提取Command.InputBindings集合中的第一个快捷键(如果存在的话)。对于ApplicationCommands.New命令对象,这意味着在菜单文本的旁边会显示Ctrl+N快捷键。

9.3.4 微调命令文本

  既然菜单具有自动提取命令项文本的功能,那ICommandSource类是否也具有类似功能,如Buuton控件。可以,但需要完成一点额外的工作。
可使用两种技术重用命令文本。一种选择是直接从静态命令对象中提取文本。XMAL可使用Static标记扩展完成这一任务。下面的示例获取命令名为New,并将它作为按钮的文本。

<Button Command="New" Content="{x:Static ApplicationCommands.New}"/>

  该方法的问题在于,它只是调用命令对象的ToString()方法。因此,得到的是命令名,而不是命令的文本(对于哪些名称中包含多个单词的命令,使用命令文本更好些,因为命令文本包含空格)。虽然可解决这一问题,但需要完成更多的工作。这种方法还存在一个问题,一个按钮将同一个命令使用两次,可能会无意间从错误的命令获取文本。
  更好的解决方案是使用数据 绑定表达式。在此使用的数据绑定有些不寻常,因为它绑定到当前元素,获取正在使用的Command对象,并提取Text属性。下面是非常复杂的语法:

<Button Command="ApplicationCommands.New" Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"/>

  可通过另一种更具想象力的方式使用该技术。例如,可使用一小幅图像设置按钮的内容,而在按钮的工具提示中使用数据绑定表达式显示命令名:

<Button Command="ApplicationCommands.New" ToolTip="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}">
	<Image ...>
</Button>

9.3.5 直接调用命令

  并非只能使用实现了ICommandSource接口的类来触发希望执行的命令。也可以使用Execute()方法直接调用来自任何事件处理程序的方法。这时需要传递参数值(或null引用)和对目标元素的引用:

ApplicationCommands.New.Execute(null, targetElement);

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

ApplicationCommands[0].Command.Execute(null, targetElement);

  这种方法只使用了半个命令模型。虽然也可触发命令,但不能响应命令的状态变化。如果希望实现该特性,当命令变为启用或禁用时,也可能希望处理RoutedCommand.CanExecuteChanged事件进行响应。当引发CanExecuteChanged事件时,需要调用RoutedCommand.CanExecute()方法检查命令是否处于可用状态。如果命令不可用,可禁用或改变用户界面中的部分内容。

9.3.6 禁用命令

  如果想要创建状态在启用和禁用之间变化的命令,将体会到命令模型的真正优势。例如,分析下图现实的窗口应用程序,它是由菜单、工具栏以及大文本框构成的简单文本编辑器。该应用程序可以打开文档,创建新的空白文档,以及保存所执行的操作。
简单的文本编辑器
  在该应用程序中,保持New、Open、Save、SaveAs以及Close命令一直可用是非常合理的。但还有一种设计,只有当某些操作使文本相对于原来的文件发生了变化才启动Save命令。根据这一约定,可在代码中使用简单的Boolean值来跟踪这一细节:

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

  现在需要从窗口向命令绑定传递消息,使连接的控件可根据需要进行更新。技巧是处理命令绑定的CanExecute事件。可通过下面的代码为该使劲按关联事件处理程序:

CommandBinding binding = new CommandBinding(ApplicationCommands.Save);
binding.Execute += SaveCommand_Executed;
binding.CanExecute += SaveCommand_CanExecute;
this.CommandBindings.Add(binding);

或使用声明方式 :

<Window.CommandBindings>
	<CommandBinding Command="ApplicationCommands.Save" Executed="SaveCommand_Executed" CanExecuted="SaveCommand_CanExecute">
</Window.CommandBindings>

  在事件处理程序中,只需要检查isDirty变量,并响应地设置CanExecuteRoutedEventArg.CanExecute属性:

private void SaveCommand_CanExecute(object sender, CanExecuteRoutedEventArge)
{
	e.CanExecute = isDirty;
}

  如果isDirty的值是false,就禁用命令。如果isDirty的值为true,就启用命令(如果没有设置CanExecute标志,就会 保持最近值)。
  当使用CanExecute标志时,还需要理解一个问题。由WPF负责调用RoutedCommand.CanExecute()方法来触发事件处理程序,并确定命令的状态。当WPF命令管理器探测到某个确信十分中要的变化时——例如,当焦点从一个控件移到另一个控件上时,或执行了某个命令后,WPF明林管理器就会完成该工作。控件还能引发CanExecuteChanged事件已通知WPF重新评估命令——例如,当用户在文本框中按下一个键时会发生该事件。总之,CanExecute使劲按会被平凡地触发,并且不应该在搞事件处理程序中使用耗时的代码。
  然而,其他因素可能影响命令状态。在当前示例中,为响应其他操作,可能会修改isDirty标志。如果发现命令状态围在正确的事件被更新,可强制WPF为所有正在使用的命令调用CanExecute()方法。通过调用静态方法CommandManager.InvalidateRequerySuggested()完成该工作。然后命令管理器触发RequerySuggested事件,通知窗口中欧给的命令源。此后命令源会重新查询它们链接的命令并相应地更新它们的状态。

9.3.7 具有内置命令的控件

  一些输入控件可自行处理命令事件。例如,TextBox类处理Cut、Copy、以及Paste命令(Undo、Redo命令,以及一些来自EditingCommand类的用于选择文本以及将光标移动到不同位置的命令)。
  当控件具有自己的硬编码命令逻辑时,为使命令工作不需要左其他任何事情。例如,对于简单文本编辑器示例,添加以下工具栏按钮,就会自动获得对键切、复制和粘贴文本的支持:

<ToolBar>
    <Button Command="Cut">Cut</Button>
    <Button Command="Copy">Copy</Button>
    <Button Command="Paste">Paste</Button>
</ToolBar>

  现在单击这些按钮中的任意一个(当文本框具有焦点时),就可以复制、剪切或从剪贴板粘贴文本。有趣的时,文本框还处理CanExecute事件。如果当前未在文本框中选中任何内容,集惠禁用逛街且和复制命令。当焦点移动到其他不支持这些命令的控件时,会自动禁用左右这三个命令(除非关联自己的CanExecute事件处理程序以启动这些命令)。
  该例有一个有趣的细节。Cut、Copy和Paste命令被具有焦点的文本框处理。然而,由工具蓝上的那妞触发的命令是完全独立的严肃。在该例中,这个过程之所以能够无缝工作,是因为按钮被梵高工具栏上,ToolBar类提供了一些内置逻辑,可将其子元素的CommandTarget属性动态设置未当前具有焦点的控件,即文本框。ToolBar控件由单独的焦点范围(focus并且在其上下文中按钮是具有焦点的。
  如果在不同容器(不是ToolBar或Menu控件)中放置按钮,即不会获得这项优势。这意味着除非手动设置CommandTarget属性,否则按钮不能工作。为此,必须使用命令目标元素的绑定表达式。例如,如果文本框被命令为txtDocument,就应该像下面这样定义按钮:

<Button Command="Cut" CommandTarget="{Binding ElementName=txtDocument}">Cut</Button>
<Button Command="Copy" CommandTarget="{Binding ElementName=txtDocument}">Copy</Button>
<Button Command="Paste" CommandTarget="{Binding ElementName=txtDocument}">Paste</Button>

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

<StackPanel FocusManager.IsFocusScope="True">
	<Button Command="Cut"   >Cut</Button>
	<Button Command="Copy"  >Copy</Button>
	<Button Command="Paste" >Paste</Button>
</StackPanel>

9.4 高级命令

9.4.1 自定义命令

  在5个命令类中存储的命令,显然不会为应用程序提供所有可能需要的命令。幸运的是,可以和方便地自定义命令,需要做的全部工作就是实例化一个新的RoutedUICommand对象。
  RoutedUICommand类提供了几个构造函数。虽然可创建没有任何附加信息的RoutedUICommand对象,但机会总是希望提供命令、命令文本以及所属类型。此外,可能希望为InputGestures集合提供快捷键。
  最佳设计方式是尊村WPF库中的范例,并通过静态属性提供自定义命令。下面的示例定义了名为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", "Requery", typeof(DataCommands), inputs);
    }
     
    public static RoutedUICommand Requery
    {
        get { return requery; }
    }
}

  一旦定义了命令,就可以在命令绑定中使用它,就像使用WPF提供的所有预先构建好的命令一样。但仍存在一个问题。如果希望在XAML使用自定义的命令,那么首先需要将.NET名称控件映射为XML名称控件。例如,如果自定义的命令类位于Commands名称空间中,那么应添加如下名称空间映射:

<Window x:Class="Commands.CustomCommand"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Commands" Height="300" Width="300"
        xmlns:local="clr-namespace:Commands">
    <Window.CommandBindings>
        <CommandBinding Command="local:DataCommands.Requery" Executed="RequeryCommand"/>
    </Window.CommandBindings>

    <Grid>
        <Button Margin="5" Command="local:DataCommands.Requery">Requery</Button>      
    </Grid>
</Window>

9.4.2 在不同位置使用相同的命令

  在WPF命令模型中,一个重要盖面是范围。尽管每个命令仅有一个腹板,但使用命令的效果却会根据触发命令的位置而异。例如,如果有两个文本框,它们都支持Cut、Copy和Paste命令,操作只会在当前具有焦点的文本框中发生。
至此,我们还没有嘘唏如何对自己关联的命令实现这种效果。例如,设想创建了一个具有两个文档的窗口。如果使用Cut、Copy和Paste命令,就会发现它们能够在正确的文本框中自动工作,然而,对于自己实现的命令——New、Open以及Save命令——情况就不同了。问题在于当为这些命令中的某个命令触发Executed事件时,不知道该事件是属于第一个文本框还是第二个文本框。尽管ExecutedRoutedEventArgs对象中提供了Source属性,但该属性反映的是具有命令绑定的元素。而到目前为止,所有命令都被绑定到了容器窗口。
  解决这个问题的方法是使用文本框的CommandBindings集合分别为每个文本框绑定命令。下面是一个示例:

<Window.Resources>
	<CommandBinding x:Key="binding" 
  		Command="ApplicationCommands.Save"
        Executed="SaveCommand" 
        CanExecute="SaveCommand_CanExecute" >
	</CommandBinding>
</Window.Resources>

<TextBox Margin="5" Grid.Row="2" TextWrapping="Wrap" AcceptsReturn="True" TextChanged="txt_TextChanged">
	<TextBox.CommandBindings>
		<StaticResource ResourceKey="binding"></StaticResource>
	</TextBox.CommandBindings>     
</TextBox>

<TextBox Margin="5" Grid.Row="3" TextWrapping="Wrap" AcceptsReturn="True" TextChanged="txt_TextChanged">
	<TextBox.CommandBindings>
        <StaticResource ResourceKey="binding"></StaticResource>
	</TextBox.CommandBindings>
</TextBox>

  现在文本框处理Executed事件。在事件处理程序中,可使用这一信息确保保存正确的信息:

private void SaveCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
	if (isDirty.ContainsKey(sender) && isDirty[sender] == true)
	{
		e.CanExecute = true;
	}
	else
	{
		e.CanExecute = false;
	}             
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhy29563

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值