WPF 中的 ICommand 接口






4.91/5 (48投票s)
ICommand 接口及其在通用命令实现中的用法。
引言
在本文中,我们将学习 ICommand
接口及其在 WPF/Silverlight 应用程序中使用 MVVM(模型-视图-ViewModel)模式时,在命令的通用实现中的用法。首先,我们将了解 Command
,然后研究 ICommand
接口的成员。我们将创建一个 WPF 演示应用程序来学习 ICommand
的用法。在演示应用程序中,我们还将使用 MVVM 模式。
注意:本文需要具备 WPF 和 MVVM 模式的基本理解。如果您是 MVVM 模式的新手,请参考 Wiki 和 MSDN 页面。
Outline
- 什么是命令
- ICommand 接口
- 为什么使用 ICommand 接口
- 演示应用程序概述
- 创建演示应用程序的 UI
- 如何使用 ICommand 接口
- INotifyPropertyChanged 的必要性
- 单独命令的问题
- 命令的通用实现
什么是命令
在 WPF 上下文中,Command
是实现 ICommand
接口的任何类。命令和事件之间存在细微差别。事件是定义并与 UI 控件相关联的。而命令更加抽象,侧重于要执行的操作。一个命令可以与多个 UI 控件/选项相关联。例如,保存命令可以在单击保存按钮时执行,也可以通过按 Ctrl+S 或从菜单栏选择 保存 选项来执行。为了在应用程序中实现交互,我们使用事件或命令。
在 WPF 应用程序中,我们有两种方式来提供 UI 交互
- 第一种方式是使用事件并在代码隐藏文件中编写我们在特定事件上想要执行的代码,如果我们不遵循 MVVM 模式。WPF 中有许多内置的
RoutedEvent
可供此场景使用。 - 第二种方式是在
ViewModel
中编写我们想要执行的代码块,并通过 Command 调用该代码,如果我们遵循 MVVM 模式。如果我们使用 MVVM 模式,那么通常不应该在代码隐藏文件中编写代码。因此,我们不能使用 RoutedEvents,因为 RoutedEvents 在 ViewModel 中是无法访问的。这就是为什么我们必须使用 Commands 来执行 ViewModel 中编写的所需代码块。因此,Commands 为 View 和 ViewModel 之间的交互提供了粘合剂。
ICommand 接口
ICommand
是一个接口,它有三个成员,如下表所示
ICommand 的成员 | 描述 |
bool CanExecute(object parameter) |
|
event EventHandler CanExecuteChanged | 这用于在 |
void Execute(object parameter) | 这是执行 |
为什么使用 ICommand 接口
ICommand
是 WPF 中 Command
的核心接口。它在 MVVM 和 Prism 应用程序中被广泛使用。但 ICommand
接口的使用不仅仅限于 MVVM。WPF/Silverlight/Prism 框架提供了许多已实现此接口的内置 Command
。基类 RoutedCommand
实现 ICommand
接口。WPF 提供了 MediaCommands
、ApplicationCommands
、NavigationCommands
、ComponentCommands
和 EditingCommands
,它们使用 RoutedCommand
类。有关更多信息,请参阅 MSDN 页面。
为了便于理解,让我们创建一个演示应用程序。
演示应用程序概述
演示应用程序进行非常基本的计算,仅用于展示 ICommand
接口的实现。演示应用程序有两个 textbox
用于接受输入值,一个标签用于显示输出,以及四个按钮用于执行计算。四个按钮用于执行加、减、乘、除运算。在 textbox
中输入值后,单击按钮,将触发关联的命令,并在标签中显示结果。我们将如何在后续步骤中实现上述功能。请下载附带的代码示例,它将有助于理解和跟进后续的解释。
演示应用程序的最终截图如下所示
创建演示应用程序的 UI
现在启动 Visual Studio,并按照以下步骤创建演示应用程序
步骤 1
创建一个名为 SimpleCommandDemoApp
的 WPF 应用程序。按照下图所示的结构添加文件夹和文件。在后续步骤中,我们将编写这些文件中的代码。
第二步
首先,我们将创建演示 UI 的布局。为此,我们将创建一个具有四行四列的网格。控件将通过指定行和列位置放置在此网格中。将以下代码写入 CalculatorView.xaml 文件。
<Grid DataContext="{Binding Source={StaticResource calculatorVM}}"
Background="#FFCCCC">
<Grid.RowDefinitions>
<RowDefinition Height="80"></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition Height="80"></RowDefinition>
<RowDefinition Height="44"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0"
Grid.ColumnSpan="4" FontSize="25" VerticalAlignment="Top"
HorizontalAlignment="Center" Foreground="Blue" Content="ICommand Demo"/>
<Label Grid.Row="0" Grid.Column="0"
Grid.ColumnSpan="2" Margin="10,0,0,0" VerticalAlignment="Bottom"
FontSize="20" Content="First Input"/>
<Label Grid.Row="0" Grid.Column="2" Grid.ColumnSpan="2"
Margin="10,0,0,0" VerticalAlignment="Bottom"
FontSize="20" Content="Second Input"/>
<TextBox Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
Margin="10,0,0,0" FontSize="20" HorizontalAlignment="Left"
Height="30" Width="150"
TextAlignment="Center" Text="{Binding FirstValue, Mode=TwoWay}"/>
<TextBox Grid.Row="1" Grid.Column="2" Grid.ColumnSpan="2"
Margin="10,0,0,0" FontSize="20" HorizontalAlignment="Left"
Height="30" Width="150" TextAlignment="Center"
Text="{Binding SecondValue, Mode=TwoWay}"/>
<Rectangle Grid.Row="2" Grid.Column="0"
Grid.ColumnSpan="4" Fill="LightBlue"></Rectangle>
<Button Grid.Row="2" Grid.Column="0" Content="+"
Margin="10,0,0,0" HorizontalAlignment="Left" Height="50"
Width="50" FontSize="30" Command="{Binding AddCommand}"></Button>
<Button Grid.Row="2" Grid.Column="1" Content="-"
Margin="10,0,0,0" HorizontalAlignment="Left" Height="50"
Width="50" FontSize="30" Command="{Binding SubstractCommand}"></Button>
<Button Grid.Row="2" Grid.Column="2" Content="*"
Margin="10,0,0,0" HorizontalAlignment="Left" Height="50"
Width="50" FontSize="30" Command="{Binding MultiplyCommand}"></Button>
<Button Grid.Row="2" Grid.Column="3" Content="%"
Margin="10,0,0,0" HorizontalAlignment="Left" Height="50"
Width="50" FontSize="30" Command="{Binding DivideCommand}"></Button>
<Label Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2"
FontSize="25" Margin="10,0,0,0" HorizontalAlignment="Left"
Height="50" Content="Result : "/>
<TextBlock Grid.Row="3" Grid.Column="2" Grid.ColumnSpan="2"
FontSize="20" Margin="10,0,0,0" Background="BlanchedAlmond"
TextAlignment="Center" HorizontalAlignment="Left" Height="36"
Width="150" Text="{Binding Output}"/>
</Grid>
现在转到 MainWindow.xaml 文件,添加 CalculatorView.xaml 文件的 namespace
,以便我们可以在 MainWindow
中访问 CalculatorView.xaml。
xmlns:views="clr-namespace:SimpleCommandDemoApp.Views"
在 MainWindow.xaml 的父网格中为 CalculatorView.xaml 视图创建一个标签。
<Grid>
<views:CalculatorView/>
</Grid>
步骤 3
现在我们需要将 CalculatorView.xaml 中两个文本框和一个标签的属性附加到其 ViewModel
,称为 CalculatorViewModel
。
正如我们在 XAML 中看到的,我们将 UI 属性与 ViewModel
属性进行了绑定。CalculatorViewModel
的属性“FirstValue
”与第一个 textbox
的“Text
”属性绑定,CalculatorViewModel
的属性“SecondValue
”与第二个 textbox
的“Text
”属性绑定。CalculatorViewModel
的属性“Output
”与 label
的“Content
”属性绑定。
要与 CalculatorViewModel
进行绑定,请创建三个 private
字段,分别称为 firstValue
、secondValue
和 output,以及三个 public
属性,其名称与我们在 UI 中为 textbox
的 text 属性绑定时使用的名称相同。我们必须为这三个属性提供与我们在 UI 中为 textbox
的 Text
属性绑定时使用的名称相同的名称。
“FirstValue
”属性的写法如下,同样,我们必须在 CalculatorViewModel
中创建另外两个名为“SecondValue
”和“Output
”的属性。
public double FirstValue
{
get
{
return firstValue;
}
set
{
firstValue = value;
}
}
如何使用 ICommand 接口
要将 Commands 与 UI 控件进行绑定,我们需要创建一个 command
类,该类必须实现 ICommand
接口。
首先,我们将为每个按钮的功能创建一个 command
类。稍后,我们将看到如何重用一个通用的 command
类来处理这些功能。现在,我们首先为“Add
”功能创建单独的 command
。
步骤 4
我们已经在 UI 页面上创建了一个“Plus”按钮。由于我们遵循 MVVM 模式,在这种情况下,要处理此类功能,我们需要实现 ICommand
接口。让我们通过在 PlusCommand
类中编写以下代码来实现。
public class PlusCommand : ICommand
{
// Creating private field of CalculatorViewModel
// and passing calculatorViewModel into the constructor
private CalculatorViewModel calculatorViewModel;
public PlusCommand(CalculatorViewModel vm)
{
calculatorViewModel = vm;
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
calculatorViewModel.Add();
}
public event EventHandler CanExecuteChanged;
}
步骤 5
现在,在 CalculatorViewModel
中创建一个 PlusCommand
类的 private
字段,并在 CalculatorViewModel
的构造函数中创建一个 PlusCommand
类的实例。
private PlusCommand plusCommand;
public CalculatorViewModel()
{
plusCommand = new PlusCommand(this);
}
步骤 6
将 CalculatorViewModel
的命名空间注册到 CalculatorView.xaml 文件。为此,需要在 namespace
区域添加以下代码行。
xmlns:vm="clr-namespace:SimpleCommandDemoApp.ViewModels"
在命名空间之后,添加 UserControl.Resources
标签,通过指定键“calculatorVM
”来注册 CalculatorViewModel
。我们在将 DataContext
绑定到网格时使用了相同的键。
<UserControl.Resources>
<vm:CalculatorViewModel x:Key="calculatorVM" />
</UserControl.Resources>
步骤 7
在 CalculatorViewModel
中实现 Add
方法。
public void Add()
{
Output = firstValue + secondValue;
}
步骤 8
在 CalculatorViewModel
中创建一个名为“AddCommand
”的 command
。Command
的名称必须与我们在 CalculatorView.xaml 文件中为按钮的 Command
属性绑定时给出的名称相同。AddCommand
的代码如下所示
internal ICommand AddCommand
{
get
{
return plusCommand;
// return new RelayCommand(Add);
}
}
步骤 9
现在运行应用程序,单击“Plus”按钮,将调用 CalculatorViewModel
构造函数,因为我们在 CalculatorView.xaml 文件中将 CalculatorViewModel
的引用作为 DataContext
提供了。在 CalculatorViewModel
的构造函数中,我们创建了 PlusCommand
的实例。在创建 PlusCommand
实例时,我们使用“this
”关键字传递了 CalculatorViewModel ViewModel
本身。
在 AddCommand
执行过程中,将首先调用 CanExecute
方法,该方法将返回“true
”(因为为了简单起见,我们已将其硬编码)。然后将调用 Execute
方法,该方法将调用 CalculatorViewModel
的 Add
方法。
在 Add
方法中设置断点并运行应用程序,我们将看到文本框中输入的值在 firstValue
、secondValue
变量中可用。计算结果将分配给 Output
属性。但结果在标签的 Content
中未在屏幕上显示。如果我们想在 UI 上看到结果,我们必须实现 INotifyPropertyChanged 接口,以便 ViewModel 中 "Output" 属性的更改可以通知到 UI。
INotifyPropertyChanged 的必要性
INotifyPropertyChanged
的目的是在属性发生任何更改时通知所有其引用(UI 控件/ViewModel)。由于默认情况下,UI 属性(即 Textbox
的 Text
属性)大部分都实现了 INotifyPropertyChanged
。现在,我们也需要为 ViewModel
实现此接口,以便当 ViewModel
端发生任何更改时,可以通知/反映到 UI。
第 10 步
正如我们在上一步中看到的,“Output
”是 CalculatorViewModel
的属性名称,我们已将其绑定到 label
的 Content
属性。当调用 Add
方法时,我们将计算值赋给“Output
”属性。由于“Output
”是一个 public
属性并绑定到 UI,因此更改将反映到 label
。现在借助 INotifyPropertyChanged
,label
的 Content
将被更新。
所以,让我们在 ViewModelBase
类中实现 INotifyPropertyChanged
接口。代码如下所示
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
第 11 步
让 CalculateViewModel
继承自 ViewModelBase
,在所有三个 public
属性的 set 块中添加一行代码,如下所示
OnPropertyChanged("FirstValue");
OnPropertyChanged
是一个以属性名作为参数的方法。我们已在 ViewModelBase
类中实现了此方法。每当属性在 UI 或 ViewModel
端发生任何更改时,都将调用 OnPropertyChanged
方法。由于我们在 XAML 代码中为第一个 textbox
和第二个 textbox
设置了 Mode=TwoWay
,因此当 UI 端或 ViewModel
端发生任何更改时,另一方(UI 或 ViewModel
)都会收到通知。
现在运行应用程序,我们将能够执行 Add
操作,并且结果将显示在 Output
标签中。我们需要对其余的按钮点击命令遵循相同的步骤来完成另外三个计算。
单独命令的问题
目前,我们需要为每个操作创建单独的命令类。正如我们所做的,对于 PlusCommand 类,它具有以下缺点
首先:PlusCommand
类与 CalculatorViewModel
紧密耦合,因为 PlusCommand
类拥有 CalculatorViewModel
的引用。
其次:从 Execute
方法中,我们调用了 CalculatorViewModel
的 Add
方法。将来,如果我们更改 Add
方法的名称,那么我们也必须修改(Execute
方法的)PlusCommand
类。
第三:由于我们无法重用 PlusCommand
类来执行其他操作,因此对于每个事件,我们需要为每个操作编写许多不同的单独命令类。在我们的例子中,在单击“Plus”按钮时,我们调用 Add 方法。但对于“Minus”按钮点击,我们需要调用 Subtract
方法,而这无法通过使用 PlusCommand
类来完成。因此,我们创建了四个 Command
类来处理每个点击事件,分别命名为 PlusCommand
、MinusCommand
、MultiplyCommand
和 DivideCommand
,这不是一个好的方法。
因此,我们创建一个通用的命令类来处理所有类型的操作。为此,我们可以使用内置的委托。
命令的通用实现
.NET Framework 中有许多内置的委托,如 Action
、Func
和 Predicate
等。在我们的演示应用程序中,我们将使用 Action
委托。有关 Action
的更多信息,请参阅 此处。然后我们将看到如何通过提高可重用性来简化我们的工作。
第 12 步
在以上步骤中,我们创建了四个命令类来处理四个按钮点击事件。现在,通过使用 Action
委托,我们将通过创建一个名为 RelayCommand
的通用命令类来删除所有这四个命令类。我们可以使用任何名称代替 RelayCommand
。作为一种好的实践,我们应该给它一个有意义的名称。将以下代码写入 RelayCommand
类
现在我们需要更改一行代码。注释掉第一行,然后取消注释 AddCommand
属性的 get
块中的第二行,如下所示
public ICommand AddCommand
{
get
{
//return plusCommand;
return new RelayCommand(Add);
}
}
在此 get
块返回之前,将调用 RelayCommand
的构造函数,该构造函数接受一个方法作为 Action
委托。在这里,我们将 Add
方法的名称传递给 Action
委托参数。
在 RelayCommand
的构造函数处设置断点并运行应用程序。我们将看到 Add
方法的名称在 workToDo
局部变量中,如下所示
现在,所有四个操作都可以仅通过使用 RelayCommand
类来处理。现在我们可以删除我们在“Commands\Specific”文件夹中创建的所有四个命令类。
结论
在本文中,我们回顾了 ICommand
接口及其用法。我们了解了如何使用 ICommand
接口创建通用的 Command
类,这是 MVVM 模式的标准实践。感谢您的阅读。非常欢迎您提出改进意见和建议。
历史
- 2015 年 11 月 8 日:初始版本