09 命令

命令

理解命令

将事件处理程序映射到任务

09 命令_第1张图片
设想有一个程序,该程序包含应用程序方法PrintDocument()。可使用4种方式触发该方法:通过主菜单(选择File | Print菜单项)、通过上下文菜单(在某处右击并选择Print菜单项)、通过键盘快捷腱(Ctrl+P)以及通过工具栏按钮。在应用
程序生命周期的特定时刻,需要暂时禁用PrintDocument()任务。这意味着需要禁用两个菜单命令和一个工具栏按钮,以使它们不能被单击,并且需要忽略Ctrl+P快捷键。编写代码完成这些工作(并在后面添加代码以启用这些控件)是很麻烦的。更糟的是,如果没有正确地完成这项工作,可能会使不同状态的代码块不正确地相互重叠,从而导致某个控件在不应该可用时而被启用。WPF使用新的命令模型帮助您解决这些问题。它增加了两个重要特性.

  • 将事件委托到适当的命令。
  • 使控件的启用状态和相应命令的状态保持同步。

改造上图后,如下。

将事件映射到命令
09 命令_第2张图片

WPF对以下方面没有提供任何支持:

  • 命令跟踪(例如,保留最近命令的历史)。
  • “可撤销的”命令。
  • 具有状态并可处于不同”模式”的命令(例如,可被打开或关闭的命令)。

WPF命令模型

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

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

ICommnd 接口

WPF命令模型的核心是System.Wmdows.Input.ICommand接口,该接口定义了命令的工作原理。

public interface ICommand
{
    voic Execute(Object parameter);
    bool CanExecute(Object parameter);
    event CanExecuteChanged;
}

Execute()——在一个简单实现中,Execute()方法将包含应用程序任务逻辑(例如,打印文档)。然而,WPF的实现更复杂。它使用Execute() 方法引发一个更复杂的过程,该过程最终触发在应用程序其他地方处理的事件。通过这种方式可以使用预先准备好的命令类,并插入自己的逻辑。还可以灵活地在几个不同的地方,使用同一个命令(如Print命令)。
CanExecute()——返回命令的状态
CanExecuteChanged——当命令状态改变时引发canExecuteChanged事件。对于使用命令的任何控件,这是
指示信号,表示它们应当调用CanExecute()方法检查命令的状态。通过使用该事件,当命令可用时,命令源(如Button或Menultem)可自动启用自身;当命令不可用时,禁用自身。

RoutedCommnd 类

当创建自己的命令时,要使用System.Windows.Input.RoutedCommand类,该类自动实现了ICommand接口。

在WPF命令模型背后的一个重要概念是,RoutedCommand类不包含任何应用程序逻辑,而只代表命令,这意味着各个RoutedCommand对象具有相同的功能。

RoutedCommand类为事件冒泡和隧道添加了一些额外的基础结构。鉴于ICommand接口封装了命令的思想一一一可被触发的动作并可被启用或禁用一一RoutedCommand类对命令进行了修改,使命令可在WPF元素层次结构中冒泡,以便获得正确的事件处理程序。

为支持路由事件,RoutedCommand类私有地实现了ICommand接口,并添加ICommand接口方法的一些不同版本。最明显的变化是,Execute()和CanExecute()方法使用了一个额外参数。下面是新的签名:

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

参数target是开始处理事件的元素。事件从target元素开始,然后冒泡至高层的容器,直到应用程序为了执行合适的任务而处理了事件(为了处理Executed事件,元素还需要借助于另一个类—CommandBmding类的帮助)。

除上面的修改外,RoutedCommand 还引入了三个属性:命令名称(Name属性)、包含命令的类(OwnerType)以及任何可用于触发命令的按键或鼠标操作(位于InputGestures集合中)。

RoutedUICommand

RoutedUICommand类用于具有文本的命令,这些文本显示在用户界面中的某些地方(例如菜单项文本、工具栏按钮的工具提示)。RoutedU℃ommand类只增加了Text属性,该属性是为命令显示的文本。

为命令定义命令文本(而不是直接在控件上定义文本)的优点是可在某个位置执行本地化。但如果命令文本永远不会在用户界面的任何地方显示,那么RoutedUICommand类和RoutedCommand类是等效的。

命令库

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

Applicationcommands类提供了一组基本命令,在所有类型的应用程序中都经常会用到这
些命令,所以在此简单介绍一下。下面列出了所有命令。

命令 命令 命令 命令 命令 命令
New Copy SelectAll Open Cut Stop
Save Paste ContextMenu SaveAs Delete CorrectionList
Close Undo Properties Print Redo Help
Printpreview Find CancelPrint Replace

执行命令

命令源

命令库中的命令始终可用。触发它们的最简单方法是将它们关联到实现了ICommandSource
接口的控件,其中包括继承自ButtonBase类的控件(Button和CheckBox等)、单独的ListBoxItem对象、Hyperlink以及Menultem。

ICommandSource接口的属性

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

如下

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

或者(不建议这种方式)

<Button Command="New">NewButton>

命令绑定

当命令关联到命令源,命令源会自动禁用。为改变这种状态,需要为命令创建绑定以明确以下三件事情。

  • 当命令被触发时执行什么操作。
  • 如何确定命令是否能够被执行(这是可选的。如果未提供这一细节,只要提供了关联的事件处理程序,命令总是可用)。
  • 命令在何处起作用。例如,命令可被限制在单个按钮中使用,或在整个窗口中使用(这
    种情况更常见)。

下面的代码片段为New命令创建绑定。可将这些代码添加到窗口的构造函数中:

CommandBinding binding = new CommandBinding(ApplicationCommands.New);
binding.Executed += NewCommand_Excuted;
this.CommandBindings.Add(binding);

或者使用xaml方式

<Window.CommandBindings>
    <CommandBinding Command="ApplicationCommands.New" Executed="NewCommand_Excuted">CommandBinding>
Window.CommandBindings>

<Button  Command="ApplicationCommands.New" Width="50" Height="25">NewButton>

使用多命令源

按钮示例中触发普通事件的方式看起来不那么直接。然而,当添加使用相同命令的更多控
件时,额外命令层的意义就会体现出来。如:

<Menu>
    <MenuItem Header="File">
        <MenuItem Command="ApplicationCommands.New">MenuItem>
    MenuItem>
Menu>

注意,New命令的这个MenuItem对象没有设置Header属性。这是因为MenuItem类足够智能,如果没有设置Header属性,它将从命令中提取文本(Button控件不具有这一特性)。如果计划使用不同的语言本地化应用程序,只需要在一个地方修改文本即可(通过设置命令的Text属性),这比在整个窗口中进行跟踪更容易。

Menultem类还有一项功能:能自动提取Command.InputBindings集合中的第一个快捷键(如
果存在的话)。对于ApplicationCommands.New命令对象,这意味着在菜单文本的旁边会显示
Ctrl十N快捷键。

微调命令文本

菜单具有自动提取命令项文本的功能,而其他ICommandSource类也具有类似功能。
两种技术:

  • 直接从静态命令对象中提取文本
  • 可通过另一种更具想象力的方式使用该技术

直接从静态命令对象中提取文本
XAML可使用Static标记扩展完成这一任务。如

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

该方法的问题在于,它只是调用命令对象的ToStnng()方法。因此,得到的是命令名,而不是命令的文本(对于那些名称中包含多个单词的命令,使用命令文本更好些,因为命令文本包含空格)。这种方法还存在一个问题,一个按钮将同一个命令使用了两次,可能会无意间从错误的命令获取文本。
更好的解决方案是使用数据绑定表达式。绑定到当前元素,获取正在使用的Command对象,并提取其Text属性。如:

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

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

按钮的内容(在此没有显示)可以是形状,也可以是显示为缩略图的位图。
显然,这种方法比直接在标记中放置命令文本要麻烦些。然而,如果准备使用不同的语言
本地化应用程序,使用这个方法是值得的。当应用程序启动时,只需要为所有命令设置命令文
本即可(如果在创建了命令绑定后改变命令文本,不会产生任何效果。因为Text属性不是依赖
项属性,所以没有自动的更改通知来更新用户界面)。

直接调用命令

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

ApplicationCommands.New.Execute(null,targetElement),

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

this.CommandBindings[0].Command.Execute(null);

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

禁用命令

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

CommandBinding binding = new CommandBinding(ApplicationCommands.New);
binding.Executed += SaveCommand_Excuted;
binding.CanExecute += SaveCommand_CanExcuted;
this.CommandBindings.Add(binding);

<Window.CommandBindings>
    <CommandBinding Command="ApplicationCommands.Save" Executed="SaveCommand_Excuted" CanExcuted="SaveCommand_CanExcuted">CommandBinding>
Window.CommandBindings>

当使用CanExcuted事件时,还需要理解一个问题。由WPF负责调用RoutedCommand.CanExcuted()方法来触发事件处理程序,并确定命令的状态。当WPF命令管理器探测到某个确信十分重要的变化时一一例如,当焦点从一个控件移到另一个控件上时,或执行了某个命令后,WPF命令管理器就会完成该工作。控件还能引发CanExecuteChanged事件以通知WPF重新评估命令一一例如,当用户在文本框中按下一个键时会发生该事件。总之,CanExecute事件会被频繁地触发,并且不应在该事件的处理程序中使用耗时的代码。

然而,其他因素可能影响命令状态。如果发现命令状态未在正确的时间被更新,可强制WPF为所有正在使用的命令调用CanExecute()方法。通过调用静态方法CommandManager.InvalidateRequerySuggested()完成该工作。然后命令管理器触发RequerySuggested事件,通知窗口中的命令源(按钮、菜单项等)。此后命令源会重新查询它们链接的命令并相应地更新它们的状态。

具有内置命令的控件

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

<ToolBar>
    <Button Command="Cut">CutButton>
    <Button Command="Copy">CopyButton>
    <Button Command="Paste">PasteButton>
ToolBar>

现在单击这些按钮中的任意一个(当文本框具有焦点时),就可以复制、剪切或从剪贴板粘贴文本。文本框还处理CanExecute事件。如果当前未在文本框中选中任何内容,就会禁用剪切和复制命令。当焦点移到其他不支持这些命令的控件时,会自动禁用所有这三个命令(除非关联自己的CanExecute事件处理程序以启用这些命令)。

Cut、Copy和paste命令被具有焦点的文本框处理。然而,由工具栏上的按钮触发的命令是完全独立的元素。在该例中,这个过程之所以能够无缝工作,是因为按钮被放到工具栏上,ToolBar类提供了一些内置逻辑,可将其子元素的CommandTarget属性动态设置为当前具有焦点的控件(从技术角度看,ToolBar控件一直在关注着其父元素,即窗口,并在上下文中查找最近具有焦点的控件,即文本框。ToolBar控件有单独的焦点范围(focus scope),并且在其上下文中按钮是具有焦点的)。

如果在不同容器(不是ToolBar或Menu控件)中放置按钮,就不会获得这项优势。这意味着
除非手动设置CommandTarget属性,否则按钮不能工作。为此,必须使用命名目标元素的绑定
表达式。例如,如果文本框被命名为txtDocument,就应该像下面这样定义按钮:



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

<StackPanel FocusManager.IsFocusScope="True">
    <Button Command="Cut">CutButton>
    <Button Command="Copy">CopyButton>
    <Button Command="Paste">PasteButton>
StackPanel>

该方法还有一个附加优点,即相同的命令可应用于多个控件,不像上个示例那样对CommandTarget进行硬编码。此外,Menu和ToolBar控件默认FocusManager.IsFocusScope
属性设置为True,但如果希望简化命令路由行为,不在父元素上下文中查找具有隹点的元素,
也可将该属性设为false。

在极少数情况下,您可能发现控件支持内置命令,而您并不想启用它。在这种情况下,可
以采用三种方法禁用命令。

理想情况下,控件会提供用于关闭命令支持的属性,从而确保控件移除这些特性并连贯地
调整自身。例如,TextBox控件提供了IsUndoEnabled属性,为阻止Undo特性,可将该属性设
置为false(如果IsUndoEnabled属性为true,Ctrl+Z组合键将触发Undo命令)。
如果这种做法行不通,可为希望禁用的命令添加新的命令绑定。然后该命令绑定可提供新
的CanExecute事件处理程序,并总是响应false。下面举一个使用该技术删除文本框Cut特性支
持的示例:

CommandBinding binding = new CommandBinding(ApplicationCommands.Cut,null,SuppressCommand);
            txt.CommandBindings.Add(binding);

private void SuppressCommand(object sender, CanExecuteRoutedEventArgs e)
{
    e.CanExecute = false;
    e.Handled = true;
}

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

该方法并不完美。它可成功地为文本框禁用Cut快捷键(Ctrl+X)和上下文菜单中的Cut命令。然而,仍会在上下文菜单中显示处于禁用状态的该选项。

最后一种选择是,使用InputBinding集合删除触发命令的输入。例如,可使用代码禁用触
发TextBox控件中Copy命令的Ctrl+C组合键,如:

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

技巧是使用特定的ApplicationCommands.NotACommand值,该命令什么都不做,它专门用于禁用输入绑定。

当使用这种方法时,仍启用copy命令。可通过自己创建的按钮触发该命令(或使用文本框的上下文菜单触发命令,除非也通过将ContextMenu属性设置为null删除了上下文菜单)。

高级命令

自定义命令

在5个命令类(ApplicationCommands、NavigationCommands、EditingCommands、ComponentCommands以及Mediacommands)中存储的命令,不会为应用程序提供所有可能需要的命令。但是,可以很方便地自定义命令,需要做的全部工作就是实例化一个新的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));
    }

    public static RoutedUICommand Requery
    {
        get
        {
            return requery;
        }
    }
}

如果希望在XAML中使用自定义的命令,那么首先需要将.NET名称空间映射为XML名称空间。例如,如果自定义的命令类位于Commands名称空间中(对于名为Commands的项目,这是默认的名称空间),那么应添加如下名称空间映射:

xmls:local="clr-namespace:Commands"

可通过local名称空间访问命令:

<CommandBinding Command="local:DataCommands.Requery" Executed="RequeryCommand_Executed">CommandBinding>

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

在不同位置是使用相同的命令时。当为这些命令中的某个命令触发Executed事件时,不知道该事件是属于第一个文本框还是第二个文本框。尽管ExecuteRoutedEventArgs对象提供了Source属性,但该属性反映的是具有命令绑定的元素(像sender引用);而到目前为止,所有命令都被绑定到了容器窗口。

解决这个问题的方法是使用文本框的CommandBindings集合分别为每个文本框绑定命令。如:

<TextBlock.CommandBindings>
  <CommandBinding Command="ApplicationCommands.Save" Executed="SaveCommand_Excuted" CanExecute="SaveCommand_CanExcuted">CommandBinding>
TextBlock.CommandBindings>

如何区分多个文本框?

  • 设置TextBlock.Tag
  • 创建私有的字典集合来保存相应的值,按照空间引用编写索引。当触发CanExecutedSave()方法时,查找属性与sender的对应的值,例:

    private Dictionarybool> isDirty = new Dictionary<object, bool>();
    
    private void txt_TextChanged(object sender, RoutedEventArgs e)
    {
        isDirty[sender] = true;
    }
    
    
    private void SaveCommand_CanExcuted(object sender, CanExecuteRoutedEventArgs e)
    {
        if (isDirty.ContainsKey(sender) && isDirty[sender])
        {
            e.CanExecute = true;
        }
        else
        {
            e.CanExecute = false;
        }
    }
  • 创建命令绑定,并向两个文本框的CommandBindings集合中添加同一个绑定,例:

    <Window.Resources>
        <CommandBinding x:Key="binding" Command="ApplicationCommands.Save" Executed="SaveCommand_Excuted" CanExecute="SaveCommand_CanExcuted">CommandBinding>
    Window.Resources>
    
    <TextBlock.CommandBindings>
        <StaticResource ResourceKey="binding">StaticResource>
    TextBlock.CommandBindings>

使用命令参数

到目前为止,您看到的所有示例都没有使用命令参数来传递额外信息。然而,有些命令总需要一些额外信息。例如,两个文本编辑器使用save命令,当保存文档时需要知道使用哪个文件。

解决方法是设置commandParameter属性。可直接为ICommandSource控件设置该属性(甚至可使用绑定表达式从其他控件获取值)。例如,下面的代码演示了如何通过从另一个文本框中读取数值,为链接到Zoom命令的按钮设置缩放百分比:

<Button Command="NavigationCommands.Zoom" CommandParameter="{Binding ElementName=txtZoom,Path=Text}">Zoom To ValueButton>

但该方法并不总是有效。例如,在具有两个文件的文本编辑器中,每个文本框重用同一个save按钮,但每个文本框需要使用不同的文件名。对于此类情况,必须在其他地方存储信息(例如,在TextBox.Tag属性或在为区分文本框而索引文件名称的单独集合中存储信息),或者需要通过代码触发命令,如:

ApplicationCommands.New.Execute(theFileName,(Button)sender);

无论使用哪种方法,都可以在Executed事件处理程序中通过ExecutedRoutedEventArgs.Parameter属性获取参数。

跟踪和翻转命令

WPF命令模型缺少的一个特性是翻转命令。尽管提供了ApplicationCommands.Undo命令,但该命令通常用于编辑控件(如TextBox控件)以维护它们自己的Undo历史。如果希望支持应用程序范围内的Undo特性,需要在内部跟踪以前的状态,并当触发Undo命令时还原该状态。

如果希望存储该状态,需要构建自己的数据结构。本例使用名为CommandHistoryItem的类。功能为刚刚删除的字符。

每个CommandHistoryItem对象跟踪以下几部分信息:

  • 命令名称。
  • 执行命令的元素。在该例中,有两个文本框,所以可以是其中的任意一个。
  • 在目标元素中被改变了的属性。在该例中是TextBox类的Text属性。
  • 可用于保存受影响元素以前状态的对象(例如,执行命令之前文本框中的文本)。

这一设计非常巧妙,可以为元素存储状态。如果存储整个窗口状态的快照,那么会显著增加内存的使用量.然而,如果具有大量数据(比如文本框有几十行文本),Undo操作的负担就很大了。解决方法是限制在历史中存储的项的数量,或使用更加智能(也更复杂)的方法只存储被改变的数据的信息,而不是存储所有数据。

public class CommandHistoryItem
{
    private string _commandName;
    private UIElement _elementActedOn;
    private string _propertyActedOn;
    private object _previousState;

    public string CommandName
        {
            get
            {
                return _commandName;
            }

            set
            {
                _commandName = value;
            }
        }

    public UIElement ElementActedOn
        {
            get
            {
                return _elementActedOn;
            }

            set
            {
                _elementActedOn = value;
            }
        }

    public string PropertyActedOn
        {
            get
            {
                return _propertyActedOn;
            }

            set
            {
                _propertyActedOn = value;
            }
        }

    public object PreviousState
        {
            get
            {
                return _previousState;
            }

            set
            {
                _previousState = value;
            }
        }

    public CommandHistoryItem(string commandName) : this(commandName, null, "", null) { }

    public CommandHistoryItem(string commandName, UIElement elementActedOn, string propertyActedOn, object previousState)
    {
        _commandName = commandName;
        _elementActedOn = elementActedOn;
        _propertyActedOn = propertyActedOn;
        _previousState = previousState;
    }

    public bool CanUndo
    {
        get { return (_elementActedOn != null && _propertyActedOn != ""); }
    }

    /// 
    /// 该方法使用反射为修改过的属性应用以前的值,用于恢复TextBox控件中的文本。
    /// 
    public void Undo()
    {
        Type elementType = _elementActedOn.GetType();
        PropertyInfo property = elementType.GetProperty(_propertyActedOn);
        property.SetValue(_elementActedOn, _previousState, null);
    }
}

需要的下一个要素是执行应用程序范围内Undo操作的命令。

public class MonitorCommands
{
    private static RoutedUICommand _applicationUndo;

    public static RoutedUICommand ApplicationUndo
    {
        get { return _applicationUndo; }
    }

    static MonitorCommands()
    {
        _applicationUndo = new RoutedUICommand("ApplicationUndo", "Application Undo", typeof(MonitorCommands));
    }
}

到目前为止,除了执行Undo操作的反射代码比较有意义外,其他代码没有什么值得注意的地方。响应特定的命令是非常简单的,但当执行任何命令时如何进行响应呢?技巧是使用CommandManager类,该类提供了几个静态事件。这些事件包括CanExecute、PreviewCanExecute、Executed以及PreviewExecuted。

下面的代码在窗口的构造函数中关联PreviewExecuted事件处理程序,并当关闭窗口时解除关联:

public MainWindow()
{
    InitializeComponent();
    this.AddHandler(CommandManager.PreviewExecutedEvent, new ExecutedRoutedEventHandler(CommandExecuted));
}

private void Window_Unloaded(object sender, RoutedEventArgs e)
{
    this.RemoveHandler(CommandManager.PreviewExecutedEvent, new ExecutedRoutedEventHandler(CommandExecuted));
}

当触发PreviewExecuted事件时,需要确定准备执行的命令是否是我们所关心的。如果是,可创建CommandHistoryItem对象,并将其添加到Undo堆栈中。还需要注意两个潜在问题。第一个问题是,当单击工具栏按钮以在文本框上执行命令时,CommandExecuted事件被引发了两次,一次是针对工具栏按钮,另一次是针对文本框。下面的代码通过忽略发送者是ICommandSource的命令,避免在Undo历史中重复条目。第二个问题是,需要明确忽略不希望
添加到Undo历史中的命令。例如ApplicationUndo命令,通过该命令可翻转上一步操作。

private void CommandExecuted(object sender, ExecutedRoutedEventArgs e)
{
    //忽略按钮源
    if (e.Source is ICommandSource) return;

    //忽略自编写的ApplicationUndo命令
    if (e.Command == UndoCommands.ApplicationUndo) return;

    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);
    }
}

该示例在 ListBox 控件中存储了所有 CommandHistoryItem 对象。 ListBox 控件的 DisplayMember 属性被设置为 CommandName ,所以它会显示每个项目的 CommandHistoryImte.CommandName 属性。以上代码只为由文本框引发的命令提供 Undo 特性。然而,处理窗口中的任何文本框通常足够了。为了支持其他控件和属性,需要对代码进行扩展。

最后一个细节是执行应用程序范围内 Undo 操作的代码。使用 CanExecute 事件处理程序,可以确保只有当在 Undo 历史中至少有一项时,才能执行Undo操作:

//评估复原命令是否可用
private void CommandBinding_CanExecute_1(object sender, CanExecuteRoutedEventArgs e)
{
    if (lstHistory == null || lstHistory.Items.Count == 0)
        e.CanExecute = false;
    else
        e.CanExecute = true;
}

为了恢复最近的修改,只需要调用 CommandHistoryItem 的 Undo() 方法,然后从 ListBox 项中删除该项即可:

//执行复原命令
private void CommandBinding_Executed_1(object sender, ExecutedRoutedEventArgs e)
{
    CommandHistoryItem historyImte = (CommandHistoryItem)lstHistory.Items[lstHistory.Items.Count - 1];

    if (historyImte.CanUndo)
        historyImte.Undo();

    lstHistory.Items.Remove(historyImte);
}

尽管该示例演示了相关概念,并提供了一个简单的应用程序,但是要在实际的应用程序中使用这一方法,还需要进行许多改进。例如,需要花费大量的时间改进 CommandMamager.PreviewExecuted 事件的处理程序,以忽略哪些明显不需要跟踪的命令(当前,如使用键盘选择文本的事件以及单击空格键引发的命令等)。类似地,可能希望为那些不是由命令表示的但应当被翻转的操作添加CommandHistoryltem对象。例如,输入一些文本,然后导航到其他控件。最后,可能希望将Undo历史限制在最近执行的命令范围之内。

你可能感兴趣的:(看书笔记_WPF4.5编程宝典)