命令是指特定指令、有明确的执行内容且具有一定的强制性。
命令VS事件
命令与控件的事件看起来有类似的地方,但是两者是不同的。
Command
属性进行命令属性的绑定,从而让View层与业务逻辑进行分离。命令的用途
第一个目的是将语义和调用命令的对象与执行命令的逻辑分开。这允许多个和不同源调用相同的命令逻辑,并允许针对不同的目标自定义命令逻辑。
另一个用途是统一的指示操作是否可用,即根据业务逻辑对界面进行操作、控制。例如当登录界面用户名或密码为空时,登录按钮不可用。
创建CommandBase类型并实现ICommand
接口和对应成员。
ICommand
中有三个必须实现的成员:
CanExecuteChanged
:事件成员,触发该事件会执行一次CanExcute
方法,然后根据该方法的返回结果,来决定绑定该命令的控件对象是否可用。也就是每当关键属性变化的时候可以通过触发此事件,来刷新一下控件对象的可用状态。
CanExecuteChanged
的订阅(CanExcute
),WPF会在运行中自动完成,不需要我们再做订阅。bool CanExecute(object parameter)
:方法成员,返回结果决定对应的控件对象是否可用,一般通过触发CanExecuteChanged
事件来调用。
CommandParameter
属性传来的参数。void Execute(object parameter)
:命令的执行内容。
CommandParameter
属性传来的参数。CommandBase代码
public class CommandBase : ICommand
{
//事件成员,调用该事件会执行一次CanExcute方法,然后根据该方法的返回结果,来决定绑定该命令的控件对象是否可用
//一般在关键数据进行变换时调用
public event EventHandler CanExecuteChanged;
//由于外界只能使用事件的订阅或取消订阅,无法直接调用事件,因此需要做函数封装
public void DoCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
public Func<object, bool> DoCanExecute { get; set; }
/// <summary>
/// 根据返回值来决定绑定了该命令的控件是否可用,一般会通过CanExecuteChanged事件来调用
/// </summary>
/// <param name="parameter"></param>
/// <returns></returns>
public bool CanExecute(object parameter)
{
//直接使用对外提供的委托,由外界决定控件对象是否可用的判定逻辑
return DoCanExecute?.Invoke(parameter) == true;
}
public Action<object> DoExecute { get; set; }
/// <summary>
/// 命令的执行内容
/// </summary>
/// <param name="parameter"></param>
public void Execute(object parameter)
{
//直接使用对外提供的委托,由外界决定命令的执行内容
DoExecute?.Invoke(parameter);
}
}
CommandBase简易版
class BaseCommand : ICommand
{
private Action<object?>? _doExecute;
public BaseCommand(Action<object?> doExecute)
{
_doExecute = doExecute;
}
public event EventHandler? CanExecuteChanged;
public bool CanExecute(object? parameter)
{
return true;
}
public void Execute(object? parameter)
{
_doExecute?.Invoke(parameter);
}
}
在对应的数据模型中,定义命令属性、命令的执行内容、命令所对应控件是否可用的判定条件、CanExecuteChanged
事件的调用时机。
关于命令部分,其实是可以放到ViewModel层中来实现的,这里为了方便就直接在Model中实现了。
MainModel
public class MainModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private double _value1 = 0;
public double Value1
{
get { return _value1; }
set
{
_value1 = value;
Command1.DoCanExecuteChanged();
}
}
private double _value2 = 0;
public double Value2
{
get { return _value2; }
set
{
_value2 = value;
Command2.DoCanExecuteChanged();
}
}
private double _value3;
public double Value3
{
get { return _value3; }
set
{
_value3 = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Value3"));
}
}
public CommandBase Command1 { get; set; }
public CommandBase Command2 { get; set; }
public MainModel()
{
Command1 = new CommandBase
{
DoExecute = Execute1,
DoCanExecute = CanExecute1
};
Command2 = new CommandBase
{
DoExecute = Execute2,
DoCanExecute = CanExecute2
};
}
private void Execute1(object obj) {
//这里进行命令执行内容的定义......
Value3 = Value1 + Value2;
//执行完后,要调用一下命令事件,重新确认控件是否可用
Command1.DoCanExecuteChanged();
}
private void Execute2(object obj) {
//这里进行命令执行内容的定义......
Value3 = Value1 + Value2;
//执行完后,要调用一下命令事件,重新确认控件是否可用
Command1.DoCanExecuteChanged();
}
private bool CanExecute1(object obj)
{
return Value1 != 0;
}
private bool CanExecute2(object obj)
{
return Value2 != 0;
}
}
public class MainViewModel
{
public MainModel mainModel { get; set; } = new MainModel();
}
<Window.DataContext>
<vm:MainViewModel/>
</Window.DataContext>
<Grid>
<StackPanel>
<TextBox Text="{Binding mainModel.Value1, UpdateSourceTrigger=PropertyChanged}"/>
<Slider Value="{Binding mainModel.Value2}"/>
<TextBlock Text="{Binding mainModel.Value3}"/>
<Button Content="CommanTest1" Command="{Binding mainModel.Command1}" CommandParameter="{Binding RelativeSource={RelativeSource Self}}"/>
<Button Content="CommanTest2" Command="{Binding mainModel.Command2}" CommandParameter="{Binding RelativeSource={RelativeSource Self}}"/>
</StackPanel>
</Grid>
在上面的自定义命令的实现示例中可以看到,命令对象的可用状态的更新操作是十分重要的(也就是CanExecuteChanged
事件的触发)。但是,如果只能按照示例中的用法,通过命令对象去触发对象中的CanExecuteChanged
事件的话,难免会造成层次之间的耦合,有没有什么办法可以将CanExecuteChanged
事件剥离出来,不需要命令对象就能随时随地的去触发呢?此时就需要使用CommandManager
类型了。
CommandManager
类型用于在全局范围内对命令的可用状态更新进行管理。
CommandManager.RequerySuggested
:命令对象事件成员CanExecuteChanged
的挂载事件。
CanExecuteChanged
事件挂载在CommandManager.RequerySuggested
事件上。RequerySuggested
事件的触发条件是 WPF 内置的,WPF 内置的触发条件会导致多次调用CanExecute
。CommandManager.InvalidateRequerySuggested()
可主动触发一次RequerySuggested
事件,但必须在UI线程。RequerySuggested
。CommandManager.InvalidateRequerySuggested()
:主动触发CommandManager.RequerySuggested
事件。
RequerySuggested
中挂载了多个对象的CanExecuteChanged
事件,那么一旦调用InvalidateRequerySuggested()
方法将会全部触发。自定义命令实现全局命令状态更新
自定义命令的CanExecuteChanged
事件挂载:
public event EventHandler CanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
}
remove
{
CommandManager.RequerySuggested -= value;
}
}
在需要更新命令可用状态的地方直接调用CommandManager.InvalidateRequerySuggested()
主动触发RequerySuggested
事件。
private void Execute1(object obj) {
//这里进行命令执行内容的定义......
Value3 = Value1 + Value2;
//执行完后,要调用一下命令事件,重新确认控件是否可用
CommandManager.InvalidateRequerySuggested();
}
实际上,RequerySuggested
事件的触发条件是 WPF 内置的,所以即使不去调用InvalidateRequerySuggested()
大多数情况下也是可以的,但是WPF内置的触发条件会导致多次调用CanExecute,
因此自定义命令不建议使用RequerySuggested
。
WPF预定了一些命令及相关操作,方便我们在开发过程中快速的实现控件的行为处理。
媒体命令(共24个)
MediaCommands.Play
、MediaCommands.Stop
、MediaCommands.Pause
、…….
应用命令(共23个)
ApplicationCommands.New
、ApplicationCommands.Open
、ApplicationCommands.Copy
、ApplicationCommands.Cut
、ApplicationCommands.Print
、………
导航命令(共16个)
NavigationCommands.GoToPage
、NavigationCommands.LastPage
、NavigationCommands.Favorites
、……
联合命令(共27个)
ComponentCommands.ScrollByLine
、ComponentCommands.MoveDown
、ComponentCommands.ExtendSelectionDown
、……
编辑命令(共54个)
EditingCommands.Delete
、EditingCommands.ToggleUnderline
、EditingCommands.ToggleBold
、……
内置命令的使用主要分以下几个步骤:
绑定内置命令
通过控件的Command
属性绑定内置命令
<Button Command="ApplicationCommands.Open" ....../>
定义命令内容
在控件中,通过设置CommandManager
的相关属性完成命令的执行内容以及命令可用判定条件的定义。
CommandManager.Executed
:当前控件命令的执行内容。
CommandManager.CanExecute
:当前控件命令的可用判定条件。
<Button Command="ApplicationCommands.Open"
CommandManager.Executed="Button_Executed"
CommandManager.CanExecute="Button_CanExecute"
Content="{Binding RelativeSource={RelativeSource Self},Path=Command.Text}"/>
private bool flag = true;
private void Button_Executed(object sender, ExecutedRoutedEventArgs e)
{
MessageBox.Show("命令被触发了");
flag = false;
}
private void Button_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = flag;
}
在使用时候发现以下几点需要注意的地方:
Command.Text
属性获得内置命令对应的文本内容,这个好处是可以根据运行环境进行语言切换的。ApplicationCommands.Open
的快捷键是Ctrl+o
,快捷键会执行命令,但传给执行函数的sender参数为窗体对象。CanExecuteChanged
事件来更新命令的可用状态的。但这里是通过CommandManager
来对命令进行管理的,WPF会自动触发去触发命令状态更新事件。当然,我们也可以通过CommandManager.InvalidateRequerySuggested()
来进行主动触发。但由于WPF对于命令可用状态更新事件的触发是很频繁的,基本上没必要。在上面例子中,虽然可以在控件中通过CommandManager
来对命令的执行内容、判定条件进行定义,但如果由很多个控件使用了内置命令的话,一个个去定义会显得很繁杂。WPF为此提供了解决方案,可以通过容器元素的CommandBindings
属性,对同一类型的内置命令进行统一的逻辑定义,使得在作用域范围内的命令都使用都一套业务逻辑。
<Window.CommandBindings>
<CommandBinding Command="ApplicationCommands.Open" Executed="Button_Executed" CanExecute="Button_CanExecute"/>
</Window.CommandBindings>
<Grid>
<StackPanel>
<Button Command="ApplicationCommands.Open"
Content="{Binding RelativeSource={RelativeSource Self},Path=Command.Text}"
/>
<Button Command="ApplicationCommands.Open"
Content="{Binding RelativeSource={RelativeSource Self},Path=Command.Text}"
/>
</StackPanel>
</Grid>
上面是通过Window
元素进行ApplicationCommands.Open
命令的统一,如果希望缩小作用域范围也可以通过<StackPanel.CommandBindings>
来进行统一定义。
内置命令实质上是路由命令,普通的路由事件的消息传递是一样的,有隧道跟冒泡机制,可以通过设置Hendled
属性进行命令的终止。
private void Button_Executed(object sender, ExecutedRoutedEventArgs e)
{
......
e.Handled = true;
}
在使用过程中发现大部分的内置业务是需要自己重新做业务定义的,但也有个别内置命令自带的业务也挺好用的,比如ApplicationCommands.Copy
,其自带业务是,当文本框绑定该命令时,如果用光标选中文本内容,则该命令对应控件为可用状态,执行命令则将选中内容进行复制。
<TextBox Name="tb"/>
<Button Command="ApplicationCommands.Copy" CommandTarget="{Binding ElementName=tb}"
Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"/>
除了Copy
外,比较好用的内置命令还有Cut
、 Paste
等等。
查阅内置命令的源码,可以看到两种路由命令类型:RoutedUICommand
和RoutedCommand
。
其实RoutedUICommand
就是在继承了RoutedCommand
的基础上新增了Text
属性。
自定义路由命令
在xaml后台代码中定义对应属性:
public partial class MainWindow : Window
{
......
public RoutedUICommand MyRoutedCommand { get; set; }
public MainWindow()
{
InitializeComponent();
//快捷键组合
InputGestureCollection inputGestureCollection = new InputGestureCollection()
{
new KeyGesture(Key.T, ModifierKeys.Alt)
};
MyRoutedCommand = new RoutedUICommand("textContent", "commandName", typeof(MainWindow),inputGestureCollection);
}
......
}
在xaml中使用自定义路由命令
<Button Command="{Binding RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor},Path=MyRoutedCommand}"
CommandManager.Executed="Button_My_Executed"
CommandManager.CanExecute="Button_My_CanExecute"
Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"/>