文章目录
前言
~~~~ 学习了WPF事件之后,你会发现还有命令章节。那命令和事件有何不同,为什么需要命令呢?本文来研究一下WPF中的命令。
一、为何要有命令?
~~~~
事件的作用是发布、传播一些消息,消息送达接收者,事件的使命也就完成了,至于如何响应事件送来的消息事件并不做规定,每个接收者可以使用自己的行为来响应事件。也就是说,事件不具有约束力。命令与事件的区别就在于命令是具有约束力的——战场上,将军一声令下:“前进!”无论是什么兵种都会执行同一个行为MoveForward()(而这个方法很可能定义在BattleUnitBase这个所有兵种继承的兵种基类里);同样当你在Visual Studio菜单栏上点击保存或者按下Ctrl+Shift+S时,所有打开的文档窗口都会执行Save()方法——若不能执行统一的行为,还能叫“命令”吗?(WPF中实际使用事件,激发事件后,你可以捕捉也可以不捕捉。但是激发的是命令时,你必须执行关联函数,当然关联函数中你可以啥都不做。)
~~~~
的确,实际编程工作中就算只使用事件、不使用命令,程序的逻辑也一样可以被驱动得很好(这方面来讲,事件包含命令的功能),但我们不能阻止程序员按自己的习惯去编写代码。比如保存事件的处理器,程序员们可以写Save()、SaveHandler()、SaveDocument()等等,这些都符合代码规范,但迟早有一天整个项目会变得难以被读懂,新来的程序员会为此抓狂。如果使用命令,情况会好很多——当Save命令到达某个组件时,命令会主动去调用组件的Save()方法,而这个方法可能被定义在基类或者接口里(即保证了这个方法一定是存在的),这就在代码结构和命名上做了约束。不但如此,命令还可控制接收者“先做校验、再保存、然后关闭”,也就是说,命令除了可以约束代码,还可以约束步骤逻辑,这让新来的程序员想犯错都难,也让修改bug的程序员很快能找到规律、容易上手。
~~~~
既然命令能帮助我们降低成本,何乐而不为呢?
二、命令系统的基本元素与关系
1.命令系统的基本元素
~~~~ WPF的命令系统由几个基本要素构成,它们是:
-
命令(Command):WPF的命令实际就是实现了ICommand接口的类,平时使用最多的是RoutedCommand类。当然还有自定义命令。
-
命令源(Command Source):即命令的发送者,是实现了ICommandSource接口的类。很多界面元素都实现了这个接口,其中包括Button、MenuItem、ListBoxItem等(从类成员中可知,命令源中有命令、命令参数和命令目标)。
-
命令目标(Command Target):即命令将发送给谁,或者说命令将作用在谁身上。命令目标必须是实现了IInputElement接口的类。
-
命令关联(Command Binding):负责把一些外围逻辑与命令关联起来,比如执行之前对命令是否可以执行进行判断、命令执行之后还有哪些后续工作等。
2.基本元素之间的关系
~~~~ 这些基本元素之间的关系体现在使用命令的过程中。命令的使用大概分为以下几步:
- 创建命令类:即获得一个实现ICommand接口的类,如果命令与具体业务逻辑无关则使用WPF类库中的RoutedCommand类即可。如果想得到与业务逻辑有关的专有命令,即需创建RoutedCommand(或者ICommand接口)的派生类。
- 声明命令实例:使用命令时需要创建命令类的实例。这里有个技巧,一般情况下程序中某种操作只需要一个命令实例与之对应即可。比如对应“保存”这个操作,你可以拿同一个实例去命令每个组件执行其保存功能,因此程序中的命令多使用单件模式(Singletone Pattern,也叫单例模式)以减少代码的复杂度。
- 指定命令的源:即指定由谁来发送这个命令。如果把命令看作炮弹,那么命令源就相当于火炮。同一个命令可以有多个源。比如保存命令,既可以由菜单中的保存项来发送,也可以由工具栏中的保存图标来发送。需要注意的是,一旦把命令指派给命令源,那么命令源就会受命令的影响,当命令不能被执行的时候作为命令源的控件将处在不可用状态。看来命令这种炮弹很智能,当不满足发射条件时还会给用来发射它的火炮上一道保险、避免走火。还需要注意,各种控件发送命令的方法不尽相同,比如Button和MenuItem上在单击时发送命令,而ListBoxItem双击时才发送命令。
- 指定命令目标:命令目标并不是命令的属性而是命令源的属性,指定命令目标是告诉命令源向哪个组件发送命令,无论这个组件是否拥有焦点它都会收到这个命令。如果没有为命令源指定命令目标,则WPF系统认为当前拥有焦点的对象就是命令目标。这个步骤有点像为火炮指定目标。
- 设置命令关联:炮兵是不能单独战斗的,就像炮兵需要侦察兵在射击前观察敌情、判断发射时机,在射击后观测射击效果、帮助修正一样,WPF命令需要CommandBinding在执行前来帮助判断是不是可以执行、在执行后做一些事情来“打扫战场”。
~~~~
在命令目标和命令关联之间还有一个微妙的关系。无论命令目标是由程序员指定还是由WPF系统根据焦点所在地判断出来的,一旦某个UI组件被命令源“瞄上”,命令源就会不停向命令目标“投石问路”,命令目标就会不停地发送出可路由的PreviewCanExecute和CanExecute附加事件,事件会沿着UI元素树向上传递并被命令关联所捕捉,命令关联捕捉到这些事件后会把命令能不能发送实时报告给命令。类似的,如果命令被发送出来并到达命令目标,命令目标就会发送PreviewExecuted和Executed两个附加事件,这两个事件也会沿着UI元素树向上传递并被命令关联所捕捉,命令关联会完成一些后续的任务。别小看是“后续任务”,对于那些和业务逻辑无关的通用命令RoutedCommand,这些后续任务才是最重要的(因为自定义操作就是放在Executed处理器里做的,相当于你如果想添加自己的逻辑,但又不想自己重新实现命令,那么你的逻辑就要放在Executed中。某种程度上来说,Executed中执行的代码就是业务逻辑了)。
~~~~
命令目标怎么会发出PreviewCanExecute、CanExecute、PreviewExecuted和Executed事件呢?其实这几个事件都是附加事件,是被CommandManager类“附加”给命令目标的。另外,PreviewCanExecute和CanExecute的执行时机不受程序员控制,而且执行频率比较高,这不但会给系统性能带来降低,偶尔还会引入几个意想不到的bug并且比较难调试,请多加小心。
3.小试命令
~~~~
说起来很热闹,现在我们动手实践一下。实现这样一个需求:定义一个命令,使用Button来发送这个命令,当命令送达TextBox时TextBox就会被清空(如果TextBox中没有文字则命令不可被发送)。
~~~~
程序的XAML界面代码如下:
<Window x:Class="WPFCommandDemo.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:WPFCommandDemo"
mc:Ignorable="d"
Title="CommandDemo" Height="200" Width="300"
Background="LightBlue">
<Grid>
<StackPanel x:Name="stackPanel">
<Button x:Name="button1" Content="Send Command" Margin="5"/>
<TextBox x:Name="textBoxA" Margin="5,0" Height="100"/>
</StackPanel>
</Grid>
</Window>
~~~~ 后台代码为:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
InitializeCommand();
}
// 声明并定义命令
private RoutedCommand clearCmd = new RoutedCommand("Clear", typeof(MainWindow));
private void InitializeCommand()
{
// 把命令赋值给命令源(命令的发送者)并制定快捷键
this.button1.Command = this.clearCmd;
this.clearCmd.InputGestures.Add(new KeyGesture(Key.C, ModifierKeys.Alt));
// 指定命令目标
this.button1.CommandTarget = this.textBoxA;
// 创建命令关联
CommandBinding cb = new CommandBinding();
cb.Command = this.clearCmd; // 只关注与clearCmd相关的事件
cb.CanExecute += new CanExecuteRoutedEventHandler(cb_CanExecute);
cb.Executed += new ExecutedRoutedEventHandler(cb_Executed);
// 把命令关联安置在外围控件上
this.stackPanel.CommandBindings.Add(cb);
}
// 当探测命令是否可以执行时,此方法被调用
void cb_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
if(string.IsNullOrEmpty(this.textBoxA.Text))
{
e.CanExecute = false;
}
else
{
e.CanExecute = true;
}
// 避免继续向上传而降低程序性能
e.Handled = true;
}
// 当命令送达目标后,此方法被调用
void cb_Executed(object sender, ExecutedRoutedEventArgs e)
{
this.textBoxA.Clear();
// 避免继续向上传而降低程序性能
e.Handled = true;
}
}
~~~~
运行程序,在TextBox中输入文字后Button在命令可执行状态的影响下变为可用,此时单击Button或者Alt+C键,TextBox都会被清空,
Notes:
~~~~
对于上面代码有几点需要注意的地方:
- 使用命令可以避免自己写代码判断Button是否可用以及添加快捷键(如何触发命令,这种操作本应该自己写函数判断按键或者按钮的,这边相当于打包在命令中,你填充即可)
- RoutedCommand是一个与业务逻辑无关的类(虽然从结果来看,确实实现了你的业务逻辑,但这与RoutedCommand本身无关),只负责在程序中“跑腿”而并不对命令目标做任何操作,TextBox并不是由它清空的,那么对TextBox的清空操作是谁做的呢?答案是CommandBinding。因为无论是探测命令是否执行还是命令送达目标,都会激发命令目标发送路由事件,这些路由事件会沿着UI树向上传递并最终被CommandBinding捕捉。本例中CommandBinding被安装在外围的StackPanel上,CommandBinding“站在高处”起一个侦听器的作用,而且专门针对clearCmd命令捕捉与其相关的路由事件。当CommandBinding捕捉到CanExecute事件就会调用cb_CanExecute方法(判断命令执行的条件是否满足,并反馈给命令供其影响命令源的状态);当捕捉到的是Executed事件(表示命令的Execute方法已经执行了,或说命令已经作用在了命令目标上,RoutedCommand的Execute方法不包含业务逻辑,只负责让命令目标激发Executed),则调用cb_Executed方法。
- 因为CanExecute事件的激发频率较高,为了避免降低性能,在处理完后建议把e.Handled设为true。
- CommandBinding一定要设置在命令目标的外围控件上,不然无法捕捉到CanExecute和Executed等路由事件。
4.WPF的命令库
~~~~ 上面这个例子中我们自定义了一个命令:
private RoutedCommand clearCmd = new RoutedCommand("Clear", typeof(MainWindow));
~~~~ 命令具有“一处声明、处处使用”的特点,比如Save命令,在程序的任何地方它都表示要求命令目标保存数据。因此微软在WPF类库里准备了一些便捷的命令库,这些命令库包括:
- ApplicationCommands
- ComponentCommands
- NavigationCommands
- MediaCommands
- EditingCommands
~~~~
它们都是静态类,而命令就是这些类的静态只读属性以单件模式暴露出来的。例如:ApplicationCommands类就包含了CancelPrint、Close、ContextMenu、Copy、CorrectionList、Cut、Delete、Find、Help、New、NotACommand、Open、Paste、Print、PrintPreview、Properties、Redo、Replace、Save、SaveAs、SelectAll、Stop、Undo这些命令,而它们的源码示意如下:
~~~~
其他几个命令库也与之类似。如果你的程序中需要诸如Open、Save、Play、Stop等标准命令那就没必要自己声明了,直接拿命令库来用就好了。
5.命令参数
~~~~
前面提到命令库里有许多WPF预制的命令,如New、Open、Copy、Cut、Paste等。这些命令都是ApplicationCommands类的静态属性,所以它们的实例永远只有一个,这就引出了一个问题:如果界面上有两个按钮,一个用来新建Teacher档案,另一个用来新建Student档案,都是用New命令的话,程序应该如何区分新建的是什么档案呢?
~~~~
答案是使用CommandParameter。命令源一定是实现了ICommandSource接口的对象,而ICommandSource有一个属性就是CommandParameter,如果把命令看作飞向目标的炮弹,那么CommandParameter就相当于装载在炮弹肚子里的“消息”。下面是程序的实现代码。
~~~~
XAML代码如下:
<Window x:Class="WPFCommandParameterDemo.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:WPFCommandParameterDemo"
mc:Ignorable="d"
Title="Command Parameter" Height="240" Width="360"
Background="LightBlue" WindowStyle="ToolWindow">
<Grid Margin="6">
<Grid.RowDefinitions>
<RowDefinition Height="24"/>
<RowDefinition Height="4"/>
<RowDefinition Height="24"/>
<RowDefinition Height="4"/>
<RowDefinition Height="24"/>
<RowDefinition Height="4"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!--命令和命令参数-->
<TextBlock Text="Name:" VerticalAlignment="Center" HorizontalAlignment="Left" Grid.Row="0"/>
<TextBox x:Name="nameTextBox" Margin="60,0,0,0" Grid.Row="0"/>
<Button Content="New Teacher" Command="New" CommandParameter="Teacher" Grid.Row="2"/>
<Button Content="New Student" Command="New" CommandParameter="Student" Grid.Row="4"/>
<ListBox x:Name="listBoxNewItems" Grid.Row="6"/>
</Grid>
<!--为窗体添加CommandBindings-->
<Window.CommandBindings>
<CommandBinding Command="New" CanExecute="New_CanExecute" Executed="New_Executed"/>
</Window.CommandBindings>
</Window>
Notes:
~~~~
代码又两个值得注意的地方:
~~~~
两个按钮都使用了New命令,但分别使用字符串Teacher和Student作为参数。(Button用作命令源,显然Button有继承ICommand或者其父类有继承ICommand)
~~~~
这次是使用XAML代码为窗体添加CommandBinding,CommandBinding的CanExecute和Executed事件处理器写在后台C#代码里。
~~~~
CommandBinding的两个事件处理器代码如下:
private void New_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
if (string.IsNullOrEmpty(this.nameTextBox.Text))
{
e.CanExecute = false;
}
else
{
e.CanExecute = true;
}
}
private void New_Executed(object sender, ExecutedRoutedEventArgs e)
{
string name = this.nameTextBox.Text;
if (e.Parameter.ToString() == "Teacher")
{
this.listBoxNewItems.Items.Add(string.Format("New Teacher:{0}", name));
}
if (e.Parameter.ToString() == "Student")
{
this.listBoxNewItems.Items.Add(string.Format("New Student:{0}", name));
}
}
~~~~
运行程序,当TextBox中没有内容时两个按钮均不可用;当输入文字后按钮变为可用,单击按钮,ListBox会加入不同条目。
6.命令与Binding的结合
~~~~
初试命令,你可能会想到这样一个问题:控件有很多事件,可以让我们进行各种各样不同的操作,可控件只有一个Command属性、而命令库却有数十种命令,这样怎么能使用唯一一个Command属性来调用那么多命令呢?答案是:使用Binding。前面说过,Binding作为一种间接的、不固定的赋值手段,可以让你有机会选择在某个条件下为目标赋特定的值(有时候需要借鉴Converter)。
~~~~
例如,如果一个Button所关联命令有可能根据某些条件而改变,我们可以把代码写成这样:
<Button x:Name = "dynamicCmdBtn" Command="{Binding Path=ppp, Source=sss}" Content="Command"/>
~~~~ 不过因为大多数命令按钮都有相对应的图标来表示固定的含义,所以日常工作中一个控件的命令一经确定就很少改变。
三、近观命令
~~~~ 一般情况下,程序中使用与逻辑无关的RoutedCommand来跑跑龙套就够了,但为了使程序结构更加简洁(比如去掉外围的CommandBinding和与之相关的事件处理器),我们常需定义自己的命令。接下来我们走进WPF命令,先剖析RoutedCommand,再创建自己的命令。
1.ICommand接口和RoutedCommand
~~~~ WPF的命令是实现了ICommand接口的类。ICommand接口非常简单,只包含两个方法和一个事件:
- Execute方法:命令执行,或者说命令作用于命令目标之上。需要注意的是,现实世界中的命令是不会自己执行的,它只能被执行,而在这里,执行变成了命令的方法,有点拟人化的味道。
- CanExecute方法:在执行之前用来探知命令是否可被执行。
- CanExecuteChanged事件:当命令可执行状态发生改变时,可激发此事件来通知其他对象。
~~~~ RoutedCommand就是这样一个实现了ICommand接口的类。RoutedCommand在实现ICommand接口时,并未向Execute和CanExecute方法中添加任何逻辑,也就是说,它是通用的、与具体业务逻辑无关的。怎么理解这个“与具体业务逻辑无关”呢?让我们从外部与内部两方面来理解。
~~~~ 从外部来看,让我们回顾一下ApplicationCommands命令库里的命令们:
~~~~ 虽然它们都有自己的名字(如:Copy、Paste、Cut、Delete,然而执行啥不是由它们的名字决定的),但它们都是普普通通的RoutedUICommand类实例。也就是说,当一个命令到达命令目标后,具体是执行Copy还是Cut(即业务逻辑)不是由命令决定的,而是外围的CommandBinding捕获到命令目标受命令激发而发送的路由事件后在其Executed事件处理器中完成。换句话说,就算你的CommandBinding在捕捉到Copy命令后执行的是清除操作也与命令无关。
~~~~ 从内部分析,我们就要读读RoutedCommand的源码了。由于源码过长且冗余,加上书上版本与我的版本隔了太久有些不同。这边我就简单叙述下书上的结论。整个RoutedCommand类中的核心方法是ExecuteImpl方法,这个方法就是借用命令目标的RaiseEvent把RoutedEvent发送出去。然后,这个事件会被外围的CommandBinding捕获然后执行程序员预设的与业务相关的逻辑。
~~~~ 最后,我们以ButtonBase(Button的基类)为例看看UI控件是如何发送命令的。ButtonBase是在Click事件发生时发送命令的,而Click事件的激发是放在OnClick方法里,ButtonBase的OnClick方法如下:
~~~~ ButtonBase调用了一个.NET Framework内部类(这个类没有向程序员暴露)CommandHelpers的ExecuteCommandSource方法,并把ButtonBase对象自己当作参数传了进去。如果我们走进ExecuteCommandSource方法内部就会发现这个方法实际上是把传进来的参数当作命令源、调用命令源的ExecuteCore方法(本质上是调用其ExecuteImpl方法)、获取命令源的CommandTarget属性值(命令目标)并使命令作用于命令目标之上。
~~~~ 所以RoutedCommand继承来的Execute到底干了些啥?发一个事件。但是呢,它被执行了,所以如果我们能往Execute里加一些自己的代码(业务代码),也可以不需要CommandBinding就能跑起来。
2.自定义Command
~~~~
说到“自定义命令”,我们可以分两个层次来理解。第一个层次比较浅,指的是当WPF命令库中没有包含想要的命令时,我们就得声明自己的RoutedCommand实例。比如你想让命令目标在命令到达时发出笑声,WPF命令库里没有这个命令,那就可以定义一个名为Laugh的RoutedCommand实例。很难说这是真正意义上的“自定义命令”,这只是对RoutedCommand的使用(只是生成了一个特定名字的命令,而它本质上与一般RoutedCommand无异)。第二个层次是指从实现ICommand接口开始,定义自己的命令并且把某些业务逻辑也包含在命令之中,这才称得上是真正意义上的自定义命令。但比较棘手的是,在WPF的命令系统中命令源(包括ButtonBase、MenuItem、ListBoxItem等)、RoutedCommand和CommandBinding三者互相依赖相当紧密。在源码级别上,不但没有将与命令相关的方法声明为virtual以供我们重写,而且还有很多未向程序员公开的逻辑。换句话说,WPF自带的命令源和CommandBinding就是专门为RoutedCommand而编写的,如果我们想使用自己的ICommand派生类就必须连命令源一起实现(即实现ICommandSource接口)。因此,为了简便地使用WPF这套成熟的体系,为了更高效率地“从零开始”打造自己的
命令系统,需要我们根据项目的实际情况进行权衡。
~~~~
既然本节命名为自定义命令,那么我们就从实现ICommand接口开始、打造一个“纯手工”的自定义命令。
~~~~
前面已经多次提到,RoutedCommand与业务逻辑无关,业务逻辑要依靠外围的CommandBinding来实现。这样一来,如果对CommandBinding管理不善就有可能造成代码杂乱无章,毕竟一个CommandBinding要牵扯到谁是它的宿主以及两个事件处理器。(一旦多了就容易混乱)
~~~~
为了简化使用CommandBinding来处理业务逻辑的程序结构,我们可能会希望把业务逻辑移入命令的Execute方法内。比如,我们可以自定义一个名为Save的命令,当命令到达一个命令目标的时候先通过命令目标的IsChanged属性判断命令目标的内容是否已经被改变,如果已经改变则命令可以执行,命令的执行会直接调用命令目标的Save方法、驱动命令目标以自己的方式保存数据。很显然,这回是命令直接在命令目标上起作用了,而不像RoutedCommand那样先在命令目标上激发出路由事件等外围控件捕捉到事件后再翻过头来对命令目标加以处理。你可能会问:“如果命令目标不包含IsChanged和Save方法怎么办?”这就要靠接口来约束了,如果我在程序中定义这样一个接口:
public interface IView
{
// 属性
bool IsChanged { get; set; }
// 方法
void SetBinding();
void Refresh();
void Clear();
void Save();
// ...
}
并且要求每个需要接受命令的组件都必须实现这个接口,这样就确保了命令可以成功地对它们执行操作。
~~~~
接下来,我们实现ICommand接口,创建一个专门作用于IView派生类的命令。
public class ClearCommand : ICommand
{
// 当命令可执行状态发生改变时,应当被激发
public event EventHandler CanExecuteChanged;
// 用于判断命令是否可以执行(暂不实现)
public bool CanExecute(object parameter)
{
throw new NotImplementedException();
}
// 命令执行,带有与业务相关的Clear逻辑
public void Execute(object parameter)
{
IView view = parameter as IView;
if (view != null)
{
view.Clear();
}
}
}
~~~~
命令实现了ICommand接口并继承了CanExecuteChanged事件、CanExecute方法和Execute方法。目前这个命令比较简单,只用到了Execute方法。在实现这个方法时,我们将这个方法唯一的参数作为命令的目标,如果目标是IView接口的派生类则调用其Clear方法——显然,我们已经把业务逻辑引入了命令的Execute方法中。
~~~~
有了自定义命令,我们拿什么命令源来“发射”它呢?前面说过,WPF命令系统的命令源是专门为RoutedCommand准备的并且不能重写的,所以我们只能通过实现ICommandSource接口来创建自己的命令源。代码如下:
// 自定义命令源
public class MyCommandSource : UserControl, ICommandSource
{
// 继承自ICommandSource的三个属性
public ICommand Command { get; set; }
public object CommandParameter { get; set; }
public IInputElement CommandTarget { get; set; }
// 在组件被单击时连带执行命令
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
// 在命令目标上执行命令,或称让命令作用于命令目标
if (this.CommandTarget != null)
{
this.Command.Execute(this.CommandTarget);
}
}
}
~~~~
ICommandSource接口只包含Command、CommandParameter和CommandTarget三个属性,至于这三个属性之间有什么样的关系就要看我们怎么实现了。在本例中,CommandParameter完全没有被用到,而CommandTarget被当做参数传递给了Command的Execute方法。命令不会自己被发出,所以一定要为命令的执行选择一个合适的时机,本例中我们在控件被左单击时执行命令。
~~~~
现在命令和命令源都有了,还差一个可用的命令目标。因为我们的ClearCommand专门作用于IView的派生类,所以合格的ClearCommand命令目标必须实现IView接口。设计这种既有UI又需要实现接口的类可以先用XAML编辑器实现其UI部分再找到它的后台C#代码实现接口,原理很简单,WPF会自动为UI元素添加partial关键字修饰,XAML代码会被翻译成类的一个部分,后台代码是类的另一个部分(甚至可以再多添加几个部分),我们可以再后台代码部分指定基类或实现接口,最终这些部分会被编译到一起。
总结
~~~~ 由于自定义这部分要改写的代码实在太多了,而且实际工作中我也暂时不会用到,所以就不往下看了。此文用于学习,对Command有个大致的认识,且能使用RoutedCommand和CommandBinding即可。
参考
《WPF深入浅出》,刘铁锰