在设计良好的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 确定将在其中执行命令的元素
命令绑定
将命令关联到命令源时,会看到命令源会被自动禁用。这是因为命令源已经查询到命令的状态,而且由于命令还没有与其关联的绑定,所以命令源被认为是禁用的
为改变这种状态,需要为命令创建绑定以明确以下三件事情:
1、当命令被触发时执行什么操作
2、如何确定命令是否能够被执行(这是可选的。如果未提供这一细节,只要提供了关联的事件处理程序,命令总是可用的)。
3、命令在何处起作用。例如,命令可被限制在单个按钮中使用,或在整个窗口中使用(这种情况更常见)。
通过代码为命令创建绑定
CommandBinding binding = new CommandBinding(ApplicationCommands.New);
binding.Executed += NewCommandExecuted;
this.CommandBindings.Add(binding);
这里创建的CommandBinding对象被添加到包含窗口的CommandBindings集合中,这通过事件冒泡进行工作。实际上,当单击按钮时,CommandBinding.Executed 事件从按钮冒泡到包含元素。
通过XAML标记创建命令绑定:
尽管习惯上为窗口添加所有绑定,单CommandBindings属性实际是在 UIElement基类中定义的。这意味着任何元素都支持该属性(例如,如果将命令绑定到使用它的按钮中)。为得到最大的灵活性,命令绑定通常被添加到顶级窗口。如果希望在多个窗口中使用相同的命令,需要在这些窗口中分别创建命令绑定。
也可以处理CommandBinding.PreviewExecuted事件,首先在最高层次的容器中引发该事件,然后隧道路由至按钮。在事件完成前,可通过事件隧道拦截和停止事件。如果将 RoutedEventArgs.Handled 属性设置为true,将永远不会发生Executed事件。
使用多命令源
在按钮中触发事件看起不那么直接,然而,当添加相同的命令的更多控件时,额外命令层的意义就会体现出来。
这里并没有为MenuItem设置Header属性。这是因为MenuItem类足够智能,如果没有设置Header属性,它将从命令中提取文件(Button控件不具有这一特性)。虽然该特性带来的便利看起来不大,但如果计划使用不同的语言本地化应用程序,这一特性就很重要了。在这一情况下,只需要在一个地方修改文本即可(通过设置命令的Text属性),这比在整个窗口中进行跟踪更容易。
MenuItem类还有一项功能:能自动提取 Command.InputBindings 集合中的第一个快捷键(如果存在的话)。对于ApplicationCommands.New 命令对象,这意味着在菜单文本旁边会显示Ctrl+N 快捷键。
需要注意的是,不需要为菜单项另外创建命令绑定。前面在窗口下创建的命令绑定现在被两个不同的控件使用,每个控件都将它们的工作传递给同一个命令事件处理程序。
微调命令文本
前面看到菜单具有自动提取命令的文本的功能,它实际获取的 RoutedUICommand 的 Text属性,那么其他的实现ICommandSource的控件应该也能提取相应Command的Text属性。我们可以通过数据绑定来获取:
直接调用命令
并非只能使用实现了ICommandSource 接口的类来触发希望执行的命令。也可以用Execute() 方法直接调用来自任何事件处理程序的方法。这是需传递参数值(或null引用)和对目标元素的引用。
ApplicationCommands.New.Execute(null, target);
也可以在关联的CommandBinding对象中调用Execute()方法。在这种情况下,不需要提供目标元素,因为会自动将公开正在使用CommandBindings集合的元素设置为目标元素
this.CommandBindings[0].Command.Execute(null);
这种方法只是用了半个命令模型,虽然也可触发命令,但不能响应命令的状态变化。
禁用命令
如果想要创建状态在启用和禁用之间变化的命令,您将体会到命令模型的真正优势。命令的启用与禁用通过CommandBinding的 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控件,添加以下工具栏按钮,就会自动获得对剪切、复制、和粘贴文本的支持
现在单击这些按钮中的任意一个(当文本框具有焦点时),就可以复制、剪切或从剪贴板粘贴文本。有趣的是,文本框还处理CanExecute事件。如果当前未在文本框中选中任何内容,就会禁用剪切和复制命令。当焦点移动到其它不支持这些命令的控件时,会自动禁用所有者三个命令(除非关联自己的CanExecute事件处理程序以启用这些命令)。
这里有一个有趣的细节。Cut、Copy、Paste命令被具有焦点的文本框处理。然而,有工具栏上的按钮触发的命令是完全独立的元素。这个过程之所以能够无缝工作,是因为按钮被放到工具栏上,ToolBar提供了一些内置逻辑,可将其子元素的CommandTarget属性动态设置为当前具有焦点的控件(从技术角度看,ToolBar控件一直在关注着其父元素,及窗口,并在上下文中查找最近具有焦点的控件,即文本框。ToolBar控件有单独的焦点范围focus scope,并且在其上下文中按钮时具有焦点的)。
如果是在不同容器(不是ToolBar或Menu控件)中放置按钮,就不会获得这项优势。这意味着除非手动设置CommandTarget 属性,否则按钮不能工作。为此,必须使用命名目标元素的绑定表达式。例如,如果文本框被命名为 txt,就应该向下面这样定义按钮:
另一个较简单的选择是使用附加属性FocusManager.IsFocusScope 创建新的焦点范围。当触发命令时,改焦点范围会通知WPF在父元素范围内查找元素:
该方法还有一个附加优点,即相同的命令可应用于多个控件,不像上面那样对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集合删除触发命令的输入。
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中使用自定义命令,注意需要带上名称空间映射。
在不同的位置使用相同的命令
在WPF命令模型中,一个重要概念是范围。尽管每个命令仅有一份副本,但使用命令的效果却会根据触发命令的位置而异。例如,有两个文本框,它们都支持Cut、Copy和Paste命令,操作只会在当前具有焦点的文本框中发生,但是对于需要自己实现的命令——New、Open、Save情况就不同了。问题在于当为这些命令的某个命令触发Executed事件时,不知道该事件是属于第一个文本框还是第二个文本框。尽管ExecuteRoutedEventArgs对象提供了Source属性,但该属性反映的是具有命令绑定的元素(像sender引用);而所有命令都被绑定到容器级。
解决这个问题的方法是使用文本框的CommandBindings集合分别为每个文本框绑定命令。这里需要为两个TextBox创建两个相同的CommandBindings,实际上只需要一个。可以向两个TextBox添加同一个命令绑定,如果使用XAML,需要将命令绑定添加到资源中,然后在TextBox的CommandBindings集合中使用StaticResource标记扩展并提供键名。
使用命令参数
有些命令需要一些额外信息,例如,NavigationCommands.Zoom命令需要用于缩放的百分数,或者前面说到的两个文本框使用Save命令时,需要知道使用的是哪个文件。
解决方法是设置CommandParameter 属性。可直接为 ICommandSource 控件设置该属性。
5
Hello
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;
}
}
要在实际应用程序中使用这一方法,还需要进行许多改进。例如,需要耗费大量时间改进 CommandManager.PreviewExecutedEvent 事件处理程序,以忽略那些明显不需要跟踪的命令(诸如使用键盘选择文本的事件、单击空格键引发的命令等)。类似地,可能希望为那些不是由命令表示的但应当被翻转的操作添加CommandHistoryItem 对象。例如,输入一些文本,然后导航到其它控件。最后,可能希望将Undo历史限制在最近执行的命令范围之内。
测试代码展示
MainWindow.xaml
5
Hello
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