WPF中的命令是一种机制,可以将用户界面中的输入操作(如按钮点击、菜单选择等)与应用程序逻辑解耦。命令比直接事件处理器更灵活,尤其在处理多个控件共享相同逻辑或启用/禁用控件时特别有用。
WPF命令包括预定义命令和自定义命令。
- 命令可以将业务的需求和执行逻辑解耦,使不同来源可以调用相同的命令逻辑,并针对不同目标自定义该逻辑。
例如,编辑操作如“复制”、“剪切”和“粘贴”可以通过按钮、菜单项或组合键调用相同的命令逻辑。 - 命令可指示操作是否可用。例如,“剪切”操作只有在选择内容时才可执行,命令通过
CanExecute
方法来确定操作是否可行,并通过CanExecuteChanged
事件通知 UI 更新状态。尽管命令的语义一致,操作逻辑由目标对象定义,不同类型的对象在执行命令时有不同的处理方式。
命令本身:
WPF中的命令由ICommand
接口定义,该接口包括以下成员:
-
Execute
:命令调用时执行什么操作 -
CanExecute
:确定命令是否可以执行的方法。 -
CanExecuteChanged
:当命令的执行状态发生变化时引发的事件。
使用RelayCommand
,RelayCommod是我们自定义的一个ICommand的实现,里面内容也非常简单,就是常规的Command
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Predicate<object> _canExecute;
public RelayCommand(Action<object> execute, Predicate<object> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return _canExecute == null || _canExecute(parameter);
}
public void Execute(object parameter)
{
_execute(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
}
然后,在ViewModel中使用这个命令作为成员:
(注意只能作为属性,如果作为字段会识别不到)
public class MainViewModel
{
public ICommand MyCustomCommand { get; private set; }
private string _textBoxContent;
public string TextBoxContent
{
get { return _textBoxContent; }
set
{
_textBoxContent = value;
OnPropertyChanged();
}
}
public MainViewModel()
{
MyCustomCommand = new RelayCommand(ExecuteMyCustomCommand, CanExecuteMyCustomCommand);
}
private void ExecuteMyCustomCommand(object parameter)
{
MessageBox.Show("Custom command executed: " + TextBoxContent);
}
private bool CanExecuteMyCustomCommand(object parameter)
{
return !string.IsNullOrEmpty(TextBoxContent);
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
最后,在XAML中绑定命令:
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp"
Title="MainWindow" Height="200" Width="300">
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<StackPanel>
<TextBox Text="{Binding TextBoxContent, UpdateSourceTrigger=PropertyChanged}" Width="200" Height="30" />
<Button Content="Custom Command" Width="100" Height="30" Command="{Binding MyCustomCommand}" />
</StackPanel>
</Window>
对于按钮,单击就能执行命令了,但是对于其他控件可能不一定,如ListBox里面的Item,单击是选中控件,所以需要双击才能执行
还可以将命令的参数传进来
<Button Content="Custom Command" Width="100" Height="30" Command="{Binding MyCustomCommand}" CommandParameter = "False"/>
private void ExecuteMyCustomCommand(object parameter)
{
MessageBox.Show(parameter);
}
private bool CanExecuteMyCustomCommand(object parameter)
{
return (bool)parameter;
}
WPF命令系统组成
- 命令本身 即希望执行的操作
- 命令源 调用命令的对象
- 命令目标 执行命令的对象
- 命令绑定 将命令绑定到具体的行动上,将命令与执行该命令的逻辑关联起来
将某个命令绑定到特定控件(如按钮、菜单项等),使该控件在用户触发相应操作时执行绑定的命令逻辑
命令源
WPF 中的命令源通常实现 ICommandSource接口,有三个属性Command,CommandTarget, 和 CommandParameter
- Command是在调用命令源时执行的命令。
- CommandTarget 是要执行命令的对象。 值得注意的是,在 WPF 中,仅当 ICommand为 RoutedCommand 时,ICommandSource 上的 CommandTarget属性才适用。(如果不是路由命令的话,也就无所谓命令目标了,所以不是RoutedCommand的话哪怕设置了也会忽略)
如果未设置 CommandTarget,则具有键盘焦点的元素将成为命令目标。 - CommandParameter 是用于将信息传递给实现命令的处理程序的用户定义数据类型。
命令源将侦听 CanExecuteChanged事件。 结合 CanExecute方法可以查询 Command当前能否执行。 如果命令无法执行,命令源可禁用自身。 比如 MenuItem,在命令无法执行时,它自身将灰显。
实现 ICommandSource 的 WPF 类是 ButtonBase、MenuItem、Hyperlink 和 InputBinding。
RoutedCommand
RoutedCommand是WPF里面ICommand的特殊实现
RoutedCommand的 Execute和 CanExecute 方法不包含该命令的应用程序逻辑,而是引发冒泡路由事件,注意不会向下传播,引发的起点是CommandTarget ,直到遇到具有 CommandBinding的对象。 CommandBinding 包含这些事件的处理程序,命令正是由这些处理程序执行。一旦命令在某个控件上成功执行,传播将停止。
可能你会觉得:前面的ICommand接口明明说的是,Execute是命令的执行逻辑,但是这里的RoutedCommand的Execute不包含执行逻辑,这样是不是不符合接口的设计
但是RoutedCommand本身不只是Command,还是Routed,也就是兼顾了路由的功能,那么作为一个路由命令,他的Execute本身的目的就是路由,找到目标命令并执行。
从代码的逻辑上讲,作为一个ICommand,RoutedCommand触发后应该调用它的Execute方法,事实上也这样,这样一想是不是还挺合理的
<StackPanel>
<Menu>
<MenuItem Command="ApplicationCommands.Paste" />
</Menu>
<TextBox />
</StackPanel>
所以这段代码只要一开始鼠标在文本框里面,就能粘贴(因为没有显示设置target,所以target是焦点)
CommandBinding
CommandBinding类包含 Command属性,及 PreviewExecuted、Executed、PreviewCanExecute和 CanExecute事件。
预定义命令
WPF提供了一组预定义命令,如剪切、复制、粘贴、撤销、重做等。这些命令定义在ApplicationCommands
、NavigationCommands
、MediaCommands
和ComponentCommands
类中。
<Window x:Class="WpfApp.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">
<Window.CommandBindings>
<CommandBinding Command="ApplicationCommands.Copy" Executed="CopyCommand_Executed" CanExecute="CopyCommand_CanExecute" />
</Window.CommandBindings>
<StackPanel>
<TextBox x:Name="textBox" Width="200" Height="30" />
<Button Command="ApplicationCommands.Copy" Content="Copy" Width="100" Height="30" />
</StackPanel>
</Window>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void CopyCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = !string.IsNullOrEmpty(textBox.Text);
}
private void CopyCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
Clipboard.SetText(textBox.Text);
}
}
绑定了ApplicationCommands.Copy
命令到一个按钮和一个文本框。CanExecute
方法确保只有在文本框中有文本时,复制按钮才是可用的。
自定义命令
有时预定义命令无法满足需求,我们需要定义自己的命令。可以使用RoutedCommand
或实现ICommand
接口来创建自定义命令。
示例:使用RoutedCommand
定义自定义命令
<Window x:Class="WpfApp.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">
<Window.CommandBindings>
<CommandBinding Command="{x:Static local:MainWindow.MyCustomCommand}" Executed="MyCustomCommand_Executed" CanExecute="MyCustomCommand_CanExecute" />
</Window.CommandBindings>
<StackPanel>
<TextBox x:Name="textBox" Width="200" Height="30" />
<Button Command="{x:Static local:MainWindow.MyCustomCommand}" Content="Custom Command" Width="100" Height="30" />
</StackPanel>
</Window>
public partial class MainWindow : Window
{
public static readonly RoutedCommand MyCustomCommand = new RoutedCommand();
public MainWindow()
{
InitializeComponent();
}
private void MyCustomCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = !string.IsNullOrEmpty(textBox.Text);
}
private void MyCustomCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
MessageBox.Show("Custom command executed: " + textBox.Text);
}
}
在这个示例中,我们定义了一个名为MyCustomCommand
的RoutedCommand
并在XAML中绑定到按钮。CanExecute
方法和Executed
方法分别定义了命令的执行条件和执行逻辑。(相当于直接重新赋值了RoutedCommand的CanExecute
方法和Executed
方法)
示例:在提交时批量验证
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Input;
public class UserViewModel : INotifyPropertyChanged, IDataErrorInfo
{
private string _name;
private int _age;
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged(nameof(Name));
}
}
public int Age
{
get => _age;
set
{
_age = value;
OnPropertyChanged(nameof(Age));
}
}
public ICommand SubmitCommand { get; }
public UserViewModel()
{
SubmitCommand = new RelayCommand(Submit, CanSubmit);
}
private void Submit()
{
// 提交逻辑
System.Windows.MessageBox.Show("Data Submitted!");
}
private bool CanSubmit()
{
// 检查是否可以提交的逻辑
return !string.IsNullOrWhiteSpace(Name) && Age >= 0 && Age <= 120;
}
public string Error => null;
public string this[string columnName]
{
get
{
if (columnName == nameof(Name) && string.IsNullOrWhiteSpace(Name))
return "Name is required.";
if (columnName == nameof(Age) && (Age < 0 || Age > 120))
return "Age must be between 0 and 120.";
return null;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
<Window x:Class="WpfApp.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"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.BindingGroup>
<BindingGroup Name="UserBindingGroup"/>
</Grid.BindingGroup>
<StackPanel>
<TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, BindingGroup=UserBindingGroup}" Width="200" Margin="10"/>
<TextBox Text="{Binding Age, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, BindingGroup=UserBindingGroup}" Width="200" Margin="10"/>
<Button Content="Submit" Width="100" Margin="10" Command="{Binding SubmitCommand}"/>
</StackPanel>
</Grid>
</Window>