在上一篇中,我们学习了WPF的路由事件,而在本节将学习一个更为抽象且松耦合的事件版本,即命令。最明显的区别是,事件是与用户动作相关联的,而命令是那些与用户界面想分离的动作,例如我们最熟悉的剪切(Cut)、复制(Copy)和粘贴(Paste)命令。这带来的好处是:命令可以实现复用,减少了代码量,从而可以在不破坏后台逻辑的条件下,更加灵活地控制你的用户界面。然而,命令并不是WPF特有的,早在MFC中已经有了类似的机制,然而,在WPF之前使用命令是一件很烦琐的事情,因为需要考虑状态间的同步问题。WPF为了解决这个问题,增加了两个重要特性:一是将事件委托到适当的命令;而是将控件的启用状态与相应命令的状态保持一致。在Caliburn.Micro中完全用Command来代替事件。本篇将从WPF命令模型、自定义命令和WPF内置命令来学习。

1.WPF命令模型

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

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

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

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

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

下面通过一个关系图来看下:

由上图,不难发现,命令目标需要通过命令关联来影响命令源。

1.1命令

WPF的命令模型的核心是System.Window.Input.ICommand接口,包含两个方法和一个事件。

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

当CanExecute方法返回命令状态,当返回true时,Execute方法得到执行,在这个方法中进行逻辑处理,当命令状态改变时,CanExecuteChanged事件触发,可以根据命令状态相应控制控件(命令源)的状态。

在WPF中,RoutedCommand类是唯一实现ICommand接口的类,它除了实现ICommand接口外,还支持事件冒泡和隧道传递,可以使命令在WPF元素层次间以冒泡或隧道方式传递时可以被恰当的处理程序处理。除了私有实现CanExecute方法和Execute方法外,还公开重载了这两个方法:

        // 摘要:
// 确定此 System.Windows.Input.RoutedCommand 在其当前状态是否可以执行。
//
// 参数:
// parameter:
// 用户定义的数据类型。
//
// target:
// 命令目标。
//
// 返回结果:
// 如果可以对当前命令目标执行此命令,则为 true;否则为 false。
//
// 异常:
// System.InvalidOperationException:
// target 不是 System.Windows.UIElement 或 System.Windows.ContentElement。
[SecurityCritical]
public bool CanExecute(object parameter, IInputElement target);
//
// 摘要:
// 对当前命令目标执行 System.Windows.Input.RoutedCommand。
//
// 参数:
// parameter:
// 要传递到处理程序的用户定义的参数。
//
// target:
// 要在其中查找命令处理程序的元素。
//
// 异常:
// System.InvalidOperationException:
// target 不是 System.Windows.UIElement 或 System.Windows.ContentElement。
[SecurityCritical]
public void Execute(object parameter, IInputElement target);

多了一个IInputElement类型(UIElement类就实现了该接口)的参数表示开始处理事件的元素,另外还有三个属性:

        public InputGestureCollection InputGestures { get; }
public string Name { get; }
public Type OwnerType { get; }

第一个属性是获取与此命令关联的 System.Windows.Input.InputGesture 对象的集合。第二个属性是获取命令名称。第三个参数是获取命令所有者类型。

还有个继承自RoutedCommand的类RoutedUICommand类,它在RoutedCommand类的基础上只增加了一个Text属性用于设置命令的文本,该属性进行了本地化的处理。

1.2命令源

在上面我们提到的命令,只是单纯的命令,没有任何硬编码功能。为了让一个命令被触发,我们需要一个命令源(命令发送者)。前面已经讲到,并不是所有的控件都可以触发命令,只有实现了ICommandSource接口的控件才可以,这样的控件主要有继承自ButtonBase类的Button和CheckBox控件、Hyperlink控件和MenuItem等。ICommandSource接口定义如下:

        ICommand Command { get; }
object CommandParameter { get; }
IInputElement CommandTarget { get; }

第一个属性是获取将在调用命令源时执行的命令。第二个属性是获取传递给命令的参数。第三个参数是获取命令目标。

1.3命令目标

在命令源发送命令之后,我们需要一个命令接受者,或者叫命令的作用者,即命令目标。它实现了IInputElement接口。在1.1中RoutedCommand类中重载的CanExecute方法和Execute方法的第二个参数就是传递命令目标。UIElement类就实现了该接口,也就是说所有的控件都可以作为命令目标。

1.4命令关联

命令目标发送路由事件,为了让命令得到恰当的响应,我们需要一个命令关联。我们来看下CommandBinding类的定义:

        public ICommand Command { get; set; }

        public event CanExecuteRoutedEventHandler CanExecute;
public event ExecutedRoutedEventHandler Executed;
public event CanExecuteRoutedEventHandler PreviewCanExecute;
public event ExecutedRoutedEventHandler PreviewExecuted;

Command属性时获取与命令关联相关的命令;PreviewCanExecute/CanExecute事件是当与命令关联相关的命令启动检查以确定是否可以在当前命令目标上执行命令时发生;PreviewExecuted/Executed事件是当执行与该命令关联相关的命令时发生。其实,这四个附加事件是定义在CommandManager类中然后附加给命令目标的。

当命令源确定了命令目标后(人为指定或焦点判断),就会不停地向命令目标询问,命令目标就会不停地发送PreviewCanExecute和CanExecute事件,这两个附加事件就会沿着元素树向上传递,然后被命令关联捕获,命令关联将命令能不能发送报告给命令。若可以发送命令,则命令源将发送命令给命令目标,命令目标会发送PreviewExecuted和Executed事件,这两个附加也会沿着元素树向上传递,然后被命令关联捕获,完成一些后续工作。

2.自定义命令

由上一节我们熟悉了WPF的命令模型,本节我们自己来定义一个命令。我们有这么几种方式来自定义命令:

一是直接实现ICommand接口,这是最彻底的方式;

二是继承自RoutedCommand类和RoutedUICommand类,这种方式可以命令路由;

三是使用RoutedCommand类和RoutedUICommand类实例,严格来讲,这种方式只是命令的应用。

这里,我们以直接实现ICommand接口来自定义一个SayCommand命令:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Input; namespace CommandDemo
{
public class SayCommand:ICommand
{
Action<string> _executedTargets = delegate { };
Func<bool> _canExecuteTargets = delegate { return false; };
bool _enableState = false;//启用状态 public bool CanExecute(object parameter)
{
Delegate[] canExecuteTargets = _canExecuteTargets.GetInvocationList();
foreach (Func<bool> item in canExecuteTargets)
{
bool flag = item.Invoke();
if (flag)
{
_enableState = true;
break;
}
}
return _enableState;
} public event EventHandler CanExecuteChanged = delegate { }; public void Execute(object parameter)
{
if (_enableState)
_executedTargets.Invoke(parameter == null ? null : parameter.ToString());
} public event Action<string> ExecutedTargets
{
add
{
_executedTargets += value;
}
remove
{
_executedTargets -= value;
}
} public event Func<bool> CanExecuteTargets
{
add
{
_canExecuteTargets += value;
CanExecuteChanged.Invoke(this, EventArgs.Empty);
}
remove
{
_canExecuteTargets -= value;
CanExecuteChanged.Invoke(this, EventArgs.Empty);
}
}
}
}

Xaml代码:

<Window x:Class="CommandDemo.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CommandDemo"
Title="Window1" Height="300" Width="300">
<Window.Resources>
<local:SayCommand x:Key="cmd" />
</Window.Resources>
<Grid>
<StackPanel>
<Button Content="Say" Command="{StaticResource ResourceKey=cmd}" CommandParameter="Hi,WPF"/>
<Button Content="AddCommandHandler" Click="Button_Click" />
</StackPanel>
</Grid>
</Window>

cs代码:

        private void Button_Click(object sender, RoutedEventArgs e)
{
SayCommand sayCmd = this.FindResource("cmd") as SayCommand;
if(sayCmd == null)return;
sayCmd.CanExecuteTargets += () =>
{
return true;
};
sayCmd.ExecutedTargets += (p) =>
{
MessageBox.Show(p);
};
}

当未点击AddCommandHandler按钮时,Say按钮的执行时机和执行操作都是未知的,所以按钮状态为禁用。效果如下:

当点击了AddCommandHandler按钮后,为命令指定了执行时机和执行操作,按钮状态由禁用变为启用。效果如下:

给命令的命令参数(CommandParameter,Object类型)设置了一个值,点击Say按钮,将会显示出来。效果如下:

自定义命令的第二种和第三种差不多,我们以第三种实例化一个RoutedUICommand类来举个例子:

这里定义了一个RoutedUICommand类型的SortCommand命令,用来进行按字段排序,并设置了快捷键。

首先,看下XAML代码:

<Window x:Class="CommandDemo.Window2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CommandDemo"
Title="Window2" Height="300" Width="300">
<Window.CommandBindings>
<CommandBinding Command="local:Window2.SortCommand" CanExecute="CommandBinding_CanExecute" Executed="CommandBinding_Executed" />
<CommandBinding Command="Delete" CanExecute="CommandBinding_CanExecute_1" Executed="CommandBinding_Executed_1" />
</Window.CommandBindings>
<Grid>
<StackPanel>
<ListBox x:Name="stuList">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<StackPanel.ContextMenu>
<ContextMenu>
<MenuItem Command="Delete" CommandTarget="{Binding PlacementTarget,RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
</ContextMenu>
</StackPanel.ContextMenu>
<TextBlock Text="{Binding ID}" Foreground="Red" Width="30" />
<TextBlock Text="{Binding Name}" Foreground="Green" Width="60" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Command="local:Window2.SortCommand" CommandParameter="Name" Content="{Binding Path=Command.Text,RelativeSource={RelativeSource Mode=Self}}"/>
</StackPanel>
</Grid>
</Window>

在XAML代码中,主要有两个控件:ListBox和Button,两个命令:自定义的SortCommand和内建的ApplicationCommands.Delete命令。

cs代码:

public partial class Window2 : Window
{
public ObservableCollection<Student> StuList;
public Window2()
{
InitializeComponent();
StuList = DataService.StuList;
this.stuList.ItemsSource = StuList; }
//实例化一个RoutedUICommand对象来进行升序排列,指定其Text属性,并设置快捷键
public static RoutedUICommand SortCommand
= new RoutedUICommand("Sort", "Sort", typeof(Window2)
, new InputGestureCollection(new KeyGesture[] { new KeyGesture(key: Key.F3, modifiers: ModifierKeys.None) })); private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
{
string orderBy = e.Parameter == null ? "ID" : e.Parameter.ToString();//默认按ID排序
ICollectionView view = CollectionViewSource.GetDefaultView(this.stuList.ItemsSource);
view.SortDescriptions.Clear();
view.SortDescriptions.Add(new SortDescription(orderBy, ListSortDirection.Ascending));
view.Refresh();
} private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
if (this.stuList == null || !this.stuList.HasItems)
e.CanExecute = false;
else
e.CanExecute = true;
e.Handled = true;
} private void CommandBinding_CanExecute_1(object sender, CanExecuteRoutedEventArgs e)
{
if (this.stuList == null || !this.stuList.HasItems)
e.CanExecute = false;
else
e.CanExecute = true;
e.Handled = true;
} private void CommandBinding_Executed_1(object sender, ExecutedRoutedEventArgs e)
{
Student stu = this.stuList.SelectedItem as Student;
if (stu != null)
{
(this.stuList.ItemsSource as ObservableCollection<Student>).Remove(stu);
}
}
}

在cs文件中主要是分别处理的两个命令的CommandBinding的CanExecute和Executed事件,在CanExecute事件中可以对命令源(此处的Button)的状态进行控制。当可用时,点击Buttion或者按下F3键,均可以执行排序命令。效果图如下:

注意:点击Sort按钮和按下F3的区别在于:前者传递了命令参数(按Name升序),而后者没有(按ID升序)。

当通过ContextMenu将这三项删除时,Sort按钮被禁用。效果如下:

3.WPF内建命令

在WPF框架中内置了许多常用的命令,主要有ApplicationCommands、NavigationCommands、EditingCommands、ComponentCommands和MediaCommands这五个静态类的静态属性提供,均为RoutedUICommand实例,如上面用到的ApplicationCommands.Delete,其优点是:

1.将命令源和命令目标解耦

2.可复用、方便使用

来看个简单的例子:

<Window x:Class="CommandDemo.Window3"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window3" Height="300" Width="300">
<StackPanel>
<ToolBar>
<Button Command="Copy" Content="{Binding Command.Text,RelativeSource={RelativeSource Mode=Self}}" />
<Button Command="Paste" Content="{Binding Command.Text,RelativeSource={RelativeSource Mode=Self}}" />
</ToolBar>
<TextBox />
<TextBox />
</StackPanel>
</Window>

没有cs代码,将这段代码Copy到XamlPad中就可以直接运行,效果如下:

未进行任何操作之前,两个按钮都是禁用的,因为没有命令目标。当TextBox获得键盘焦点后且剪切板有文本,粘贴按钮就会被启用,否则被禁用。效果如下:

当选中TextBox的文本时,复制按钮被启用。点击复制按钮,选中的文本存储到了剪切板,当下面的TextBox获得焦点时,一直点击粘贴按钮,发现可以一直进行粘贴。这里也行你已经发现了,键盘焦点貌似没切换啊。其实,键盘焦点时有切换的,只不过在点击粘贴按钮后,TextBox又获得了键盘焦点。这里需要弄清楚的是Logical Focus和Keyboard Focus,以及它们在不同Focus Scope的不同行为,以及它们对RoutedCommand的影响,这里不再赘述,请查看下面的blog了解:

1.Using the WPF FocusScope

2.A FocusScope Nightmare (Bug?): Commands Are Disabled

WPF学习(7)命令的更多相关文章

  1. WPF学习之深入浅出话命令

    WPF为我们准备了完善的命令系统,你可能会问:"有了路由事件为什么还需要命令系统呢?".事件的作用是发布.传播一些消息,消息传达到了接收者,事件的指令也就算完成了,至于如何响应事件 ...

  2. 命令——WPF学习之深入浅出

    WPF学习之深入浅出话命令   WPF为我们准备了完善的命令系统,你可能会问:“有了路由事件为什么还需要命令系统呢?”.事件的作用是发布.传播一些消息,消息传达到了接收者,事件的指令也就算完成了,至于 ...

  3. 【WPF学习】第三十二章 执行命令

    前面章节已经对命令进行了深入分析,分析了基类和接口以及WPF提供的命令库.但尚未例举任何使用这些命令的例子. 如前所述,RoutedUICommand类没有任何硬编码的功能,而是只表达命令,为触发命令 ...

  4. WPF学习之路初识

    WPF学习之路初识   WPF 介绍 .NET Framework 4 .NET Framework 3.5 .NET Framework 3.0 Windows Presentation Found ...

  5. (转)WPF学习资源整理

    由于笔者正在学习WPF,所以整理出网络中部分WPF的学习资源,希望对同样在学习WPF的朋友们有所帮助. 首推刘铁猛的<深入浅出WPF>系列博文 1.深入浅出WPF(1)——什么是WPFht ...

  6. WPF学习资源整理

    WPF(WindowsPresentation Foundation)是微软推出的基于Windows Vista的用户界面框架,属于.NET Framework 3.0的一部分.它提供了统一的编程模型 ...

  7. WPF中的命令与命令绑定(二)

    原文:WPF中的命令与命令绑定(二) WPF中的命令与命令绑定(二)                                              周银辉在WPF中,命令(Commandi ...

  8. WPF学习11:基于MVVM Light 制作图形编辑工具(2)

    本文是WPF学习10:基于MVVM Light 制作图形编辑工具(1)的后续 这一次的目标是完成 两个任务. 画布 效果: 画布上,选择的方案是:直接以Image作为画布,使用RenderTarget ...

  9. WPF学习08:MVVM 预备知识之COMMAND

    WPF内建的COMMAND是GOF 提出的23种设计模式中,命令模式的实现. 本文是WPF学习07:MVVM 预备知识之数据绑定的后续,将说明实现COMMAND的三个重点:ICommand  Comm ...

随机推荐

  1. 共同发展一个以上的开发者账户多台电脑 证书 p12 型材 进出口

    1:导出相应的开发人员证书的配置文件. 2:依据相应的app id 创建配置文件. 3:打开钥匙串,把你的公布证书导出p12文件. 4:用开发人员账号导出developerprofile文件. 5:把 ...

  2. Linq 导出Excel

    var d = db.User; Repeater1.DataSource = d.ToList(); Repeater1.DataBind(); string guid = Guid.NewGuid ...

  3. 解决错误 fatal error C1010: unexpected end of file while looking for precompiled head

    在编译VS时候,出现fatal error C1010: unexpected end of file while looking for precompiled head. 问题详解:致命错误C10 ...

  4. 【剑指offer】面试题35:第一个数字只出现一次

    def FirstNotRepeatingChar(string): hashStr = [0] * 256 for c in string: hashStr[ord(c)] += 1 for c i ...

  5. Android Application plugin

          在网易云阅读App上看到了插件管理功能,刚好自己也需要以插件的模式来扩展已有的功能,于是研究了一下,下面是一张网易云阅读App提供的插件模式,只需下载相应的插件就扩展了相应的功能,非常方便 ...

  6. Windows Phone开发(40):漫谈关键帧动画之中篇

    原文:Windows Phone开发(40):漫谈关键帧动画之中篇 一.DiscreteDoubleKeyFrame 离散型关键帧动画,重点,我们理解一下"离散"的意思,其实你查一 ...

  7. 1104. Don’t Ask Woman about Her Age(数论)

    a*b^n(mod(b-1) =a(mod(b-1) http://acm.timus.ru/problem.aspx?space=1&num=1104 #include <stdio. ...

  8. Chrome应用技巧之代码整理。

    我们有时候在看别人站点代码时往往是经过压缩的,代码都在一行上了,调试非常是困难,今天给大家介绍一种基本Chrome浏览器的代码整理方法.请看图:

  9. EHCache的使用

    在开发高并发量,高性能的网站应用系统时,缓存Cache起到了非常重要的作用.本文主要介绍EHCache的使用,以及使用EHCache的实践经验.笔者使用过多种基于Java的开源Cache组件,其中包括 ...

  10. WPF 3D:使用GeometryModel3D的BackMaterial

    原文 WPF 3D:使用GeometryModel3D的BackMaterial 使用BackMaterial,我们可以定义3D物体的内部材质(或者说是背面),比如,我们定义一个四方体容器,外面现实的 ...