65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF 中的 ICommand 接口

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (48投票s)

2015 年 11 月 8 日

CPOL

11分钟阅读

viewsIcon

117899

downloadIcon

3252

ICommand 接口及其在通用命令实现中的用法。

引言

在本文中,我们将学习 ICommand 接口及其在 WPF/Silverlight 应用程序中使用 MVVM(模型-视图-ViewModel)模式时,在命令的通用实现中的用法。首先,我们将了解 Command,然后研究 ICommand 接口的成员。我们将创建一个 WPF 演示应用程序来学习 ICommand 的用法。在演示应用程序中,我们还将使用 MVVM 模式。

注意:本文需要具备 WPF 和 MVVM 模式的基本理解。如果您是 MVVM 模式的新手,请参考 WikiMSDN 页面

Outline

什么是命令

在 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)

CanExecute 方法接受一个 object 作为参数并返回 bool。如果返回 true,则可以执行关联的命令。如果返回 false,则不能执行关联的命令。通常,我们将一个委托作为 object 传递给此方法,该委托返回一个 bool。为此,我们可以使用内置的委托,如 ActionPredicateFunc

event EventHandler CanExecuteChanged

这用于在 Command 可以执行时通知与 Command 关联的 UI 控件。基于此事件的通知,UI 控件会更改其状态,变为启用或禁用。基本上,当 "CanExecute" 方法执行时,如果方法的输出(true/false)发生变化,则会触发此事件。

void Execute(object parameter)

这是执行 Command 预期工作的实际方法。仅当 CanExecute 方法返回 true 时,此方法才会被执行。它接受一个 object 作为参数,并且我们通常将一个 delegate 传递给此方法。该 delegate 持有一个方法引用,该方法将在 Command 被触发时执行。

为什么使用 ICommand 接口

ICommand 是 WPF 中 Command 的核心接口。它在 MVVM 和 Prism 应用程序中被广泛使用。但 ICommand 接口的使用不仅仅限于 MVVM。WPF/Silverlight/Prism 框架提供了许多已实现此接口的内置 Command。基类 RoutedCommand 实现 ICommand 接口。WPF 提供了 MediaCommandsApplicationCommandsNavigationCommandsComponentCommandsEditingCommands,它们使用 RoutedCommand 类。有关更多信息,请参阅 MSDN 页面

为了便于理解,让我们创建一个演示应用程序。

演示应用程序概述

演示应用程序进行非常基本的计算,仅用于展示 ICommand 接口的实现。演示应用程序有两个 textbox 用于接受输入值,一个标签用于显示输出,以及四个按钮用于执行计算。四个按钮用于执行加、减、乘、除运算。在 textbox 中输入值后,单击按钮,将触发关联的命令,并在标签中显示结果。我们将如何在后续步骤中实现上述功能。请下载附带的代码示例,它将有助于理解和跟进后续的解释。

演示应用程序的最终截图如下所示

Final Screen Shot of Demo App

创建演示应用程序的 UI

现在启动 Visual Studio,并按照以下步骤创建演示应用程序

步骤 1

创建一个名为 SimpleCommandDemoApp 的 WPF 应用程序。按照下图所示的结构添加文件夹和文件。在后续步骤中,我们将编写这些文件中的代码。

Application Solution Image

第二步

首先,我们将创建演示 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 字段,分别称为 firstValuesecondValue 和 output,以及三个 public 属性,其名称与我们在 UI 中为 textbox 的 text 属性绑定时使用的名称相同。我们必须为这三个属性提供与我们在 UI 中为 textboxText 属性绑定时使用的名称相同的名称。

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”的 commandCommand 的名称必须与我们在 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 方法,该方法将调用 CalculatorViewModelAdd 方法。

Add 方法中设置断点并运行应用程序,我们将看到文本框中输入的值在 firstValuesecondValue 变量中可用。计算结果将分配给 Output 属性。但结果在标签的 Content 中未在屏幕上显示。如果我们想在 UI 上看到结果,我们必须实现 INotifyPropertyChanged 接口,以便 ViewModel 中 "Output" 属性的更改可以通知到 UI。

INotifyPropertyChanged 的必要性

INotifyPropertyChanged 的目的是在属性发生任何更改时通知所有其引用(UI 控件/ViewModel)。由于默认情况下,UI 属性(即 TextboxText 属性)大部分都实现了 INotifyPropertyChanged。现在,我们也需要为 ViewModel 实现此接口,以便当 ViewModel 端发生任何更改时,可以通知/反映到 UI。

第 10 步

正如我们在上一步中看到的,“Output”是 CalculatorViewModel 的属性名称,我们已将其绑定到 labelContent 属性。当调用 Add 方法时,我们将计算值赋给“Output”属性。由于“Output”是一个 public 属性并绑定到 UI,因此更改将反映到 label。现在借助 INotifyPropertyChangedlabelContent 将被更新。

所以,让我们在 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 方法中,我们调用了 CalculatorViewModelAdd 方法。将来,如果我们更改 Add 方法的名称,那么我们也必须修改(Execute 方法的)PlusCommand 类。

第三:由于我们无法重用 PlusCommand 类来执行其他操作,因此对于每个事件,我们需要为每个操作编写许多不同的单独命令类。在我们的例子中,在单击“Plus”按钮时,我们调用 Add 方法。但对于“Minus”按钮点击,我们需要调用 Subtract 方法,而这无法通过使用 PlusCommand 类来完成。因此,我们创建了四个 Command 类来处理每个点击事件,分别命名为 PlusCommandMinusCommandMultiplyCommandDivideCommand,这不是一个好的方法。

因此,我们创建一个通用的命令类来处理所有类型的操作。为此,我们可以使用内置的委托。

命令的通用实现

.NET Framework 中有许多内置的委托,如 ActionFuncPredicate 等。在我们的演示应用程序中,我们将使用 Action 委托。有关 Action 的更多信息,请参阅 此处。然后我们将看到如何通过提高可重用性来简化我们的工作。

第 12 步

在以上步骤中,我们创建了四个命令类来处理四个按钮点击事件。现在,通过使用 Action 委托,我们将通过创建一个名为 RelayCommand 的通用命令类来删除所有这四个命令类。我们可以使用任何名称代替 RelayCommand。作为一种好的实践,我们应该给它一个有意义的名称。将以下代码写入 RelayCommand

Code of RelayCommand Class

现在我们需要更改一行代码。注释掉第一行,然后取消注释 AddCommand 属性的 get 块中的第二行,如下所示

    public ICommand AddCommand 
        {
            get
            {
                //return plusCommand;
                return new RelayCommand(Add);
            }
        }

在此 get 块返回之前,将调用 RelayCommand 的构造函数,该构造函数接受一个方法作为 Action 委托。在这里,我们将 Add 方法的名称传递给 Action 委托参数。

RelayCommand 的构造函数处设置断点并运行应用程序。我们将看到 Add 方法的名称在 workToDo 局部变量中,如下所示

Code Execution of RelayCommand Class

现在,所有四个操作都可以仅通过使用 RelayCommand 类来处理。现在我们可以删除我们在“Commands\Specific”文件夹中创建的所有四个命令类。

结论

在本文中,我们回顾了 ICommand 接口及其用法。我们了解了如何使用 ICommand 接口创建通用的 Command 类,这是 MVVM 模式的标准实践。感谢您的阅读。非常欢迎您提出改进意见和建议。

历史

  • 2015 年 11 月 8 日:初始版本
© . All rights reserved.