【WPF 体系结构】 命令系统原理- Command of CommandManager.RequerySuggested
大部分内容来源:https://www.cnblogs.com/zhili/p/wpfcommand.html
一、引言
WPF命令相对来说是一个崭新的概念,因为命令对于之前的WinForm根本没有实现这个概念,但是这并不影响我们学习WPF命令,因为设计模式中有命令模式,关于命令模式可以参考我设计模式的博文:http://www.cnblogs.com/zhili/p/CommandPattern.html。
命令是 Windows Presentation Foundation (WPF) 中的一种输入机制,与设备输入相比,它提供的输入处理更侧重于语义级别。 示例命令如许多应用程序均具有的“复制”、“剪切”和“粘贴”操作。
命令模式的要旨在于把命令的发送者与命令的执行者之间的依赖关系分割开了。对此,WPF中的命令也是一样的,WPF命令使得命令源(即命令发送者,也称调用程序)和命令目标(即命令执行者,也称处理程序)分离。现在是不是感觉命令是不是亲切了点了呢?下面就详细分享下我对WPF命令的理解。
二、命令是什么呢?
什么是命令
命令具有多个用途。
第一个用途是分隔语义和从执行命令的逻辑调用命令的对象。 这可使多个不同的源调用同一命令逻辑,并且可针对不同目标自定义命令逻辑。 例如,许多应用程序中均有的编辑操作“复制”、“剪切”和“粘贴”若通过使用命令来实现,那么可通过使用不同的用户操作来调用它们。 应用程序可允许用户通过单击按钮、选择菜单中的项或使用组合键(例如 Ctrl+X)来剪切所选对象或文本。 通过使用命令,可将每种类型的用户操作绑定到相同逻辑。
命令的另一用途是指示操作是否可用。 继续以剪切对象或文本为例,此操作只有在选择了内容时才会发生作用。 如果用户在未选择任何内容的情况下尝试剪切对象或文本,则不会发生任何操作。 为了向用户指示这一点,许多应用程序通过禁用按钮和菜单项来告知用户是否可以执行某操作。 命令可以通过实现 CanExecute 方法来指示操作是否可行。 按钮可以订阅 CanExecuteChanged 事件,如果 CanExecute 返回 false
则禁用,如果 CanExecute 返回 true
则启用。
虽然命令的语义在应用程序和类之间可保持一致,但操作的逻辑特定于操作所针对的特定对象。 组合键 Ctrl+X 调用文本类、图像类和 Web 浏览器中的“剪切”命令,但执行“剪切”操作的实际逻辑由执行剪切的应用程序定义。 RoutedCommand 使客户端实现逻辑。 文本对象可将所选文本剪切到剪贴板,而图像对象则剪切所选图像。 应用程序处理 Executed 事件时可访问命令的目标,并根据目标的类型采取相应操作。
上面通过命令模式引出了WPF命令的要旨,那在WPF中,命令是什么呢?对于程序来说,命令就是一个个任务,例如保存,复制,剪切这些操作都可以理解为一个个命令。即当我们点击一个复杂按钮时,此时就相当于发出了一个复制的命令,即告诉文本框执行一个复杂选中内容的操作,然后由文本框控件去完成复制的操作。在这里,复杂按钮就相当于一个命令发送者,而文本框就是命令的执行者。它们之间通过命令对象分割开了。如果采用事件处理机制的话,此时调用程序与处理程序就相互引用了。
所以对于命令只是从不同角度理解问题的一个词汇,之前理解点击一个按钮,触发了一个点击事件,在WPF编程中也可以理解为触发了一个命令。说到这里,问题又来了,WPF中既然有了命令了?那为什么还需要路由事件呢?对于这个问题,我的理解是,事件和命令是处理问题的两种方式,它们之间根本不存在冲突的,并且WPF命令中使用了路由事件。所以准确地说WPF命令应该是路由命令。那为什么说WPF命令是路由的呢?这个疑惑将会在WPF命令模型介绍中为大家解答。
另外,WPF命令除了使命令源和命令目标分割的优点外,它还具有另一个优点:
- 使得控件的启用状态和相应的命令状态保持同步,即命令被禁用时,此时绑定命令的控件也会被禁用。
三、WPF命令模型
经过前面的介绍,大家应该已经命令了WPF命令吧。即命令就是一个操作,任务。接下来就要详细介绍了WPF命令模型了。
WPF命令模型具有4个重要元素:
- 命令——命令表示一个程序任务,并且可跟踪该任务是否能被执行。然而,命令实际上不包含执行应用程序的代码,真正处理程序在命令目标中。
- 命令源——命令源触发命令,即命令的发送者。例如Button、MenuItem等控件都是命令源,单击它们都会执行绑定的命令。
- 命令目标——命令目标是在其中执行命令的元素。如Copy命令可以在TextBox控件中复制文本。
- 命令绑定——前面说过,命令是不包含执行程序的代码的,真正处理程序存在于命令目标中。那命令是怎样映射到处理程序中的呢?这个过程就是通过命令绑定来完成的,命令绑定完成的就是红娘牵线的作用。
WPF命令模型的核心就在于ICommand接口了,该接口定义命令的工作原理。该接口的定义如下所示:
public interface ICommand { // Events event EventHandler CanExecuteChanged; // Methods bool CanExecute(object parameter); void Execute(object parameter); }
该接口包括2个方法和一个事件。CanExecute方法返回命令的状态——指示命令是否可执行,例如,文本框中没有选择任何文本,此时Copy命令是不用的,CanExecute则返回为false。
Execute方法就是命令执行的方法,即处理程序。当命令状态改变时,会触发CanExecuteChanged事件。
当自定义命令时,不会直接去实现ICommand接口。而是使用RoutedCommand类,该类实是WPF中唯一现了ICommand接口的类。所有WPF命令都是RoutedCommand类或其派生类的实例。然而程序中处理的大部分命令不是RoutedCommand对象,而是RoutedUICommand对象。RoutedUICommand类派生与RoutedCommand类。
接下来介绍下为什么说WPF命令是路由的呢?实际上,RoutedCommand上Execute和CanExecute方法并没有包含命令的处理逻辑,而是将触发遍历元素树的事件来查找具有CommandBinding的对象。而真正命令的处理程序包含在CommandBinding的事件处理程序中。所以说WPF命令是路由命令。该事件会在元素树上查找CommandBinding对象,然后去调用CommandBinding的CanExecute和Execute来判断是否可执行命令和如何执行命令。那这个查找方向是怎样的呢?对于位于工具栏、菜单栏或元素的FocusManager.IsFocusScope设置为”true“是从元素树上根元素(一般指窗口元素)向元素方向向下查找,对于其他元素是验证元素树根方向向上查找。
WPF中提供了一组已定义命令,命令包括以下类:ApplicationCommands、NavigationCommands、MediaCommands、EditingCommands 以及ComponentCommands。 这些类提供诸如 Cut、BrowseBack、BrowseForward、Play、Stop 和 Pause 等命令。
四、CommandManager原理
提供了弱事件方式订阅命令
什么是 RequerySuggested,为什么要使用它?
RequerySuggested 机制是 RoutedCommand 有效处理 ICommand.CanExecuteChanged 的方式。
在非 RoutedCommand 世界中,每个 ICommand 都有自己的 CanExecuteChanged 订阅者列表,此时要判断这个命令是否变更,需要手动触发CanExecuteChanged事件。例如 自定义命令DelegateCommand类:
View Code
public class DelegateCommand : ICommand
{
#region Fields
readonly Action<object> _execute;
readonly Predicate<object> _canExecute;
#endregion
#region Constructors
public DelegateCommand(Action<object> execute)
: this(execute, null)
{
}
public DelegateCommand(Action<object> execute, Predicate<object> canExecute)
{
if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute;
}
#endregion
#region ICommand Members
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute(parameter);
}
public event EventHandler CanExecuteChanged;
// The CanExecuteChanged is automatically registered by command binding, we can assume that it has some execution logic
// to update the button's enabled\disabled state(though we cannot see). So raises this event will cause the button's state be updated.
public void RaiseCanExecuteChanged()
{
if (CanExecuteChanged != null)
CanExecuteChanged(this, EventArgs.Empty);
}
public void Execute(object parameter)
{
_execute(parameter);
}
#endregion
}
该命令需要自己手动的调用RaiseCanExecuteChanged()方法来 更新命令可用不可用的状态的逻辑。
但对于 RoutedCommand,订阅 ICommand.CanExecuteChanged 的任何客户端实际上都是订阅 CommandManager.RequerySuggested。
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
它把更新命令可用不可用的状态的逻辑(上面代码中的value)代理给了CommandManager.RequerySuggested事件,而这个事件的触发是由CommandManager自己来检测的,例如UI界面上的空间焦点改变时,就会触发RequerySuggested。这种实现是一种懒的方式,不需要自己调用,由系统检测。这种懒的方式带来的问题就是导致CanExecute方法多次被执行,可能会带来性能影响。当然也可以手动调用CommandManager.InvalidateRequerySuggested() 来更新命令状态,这将执行与触发 ICommand.CanExecuteChanged 相同的操作,但同时在后台线程上对所有 RoutedCommand 执行此操作。此外,RequerySuggested 调用组合在一起,因此如果发生许多更改,则 CanExecute 只需要调用一次。
自定义命令时候,我建议您订阅 CommandManager.RequerySuggested 而不是 ICommand.CanExecuteChanged 的原因是:
1. 您不需要代码来删除旧订阅并在每次命令附加属性的值更改时添加新订阅。
2. CommandManager.RequerySuggested 具有内置的弱引用功能,允许您设置事件处理程序并仍然被垃圾收集。对 ICommand 执行相同操作需要您实现自己的弱引用机制。
3.如果您订阅 CommandManager.RequerySuggested 而不是 ICommand.CanExecuteChanged,您将只能获得 RoutedCommands 的更新。
我专门使用 RoutedCommands,所以这对我来说不是问题,但我应该提到,如果你使用常规 ICommands,有时你应该考虑做一些额外的工作,即弱订阅 ICommand.CanExecutedChanged。请注意,如果您这样做,您也不需要订阅 RequerySuggested,因为 RoutedCommand.add_CanExecutedChanged 已经为您完成了这项工作。
手动更新命令系统
某些时候命令绑定可能会存在刷新不及时,往往需要点击一次程序才能激活,特此记录下解决方案
比如命令执行后进行一些异步耗时操作,操作完成后会影响CanExecute事件结果,但是WPF不会立即做出反应,那么这个时侯就需要手动调用CommandManager.InvalidateRequerySuggested对命令系统进行一次刷新。
System.Windows.Input.CommandManager.InvalidateRequerySuggested();
WPF 的命令在何时刷新
默认情况下,WPF 的命令只会在以下时机刷新可用性:
KeyUp
MouseUp
GotKeyboardFocus
LostKeyboardFocus
使用通俗的话来说,就是:
键盘按下的按键抬起的时候
在鼠标的左键或者右键松开的时候
在任何一个控件获得键盘焦点或者失去键盘焦点的时候
这部分的代码可以在这里查看:
CommandDevice.PostProcessInput
最关键的代码贴在这里:
// 省略前面。
if (e.StagingItem.Input.RoutedEvent == Keyboard.KeyUpEvent ||
e.StagingItem.Input.RoutedEvent == Mouse.MouseUpEvent ||
e.StagingItem.Input.RoutedEvent == Keyboard.GotKeyboardFocusEvent ||
e.StagingItem.Input.RoutedEvent == Keyboard.LostKeyboardFocusEvent)
{
CommandManager.InvalidateRequerySuggested();
}
然而,并不是只在这些时机进行刷新,还有其他的时机,比如这些:
在 Menu 菜单的子菜单项打开的时候(参见 MenuItem.OnIsSubmenuOpenChanged)
在长按滚动条中的按钮以连续滚动的过程中(参见 Tracker.DecreaseRepeatButton)
在 DataGridCell 的只读属性改变的时候(参见 DataGridCell.OnNotifyIsReadOnlyChanged)
在 DataGrid 中的各种各样的操作中(参见 DataGrid)
在 JournalNavigationScope 向后导航的时候(参见 JournalNavigationScope.OnBackForwardStateChange)
还有其他,你可以在此链接双击 InvalidateRequerySuggested 查看:InvalidateRequerySuggested
五、使用命令
前面都是介绍了一些命令的理论知识,下面介绍了如何使用WPF命令来完成任务。XAML具体实现代码如下所示:
<Window x:Class="WPFCommand.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="200" Width="300"> <!--定义窗口命令绑定,绑定的命令是New命令,处理程序是NewCommand--> <Window.CommandBindings> <CommandBinding Command="ApplicationCommands.New" Executed="NewCommand"/> </Window.CommandBindings> <StackPanel> <Menu> <MenuItem Header="File"> <!--WPF内置命令都可以采用其缩写形式--> <MenuItem Command="New"></MenuItem> </MenuItem> </Menu> <!--获得命令文本的两种方式--> <!--直接从静态的命令对象中提取文本--> <Button Margin="5" Padding="5" Command="ApplicationCommands.New" ToolTip="{x:Static ApplicationCommands.New}">New</Button> <!--使用数据绑定,获得正在使用的Command对象,并提取其Text属性--> <Button Margin="5" Padding="5" Command="ApplicationCommands.New" Content="{Binding RelativeSource={RelativeSource Self},Path=Command.Text}"/> <Button Margin="5" Padding="5" Visibility="Visible" Click="cmdDoCommand_Click" >DoCommand</Button> </StackPanel> </Window>
其对应的后台代码实现如下所示:
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); //// 后台代码创建命令绑定 //CommandBinding bindingNew = new CommandBinding(ApplicationCommands.New); //bindingNew.Executed += NewCommand; //// 将创建的命令绑定添加到窗口的CommandBindings集合中 //this.CommandBindings.Add(bindingNew); } private void NewCommand(object sender, ExecutedRoutedEventArgs e) { MessageBox.Show("New 命令被触发了,命令源是:" + e.Source.ToString()); } private void cmdDoCommand_Click(object sender, RoutedEventArgs e) { // 直接调用命令的两种方式 ApplicationCommands.New.Execute(null, (Button)sender); //this.CommandBindings[0].Command.Execute(null); } }
上面程序的运行结果如下图所示:
六、自定义命令
1)自定义命令需要继承Icommand接口
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Input; namespace Login.ViewModels { public class LoginCommand : ICommand { //fileds private readonly Action<object> _executeAction; private readonly Predicate<object> _canExecutePredicate; //Constructors public LoginCommand(Action<object> executeAction) { _executeAction = executeAction; _canExecutePredicate = null; } public LoginCommand(Action<object> executeAction, Predicate<object> canpExecutePredicate) { _executeAction = executeAction; _canExecutePredicate = canpExecutePredicate; } //event public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } //Methods public bool CanExecute(object? parameter) { return _canExecutePredicate == null ? false : _canExecutePredicate(parameter); } void ICommand.Execute(object? parameter) { _executeAction(parameter); } } }
这样一个命令就自定义完成了。
2)创建一个自定义路由命令
首先,定义一个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; } } }
上面代码实现了一个Requery命令,为了演示效果,我们需要把该命令应用到XAML标签上,具体的XAML代码如下所示:
<!--要使用自定义命令,首先需要将.NET命名空间映射为XAML名称空间,这里映射的命名空间为local--> <Window x:Class="WPFCommand.CustomCommand" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WPFCommand" Title="CustomCommand" Height="300" Width="300" > <Window.CommandBindings> <!--定义命令绑定--> <CommandBinding Command="local:CustomCommands.Requery" Executed="RequeryCommand_Execute"/> </Window.CommandBindings> <StackPanel> <!--应用命令--> <Button Margin="5" Command="local:CustomCommands.Requery" Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"></Button> </StackPanel> </Window>
接下来,看看程序的运行效果,具体的运行结果如下图所示:
七、内置WPF命令
两个方法可以在自定义控件中建立内置WPF命令,比如WPF中TextBox内置复制,剪切,粘贴命令。其中RegisterClassInputBinding将一个命令和输入笔势(Input Gesture)联系在一起。这样响应输入笔势发生后,命令逻辑会运行。而RegisterClassCommandBinding则将命令和具体命令执行逻辑和命令判断逻辑联系起来。这样一个命令就有具体逻辑执行代码了。
下面自定义一个控件,继承与Button类型,使得按钮中的文字支持剪切和粘贴。在自定义类型的静态构造函数中用RegisterClassCommandBinding把剪切和粘贴的命令(对应ApplicationCommands类型的Cut和Paste属性)执行实现。用RegisterClassInputBinding则定义命令执行的快捷键。
class MyButton : Button { static MyButton() { //使用CommandManager.RegisterClassCommandBinding和RegisterClassInputBinding方法 //注册我们需要的剪切和粘贴命令。 CommandManager.RegisterClassCommandBinding(typeof(MyButton), new CommandBinding(ApplicationCommands.Cut, OnCutCommand)); CommandManager.RegisterClassInputBinding(typeof(MyButton), new InputBinding(ApplicationCommands.Cut, new KeyGesture(Key.X, ModifierKeys.Control))); CommandManager.RegisterClassCommandBinding(typeof(MyButton), new CommandBinding(ApplicationCommands.Paste, OnPasteCommand)); CommandManager.RegisterClassInputBinding(typeof(MyButton), new InputBinding(ApplicationCommands.Paste, new KeyGesture(Key.V, ModifierKeys.Control))); } public MyButton() { Focusable = true; Loaded += (ss, ee) => this.Focus(); } //控件内命令执行 protected virtual void OnCut() { Clipboard.SetText(Content.ToString()); Content = null; } protected virtual void OnPaste() { Content = Clipboard.GetText(); } //CommandBinding命令绑定事件方法 private static void OnCutCommand(object sender, ExecutedRoutedEventArgs e) { var control = sender as MyButton; if (control != null) { control.OnCut(); } } private static void OnPasteCommand(object sender, ExecutedRoutedEventArgs e) { var control = sender as MyButton; if (control != null) { control.OnPaste(); } } }
然后在界面上,可以直接用Ctrl+X和Ctrl+V对按钮内容进行剪切粘贴(当然前提是按钮得有焦点),也可以用其他控件(ICommandSource执行者)来触发针对自定义按钮的命令(通过设置ICommandSource.CommandTarget)。
XAML:
<StackPanel> <loc:MyButton x:Name="mybtn" Height="50">Mgen!</loc:MyButton> <Button Command="Cut" CommandTarget="{Binding ElementName=mybtn}">剪切</Button> <Button Command="Paste" CommandTarget="{Binding ElementName=mybtn}">粘贴</Button> </StackPanel>