WPF学习笔记(四)
1.1 命令概括
属性、事件在以往的记忆中还是依稀有些印象的,虽然到了WPF上写法有些变化,但归根结底圆西瓜或是方西瓜还是那西瓜并未因其形状变化而对本质造成影响。而命令(Command)这个概念确是WPF中新提出来的,所以为自己对这一章晚写近5个月之久狡辩一句。
1.11 什么叫命令
这当然不是官方解释,可以认为是我个人杜撰YY出来的,命令在现实生活中一般是上级对下级的发出的强制性规定,虽然在这里没有王候将相没有阶级之分,但是命令作为一种指令当然须有两个对象,发出命令者和执行命令者,当然有时候两者可以为一,就像我们命令自己做出一些良好习惯,比如戒烟戒酒每天早睡早起一样(效果自然比我们强多了),也就是自己下的命令由自己来执行;在执行命令之前要先确认是否能执行或执行条件是否齐全,难道有人让你从台湾的101跳下来也照做?或是在没有工具的前提下你能飞向月球(快醒醒,太阳很大咯),这点也是命令的关键点;最后一个点是命令本身,是越野50公里还是蛙跳1公里,我已闻到鸡蛋的味道,很多人已经开始发牢骚,这不是废话是个人都知道。所以说MS取的贴切嘛。
1.12命令的具体应用
定义啥都说了,好比推销产品说了它是用黄金打造还是砖石镶刻,还得说明实际用途,总不能老拿观赏价值、收藏价值来忽悠吧。那它的实际价值又在哪呢,假设说现在有个TextBox,还有个Button,要求点击Button后把剪贴板中的文字给付给TextBox,看到这里很多人笑了,可我还有个要求就是当剪切板中的内容为空时要求Button的IsEnable属性为false呢?
毕竟程序的一些逻辑,比如这次的剪切板是否为空对剪切板的开发者来说自然是容易捕捉的,可对于外部使用者得到这一变更显得并不是那么容易要——要注册事件再进行判断,如果可以把Button的功能直接定义为粘贴作用,并在粘贴功能不存在的时候(即剪贴板内容为空时)自动的让按钮失效有多好,在WPF中这成为了可能,你只需要两句:
button.CommandTarget = pasteTextBox;
button.Command = ApplicationCommands.Paste;
也就是说命令可以帮助使用者避免在不该使用的情景下使用,并把这一特性集中到了可视控件中,在这里Button是发出命令者,TextBox是执行命令者,剪贴板是否为空是检查在执行命令之前要先确认是否能执行或执行条件是否齐全,系统的粘贴命令是命令本身。
这里的ApplicationCommands的是微软封装的一个常用命令集,里面还有Paste、Cut、Copy、Undo 和 Redo等等,另外MS还封装的命令集合有NavigationCommands、MediaCommands、EditingCommands 以及 ComponentCommands。
1.13自定义命令
刚才的ApplicationCommands.Paste是MS定义,那怎么来定义我们自己的命令呢?
WPF 中的命令系统基于 RoutedCommand 和 RoutedEvent。 命令不必是 RoutedCommand,可以仅仅实现 ICommand,在一般情况下RoutedCommand用的较多。
ICommand接口中主要有两个方法:
CanExecute
定义用于确定此命令是否可以在其当前状态下执行的方法,这个就是我说的在执行命令之前要先确认是否能执行或执行条件是否齐全,能执行返回true,不能返回false.
Execute
定义在调用此命令时调用的方法,这个就是执行命令本身,可以传递一个object类型的参数,啰嗦的讲一句没东西可传可以传NULL。
还有一个事件:
CanExecuteChanged
当CanExecute的值发生变化时便发生。
而RoutedCommand则是实做了ICommand这一接口,做好的东西当然用的人多了要不然你为什么选择别人的操作系统,不要说是被迫的!!!
方便起见我们用MS自带NumericUpDown这个Sample来做说明,定义RoutedCommand 的静态属性,在构造函数中通过CommandManager来为NumericUpDown这个类注册命令IncreaseCommand,并在OnIncreaseCommand这个静态函数中具体实现
private static RoutedCommand _increaseCommand;
public static RoutedCommand IncreaseCommand
{
get
{
return _increaseCommand;
}
}
_increaseCommand = new RoutedCommand("IncreaseCommand", typeof(NumericUpDown));
CommandManager.RegisterClassCommandBinding(typeof(NumericUpDown), new CommandBinding(_increaseCommand, OnIncreaseCommand));
private static void OnIncreaseCommand(object sender, ExecutedRoutedEventArgs e)
{
NumericUpDown control = sender as NumericUpDown;
if (control != null)
{
control.OnIncrease();
}
}
protected virtual void OnIncrease()
{
if (this.Value < MaxValue)
{
this.Value += 1;
}
}
1.14CommandBinding
在上面的介绍中需要在构造函数中注册命令,而用到CommandBinding这个类,这里简单说明分析下用法。
四个构造函数:
public CommandBinding();
public CommandBinding(ICommand command);
public CommandBinding(ICommand command, ExecutedRoutedEventHandler executed);
public CommandBinding(ICommand command, ExecutedRoutedEventHandler executed, CanExecuteRoutedEventHandler canExecute);
一个属性:
public ICommand Command { get; set; }
四个事件:
public event CanExecuteRoutedEventHandler CanExecute;
public event ExecutedRoutedEventHandler Executed;
public event CanExecuteRoutedEventHandler PreviewCanExecute;
public event ExecutedRoutedEventHandler PreviewExecuted;
从第一个构造函数看出它可以动态改变绑定的命令,从Command 这个属性改变。
从第二个构造函数看出它没有具体的执行命令所以绑定可以不单单是自己定义的命令,还可以是其他的控件或类的命令,这个在复合控件中可以把内控件命令曝露给外部调用。
从第三个构造函数看出它可以使CanExecute默认为true(如果是false怎么执行)。
从第四个函数看出,它完全是在搞委托为实现ICommand 接口中的方法,可以定义执行函数和执行命令的条件。
事件的话如果在PreviewCanExecute或PreviewExecuted中的参数同事件e.Handled = true的话,CanExecute或Executed将不会执行。
一直在强调的就是命令的主要用途是为了命令可以避免函数在不该执行的时候执行。
CommandBinding还有个作用就是假设说我在开始举例的Button和TextBox还有个外容器StackPanel,我要在上面做些判断当TextBox是ReadOnly或是IsEnable=false时命令就失效,就可以在StackPanel的CommandBinding属性中赋值
CommandBinding commandBinding = new CommandBinding(ApplicationCommands.Paste);
commandBinding.PreviewCanExecute+=new CanExecuteRoutedEventHandler(textBoxCanExecute);
mainStackPanel.CommandBindings.Add(commandBinding);
void textBoxCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
TextBox target = e.Source as TextBox;
if (target != null)
{
if (target.IsEnabled == true && target.IsReadOnly==false)
{
e.CanExecute = true;
e.Handled = true;
}
else
{
e.CanExecute = false;
e.Handled = true;
}
}
}
可能你已经看出这里实际上也是种路由事件的处理,这里用了隧道路由;在textBoxCanExecute这个函数中下个断点,发现在UI变更时才会引发,那如果数据是后端给的怎么执行验证?可以通过 CommandManager.InvalidateRequerySuggested()来强制引发RequerySuggested 事件以达到执行验证的目的。
1.15如何给自定义控件加命令
上面的示例中我们有给Button的Command属性赋值并通过button.CommandTarget = pasteTextBox这句指定命令的执行体。那这些操作都是如何进行,怎样才能使我们自定的控件也“享受”命令的待遇?不过种瓜得瓜种豆得豆这些操作还是我们自己来完成的。
只要你的控件曾自ICommandSource接口并实现其方法和属性即可。
ICommand Command { get; }
object CommandParameter { get; }
IInputElement CommandTarget { get; }
从字面意思我们便可以得出一个是命令,一个是命令执行体,还有个就是命令的参数。
看到接口中的{ get; }方法,大家不要误会,具体实现时还是可以加入set的,只不过一定要实现get而已。
这样我们就明了,这些接口所需要实现的东西,实际上还是无法脱离ICommand 中的具体实现。Execute执行命令,Execute上可是可以传参数的哦,只要注册事件CanExecuteChanged并根据上面的CanExecute中的值来确定控件的状态——是否为IsEnable或其他你自己认为有必要的设定,Button是在Click的时候调用Execute,我们也可以在MouseOver,MouseEnter等时候注册,或是自己控件的值发生变化的时候调用Execute就实现命令了,说白了执行命令的也就执行函数嘛。
注意的一点是当然命令更换的时候最好把原来的命令中加载的事件先卸载掉比如监听状态的CanExecuteChanged事件。
具体的让我们来分析下微软的例子(CommandSlider):
public class CommandSlider : Slider, ICommandSource
{
public CommandSlider() : base()
{
}
// ICommand Interface Memembers
//<SnippetImplementICommandSourceCommandPropertyDefinition>
// Make Command a dependency property so it can use databinding.
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register(
"Command",
typeof(ICommand),
typeof(CommandSlider),
new PropertyMetadata((ICommand)null,
new PropertyChangedCallback(CommandChanged)));
public ICommand Command
{
get
{
return (ICommand)GetValue(CommandProperty);
}
set
{
SetValue(CommandProperty, value);
}
}
//</SnippetImplementICommandSourceCommandPropertyDefinition>
// Make CommandTarget a dependency property so it can use databinding.
public static readonly DependencyProperty CommandTargetProperty =
DependencyProperty.Register(
"CommandTarget",
typeof(IInputElement),
typeof(CommandSlider),
new PropertyMetadata((IInputElement)null));
public IInputElement CommandTarget
{
get
{
return (IInputElement)GetValue(CommandTargetProperty);
}
set
{
SetValue(CommandTargetProperty, value);
}
}
// Make CommandParameter a dependency property so it can use databinding.
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.Register(
"CommandParameter",
typeof(object),
typeof(CommandSlider),
new PropertyMetadata((object)null));
public object CommandParameter
{
get
{
return (object)GetValue(CommandParameterProperty);
}
set
{
SetValue(CommandParameterProperty, value);
}
}
//<SnippetImplementICommandSourceCommandChanged>
// Command dependency property change callback.
private static void CommandChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
CommandSlider cs = (CommandSlider)d;
cs.HookUpCommand((ICommand)e.OldValue,(ICommand)e.NewValue);
}
//</SnippetImplementICommandSourceCommandChanged>
//<SnippetImplementICommandSourceHookUnHookCommands>
// Add a new command to the Command Property.
private void HookUpCommand(ICommand oldCommand, ICommand newCommand)
{
// If oldCommand is not null, then we need to remove the handlers.
if (oldCommand != null)
{
RemoveCommand(oldCommand, newCommand);
}
AddCommand(oldCommand, newCommand);
}
// Remove an old command from the Command Property.
private void RemoveCommand(ICommand oldCommand, ICommand newCommand)
{
EventHandler handler = CanExecuteChanged;
oldCommand.CanExecuteChanged -= handler;
}
// Add the command.
private void AddCommand(ICommand oldCommand, ICommand newCommand)
{
EventHandler handler = new EventHandler(CanExecuteChanged);
canExecuteChangedHandler = handler;
if (newCommand != null)
{
newCommand.CanExecuteChanged += canExecuteChangedHandler;
}
}
//</SnippetImplementICommandSourceHookUnHookCommands>
//<SnippetImplementICommandCanExecuteChanged>
private void CanExecuteChanged(object sender, EventArgs e)
{
if (this.Command != null)
{
RoutedCommand command = this.Command as RoutedCommand;
// If a RoutedCommand.
if (command != null)
{
if (command.CanExecute(CommandParameter, CommandTarget))
{
this.IsEnabled = true;
}
else
{
this.IsEnabled = false;
}
}
// If a not RoutedCommand.
else
{
if (Command.CanExecute(CommandParameter))
{
this.IsEnabled = true;
}
else
{
this.IsEnabled = false;
}
}
}
}
//</SnippetImplementICommandCanExecuteChanged>
//<SnippetImplementICommandExecute>
// If Command is defined, moving the slider will invoke the command;
// Otherwise, the slider will behave normally.
protected override void OnValueChanged(double oldValue, double newValue)
{
base.OnValueChanged(oldValue, newValue);
if (this.Command != null)
{
RoutedCommand command = Command as RoutedCommand;
if (command != null)
{
command.Execute(CommandParameter, CommandTarget);
}
else
{
((ICommand)Command).Execute(CommandParameter);
}
}
}
//</SnippetImplementICommandExecute>
//<SnippetImplementICommandHandlerReference>
// Keep a copy of the handler so it doesn't get garbage collected.
private static EventHandler canExecuteChangedHandler;
//</SnippetImplementICommandHandlerReference>
}
他是在OnValueChanged的时候执行Execute,如果是RoutedCommand 的话可以把执行的对象传入,ICommand接口并未定义对象的执行体所以不必也不能传入;在CommandProperty 定义的时候在命令改变的时候实际执行
private void HookUpCommand(ICommand oldCommand, ICommand newCommand){
// If oldCommand is not null, then we need to remove the handlers.
if (oldCommand != null)
{
RemoveCommand(oldCommand, newCommand);
}
AddCommand(oldCommand, newCommand);
}
就是为了先把原先的命令中的事件给注销掉,然后才附加新的命令事件,如果不注销原命令的事件又不销毁命令那么就会发生内存泄露,所以要养成有始有终的习惯有注册事件的就应该有相应的注销事件。
1.16小结
看到这里基本上你就明白了命令也不过是执行一个可传参的函数而已,只是多加了什么时候才能执行的条件,并可以和控件绑定使用的东西,因此只要做好命令的定义也就是写个函数并说明什么时候可以用(CanExecute输出的为true)便可,当然也可以根据传入的执行对象CommandTarget中的控件类型来判断,如TextBox的话剪贴板中只可以是string类型的东西才允许贴入。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· [AI/GPT/综述] AI Agent的设计模式综述