WPF命令

29 篇文章 0 订阅
19 篇文章 1 订阅

在设计良好的Windows应用程序中,应用程序逻辑不应位于事件处理程序中,而应在更高层的方法中编写代码。其中的每个方法都代表单独的应用程序任务。每个任务可能依赖其他库。

使用这种设计最明显的方式是在需要的地方添加事件处理程序,并使用各个事件处理程序调用恰当的应用程序方法。本质上,窗口代码变成了一个精简的交换台,可以响应输入,并将请求转发到应用程序。

尽管这种设计非常合理,但却没有减少任何工作。许多应用程序任务可通过各种不同的路由触发,所以经常需要编写多个时间处理程序来调用相同的应用程序方法。就其本身而言,这并不是什么问题(因为交换台代码非常简单),但当需要处理用户界面的状态时,问题就变复杂了。

设想有一个程序,该程序包含方法 PrintDocument()。使用4中方式触发该方法:通过主菜单(选择File|Print菜单项),通过上下文菜单(在某处右击并选择Print菜单项),通过键盘快捷键(Ctrl+P)以及通过工具栏。在应用程序生命周期的特定时刻,需要暂时禁用PrintDocument() 任务。这意味着需要禁用两个菜单选项和一个工具栏按钮,使他们不能被单击,并需要忽略 Ctrl+P快捷键。编写代码完成这些工作(并在后面添加代码以启用这些控件)是很麻烦的,更糟的是,如果没有正确完成这项工作,可能会使不同状态的代码块不正确的相互重叠,从而导致某个控件在可应该可用时而被启用。编写和调试这类代码是Windows开发中最枯燥的内容之一。

幸运的是,WPF使用新的命令模型帮助解决这些问题。它增加了两个重要特性:

1、将事件委托到适当的命令

2、使控件的启用状态和相应的命令状态保持同步

命令模型

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

命令 命令表示应用程序任务,并且跟踪任务是否能够被执行。然而,命令实际上不包含执行应用程序任务的代码。

命令绑定 每个命令绑定针对用户界面的具体区域,将命令连接到相关的应用程序逻辑。这种分解的设计是非常重要的,因为单个命令可用于应用程序中的多个地方,并且在每个地方具有不同的意义。为处理这一问题,需要将同一命令与不同的命令源绑定。

命令源 命令源触发命令。例如,MenuItem和Button都是命令源。单击它们都会执行绑定命令。

命令目标 命令目标是在其中执行命令的元素。例如,Paste命令可在TextBox 控件中插入文本,而OpenFile 命令可在DocumentViewer中打开文档。根据命令的本质,目标可能很重要,也可能不重要。

ICommand接口

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

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

在一个简单实现中,Execute()方法将包含应用程序任务逻辑。然而,WPF的实现更复杂,它使用Execute()方法引发一个更复杂的过程,该过程最终触发在应用程序其他地方处理的事件。通过这种方式可以使用预先准备好的命令类,并插入自己的逻辑。还可以灵活地在几个不同的地方使用同一个命令。

CanExecute()方法返回命令的状态——如果命令可用,就返回true;如果不可以,就返回false。Execute() 和 CanExecute()方法都接受一个附加的参数对象,可使用该对象传递所需的任何附加信息。

最后,当命令状态改变时,引发CanExecuteChanged事件。对于使用命令的任何控件,这是指示信号,表示他们应当调用CanExecute()方法检查命令的状态。通过使用该事件,当命令可用时,命令源可自动启用自身;当命令不可用是,禁用自身。

RoutedCommand

当创建自己的命令时,不会直接实现ICommand接口,而是使用System.Windows.Input.RoutedCommand类,该类自动实现ICommand接口。RoutedCommand类是WPF中唯一实现了ICommand接口的类。换句话说,所有WPF命令都是RoutedCommand类及其派生类的实例。

RoutedCommand类不包含任何应用程序逻辑,而只代表命令,这意味着各个RoutedCommand对象具有相同的功能。

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

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

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

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

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

RoutedUICommand

在程序中处理的大部分命令不是RoutedCommand对象,而是RoutedUICommand类的实例,RoutedUICommand类继承自RoutedCommand类(实际上,WPF提供的所有预先构建好的命令都是RoutedUICommand对象)。

RoutedUICommand类用于具有文本的命令,这些文本显示在用户界面中的某些地方。RoutedUICommand类只增加了Text属性,该属性是为命令显示的文本。

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

命令库

WPF设计者认识到,每个应用程序可能有大量的命令,并且对于许多不用的应用程序,很多命令是通用的。为减少创建这些命令所需的工作,WPF提供了基本命令库,基本命令库中保存的命令超过100条。这些命令通过以下5个专门的静态类的静态属性提供:

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以及IncreaseVolume)。

这些单独的命令对象仅是一些标志器,不具有实际功能。然而,许多命令都有一个额外的特征:默认输入绑定。例如,ApplicationCommands.Open 命令被映射到Ctrl+O快捷键。只要将命令绑定到命令源,并未窗口添加命令源,这个快捷键就会被激活,即使没有在用户界面的任何地方显示该命令也同样如此。

命令源

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

ICommandSource接口定义了三个属性:

    public interface ICommandSource
    {
        ICommand Command { get; }
        object CommandParameter { get; }
        IInputElement CommandTarget { get; }
    }

Command 指向连接的命令,这是唯一必需的细节

CommandParameter 提供其他希望随命令发送的数据

CommandTarget 确定将在其中执行命令的元素

命令绑定

将命令关联到命令源时,会看到命令源会被自动禁用。这是因为命令源已经查询到命令的状态,而且由于命令还没有与其关联的绑定,所以命令源被认为是禁用的

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

为改变这种状态,需要为命令创建绑定以明确以下三件事情:

1、当命令被触发时执行什么操作

2、如何确定命令是否能够被执行(这是可选的。如果未提供这一细节,只要提供了关联的事件处理程序,命令总是可用的)。

3、命令在何处起作用。例如,命令可被限制在单个按钮中使用,或在整个窗口中使用(这种情况更常见)。

通过代码为命令创建绑定

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

这里创建的CommandBinding对象被添加到包含窗口的CommandBindings集合中,这通过事件冒泡进行工作。实际上,当单击按钮时,CommandBinding.Executed 事件从按钮冒泡到包含元素。

通过XAML标记创建命令绑定:

<Window.CommandBindings>
    <CommandBinding Command="Open" Executed="OpenCommandExecuted" />
</Window.CommandBindings>

尽管习惯上为窗口添加所有绑定,单CommandBindings属性实际是在 UIElement基类中定义的。这意味着任何元素都支持该属性(例如,如果将命令绑定到使用它的按钮中)。为得到最大的灵活性,命令绑定通常被添加到顶级窗口。如果希望在多个窗口中使用相同的命令,需要在这些窗口中分别创建命令绑定。

也可以处理CommandBinding.PreviewExecuted事件,首先在最高层次的容器中引发该事件,然后隧道路由至按钮。在事件完成前,可通过事件隧道拦截和停止事件。如果将 RoutedEventArgs.Handled 属性设置为true,将永远不会发生Executed事件。

使用多命令源

在按钮中触发事件看起不那么直接,然而,当添加相同的命令的更多控件时,额外命令层的意义就会体现出来。

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

这里并没有为MenuItem设置Header属性。这是因为MenuItem类足够智能,如果没有设置Header属性,它将从命令中提取文件(Button控件不具有这一特性)。虽然该特性带来的便利看起来不大,但如果计划使用不同的语言本地化应用程序,这一特性就很重要了。在这一情况下,只需要在一个地方修改文本即可(通过设置命令的Text属性),这比在整个窗口中进行跟踪更容易。

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

需要注意的是,不需要为菜单项另外创建命令绑定。前面在窗口下创建的命令绑定现在被两个不同的控件使用,每个控件都将它们的工作传递给同一个命令事件处理程序。

微调命令文本

前面看到菜单具有自动提取命令的文本的功能,它实际获取的 RoutedUICommand 的 Text属性,那么其他的实现ICommandSource的控件应该也能提取相应Command的Text属性。我们可以通过数据绑定来获取:

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

直接调用命令

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

ApplicationCommands.New.Execute(null, target);

也可以在关联的CommandBinding对象中调用Execute()方法。在这种情况下,不需要提供目标元素,因为会自动将公开正在使用CommandBindings集合的元素设置为目标元素

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

这种方法只是用了半个命令模型,虽然也可触发命令,但不能响应命令的状态变化。

禁用命令

如果想要创建状态在启用和禁用之间变化的命令,您将体会到命令模型的真正优势。命令的启用与禁用通过CommandBinding的 CanExecute 事件控制,可通过如下方式为该事件关联事件处理程序:

<CommandBinding Command="Save" Executed="SaveCommand_Executed" CanExecute="SaveCommand_CanExecute"/>

在事件处理程序中,只需相应的设置 CanExecuteRoutedEventArgs.CanExecute 属性

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

在这里,当isDirty为false时,就禁用命令;当isDirty为true时,启用命令。

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

具有内置命令的控件

一些输入控件可以自行处理命令事件。例如,TextBox类处理Cut、Copy、Paste命令等。当控件具有自己的硬编码命令逻辑时,为使命令工作不需要做其他任何事情。例如,对于TextBox控件,添加以下工具栏按钮,就会自动获得对剪切、复制、和粘贴文本的支持

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

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

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

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

<Button Command="Cut" CommandTarget="{Binding ElementName=txt}" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/>

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

<StackPanel>
    <StackPanel Orientation="Horizontal" FocusManager.IsFocusScope="True">
        <Button Command="Cut" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/>
        <Button Command="Copy" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/>
        <Button Command="Paste" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/>
    </StackPanel>
    <TextBox TextWrapping="Wrap" AcceptsReturn="True"></TextBox>
</StackPanel>

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

在极少数情况下,控件支持内置命令,但并不需要,这种情况下,可以采用三种方式禁用命令:

1、理想情况下,控件会提供用于关闭命令支持的属性,从而确保控件移除这些特性并连贯地调整自身。例如,TextBox控件提供了IsUndoEnabled 属性,为阻止Undo特性,可将该属性设置为false(如果IsUndoEnabled属性为true,Ctrl+Z组合键将触发Undo命令)。

2、可为希望禁用的命令添加新的命令绑定。然后该命令绑定可提供新的 CanExecute 事件处理程序,并总是响应 false,并注意还要设置Handled 标志以阻止文本框自我执行计算,而文本框可能将CanExecute 属性设置为true。

3、使用InputBinding集合删除触发命令的输入。

<TextBox IsUndoEnabled="False" Text="Hello">
    <TextBox.CommandBindings>
        <CommandBinding Command="Cut" CanExecute="CommandBinding_CanExecute"/>
    </TextBox.CommandBindings>
    <TextBox.InputBindings>
        <KeyBinding Command="NotACommand" Key="C" Modifiers="Ctrl"/>
    </TextBox.InputBindings>
</TextBox>
    private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
    {
        e.CanExecute = false;
        e.Handled = true;
    }

这样一个文本框将会阻止撤销、剪切以及Ctrl+C键的复制。

自定义命令

RoutedUICommand 类提供了几个构造函数。虽然可创建没有任何附加信息的RoutedUICommand对象,但几乎总是希望提供命令名、命令文本以及所属类型。此外,可能希望为InputGestures 集合提供快捷键。

最佳设计是遵循WPF库中的范例,并通过静态属性提供自定义命令。

public class CustomCommands
{
    private static RoutedUICommand requery;
    public static RoutedUICommand Requery { get => requery; }

    static CustomCommands()
    {
        InputGestureCollection inputs = new InputGestureCollection();
        inputs.Add(new KeyGesture(Key.R, ModifierKeys.Control, "Ctrl+R"));
        requery = new RoutedUICommand("Requery", "Requery", typeof(CustomCommands), inputs);
    }
}

一旦定义了命令,就可以在命令绑定中使用它,就像使用WPF提供的所有预先构建好的命令那样。如果希望在XAML中使用自定义命令,注意需要带上名称空间映射。

<StackPanel>
    <StackPanel.CommandBindings>
        <CommandBinding Command="local:DataCommands.Requery" Executed="CommandBinding_Executed"/>
    </StackPanel.CommandBindings>
    <Button Command="local:DataCommands.Requery" Content="Requery"/>
</StackPanel>

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

在WPF命令模型中,一个重要概念是范围。尽管每个命令仅有一份副本,但使用命令的效果却会根据触发命令的位置而异。例如,有两个文本框,它们都支持Cut、Copy和Paste命令,操作只会在当前具有焦点的文本框中发生,但是对于需要自己实现的命令——New、Open、Save情况就不同了。问题在于当为这些命令的某个命令触发Executed事件时,不知道该事件是属于第一个文本框还是第二个文本框。尽管ExecuteRoutedEventArgs对象提供了Source属性,但该属性反映的是具有命令绑定的元素(像sender引用);而所有命令都被绑定到容器级。

解决这个问题的方法是使用文本框的CommandBindings集合分别为每个文本框绑定命令。这里需要为两个TextBox创建两个相同的CommandBindings,实际上只需要一个。可以向两个TextBox添加同一个命令绑定,如果使用XAML,需要将命令绑定添加到资源中,然后在TextBox的CommandBindings集合中使用StaticResource标记扩展并提供键名。

<StackPanel>
    <StackPanel.Resources>
        <CommandBinding x:Key="saveCommandBinding" Command="Save" Executed="TwoTextBox_SaveCommand_Executed" CanExecute="TwoTextBox_SaveCommand_CanExecute"/>
    </StackPanel.Resources>
    <ToolBarTray >
        <ToolBar>
            <Button Command="Save" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/>
        </ToolBar>
    </ToolBarTray>
    <TextBox TextWrapping="Wrap" AcceptsReturn="True" TextChanged="TwoTextBox_TextChanged">
        <TextBox.CommandBindings>
            <StaticResource ResourceKey="saveCommandBinding"/>
        </TextBox.CommandBindings>
    </TextBox>
    <TextBox TextWrapping="Wrap" AcceptsReturn="True" TextChanged="TwoTextBox_TextChanged">
        <TextBox.CommandBindings>
            <StaticResource ResourceKey="saveCommandBinding"/>
        </TextBox.CommandBindings>
    </TextBox>
</StackPanel>

使用命令参数

有些命令需要一些额外信息,例如,NavigationCommands.Zoom命令需要用于缩放的百分数,或者前面说到的两个文本框使用Save命令时,需要知道使用的是哪个文件。

解决方法是设置CommandParameter 属性。可直接为 ICommandSource 控件设置该属性。

<StackPanel>
    <StackPanel.Resources>
        <local:FontStringValueConverter x:Key="StringConverterResource"/>
    </StackPanel.Resources>
    <TextBox Name="txtValue" Background="AliceBlue" Margin="5">5</TextBox>
    <Button Command="{x:Static local:DataCommands.Requery}" CommandTarget="{Binding ElementName=txtBoxTarget}"
            CommandParameter="{Binding ElementName=txtValue, Path=Text, Converter={StaticResource StringConverterResource}}" Content="UpdateFontSize"/> 
    <TextBox Name="txtBoxTarget" Height="100" Margin="3">
        <TextBox.CommandBindings>
            <CommandBinding Command="{x:Static local:DataCommands.Requery}" Executed="FontSizeCommandBinding_Executed"/>
        </TextBox.CommandBindings>
        Hello
    </TextBox>
</StackPanel>
    private void FontSizeCommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
    {
        TextBox? source = sender as TextBox;
        if(source != null)
        {
            if(e.Parameter != null)
            {
                try
                {
                    if ((int)e.Parameter > 0 && (int)e.Parameter < 60)
                    {
                        source.FontSize = (int)e.Parameter;
                    }
                }
                catch
                {
                    MessageBox.Show("in Command \n Parameter: " + e.Parameter);
                }
            }
        }
    }

跟踪和翻转命令

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

遗憾的是,扩展WPF命令系统并不容易。相对来说没几个入口点可用于连接自定义逻辑,并且对于可用的几个入口点也没有提供说明文档。为创建通用的、可重用的Undo特性,需要创建一组全新的“能够撤销的”命令类,以及一个特定的命令绑定。本质上,必须使用自己创建的新命令系统替换WPF命令系统。

更好的解决方案是设计自己的用于跟踪和翻转命令的系统,但使用CommandManager类保存命令历史。

第一个细节是用于跟踪命令历史的类。为构建保存最近命令历史的撤销系统,可能需要用到这样的类(比如创建派生的ReversibleCommand类,提供诸如Unexecute的方法来翻转以前的任务)。但该系统不能工作,因为所有WPF命令都是唯一的。这意味着在应用程序中每个命令只有一个实例。

为理解该问题,假设提供EditingCommands.Backspace 命令,而且用户在一行中回退了几个空格。可通过向最近命令堆栈中添加Backspace命令来记录这一操作,但实际上每次添加的是相同的命令对象。因此,没有简单的方法用于存储命令的其它信息,例如刚刚删除的字符。如果希望存储该状态,需要构建自己的数据结构。该结构跟踪以下几部分信息:

1、命令名称。

2、执行命令的元素。在这里有两个文本框,所以可以是其中任意一个

3、在目标元素中被改变了的属性。在这里是TextBox类的Text属性

4、可用于保存受影响元素以前状态的对象。

CommandHistoryItem类还提供了通用的Undo方法,该方法使用反射为修改过的属性应用以前的值,用于恢复TextBox控件中的文本。但对于更复杂的应用程序,需要使用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 propertyActedOn, object? previousState)
    {
        CommandName = commandName;
        ElementActedOn = elementActedOn;
        PropertyActedOn = propertyActedOn;
        PreviousState = previousState;
    }
    public bool CanUndo
    {
        get { return (ElementActedOn != null && PropertyActedOn != ""); }
    }
    public void Undo()
    {
        Type? elementType = ElementActedOn?.GetType();
        PropertyInfo? property = elementType?.GetProperty(PropertyActedOn);
        property?.SetValue(ElementActedOn, PreviousState, null);
    }
}

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

需要的下一个操作要素是执行应用程序范围内Undo操作的命令。ApplicationCommands.Undo命令是不合适的,原因是为了达到不同的目的,它已经被用于单独的文本框控件(翻转最后的编辑变化)。相反,需要创建一个新命令:CustomCommands.ApplicationUndo

public class CustomCommands
{
    private static RoutedUICommand requery;
    public static RoutedUICommand Requery { get => requery; }
    private static RoutedUICommand applicationUndo;
    public static RoutedUICommand ApplicationUndo { get=> applicationUndo; }

    static CustomCommands()
    {
        InputGestureCollection inputs = new InputGestureCollection();
        inputs.Add(new KeyGesture(Key.R, ModifierKeys.Control, "Ctrl+R"));
        requery = new RoutedUICommand("Requery", "Requery", typeof(CustomCommands), inputs);

        applicationUndo = new RoutedUICommand("ApplicationUndo", "Application Undo", typeof(CustomCommands));
    }
}

响应特定命令非常简单,但当执行任何命令时,如何进行响应并记录呢?技巧是使用CommandManager类,该类提供了几个静态事件:

        public static readonly RoutedEvent CanExecuteEvent;
        public static readonly RoutedEvent ExecutedEvent;
        public static readonly RoutedEvent PreviewCanExecuteEvent;
        public static readonly RoutedEvent PreviewExecutedEvent;

这里我们主要关注ExecutedEvent 和 PreviewExecutedEvent 事件,因为每当执行任何一个命令时都会引发它们。

尽管CommandManager类挂起了Executed事件,但仍可使用UIElement.AddHandler()方法关联事件处理程序,并未可选的第三个参数传递true值。这样将允许接收事件,即使事件已经被处理过也同样如此。然而,Executed事件是在命令执行完成之后被触发的,这时已经来不及在命令历史中保存被影响的控件的状态了。相反,需要响应PreviewExecuted事件,该事件在命令执行前一刻被触发。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        AddCommandBinding();

        this.AddHandler(CommandManager.PreviewExecutedEvent, new ExecutedRoutedEventHandler(CommandExecuted));
    }

    private void NewCommandExecuted(object sender, ExecutedRoutedEventArgs e)
    {
        MessageBox.Show("New command triggered by " + e.Source.ToString());
        if(e.Source != null)
        {
            var target = e.Source as Control;
            if (target != null)
            {
                if (target.Background == Brushes.Blue)
                {
                    target.Background = Brushes.Red;
                }
                else
                {
                    target.Background = Brushes.Blue;
                }
            }
        }
    }
    private void ApplicationUndoCommand_Executed(object sender, ExecutedRoutedEventArgs e)
    {
        CommandHistoryItem historyItem = (CommandHistoryItem)lstHistory.Items[^1];
        if (historyItem.CanUndo)
            historyItem.Undo();
        lstHistory.Items.Remove(historyItem);
    }

    private void ApplicationUndoCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
    {
        if (lstHistory == null || lstHistory.Items.Count == 0)
            e.CanExecute = false;
        else
            e.CanExecute = true;
    }
}
<StackPanel>
    <StackPanel.CommandBindings>
        <CommandBinding Command="local:CustomCommands.ApplicationUndo" Executed="ApplicationUndoCommand_Executed" CanExecute="ApplicationUndoCommand_CanExecute"></CommandBinding>
    </StackPanel.CommandBindings>

    <ToolBarTray>
        <ToolBar>
            <Button Command="ApplicationCommands.Cut">Cut</Button>
            <Button Command="ApplicationCommands.Copy">Copy</Button>
            <Button Command="ApplicationCommands.Paste">Paste</Button>
            <Button Command="ApplicationCommands.Undo">Undo</Button>
        </ToolBar>
        <ToolBar>
            <Button Command="local:CustomCommands.ApplicationUndo">ReverseLastCommand</Button>
        </ToolBar>
    </ToolBarTray>
    <TextBox Margin="5" TextWrapping="Wrap" AcceptsReturn="True"/>
    <TextBox Margin="5" TextWrapping="Wrap" AcceptsReturn="True"/>
    <ListBox Grid.Row="3" Name="lstHistory" Margin="5" DisplayMemberPath="CommandName"></ListBox>
</StackPanel>

要在实际应用程序中使用这一方法,还需要进行许多改进。例如,需要耗费大量时间改进 CommandManager.PreviewExecutedEvent 事件处理程序,以忽略那些明显不需要跟踪的命令(诸如使用键盘选择文本的事件、单击空格键引发的命令等)。类似地,可能希望为那些不是由命令表示的但应当被翻转的操作添加CommandHistoryItem 对象。例如,输入一些文本,然后导航到其它控件。最后,可能希望将Undo历史限制在最近执行的命令范围之内。

测试代码展示

MainWindow.xaml

<Window x:Class="TestCommand.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:TestCommand"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel Name="stackPanel">
        <StackPanel Name="stackPanel1">
            <StackPanel.CommandBindings>
                <CommandBinding Command="Open" Executed="OpenCommandExecuted" />
            </StackPanel.CommandBindings>
            <Menu FocusManager.IsFocusScope="False">
                <MenuItem Header="File">
                    <MenuItem Command="New" />
                </MenuItem>
            </Menu>
            <Button Command="ApplicationCommands.New"  Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}" />
            <Button Click="cmdDoCommand_Click">DoCommand</Button>
        </StackPanel>
        <StackPanel>
            <StackPanel.CommandBindings>
                <CommandBinding Command="New" Executed="NewCommand_Executed"/>
                <CommandBinding Command="Open" Executed="OpenCommand_Executed"/>
                <CommandBinding Command="Save" Executed="SaveCommand_Executed" CanExecute="SaveCommand_CanExecute"/>
            </StackPanel.CommandBindings>
            <Menu>
                <MenuItem Header="File">
                    <MenuItem Command="New"></MenuItem>
                    <MenuItem Command="Open"></MenuItem>
                    <MenuItem Command="Save"></MenuItem>
                    <MenuItem Command="SaveAs"></MenuItem>
                    <Separator></Separator>
                    <MenuItem Command="Close"></MenuItem>
                </MenuItem>
            </Menu>
            <ToolBarTray >
                <ToolBar>
                    <Button Command="New" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/>
                    <Button Command="Open" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/>
                    <Button Command="Save" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/> 
                </ToolBar>
                <ToolBar>
                    <Button Command="Cut" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/>
                    <Button Command="Copy" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/>
                    <Button Command="Paste" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/>
                </ToolBar>
            </ToolBarTray>
            <TextBox TextWrapping="Wrap" AcceptsReturn="True" TextChanged="TextBox_TextChanged"></TextBox>
        </StackPanel>

        <StackPanel>
            <StackPanel Orientation="Horizontal">
                <Button Command="Cut" CommandTarget="{Binding ElementName=txt}" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/>
                <Button Command="Copy" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/>
                <Button Command="Paste" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/>
            </StackPanel>
            <TextBox Name="txt" TextWrapping="Wrap" AcceptsReturn="True"></TextBox>
        </StackPanel>

        <StackPanel>
            <StackPanel Orientation="Horizontal" FocusManager.IsFocusScope="True">
                <Button Command="Cut" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/>
                <Button Command="Copy" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/>
                <Button Command="Paste" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/>
            </StackPanel>
            <TextBox TextWrapping="Wrap" AcceptsReturn="True"></TextBox>
        </StackPanel>
        <StackPanel >
            <TextBox IsUndoEnabled="False" Text="Hello">
                <TextBox.CommandBindings>
                    <CommandBinding Command="Cut" CanExecute="CommandBinding_CanExecute"/>
                </TextBox.CommandBindings>
                <TextBox.InputBindings>
                    <KeyBinding Command="NotACommand" Key="C" Modifiers="Ctrl"/>
                </TextBox.InputBindings>
            </TextBox>
        </StackPanel>

        <StackPanel>
            <StackPanel.CommandBindings>
                <CommandBinding Command="local:CustomCommands.Requery" Executed="CommandBinding_Executed"/>
            </StackPanel.CommandBindings>
            <Button Command="local:CustomCommands.Requery" Content="Requery"/>
        </StackPanel>


        <StackPanel>
            <StackPanel.Resources>
                <CommandBinding x:Key="saveCommandBinding" Command="Save" Executed="TwoTextBox_SaveCommand_Executed" CanExecute="TwoTextBox_SaveCommand_CanExecute"/>
            </StackPanel.Resources>
            <ToolBarTray >
                <ToolBar>
                    <Button Command="Save" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/>
                </ToolBar>
            </ToolBarTray>
            <TextBox TextWrapping="Wrap" AcceptsReturn="True" TextChanged="TwoTextBox_TextChanged">
                <TextBox.CommandBindings>
                    <StaticResource ResourceKey="saveCommandBinding"/>
                </TextBox.CommandBindings>
            </TextBox>
            <TextBox TextWrapping="Wrap" AcceptsReturn="True" TextChanged="TwoTextBox_TextChanged">
                <TextBox.CommandBindings>
                    <StaticResource ResourceKey="saveCommandBinding"/>
                </TextBox.CommandBindings>
            </TextBox>
        </StackPanel>
        
        <StackPanel>
            <StackPanel.Resources>
                <local:FontStringValueConverter x:Key="StringConverterResource"/>
            </StackPanel.Resources>
            <TextBox Name="txtValue" Background="AliceBlue" Margin="5">5</TextBox>
            <Button Command="{x:Static local:CustomCommands.Requery}" CommandTarget="{Binding ElementName=txtBoxTarget}"
                    CommandParameter="{Binding ElementName=txtValue, Path=Text, Converter={StaticResource StringConverterResource}}" Content="UpdateFontSize"/> 
            <TextBox Name="txtBoxTarget" Height="100" Margin="3">
                <TextBox.CommandBindings>
                    <CommandBinding Command="{x:Static local:CustomCommands.Requery}" Executed="FontSizeCommandBinding_Executed"/>
                </TextBox.CommandBindings>
                Hello
            </TextBox>
        </StackPanel>

        <StackPanel>
            <StackPanel.CommandBindings>
                <CommandBinding Command="local:CustomCommands.ApplicationUndo" Executed="ApplicationUndoCommand_Executed" CanExecute="ApplicationUndoCommand_CanExecute"></CommandBinding>
            </StackPanel.CommandBindings>

            <ToolBarTray>
                <ToolBar>
                    <Button Command="ApplicationCommands.Cut">Cut</Button>
                    <Button Command="ApplicationCommands.Copy">Copy</Button>
                    <Button Command="ApplicationCommands.Paste">Paste</Button>
                    <Button Command="ApplicationCommands.Undo">Undo</Button>
                </ToolBar>
                <ToolBar>
                    <Button Command="local:CustomCommands.ApplicationUndo">ReverseLastCommand</Button>
                </ToolBar>
            </ToolBarTray>
            <TextBox Margin="5" TextWrapping="Wrap" AcceptsReturn="True"/>
            <TextBox Margin="5" TextWrapping="Wrap" AcceptsReturn="True"/>
            <ListBox Grid.Row="3" Name="lstHistory" Margin="5" DisplayMemberPath="CommandName"></ListBox>
        </StackPanel>
    </StackPanel>
</Window>

MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace TestCommand;

[ValueConversion(typeof(string), typeof(int))]
public class FontStringValueConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        string fontSize = (string)value;
        int intFont;
        try
        {
            intFont = int.Parse(fontSize);
            return intFont;
        }
        catch
        {
            return 0;
        }
    }
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return 0;
    }
}
public class CustomCommands
{
    private static RoutedUICommand requery;
    public static RoutedUICommand Requery { get => requery; }
    private static RoutedUICommand applicationUndo;
    public static RoutedUICommand ApplicationUndo { get=> applicationUndo; }

    static CustomCommands()
    {
        InputGestureCollection inputs = new InputGestureCollection();
        inputs.Add(new KeyGesture(Key.R, ModifierKeys.Control, "Ctrl+R"));
        requery = new RoutedUICommand("Requery", "Requery", typeof(CustomCommands), inputs);

        applicationUndo = new RoutedUICommand("ApplicationUndo", "Application Undo", typeof(CustomCommands));
    }
}

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 propertyActedOn, object? previousState)
    {
        CommandName = commandName;
        ElementActedOn = elementActedOn;
        PropertyActedOn = propertyActedOn;
        PreviousState = previousState;
    }
    public bool CanUndo
    {
        get { return (ElementActedOn != null && PropertyActedOn != ""); }
    }
    public void Undo()
    {
        Type? elementType = ElementActedOn?.GetType();
        PropertyInfo? property = elementType?.GetProperty(PropertyActedOn);
        property?.SetValue(ElementActedOn, PreviousState, null);
    }
}


public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        AddCommandBinding();

        this.AddHandler(CommandManager.PreviewExecutedEvent, new ExecutedRoutedEventHandler(CommandExecuted));
    }
    private void AddCommandBinding()
    {
        CommandBinding binding = new CommandBinding(ApplicationCommands.New);
        binding.Executed += NewCommandExecuted;
        stackPanel1.CommandBindings.Add(binding);
    }

    private void NewCommandExecuted(object sender, ExecutedRoutedEventArgs e)
    {
        MessageBox.Show("New command triggered by " + e.Source.ToString());
        if(e.Source != null)
        {
            var target = e.Source as Control;
            if (target != null)
            {
                if (target.Background == Brushes.Blue)
                {
                    target.Background = Brushes.Red;
                }
                else
                {
                    target.Background = Brushes.Blue;
                }
            }
        }
    }
    private void OpenCommandExecuted(object sender, ExecutedRoutedEventArgs e)
    {
        MessageBox.Show("Open command triggered by " + e.Source.ToString());
    }
    private void cmdDoCommand_Click(object sender, RoutedEventArgs e)
    {
        ApplicationCommands.New.Execute(null, null);
        //stackPanel1.CommandBindings[0].Command.Execute(null);
    }

    private bool isDirty = false;
    private void NewCommand_Executed(object sender, ExecutedRoutedEventArgs e)
    {
        MessageBox.Show("New command triggered with " + e.Source.ToString());
        isDirty = false;
    }

    private void OpenCommand_Executed(object sender, ExecutedRoutedEventArgs e)
    {
        isDirty = false;
    }

    private void SaveCommand_Executed(object sender, ExecutedRoutedEventArgs e)
    {
        MessageBox.Show("Save command triggered with " + e.Source.ToString());
        isDirty = false;
    }
    private void SaveCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
    {
        e.CanExecute = isDirty;
    }
    private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
    {
        isDirty = true;
    }

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

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

    private Dictionary<Object, bool> isDirtys = new Dictionary<Object, bool>();
    private void TwoTextBox_TextChanged(object sender, TextChangedEventArgs e)
    {
        isDirtys[sender] = true;
    }
    private void TwoTextBox_SaveCommand_Executed(object sender, ExecutedRoutedEventArgs e)
    {
        string text = ((TextBox)sender).Text;
        MessageBox.Show("About to save: " + text);
        isDirtys[sender] = false;
    }
    private void TwoTextBox_SaveCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
    {
        if (isDirtys.ContainsKey(sender) && isDirtys[sender] == true)
        {
            e.CanExecute = true;
        }
        else
        {
            e.CanExecute = false;
        }
    }
    private void FontSizeCommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
    {
        TextBox? source = sender as TextBox;
        if(source != null)
        {
            if(e.Parameter != null)
            {
                try
                {
                    if ((int)e.Parameter > 0 && (int)e.Parameter < 60)
                    {
                        source.FontSize = (int)e.Parameter;
                    }
                }
                catch
                {
                    MessageBox.Show("in Command \n Parameter: " + e.Parameter);
                }
            }
        }
    }
    private void CommandExecuted(object sender, ExecutedRoutedEventArgs e)
    {
        // Ignore menu button source.
        if (e.Source is ICommandSource)
            return;

        // Ignore the ApplicationUndo command.
        if (e.Command == CustomCommands.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();
        }
    }
    private void ApplicationUndoCommand_Executed(object sender, ExecutedRoutedEventArgs e)
    {
        CommandHistoryItem historyItem = (CommandHistoryItem)lstHistory.Items[^1];
        if (historyItem.CanUndo)
            historyItem.Undo();
        lstHistory.Items.Remove(historyItem);
    }

    private void ApplicationUndoCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
    {
        if (lstHistory == null || lstHistory.Items.Count == 0)
            e.CanExecute = false;
        else
            e.CanExecute = true;
    }
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值