WPF快速入门系列(5)——深入解析WPF命令

时间:2023-03-09 05:04:37
WPF快速入门系列(5)——深入解析WPF命令

一、引言

  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);
} }

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

WPF快速入门系列(5)——深入解析WPF命令

五、自定义命令

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

  首先,定义一个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快速入门系列(5)——深入解析WPF命令

六、实现可撤销的命令程序

  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快速入门系列(5)——深入解析WPF命令

七、小结

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

  本文所有源码:WPFCommandDemo.zip