WPF命令是一个任务的完整封装,例如保存,复制,剪切这些操作都可以理解为一个个独立的命令。乍一看,命令和传统的事件机制似乎很相似,但命令和事件并不冲突,命令和事件的区别就在于命令是具有约束力。
WPF命令系统模型主要由以下四个元素组成:
- 命令(Command):命令表示一个任务单元,并且可跟踪该任务的状态,实际上是实现了ICommand接口的类。
- 命令源(CommandSource):即命令的发送者/行为的触发者,实际上是实现了ICommandSource接口的类。
- 命令目标(CommandTarget):命令的接收者/命令的作用对象,命令目标必须是实现了 IInputElement接口的类。
- 命令关联(CommandBinding):负责把一些外围逻辑与命令关联起来,是将命令逻辑映射到命令的对象,包括命令是否可以执行前后的操作、命令执行前后的操作。
命令的特点如下:
复用: 统一命令逻辑,减少代码冗余,封装独立、可复用的任务执行单元。
分离: 通过命令可以使命令源和命令目标分离,减少代码耦合。
状态同步: 命令可以使得控件的启用状态和相应的命令状态保持同步,从而指示操作是否可用。
ICommand接口
WPF命令模型的核心是System.Windows.Input.ICommand接口,该接口定义了命令的工作原理。该接口包含两个方法和一个事件:
public interface ICommand
{
//触发程序事件处理
void Execute(object parameter);
//命令可用,返回true;命令不可用,返回false
bool CanExecute(object parameter);
//当命令状态改变时引发
event EventHandler CanExecuteChanged;
}
RoutedCommand类
当创建自己的命令时,不会直接实现ICommand接口;而是使用System.Windows.Input.RoutedCommand类,该类自动实现了ICommand接口。RoutedCommand类是WPF中唯一实现了ICommand接口的类。换句话说,所有WPF命令都是RoutedCommand类及其派生类的实例。
RoutedCommand类为事件冒泡和隧道添加了一些额外的基础结构。鉴于ICommand接口封装了命令的思想——可被触发的动作并可被启用或禁用——RoutedCommand类对命令进行了修改,使命令可在WPF元素层次结构中冒泡,以便获得正确的事件处理程序。
为支持路由事件,RoutedCommand类私有地实现了ICommand接口,并添加了ICommand接口方法的一些不同版本。最明显的变化是,Execute()和CanExecute()方法使用了一个额外参数。下面的新的签名:
public void Execute(object parameter,IInputElement target)
{
//...
}
public void CanExecute(object parameter,IInputElement target)
{
//...
}
参数target是开始处理事件的元素。事件从target元素开始,然后冒泡至高层的容器,直到应用程序为了执行合适的任务而处理了事件(为了处理Executed事件,元素还需要借助于另一个类——CommandBinding类的帮助)。
除上面的修改外,RoutedCommand类还引入了三个属性:命令名称(Name属性)、包含命令的类(OwnerType)以及任何可用于触发命令的按键或鼠标操作(位于InputGestures集合中)。
RoutedUICommand类
在程序中处理的大部分命令不是RoutedCommand对象,而是RoutedUICommand类的实例,RoutedUICommand类继承自RoutedCommand类(实际上,WPF提供的所有预先构建好的命令都是RoutedUICommand对象)。
RoutedUICommand类用于具有文本的命令,这些文本显示在用户界面中的某些地方(例如菜单项文本、工具栏按钮的工具栏提示)。RoutedUICommand类只添加了Text属性,该属性是为命令显示的文本。
为命令定义命令文本(而不是直接在控件上定义文本)的优点是可在某个位置执行本地化。但如果命令文本永远不会在用户界面的任何地方显示,那么RoutedUICommand类和RoutedCommand类是等效的。
以上三个类继承结构为RoutedUICommand->RoutedCommand->ICommand
三种命令实现方式可参考:WPF之命令Command - 简书
WPF命令库
WPF设计者认识到,每个应用程序可能都有大量命令,并且对于许多不同的应用程序,很多命令时通用的,例如,所有基于文档的应用程序都有他们自己版本的New、Open以及Save命令。为减少创建这些命令所需的工作,WPF提供了基本命令库,基本命令库中保存的命令超过100条。这些命令通过以下5个专门的静态类的属性提供:
- ApplicationCommands:该类提供了通用命令,包括剪贴板命令(如Copy、Cut和Paste)以及文档命令(如New、Open、Save、SaveAs和Print等)。
- NavigationCommands:该类提供了用于导航的命令,包括为基于页面的应用程序设计的一些命令(如BrowseBack、BrowSeForward和NextPage),以及其他适合于基于文档的应用程序的命令(如IncreaseZoom和Refresh).
- EditingCommands:该类提供了许多重要的文档编辑命令,包括用于移动的命令(MoveToLineEnd、MoveLeftByWord和MoveUpByPage等),选择内容的命令(SelectToLineEnd、SelectLeftByWord),以及改变格式的命令(ToggleBold和ToggleUnderLine)。
- ComponentCommands:该类提供了由用户界面组件使用的命令,包括用于移动和选择内容的命令,这些命令和EditingCommands类中的一些命令类似(甚至完全相同)。
- MediaCommands:该类提供了一组用于处理多媒体的命令(如Play、Pause、NextTrack以及IncreaseVolume).
ApplicationCommands类提供了一组基本命令,在所有类别的应用程序中都经常会用到这些命令:New、Open、Close、Copy、Cut、Paste、Save、Delete、Stop等,如果你的程序中需要这些命令,那就不需要自己声明,直接拿来使用就可以。
以下示例显示了如何设置 MenuItem,以便在单击时它将调用 TextBox 上的 Paste 命令,假定 TextBox 具有键盘焦点:
<StackPanel>
<Menu>
<MenuItem Command="ApplicationCommands.Paste" />
</Menu>
<TextBox />
</StackPanel>
无Command属性控件实现Command绑定:
方法一:使用InputBingdings(包含KeyBinding和MouseBinding)
<!--没有Command属性的控件可以使用KeyBinding或MouseBinding来绑定命令-->
<TextBox Text="KeyBinding and MouseBinding" Height="40">
<TextBox.InputBindings>
<!--按下Alt+D,最大化窗体-->
<KeyBinding Key="D" Modifiers="Alt" Command="SystemCommands.MaximizeWindowCommand"></KeyBinding>
<!--KeyBinding绑定对象需要可聚焦,MouseBinding中MouseAction的优先级高于Gesture,当MouseAction设置以后,Gesture将失效-->
<MouseBinding Gesture="LeftClick" MouseAction="RightClick" Command="SystemCommands.RestoreWindowCommand"></MouseBinding>
</TextBox.InputBindings>
</TextBox>
方法二:使用Microsoft.Xaml.Behaviors库(System.Windows.Interactivity已被弃用)
<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"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
mc:Ignorable="d" WindowStartupLocation="CenterScreen"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Border Width="100" Height="40" Grid.Row="1" Background="LightBlue">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonDown">
<i:InvokeCommandAction Command="{Binding SelectedCommand}" CommandParameter="Microsoft.Xaml.Behaviors"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Border>
</Grid>
</Window>
方法三:使用附加属性绑定命令
后台:
public class ElementAttachProperty
{
public static ICommand GetTextBoxItemSelectCommand(DependencyObject obj)
{
return (ICommand)obj.GetValue(TextBoxItemSelectCommandProperty);
}
public static void SetTextBoxItemSelectCommand(DependencyObject obj, ICommand value)
{
obj.SetValue(TextBoxItemSelectCommandProperty, value);
}
public static readonly DependencyProperty TextBoxItemSelectCommandProperty =
DependencyProperty.RegisterAttached("TextBoxItemSelectCommand", typeof(ICommand), typeof(ElementAttachProperty), new PropertyMetadata(OnTextBoxItemSelectCommand));
private static void OnTextBoxItemSelectCommand(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var element = d as TextBox;
element.TextChanged += Element_ItemSelecting;
}
private static void Element_ItemSelecting(object sender, EventArgs e)
{
var element = sender as TextBox;
var command = (ICommand)element.GetValue(TextBoxItemSelectCommandProperty);
command?.Execute(element.Text);
}
}
public class MainViewModel
{
public MainViewModel()
{
}
public ICommand ItemSelectedCmd
{
get { return new DelegateCommand(ItemSelected); }
}
private void ItemSelected(object o)
{
MessageBox.Show($"Command param is {o}");
}
}
前台:
<TextBox Width="100" Height="30" local:ElementAttachProperty.TextBoxItemSelectCommand="{Binding ItemSelectedCmd}" Margin="0,10,0,0"/>
使用自定义控件添加COMMAND依赖属性传递命令:
右键项目添加"WPF自定义控件",命名CommandControl,在项目中会生成一个Themes文件夹,存放自定义控件样式主题文件Generic.xaml。具体代码如下:
CommandControl.cs文件:
public class CommandControl : ContentControl
{
static CommandControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CommandControl), new FrameworkPropertyMetadata(typeof(CommandControl)));
}
public CommandControl()
{
MouseLeftButtonDown += CommandControl_MouseLeftButtonDown;
}
private void CommandControl_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (Command != null)
{
if (Command.CanExecute(CommandParameter))
{
Command.Execute(CommandParameter);
}
}
}
private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs mouseButtonEventArgs)
{
if (Command != null)
{
if (Command.CanExecute(CommandParameter))
{
Command.Execute(CommandParameter);
}
}
}
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register("Command", typeof(ICommand),
typeof(CommandControl),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.None));
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.Register("CommandParameter", typeof(object),
typeof(CommandControl),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.None));
public object CommandParameter
{
get { return (object)GetValue(CommandParameterProperty); }
set { SetValue(CommandParameterProperty, value); }
}
}
Generic.xaml样式主题文件:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfCommandDemo">
<Style TargetType="{x:Type local:CommandControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:CommandControl}">
<ContentControl>
<ContentPresenter/>
</ContentControl>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
自定义控件调用:
<Border Grid.Row="1" Grid.Column="1" Width="100" Height="40" Background="LightCoral">
<local:CommandControl Command="{Binding ItemSelectedCmd}" CommandParameter="自定义控件命令">
<TextBlock Text="UserCommand"/>
</local:CommandControl>
</Border>