学习WPF的命令机制,我认为最重要的是能够找到一种在MVVM模式中使用它的最佳工程实践方式。
绑定和命令是MVVM模式的核心,后面介绍MVVM模式基本是顺水推舟了。
与路由事件的关系
在深入了解WPF的命令之前,有必要理清命令和路由事件的关系:
(1)路由事件提供了一种灵活的事件传递机制,主要用于处理界面状态的变化;
(2)命令主要用于封装程序的操作逻辑,从而保证了操作逻辑的复用性。
(3)路由事件和命令是两套出发点不同的机制,不是相互替换的关系。在大型软件系统中,通常需要路由事件和命令相互协作来完成复杂功能。
使用RoutedCommand
在WPF中,比较典型的是按钮控件,它有Command属性,设置这个属性就可以绑定命令。
一种方式是使用RoutedCommand类,这是标准库提供的实现了ICommand接口的命令类,但是它没有实现CanExecute()和Executed()逻辑,需要通过命令关联的方式添加该逻辑。
这种方式需要使用命令关联统一定义命令的逻辑,每个命令没有形成清晰的封闭整体,不是推荐用法,不详细介绍。
较好的工程实践
回归命令机制的初衷,开发者不希望在窗体的事件处理函数中写业务逻辑,而是倾向于将每个业务逻辑写成普通函数,然后包装成命令供界面控件使用。
那么,如何将业务处理逻辑包装成命令呢?我认为以下方式是比较好的实践思路。
(1)自定义以下命令类,其构造函数可以传入实际的Execute()和CanExecute()执行逻辑,开发者显式调用RaiseCanExecuteChange()函数可以触发CanExecute()的执行,从而更新控件的状态,一般可以在窗体的事件处理器中调用。
public class MyCommand : ICommand {
private Action<object> execute;
private Func<object, bool> canExecute;
public MyCommand(Action<object> execute, Func<object, bool> canExecute = null) {
this.execute = execute;
this.canExecute = canExecute;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter) {
if (canExecute != null)
return CanExecute(parameter);
return true;
}
public void Execute(object parameter) {
execute?.Invoke(parameter);
}
public void RaiseCanExecuteChange() {
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
(2)在ViewModel类中定义命令,并用属性将其包装供View绑定,这里采用的是经典的DataContext方式。
public class TestViewModel : NotifyPropertyChanged {
private MyCommand _command1;
public MyCommand Command1 {
get {
return _command1;
}
set {
_command1 = value;
RaisePropertyChanged();
}
}
public TestViewModel() {
_command1 = new MyCommand(ShowMessage, SetEnbale);
}
#region 业务处理逻辑,程序的核心
private void ShowMessage(object parameter) {
var para = parameter as Window;
if (para != null)
MessageBox.Show($"窗体宽度为:{para.Width}");
}
private bool SetEnbale(object parameter) {
var para = parameter as Window;
if (para != null && para.Width > 300)
return true;
return false;
}
#endregion
}
<Window x:Class="TestMvvm.Views.Test"
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:TestMvvm.Views"
mc:Ignorable="d"
x:Name="myWin"
SizeChanged="myWin_SizeChanged"
Title="Test" SizeToContent="WidthAndHeight">
<StackPanel Orientation="Vertical">
<Button x:Name="btn1" Content="按钮1" Command="{Binding Command1}"
CommandParameter="{Binding ElementName=myWin}"/>
</StackPanel>
</Window>
以上代码有几个关键点:
- 传参:这里在XAML中通过绑定将窗体传递到命令,CanExecute()和Execute()都能获得这个参数。
- 更新按钮状态:本例子想在窗体宽度大于300时更新按钮状态为可见,点击按钮后弹窗显示窗体宽度。为了更新按钮状态,必须在窗体尺寸变化函数中调用RaiseCanExecuteChange()函数。不直接调用CanExecute()函数,是因为开发者没有办法显式向其传递参数。