一、引言

  WPF命令相对来说是一个崭新的概念,因为命令对于之前的WinForm根本没有实现这个概念,但是这并不影响我们学习WPF命令,因为设计模式中有命令模式,关于命令模式可以参考我设计模式的博文:http://www.cnblogs.com/zhili/p/CommandPattern.html。命令模式的要旨在于把命令的发送者与命令的执行者之间的依赖关系分割开了。对此,WPF中的命令也是一样的,WPF命令使得命令源(即命令发送者,也称调用程序)和命令目标(即命令执行者,也称处理程序)分离。现在是不是感觉命令是不是亲切了点了呢?下面就详细分享下我对WPF命令的理解。

二、命令是什么呢?

  上面通过命令模式引出了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中提供了一组已定义命令,命令包括以下类:ApplicationCommandsNavigationCommandsMediaCommandsEditingCommands 以及ComponentCommands。 这些类提供诸如 CutBrowseBackBrowseForwardPlayStop 和 Pause 等命令。

四、使用命令

  前面都是介绍了一些命令的理论知识,下面介绍了如何使用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);
} }

  上面程序的运行结果如下图所示:

五、自定义命令

  在开发过程中,自然少不了自定义命令来完成内置命令所没有提供的任务。下面通过一个例子来演示如何创建一个自定义命令。

  首先,定义一个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命令模型缺少的一个特征就是Undo命令,尽管提供了一个ApplicationCommands.Undo命令,但是该命令通常被用于编辑控件,如TextBox控件。如果希望支持应用程序范围内的Undo操作,就需要在内部跟踪以前的命令,并且触发Undo操作时还原该命令。这个实现原理就是保持用一个集合对象保存之前所有执行过的命令,当触发Undo操作时,还要上一个命令的状态。这里除了需要保存执行过的命令外,还需要保存触发命令的控件以及状态,所以我们需要抽象出一个类来保存这些属性,我们取名这个类为CommandHistoryItem。为了保存命令和命令的状态,自然就需要在完成命令之前进行保存,所以自然联想到是否有Preview之类的事件呢?实际上确实有,这个事件就是PreviewExecutedEvent,所以我们需要在窗口加载完成后把这个事件注册到窗口上,这里在触发这个事件的时候就可以保存即将要执行的命令、命令源和命令源的内容。另外,之前的命令自然需要保存到一个列表中,这里使用ListBox控件作为这个列表,如果不希望用户在界面上看到之前的命令列表的话,也可以使用List等集合容器。

  上面讲解完了主要实现思路之后,下面我们梳理下实现思路:

  1. 抽象一个CommandHistoryItem来保存命令相关的属性。
  2. 注册PreviewExecutedEvent事件,为了在命令执行完之前保存命令、命令源以及命令源当前的状态。
  3. 在PreviewExecutedEvent事件处理程序中,把命令相关属性添加到ListBox列表中。
  4. 当执行撤销操作时,可以从ListBox.Items列表中取出上一个执行的命令进行恢复之前命令的状态。

  有了上面的实现思路之后,实现这个可撤销的命令程序也就是码代码的过程了。具体的后台代码实现如下所示:

  public partial class CommandsMonitor : Window
{
private static RoutedUICommand undo;
public static RoutedUICommand Undo
{
get { return CommandsMonitor.undo; }
} static CommandsMonitor()
{
undo = new RoutedUICommand("Undo", "Undo", typeof(CommandsMonitor));
} public CommandsMonitor()
{
InitializeComponent();
// 按下菜单栏按钮时,PreviewExecutedEvent事件会被触发2次,即CommandExecuted事件处理程序被触发了2次
// 一次是菜单栏按钮本身,一次是目标源触发命令的执行,所以在CommandExecuted要过滤掉不关心的命令源
this.AddHandler(CommandManager.PreviewExecutedEvent, new ExecutedRoutedEventHandler(CommandExecuted));
} public void CommandExecuted(object sender, ExecutedRoutedEventArgs e)
{
// 过滤掉命令源是菜单按钮的,因为我们只关心Textbox触发的命令
if (e.Source is ICommandSource)
return;
// 过滤掉Undo命令
if (e.Command == CommandsMonitor.Undo)
return; TextBox txt = e.Source as TextBox;
if (txt != null)
{
RoutedCommand cmd = e.Command as RoutedCommand;
if (cmd != null)
{
CommandHistoryItem historyItem = new CommandHistoryItem()
{
CommandName = cmd.Name,
ElementActedOn = txt,
PropertyActedOn = "Text",
PreviousState = txt.Text
}; ListBoxItem item = new ListBoxItem();
item.Content = historyItem;
lstHistory.Items.Add(item);
} }
} private void window_Unloaded(object sender, RoutedEventArgs e)
{
this.RemoveHandler(CommandManager.PreviewExecutedEvent, new ExecutedRoutedEventHandler(CommandExecuted));
} private void UndoCommand_Executed(object sender, RoutedEventArgs e)
{
ListBoxItem item = lstHistory.Items[lstHistory.Items.Count - ] as ListBoxItem; CommandHistoryItem historyItem = item.Content as CommandHistoryItem;
if (historyItem == null)
{
return;
} if (historyItem.CanUndo)
{
historyItem.Undo();
}
lstHistory.Items.Remove(item);
} private void UndoCommand_CanExecuted(object sender, CanExecuteRoutedEventArgs e)
{
if (lstHistory == null || lstHistory.Items.Count == )
{
e.CanExecute = false;
}
else
{
e.CanExecute = true;
}
}
} public class CommandHistoryItem
{
public String CommandName { get; set; }
public UIElement ElementActedOn { get; set; } public string PropertyActedOn { get; set; } public object PreviousState { get; set; } public bool CanUndo
{
get { return (ElementActedOn != null && PropertyActedOn != ""); }
} public void Undo()
{
Type elementType = ElementActedOn.GetType();
PropertyInfo property = elementType.GetProperty(PropertyActedOn);
property.SetValue(ElementActedOn, PreviousState, null);
}
}
}

  其对应的XAML界面设计代码如下所示:

<Window x:Class="WPFCommand.CommandsMonitor"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="CommandsMonitor" Height="300" Width="350"
xmlns:local="clr-namespace:WPFCommand"
Unloaded="window_Unloaded">
<Window.CommandBindings>
<CommandBinding Command="local:CommandsMonitor.Undo"
Executed="UndoCommand_Executed"
CanExecute="UndoCommand_CanExecuted"/>
</Window.CommandBindings>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<ToolBarTray Grid.Row="0">
<ToolBar>
<Button Command="ApplicationCommands.Cut">Cut</Button>
<Button Command="ApplicationCommands.Copy">Copy</Button>
<Button Command="ApplicationCommands.Paste">Paste</Button>
</ToolBar>
<ToolBar>
<Button Command="local:CommandsMonitor.Undo">Reverse Last Command</Button>
</ToolBar>
</ToolBarTray> <TextBox Margin="5" Grid.Row="1"
TextWrapping="Wrap" AcceptsReturn="True">
</TextBox>
<TextBox Margin="5" Grid.Row="2"
TextWrapping="Wrap" AcceptsReturn="True">
</TextBox> <ListBox Grid.Row="3" Name="lstHistory" Margin="5" DisplayMemberPath="CommandName"></ListBox>
</Grid>
</Window>

  上面程序的运行效果如下图所示:

七、小结

  到这里,WPF命令的内容就介绍结束了,关于命令主要记住命令模型四要素——命令、命令绑定、命令源和命令目标。后面继续为大家分享WPF的资源和样式的内容。

  本文所有源码:WPFCommandDemo.zip

WPF快速入门系列(5)——深入解析WPF命令的更多相关文章

  1. WPF快速入门系列(4)——深入解析WPF绑定

    一.引言 WPF绑定使得原本需要多行代码实现的功能,现在只需要简单的XAML代码就可以完成之前多行后台代码实现的功能.WPF绑定可以理解为一种关系,该关系告诉WPF从一个源对象提取一些信息,并将这些信 ...

  2. WPF快速入门系列(3)——深入解析WPF事件机制

    一.引言 WPF除了创建了一个新的依赖属性系统之外,还用更高级的路由事件功能替换了普通的.NET事件. 路由事件是具有更强传播能力的事件——它可以在元素树上向上冒泡和向下隧道传播,并且沿着传播路径被事 ...

  3. WPF快速入门系列(7)——深入解析WPF模板

    一.引言 模板从字面意思理解是“具有一定规格的样板".在现实生活中,砖块都是方方正正的,那是因为制作砖块的模板是方方正正的,如果我们使模板为圆形的话,则制作出来的砖块就是圆形的,此时我们并不 ...

  4. WPF快速入门系列(2)——深入解析依赖属性

    一.引言 感觉最近都颓废了,好久没有学习写博文了,出于负罪感,今天强烈逼迫自己开始更新WPF系列.尽管最近看到一篇WPF技术是否老矣的文章,但是还是不能阻止我系统学习WPF.今天继续分享WPF中一个最 ...

  5. WPF快速入门系列(1)——WPF布局概览

    一.引言 关于WPF早在一年前就已经看过<深入浅出WPF>这本书,当时看完之后由于没有做笔记,以至于我现在又重新捡起来并记录下学习的过程,本系列将是一个WPF快速入门系列,主要介绍WPF中 ...

  6. WPF快速入门系列(8)——MVVM快速入门

    一.引言 在前面介绍了WPF一些核心的内容,其中包括WPF布局.依赖属性.路由事件.绑定.命令.资源样式和模板.然而,在WPF还衍生出了一种很好的编程框架,即WVVM,在Web端开发有MVC,在WPF ...

  7. .Net5 WPF快速入门系列教程

    一.概要 在工作中大家会遇到需要学习新的技术或者临时被抽调到新的项目当中进行开发.通常这样的情况比较紧急没有那么多的时间去看书学习.所以这里向wpf技术栈的开发者分享一套wpf教程,基于.net5框架 ...

  8. WPF快速入门系列(9)——WPF任务管理工具实现

    转载自:http://www.cnblogs.com/shanlin/p/3954531.html WPF系列自然需要以一个实际项目为结束.这里分享一个博客园博客实现的一个项目,我觉得作为一个练手的项 ...

  9. WPF快速入门系列(6)——WPF资源和样式

    一.引言 WPF资源系统可以用来保存一些公有对象和样式,从而实现重用这些对象和样式的作用.而WPF样式是重用元素的格式的重要手段,可以理解样式就如CSS一样,尽管我们可以在每个控件中定义格式,但是如果 ...

随机推荐

  1. 更改form字段内容颜色

    1.fnd_global.Newline ---换行2.设置栏位值颜色:POST-QUERY SET_ITEM_INSTANCE_PROPERTY('FIND_RESULT.STATUS',CURRE ...

  2. nuint笔记

    注意:单元测试中,Case 与 Case 之间不能有任何关系 测试方法不能有返回值,不能有参数,测试方法必须声明为 public [TestFixture] //声明测试类 [SetUp] //建立, ...

  3. 树莓派USB摄像头与camera模块对比

    http://www.cnblogs.com/weixinforspurs/p/5575962.html ——————————————————————————————————————————————— ...

  4. 《C++编程规范:101条规则、准则与最佳实践》学习笔记

    转载:http://dsqiu.iteye.com/blog/1688217 组织和策略问题 0. 不要为小事斤斤计较.(或者说是:知道什么东西不需要标准化) 无需在多个项目或者整个公司范围内强制实施 ...

  5. SQL SERVER 批量插入记录

    --create function insertData(@count as int,@tsn as bigint,@id as int) --as --begin SET IDENTITY_INSE ...

  6. C/C++关键字 extern

    1.基本解释:extern 可置于变量或函数前面,表示变量或函数的定义在别的文件中,以提示编译器遇到此变量或函数时在其他模块中寻找定义. extern还有另外2个作用.第一:与“C”连用时,如 ext ...

  7. vertical-align 属性设置元素的垂直对齐方式。

     值 描述 baseline 默认.元素放置在父元素的基线上. sub 垂直对齐文本的下标. super 垂直对齐文本的上标 top 把元素的顶端与行中最高元素的顶端对齐 text-top 把元素的顶 ...

  8. The certificate used to sign “AppName” has either expired or has been revoked. An updated certificate is required to sign and install the application解决

    问题 The certificate used to sign "AppName" has either expired or has been revoked. An updat ...

  9. Solr Zookeeper ACL权限配置

    首先注意:在配置ACL的时候,请关闭solr运行实例!!否则可能对集群造成不可恢复的损坏 开始: 1.修改solr.xml,在solrCloud节点添加,告诉solr要使用的provider: < ...

  10. Dojo学习_组件属性

    注意组件的引用顺序,避免出现对象不是构造函数或属性undefined的情况! 1.修改文本  require([ 'dojo/dom', 'dojo/domReady!' ], function (d ...