我们把世界看错,反说它欺骗了我们。 --飞鸟集

前言

相较而言,命令对我来说是一个新概念,因为在Winform中压根没有所谓的命令这个概念。从文字角度理解,"命令"可以指代一种明确的指令或要求,用于向某个实体传达特定的操作或行为。它可以是一个动词性的词语,表示对某个对象或主体的要求或指示。命令通常具有明确的目标和执行内容,它告诉接收者要执行什么操作,并在某种程度上对行为进行约束。

在软件开发中,"命令"是一种设计模式,它描述了将操作封装为对象的方法,以便在不同的上下文中使用和重用。这种命令模式通过将请求和操作封装到一个命令对象中,使得发送者和接收者之间解耦,从而实现了更灵活和可扩展的设计。在这种模式下,命令对象充当了发送者和接收者之间的中间媒介,接收者通过执行命令对象来完成特定的操作。

在提到命令的时候,我们通常拿它来和事件作比较。我们都知道,WPF里已经有了路由事件,比如按钮在被点击以后做出的反应(直接事件),我们一般会通过ButtonClick新建一个事件,然后在这个事件里面写一些业务代码:

private void Button_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("You click me.");
}

当我们运行程序点击按钮时,Button_Click事件会被驱动响应点击的动作弹出一个消息对话框,没有问题对吧。我们都知道,在早期的GUI框架中,应用程序的外观和行为方式之间没有真正的分离,回到这里也一样,将业务代码直接写在事件处理程序中会导致界面和业务逻辑紧密耦合在一起,使得两者难以分离和独立变更。实际开发中,我们会给按钮一个特定功能,当按钮的功能发生变化时,我们需要在UI界面中修改与这个功能所绑定的东西,同时也需要调整业务代码,这就导致了界面元素(外观)和业务逻辑(行为)混在一起,使得按钮的XAML代码既承担了外观的定义,又承担了业务逻辑的实现。此外,假设我们有很多个一样的按钮,都需要在用户点击按钮后为它提供相同的功能,通过上述面的方法,当这个功能发生改变时,我们就需要调正每一个涉及到的按钮以及相应的事件。为了解决这个问题,WPF提供了命令机制(Command),可以将按钮的行为与其外观进行分离。

命令(Command)在WPF中是一种用于处理用户界面交互的机制,它们提供了一种在界面元素(UI)和后台逻辑之间进行解耦的方式,使得交互操作可以以一种统一的、可重用的方式进行处理。WPF命令的概念和实现是基于MVVM(Model-View-ViewModel)架构模式的,它使得界面元素的交互操作可以通过命令对象进行管理和处理,而不需要直接在界面代码中编写事件处理程序。

通过使用命令,我们能够更好地组织和管理界面交互行为,使得代码结构清晰,易于维护和扩展。同时,命令还提供了一些额外的功能,如参数传递、命令的可用性控制等,使得我们能够更灵活地处理用户的操作。

事件和命令:

事件是与用户动作进行联动的,而命令是那些想要与界面分离的动作,比如常见的复制粘贴命令,当我们点击一个具有复制功能的按钮时,相当于我们通过点击的这个动作触发了一个复制的命令,这样做的好处就是 - 界面的交互操作变得简单、代码可重用性提高,在不破坏后台逻辑的情况下可以更加灵活的控制用户界面。

命令模型

WPF命令模型主要包含以下几个基本元素:

命令(Command):指的是实现了ICommand接口的类,例如RoutedCommand类及其子类RoutedUICommand类,一般不包含具体逻辑。

命令源(Command Source):即命令的发送者,指的是实现了ICommandSource接口的类。像ButtonMenuItem等界面元素都实现了这个接口,单击它们都会执行绑定的命令。

命令目标(Command Target):即命令的接受者,指的是实现了IInputElement接口的类。

命令关联(Command Binding):即将一些外围逻辑和命令关联起来。

借用刘老师的图来看一下他们的关系:

ICommand

ICommand接口,包含一个事件两个方法:

public interface ICommand
{
//
// 摘要:
// 当出现影响是否应执行该命令的更改时发生。
event EventHandler CanExecuteChanged; // 摘要:
// 定义用于确定此命令是否可以在其当前状态下执行的方法。
//
// 参数:
// parameter:
// 此命令使用的数据。如果此命令不需要传递数据,则该对象可以设置为 null。
//
// 返回结果:
// 如果可以执行此命令,则为 true;否则为 false。
bool CanExecute(object parameter); //
// 摘要:
// 定义在调用此命令时调用的方法。
//
// 参数:
// parameter:
// 此命令使用的数据。如果此命令不需要传递数据,则该对象可以设置为 null。
void Execute(object parameter);
}

反过来看:

  • Execute - 执行某个动作

  • CanExecute - 能不能执行动作

  • CanExecuteChanged - 命令状态发生变化是响应的事件

通过实现ICommand接口,我们可以创建自定义的命令对象,并将其与界面元素进行绑定。这样,界面元素就可以与命令相关联,通过调用命令的Execute方法来执行具体的操作,而无需直接编写事件处理程序。

定义命令

下面我们试着用命令的方式来实现上面的点击事件,并逐步理解模型中的内容。我们新建一个类MainViewModel来提供我们需要的功能方法:

using System.Windows;

namespace WPFDemo
{
public class MainViewModel
{
public void ShowInfo()
{
MessageBox.Show("You click me.");
}
}
}

ShowInfo()这个时候跟UI界面是分开的对吧,有了方法之后,我们可以说MainViewModel中的ShowInfo就命令了吗,根据模型来看显然还不行。继续走,写一个实现ICommand接口但不带具体逻辑的类,比如CustomCommand:

using System;
using System.Windows.Input; namespace WPFDemo
{
public class CustomCommand : ICommand
{
private readonly Action _execute;
public CustomCommand(Action execute)
{
_execute = execute;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
// 在这里实现命令的可执行逻辑
return true; // 默认返回true,表示命令可执行
}
public void Execute(object parameter)
{
// 在这里实现命令的执行逻辑
_execute?.Invoke();
}
}
}

之后在MainViewModel类中需要添加一个公共属性来暴露CustomCommand实例作为ShowInfoCommand,以便在XAML中进行绑定;

using System.Windows;

namespace WPFDemo
{
public class MainViewModel
{
public CustomCommand ShowInfoCommand { get; set; } public MainViewModel()
{
ShowInfoCommand = new CustomCommand(ShowInfo);
}
public void ShowInfo()
{
MessageBox.Show("You click me.");
}
}
}

最后,将CustomCommand实例与界面元素进行绑定:

<Window x:Class="WPFDemo.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:local="clr-namespace:WPFDemo"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="800"
Height="450"
mc:Ignorable="d">
<Window.Resources>
<local:MainViewModel x:Key="ViewModel" />
</Window.Resources>
<Grid>
<Button Command="{Binding ShowInfoCommand}" Content="Click Me" DataContext="{StaticResource ViewModel}" />
</Grid>
</Window>

设置数据上下文也可以在后台代码中完成

回到命令模型上来,梳理以下对应关系:

  1. CustomCommand 类本身是一个命令对象(ICommand),它实现了命令接口,包括 ExecuteCanExecute 方法。这个命令对象是命令模式中的 "具体命令"。
  2. MainViewModel 类作为命令的执行者(或者称为命令目标),其中包含了 ShowInfo 方法。在命令模式中,执行者负责实际执行命令所需的操作。在我们的示例中,ShowInfo 方法是具体执行的业务逻辑。
  3. XAML中,我们使用 CommandBinding 将按钮的 Click 事件与 ShowInfoCommand 关联起来。这个关联是通过在 WindowUserControlCommandBindings 集合中添加一个新的 CommandBinding 对象来完成的。这个 CommandBinding 指定了 ShowInfoCommand 作为命令和 MainViewModel 作为命令的执行者。

注意:Command属性仅仅作为Click行为的绑定,其他行为,如鼠标移入、移出。。。等行为,要使用另外的MVVM方式进行绑定。

最后,梳理下程序结构,可以看到,我们分别在MainViewModel.csMainWindow.xaml中书写业务代码和逻辑。

通知更改

WPF中,实现属性的通知更改是通过实现 INotifyPropertyChanged 接口来实现的。这个接口定义了一个 PropertyChanged 事件,当属性的值发生变化时,可以通过触发该事件来通知界面进行更新。

在进行演示之前,先来看看我们上面所使用的例子能否像之前学习的绑定一样实现自动更新,按照业务分离的逻辑,我们在MainViewModel.cs中添加一个Name字段并在页面进行绑定。

MainViewModel.cs

using System.Windows;

namespace WPFDemo
{
public class MainViewModel
{
public CustomCommand ShowInfoCommand { get; set; }
public string Name { get; set; } public MainViewModel()
{
Name = "狗熊岭第一狙击手";
ShowInfoCommand = new CustomCommand(ShowInfo);
}
public void ShowInfo()
{
Name = "光头强";
MessageBox.Show("You click me.");
}
}
}

MainWindow.xaml

<Window x:Class="WPFDemo.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:local="clr-namespace:WPFDemo"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="800"
Height="450"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<Grid>
<StackPanel>
<Button Command="{Binding ShowInfoCommand}" Content="Click Me" />
<TextBox Text="{Binding Name}" />
</StackPanel>
</Grid>
</Window>

之前xaml中的数据上下文不太好,需要在每个控件中都定义一次

上面的代码中,我们将Name绑定到了TextBox的文本中,并在点击按钮是改变Name的值,如果它自己可以通知更改,自动更新的话那么在Name变化的时候文本也应该变化,对吧。我们运行试一下:

可以看到Name的变化时Text并没有随之变化,这说明Name发生改变以后并没有通知Text也进行变化。如果你喜欢捣鼓的话,可以看看Text如果被修改以后Name会不会变化。

回到正题,上文提到过,实现属性的通知更改是通过实现 INotifyPropertyChanged 接口来实现的,我们来对自定义的MainViewModel.cs稍作修改实现属性的通知更改:

using System.ComponentModel;
using System.Windows; namespace WPFDemo
{
public class MainViewModel : INotifyPropertyChanged
{
public CustomCommand ShowInfoCommand { get; set; }
public event PropertyChangedEventHandler PropertyChanged; private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged(nameof(Name));
}
} private void OnPropertyChanged(string name)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
} public MainViewModel()
{
Name = "狗熊岭第一狙击手";
ShowInfoCommand = new CustomCommand(ShowInfo);
} public void ShowInfo()
{
Name = "光头强";
MessageBox.Show("You click me.");
}
}
}

nameof(Name) 是C# 6.0 引入的一个语法糖,它可以在编译时获取属性、方法、字段、类型等成员的名称作为一个字符串。

WPF中,nameof(Name) 用于在属性更改通知中指定属性的名称。它的作用是避免硬编码属性名称,从而减少在重构过程中出现由于重命名属性而导致的错误。

修改过后,MainViewModel 类实现了 INotifyPropertyChanged 接口,并在 Name 属性的 setter 中进行了属性更改通知。当 Name 属性的值发生变化时,会触发 PropertyChanged 事件,通知界面进行更新。

进阶玩法

在实现通知更改的方式中,可以将通知更改的逻辑定义在一个基类中,例如 ViewModelBase 类。这个基类可以包含通用的属性更改通知实现,以便其他具体的视图模型类可以继承该基类并重用这些通知更改的功能。

以下是一个简单的示例,展示了如何在 ViewModelBase 类中实现通知更改:

using System.ComponentModel;
using System.Runtime.CompilerServices; namespace WPFDemo
{
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

在上面的代码中,ViewModelBase 类实现了 INotifyPropertyChanged 接口,并提供了 OnPropertyChanged 方法用于触发属性更改通知事件,并将属性名称作为参数传递。

默认情况下,CallerMemberName 特性用于自动获取调用该方法的成员的名称,并作为属性名称传递。

通过继承 ViewModelBase 类,并使用 OnPropertyChanged 方法来设置属性,可以简化视图模型类中属性更改通知的实现:

MainViewModel.cs

using System.Windows;

namespace WPFDemo
{
public class MainViewModel : ViewModelBase
{
public CustomCommand ShowInfoCommand { get; set; }
private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged();
}
} private string _description;
public string Description
{
get { return _description; }
set { _description = value; OnPropertyChanged(); }
} public MainViewModel()
{
Name = "狗熊岭第一狙击手";
Description = "光头强";
ShowInfoCommand = new CustomCommand(ShowInfo);
}
public void ShowInfo()
{
Name = "光头强";
Description = "狗熊岭第一突破手,麦克阿瑟如是说。";
MessageBox.Show("You click me.");
}
}
}

在这个示例中,MainViewModel 类继承了 ViewModelBase 类,并使用 OnPropertyChanged 方法来设置 Name 属性。当 Name 属性的值发生更改时,SetProperty 方法会自动处理属性更改通知的逻辑,无需手动触发事件或编写重复的代码。

通过将通知更改的实现定义在基类中,可以实现更简洁、可维护和可重用的代码,避免在每个具体的视图模型类中重复编写通知更改的逻辑。

夹带私货 - 命令参数

有时候我们需要在执行命令时传递参数。在WPF中,可以使用CommandParameter属性来传递参数给命令。

CommandParameter是一个附加属性,可以将任意对象指定为命令的参数。当命令执行时,命令的Execute方法会接收到该参数,并可以在命令处理逻辑中使用它。

以下是一个示例,展示如何在XAML中为命令指定CommandParameter

<Button Content="Click Me" Command="{Binding MyCommand}" CommandParameter="Hello, World!" />

在这个示例中,我们将ButtonCommand属性绑定到MyCommand命令。同时,我们通过CommandParameter属性指定了一个字符串参数"Hello, World!"。当点击按钮时,该参数将传递给MyCommand命令的Execute方法。

在命令的执行逻辑中,可以通过命令参数来获取传递的值。以下是一个简单的命令类示例:

CustomCommand.cs

using System;
using System.Windows.Input; namespace WPFDemo
{
public class CustomCommand : ICommand
{
private readonly Action<object> _execute;
public CustomCommand(Action<object> execute)
{
_execute = execute;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
// 在这里实现命令的可执行逻辑
return true; // 默认返回true,表示命令可执行
}
public void Execute(object parameter)
{
// 在这里实现命令的执行逻辑
_execute?.Invoke(parameter as string);
}
}
}

CustomCommand 类接受一个 Action<object> 类型的参数,在构造函数中将传递的方法保存到 _execute 字段中。然后,在 Execute 方法中,通过调用 _execute?.Invoke(parameter) 来执行传递的方法,并将 parameter 作为参数传递给该方法。

这样,当你在 MainViewModel 中创建 CustomCommand 实例时,可以将 ShowInfo 方法作为参数传递进去,

MainViewModel.cs:

using System.Windows;

namespace WPFDemo
{
public class MainViewModel
{
public CustomCommand ShowInfoCommand { get; set; }
public MainViewModel()
{
ShowInfoCommand = new CustomCommand(ShowInfo);
}
public void ShowInfo(object parameter)
{
MessageBox.Show(parameter as string);
}
}
}

那么ShowInfo(object parameter)的参数从哪里来呢 - CommandParameter附加属性:

<Window x:Class="WPFDemo.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:local="clr-namespace:WPFDemo"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="800"
Height="450"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<Grid>
<StackPanel>
<Button Command="{Binding ShowInfoCommand}" Content="Click Me" CommandParameter="Hello, World!" />
</StackPanel>
</Grid>
</Window>

这样看上去可能比较蠢,我们可以些微调整一下在页面中完成显示内容的修改,代码就不贴了,大家应该知道:

通过命令参数,可以实现更灵活的命令处理逻辑,根据传递的参数来执行不同的操作。同时,使用命令参数也可以实现与界面元素的交互,例如根据按钮的点击位置传递坐标信息等。

以上就是本篇文章的所有内容了

WPF 入门笔记 - 06 - 命令的更多相关文章

  1. WPF 入门笔记之控件内容控件

    一.控件类 在WPF中和用户交互的元素,或者说.能够接受焦点,并且接收键盘鼠标输入的元素所有的控件都继承于Control类. 1. 常用属性: 1.1 Foreground:前景画刷/前景色(文本颜色 ...

  2. WPF 入门笔记之事件

    一.事件路由 1. 直接路由事件 起源于一个元素,并且不能传递给其他元素 MouserEnter 和MouserLeave 就是直接事件路由 2. 冒泡路由事件 在包含层次中向上传递,首先由引发的元素 ...

  3. WPF 入门笔记之布局

    一.布局原则: 1. 不应显示的设定元素的尺寸,反而元素可以改变它的尺寸,并适应它们的内容 2. 不应使用平布的坐标,指定元素的位置. 3. 布局容器和它的子元素是共享可以使用的空间 4. 可以嵌套的 ...

  4. WPF 入门笔记之基础

    一.创建WPF程序 1. App.xaml 相当于窗体的配置文件 2. xmlns:xml名称空间的缩写 xmlns="http://schemas.microsoft.com/winfx/ ...

  5. WPF自学入门(十一)WPF MVVM模式Command命令 WPF自学入门(十)WPF MVVM简单介绍

    WPF自学入门(十一)WPF MVVM模式Command命令   在WPF自学入门(十)WPF MVVM简单介绍中的示例似乎运行起来没有什么问题,也可以进行更新.但是这并不是我们使用MVVM的正确方式 ...

  6. MySQL入门笔记(二)

    MySQL的数据类型.数据库操作.针对单表的操作以及简单的记录操作可参考:MySQL入门笔记(一) 五.子查询   子查询可简单地理解为查询中的查询,即子查询外部必然还有一层查询,并且这里的查询并非仅 ...

  7. 《深入浅出WPF》笔记——事件篇

    如果对事件一点都不了解或者是模棱两可的话,建议先去看张子阳的委托与事件的文章(比较长,或许看完了,也忘记看这一篇了,没事,我会原谅你的)http://www.cnblogs.com/JimmyZhan ...

  8. golang微服务框架go-micro 入门笔记2.1 micro工具之micro api

    micro api micro 功能非常强大,本文将详细阐述micro api 命令行的功能 重要的事情说3次 本文全部代码https://idea.techidea8.com/open/idea.s ...

  9. Centos7——docker入门(笔记)

    docker 入门(笔记) 一.Docker是什么? 官方原话: Docker provides a way to run applications securely isolated in a co ...

  10. ES6入门笔记

    ES6入门笔记 02 Let&Const.md 增加了块级作用域. 常量 避免了变量提升 03 变量的解构赋值.md var [a, b, c] = [1, 2, 3]; var [[a,d] ...

随机推荐

  1. Service Mesh之Istio部署bookinfo

    前文我们了解了service mesh.分布式服务治理和istio部署相关话题,回顾请参考https://www.cnblogs.com/qiuhom-1874/p/17281541.html:今天我 ...

  2. python入门教程之六运算符

    什么是运算符? 本章节主要说明Python的运算符.举个简单的例子 4 +5 = 9 . 例子中,4 和 5 被称为操作数,"+" 称为运算符. Python语言支持以下类型的运算 ...

  3. 在英特尔 CPU 上加速 Stable Diffusion 推理

    前一段时间,我们向大家介绍了最新一代的 英特尔至强 CPU (代号 Sapphire Rapids),包括其用于加速深度学习的新硬件特性,以及如何使用它们来加速自然语言 transformer 模型的 ...

  4. win10环境下 VMware Workstation Pro 安装centos7无法上网

    一.安装centos7 网上类似的教程太多了,我就不一一写了,提供两个网址,先按照教程安装 VMware Workstation Pro ,秘钥在第二个链接里面(亲测可用), 安装完VMware在根据 ...

  5. 笔记:C++学习之旅---顺序容器

    笔记:C++学习之旅---顺序容器 STL = Standard Template Library   标准库模版 容器可以使用范围for输出或者迭代器进行输出 一个容器就是一些特定类型对象的集合.顺 ...

  6. OceanBase的学习与使用

    OceanBase的学习与使用 简介 1. OceanBase数据库 注意这一块下载的其实是rpm包. 一般是通过下面的OAT或者是OCP工具进行安装. 有x86还有ARM两种架构. 虽然是el7结尾 ...

  7. CUDA 的随机数算法 API

    参考自 Nvidia cuRand 官方 API 文档 一.具体使用场景 如下是是在 dropout 优化中手写的 uniform_random 的 Kernel: #include <cuda ...

  8. GE反射内存实时通讯网络解决方案

    时通讯网络是用于需要较高实时性要求的应用领域的专用网络通讯技术,一般采用基于高速网络的共享存储器技术实现.它除了具有严格的传输确定性和可预测性外,还具有速度高.通信协议简单.宿主机负载轻.软硬件平台适 ...

  9. 在vue标签代码块中定义变量

    方式一: 在标签上使用:set关键字,不管什么标签都可以 <template> <h1>test</h1> <template :set="firs ...

  10. 京喜APP - 图片库优化

    作者:京东零售 何骁 介绍 京喜APP早期开发主要是快速原生化迭代替代原有H5,提高用户体验,在这期间也积累了不少性能问题.之后我们开始进行一些性能优化相关的工作,本文主要是介绍京喜图片库相关优化策略 ...