wpf Command Binding使用
在WPF中,有一个非常有意思的Command Binding(命令绑定机制),这种机制在原有的Windows Form中没有提供。本文设计了一个实例,直观地展示出Command Binding的应用场景,并对其机制进行了剖析。
1 Command Binding有什么用?
这个机制有何作用?看一下下面这个例子就清楚了(图 1),此例是由Visual C# 2008 Express创建的标准Wpf应用程序,项目中有一个Window1.xaml作为主窗体:
图 1
从图中可以看到,窗体上有一个菜单和一个按钮,当用户点击这两个控件时,它们执行相同的功能。
多个控件执行同一个功能在桌面应用程序中是非常常见的,比如相同的命令可以通过选择“菜单”命令或点击工具栏上的特定按钮执行。
以传统方式开发这样的程序,往往需要针对每个控件的Click事件分别编码来实现。
然而,在许多时候我们需要同步多个控件的状态。比如在一个文本编辑器中 ,当用户没有选中任何文本时,菜单中的“Copy”和工具栏上的“Copy”按钮都需要禁用,因此,传统方式下还必须写额外代码来实现这一点。
Command Binding弥补传统编程方式的缺陷,可以帮助我们以很少的代码实现同样的功能。
2 实现Command Binding
让我们修改示例以利用命令绑定机制。
首先,向项目中加入一个MyAppCommands类,其内容如下:
namespace UnderStandCommandBinding
{
public class MyAppCommands
{
public static RoutedUICommand MyCommand = new RoutedUICommand();
}
}
请特别注意其中的RoutedUICommand 类型的字段MyCommand。它定义了一个将被窗体控件所调用的命令。
在Window1.cs中书写以下代码:
partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
CommandBinding cb = new CommandBinding();
cb.Executed += new ExecutedRoutedEventHandler(cb_Executed);
}
void cb_Executed(object sender, ExecutedRoutedEventArgs e)
{
MessageBox.Show("响应自定义命令MyCommand");
}
}
上述代码创建一个CommandBinding对象,此对象指明cb_Executed函数响应MyCommand命令(其实是被MyCommand命令的Execute方法自动调用,这里借用事件处理机制的相关术语以便于理解,事实上,命令绑定与事件响应不是一回事,简单地说,命令绑定建构于Wpf的事件路由机制之上)。
下一步则需要指定窗体上的哪几个控件用于调用此命令:
partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
//创建命令对象
CommandBinding cb = new CommandBinding();
cb.Command = MyAppCommands.MyCommand;
cb.Executed += new ExecutedRoutedEventHandler(cb_Executed);
//将要执行的命令对象添加到窗体的命令对象集合中
this.CommandBindings.Add(cb);
//设定菜单项和按钮都执行MyCommand命令
mnuInvokeMyCommand.Command = MyAppCommands.MyCommand;
btnInvokeMyCommand.Command = MyAppCommands.MyCommand;
}
void cb_Executed(object sender, ExecutedRoutedEventArgs e)
{
MessageBox.Show("响应自定义命令MyCommand");
}
}
现在运行程序,可以发现,单击菜单项和按钮都会调用cb_Executed函数。
3 使用XAML实现DataBinding
前面使用代码实现了数据绑定。现在,改用XAML实现相同的功能。
首先,删除Window1构造函数中的代码,只留下cb_Executed()函数:
partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
void cb_Executed(object sender, ExecutedRoutedEventArgs e)
{
MessageBox.Show("响应自定义命令MyCommand");
}
}
回到Window1.xaml文件中,首先,在其Window元素中加入对本项目命名空间的引用(其目的是在XAML中使用代码中的类):
<Window x:Class="UnderStandCommandBinding.Window1" ……
xmlns:myapp="clr-namespace:UnderStandCommandBinding">
……
</Window》
然后,修改MenuItem的声明,加上Command属性:
<MenuItem Header="Invoke My Command" Name="mnuInvokeMyCommand" Command="MyAppCommands.MyCommand" />
再修改Button的声明,也加上Command属性:
<Button Name="btnInvokeMyCommand"
Command="myapp:MyAppCommands.MyCommand">
Invoke My Command
</Button>
最后,给最顶层元素Window添加命令绑定:
<Window.CommandBindings>
<CommandBinding
Command="myapp:MyAppCommands.MyCommand"
Executed="cb_Executed" />
</Window.CommandBindings>
现在运行示例,可以看到运行效果与代码一样。
由此可知,我们可使用XAML以比使用C#编写代码方式更少的代码量实现同样的功能。
4 实现控件同步
现在开始展示一下命令绑定的神奇之处。向窗体上加入一个CheckBox(取名chkActivateCommand),我们将用它来控制是否可以执行MyCommand命令(图 2):
图 2
修改命令绑定对象:
<Window.CommandBindings>
<CommandBinding Command="myapp:MyAppCommands.MyCommand" Executed="cb_Executed" CanExecute="CommandBinding_CanExecute" />
</Window.CommandBindings>
在Window.xaml.cs中添加一个新函数:
private void CommandBinding_CanExecute(
object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = chkActivateCommand.IsChecked.Value;
}
其余代码不需要动。OK。现在运行一下示例程序:
图 3
可以看到,现在按钮和菜单命令的激活与由CheckBox控制了。我们甚至没有写一行代码去同步这三个控件的状态!
5 向命令传送参数
添加一个文本框txtPara用于输入参数。
<TextBox Name="txtPara" Background="Wheat" />
控件的CommandParameter属性可用来向命令对象提供参数,我们使用数据绑定机制将文本框中的文本作为命令参数:
<MenuItem Header="Invoke My Command" Name="mnuInvokeMyCommand" Command="myapp:MyAppCommands.MyCommand" CommandParameter="{Binding ElementName=txtPara,Path=Text}" />
……
<Button Height="23" Name="btnInvokeMyCommand" HorizontalAlignment="Center" Command="myapp:MyAppCommands.MyCommand" CommandParameter="{Binding ElementName=txtPara,Path=Text}" >Invoke My Command</Button>
传入的参数会被命令响应函数的ExecutedRoutedEventArgs参数接收,示例程序中修改后的命令响应函数如下:
void cb_Executed(object sender, ExecutedRoutedEventArgs e)
{
MessageBox.Show(e.Parameter.ToString());
}
运行屏幕截图如图 4:
图 4
在文本框中输入文字,点击按钮或菜单项,将弹出一个消息框显示用户输入的文本。
我们的示例到此结束。
6 Command Binding有何用处?
根据这个例子,大家体会到了Command Binding的好处了吗?
仔细看一下MyAppCommands类,就发现我们可以向其中添加多个自定义的命令,只需指定好这些命令对象的Execute和CanExecute两个事件响应属性,就可以方便地设定窗体上的控件执行特定的命令(通过其Command属性),而且WPF框架会自动帮助我们同步这些控件的状态!
这样一来,我们就得到了一种比较模块化的Wpf桌面应用程序结构:
(1) 将应用程序要执行的功能封装到中间层组件或独立的类中;
(2) 创建一个专用保存命令对象的类(比如本例中的MyAppCommands类),在此类中集中存放应用程序要执行的命令对象。
(3) 在窗体中创建命令绑定对象,为每个命令对象创建响应函数,并将其添加到Window对象的CommandBindings属性中,之后,即可将控件与命令对象相互绑定。
这种架构的优点是增强了代码的隔离性,并减少了总体的代码量:
(1) 业务逻辑代码在中间层中;
(2) 专用于提供特定命令对象的类实际上成了一个功能调用接口层,可以很方便地增删调整程序提供的功能。
(3) 现在控件基本上不需要写大量的代码响应特定的事件和同步多个控件的状态。
7 剖析Command Binding机制
Command Binding机制主要与两个接口密切相关:一个是ICommand,用于定义命令对象类,另一个是ICommandSource,用于定义可发出“调用命令”的源控件。RoutedUICommand类实现了ICommand接口,通常用此类的对象表示应用程序需要执行的命令。而Button等控件则实现了ICommandSource接口,并且拥有此接口所定义的Command和CommandParameter属性,可用于调用命令。
Wpf程序主要通过以下两种对象实现命令绑定:CommandBinding对象将命令对象绑定到特定的命令处理函数(示例就是这样的),当执行命令对象时,这些命令处理函数被调用(命令对象和命令响应函数的这种关系称为“绑定”)。InputBinding对象将命令对象与特定的按键关联起来,当用户按下特定键时,相关联的命令对象被调用。
许多Wpf控件都拥有CommandBindings和InputBindings两个集合属性,其中成员就是CommandBinding对象和InputBinding对象,从而使这些控件能够响应特定的命令,比如TextBox就是这样,它在内部包容了响应标准的Cut/Copy/Paste命令对象的代码,而wpf默认提供的ApplicationCommands类则定义了Cut/Copy/Paste命令对象,这就是为何我们可以不写一行代码直接在TextBox中使用Ctl-C等命令的缘故。
Command Binding机制的内部机制比较复杂,简单地说:
执行Command对象时,会引发PreviewExecute/Execute和PreviewCanExecute/CanExecute事件,这些事件都是RoutedEvent,可以在Wpf元素树中传播(从树叶到树根方向称为Bubble,从树根到树叶方向称为tunnel),正是这种事件路由机制构筑了整个Command Binding机制运转起来的根基。
再进一步追问一下,事件路由是如何实现的?这就涉及到Wpf另一个非常重要的新特性——依赖属性。
有关路由事件和依赖属性的剖析,将会在另外的文章中进行介绍。需要指出的是,如果您对.NET 2.0所提供的委托、事件等还不清楚,那么,要弄明白wpf的路由事件和依赖属性比较困难。
这也再次验证了一句老话:新路接在旧路的前头。