WPF中文网

ICommand接口

ICommand是WPF命令的代码协定,也就是说,WPF中所有的命令都要继承这个接口,不管是预定义命令还是自定义命令。

一、ICommand的定义

public interface ICommand
{
    //
    // 摘要:
    //     当出现影响是否应执行该命令的更改时发生。
    event EventHandler CanExecuteChanged;

    //
    // 摘要:
    //     定义确定此命令是否可在其当前状态下执行的方法。
    //
    // 参数:
    //   parameter:
    //     此命令使用的数据。 如果此命令不需要传递数据,则该对象可以设置为 null。
    //
    // 返回结果:
    //     如果可执行此命令,则为 true;否则为 false。
    bool CanExecute(object parameter);
    //
    // 摘要:
    //     定义在调用此命令时要调用的方法。
    //
    // 参数:
    //   parameter:
    //     此命令使用的数据。 如果此命令不需要传递数据,则该对象可以设置为 null。
    void Execute(object parameter);

}

这个接口比较关键的是CanExecute和Execute两个方法成员。前者表示当前命令是否可以执行,如果可以的话,WPF命令系统会自动帮我们去调用Execute方法成员。那么,我们要实现这个接口的话,通常只需要在CanExecute编写一些判断逻辑,在Execute调用一个委托就行了。至于这个委托的的签名和具体的代码内容,则是在实际应用时由开发者去编写不同的业务代码。接下来,我们来实现一个ICommand接口。

二、ICommand的实现

public class RelayCommand : ICommand
{
    public event EventHandler CanExecuteChanged;
    
    private Action action;

    public RelayCommand(Action action)
    {
        this.action = action;
    }

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public void Execute(object parameter)
    {
        action?.Invoke();
    }
}

在上面的例子中,我们自定义了一个叫RelayCommand的Command类,非常重要的一点是,它的构造函数要求传入一个Action,这个委托传进来后,将来在Execute成员中被执行。

接下来,我们看看它的具体使用。

public class MainViewModel : ObservableObject
{
    public RelayCommand OpenCommand { get; set; } = new RelayCommand(() =>
    {
        MessageBox.Show("Hello,Command");
    });
}

我们在MainViewModel中编写了一个叫OpenCommand的属性,而属性的类型就是RelayCommand,在构造这个RelayCommand时,我们传入了一个匿名函数。如此,前端就可以绑定这个OpenCommand命令了。

前端代码

<Window x:Class="HelloWorld.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"
        xmlns:local="clr-namespace:HelloWorld" 
        xmlns:forms="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
        mc:Ignorable="d" FontSize="14"
        Title="WPF中文网 - 命令 - www.wpfsoft.com" Height="350" Width="500">
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <Window.Resources>
        
    </Window.Resources>
    <Grid>
        <Button Width="100" Height="30" Content="打开" Command="{Binding OpenCommand}" />
    </Grid>
</Window>

单击按钮,弹出对话框,说明OpenCommand所指向的匿名函数代码块被执行。这便是ICommand接口最简单的自定义实现之一,如果我们需要在单击按钮时传入一些参数该怎么办呢?这就需要稍微改一下RelayCommand的代码了。

三、ICommand带参数的实现

public class RelayCommand : ICommand
{
    public event EventHandler CanExecuteChanged;
    
    private Action action;
    private Action<object> objectAction;
    public RelayCommand(Action action)
    {
        this.action = action;
    }

    public RelayCommand(Action<object> objectAction)
    {
        this.objectAction = objectAction;
    }
    public bool CanExecute(object parameter)
    {
        return true;
    }

    public void Execute(object parameter)
    {
        action?.Invoke();
        objectAction?.Invoke(parameter);
    }
}

在这里,我们增加了一个带Action<object>参数的构造函数,将来定义命令时,就可以将一个带有object参数的方法传到这个RelayCommand中来。

MainViewModel代码如下

public class MainViewModel : ObservableObject
{
    public RelayCommand OpenCommand { get; set; } = new RelayCommand(() =>
    {
        MessageBox.Show("Hello,Command");
    });

    public RelayCommand OpenParamCommand { get; set; } = new RelayCommand((param) =>
    {
        MessageBox.Show(param.ToString());
    });
    
}

在MainViewModel中,我们增加了一个OpenParamCommand命令,并传入了一个带参数的匿名函数。最后看看前端代码如何调用。

<StackPanel VerticalAlignment="Center">
    <Button Width="100" Height="30" Content="打开" Command="{Binding OpenCommand}" />
    <Button Width="100" Height="30" 
            Content="打开" 
            Command="{Binding OpenParamCommand}" 
            CommandParameter="{Binding RelativeSource={RelativeSource Mode=Self}}"/>
</StackPanel>

由此可见,我们利用CommandParameter属性将前端的button对象传递到了命令的回调函数中。虽然RelayCommand已经具备了参数的传递,但是,我们还有更优雅的方式去实现它。

四、ICommand的泛型参数实现

public class RelayCommand<T> : ICommand
{
    public event EventHandler CanExecuteChanged;
    public Action<T> Action { get; }
    public RelayCommand(Action<T> action)
    {
        Action = action;
    }
    public bool CanExecute(object parameter)
    {
        return true;
    }

    public void Execute(object parameter)
    {
        Action?.Invoke((T)parameter);
    }
}

我们利用泛型来简化了RelayCommand的定义,而在定义泛型RelayCommand和使用时,与前面带object参数的RelayCommand的用法是差不多的。

public RelayCommand<object> OpenTParamCommand { get; set; } = new RelayCommand<object>((t) =>
{
    MessageBox.Show(t.ToString());
});

当前课程源码下载:(注明:本站所有源代码请按标题搜索)

文件名:071-《ICommand接口》-源代码
链接:https://pan.baidu.com/s/1yu-q4tUtl0poLVgmcMfgBA
提取码:wpff

官方的说法是:命令是WPF 中的一种输入机制,与设备输入相比,它提供的输入处理更侧重于语义级别。我觉得这样的解释有点抽象,在实际的使用过程中我们会发现,传统的事件驱动模式下,比如一个按钮的Click单击事件,我们会给它订阅一个回调函数,以便触发该事件时,去执行这个回调函数的业务逻辑代码。

Button button = new Button();
button.Click += (s, e) =>
{
    MessageBox.Show("HelloWorld");
};

或者在XAML前端实例化一个Button,然后订阅Click事件。

//XAML代码
<Grid>
    <Button Content="确定" Click="Button_Click"/>
</Grid>

//C#代码
private void Button_Click(object sender, RoutedEventArgs e)
{
    MessageBox.Show("HelloWorld");
}

事件驱动模式有一个不太好的地方是,它的前端XAML代码和后端的C#代码建立了一种强关联的耦合关系,无法体现WPF的MVVM开发模式的优势, 于是,WPF提供了一个ICommand接口,以及一个强大的命令系统,将控件的各种事件都能转换成一个命令。这个命令依然像绑定依赖属性一样,将命令定义成一个ViewModel中的命令属性,而ViewModel又被赋值到控件或窗体的DataContext数据上下文中,于是,窗体内的所有控件的事件都可以绑定这个命令,只要控件的事件被触发,命令将会被执行。

WPF的命令是通过ICommand接口创建的,并且微软已经为我们预定义了一部分命令,这些我们都称为预定义命令,它们的类型是RoutedUICommand或RoutedCommand。RoutedUICommand继承于RoutedCommand,而RoutedCommand继承于ICommand。

事实上,在WPF应用开发过程中,光靠这些预定义命令是不足以满足我们的开发需求的,所以我们肯定要另写一些命令类型,当然,自定义命令必须继承于ICommand。

厘清这一点之后,我们就明白了接下来要学习的几个方面:

  • 第一点:命令的4个主要概念,它解决了我们如何正确运用命令知识。
  • 第二点:预定义命令的熟悉与使用。
  • 第三点:自定义命令的定义与应用。
  • 第四点:常见框架(例如Prism,mvvmlight,ReactiveUI)中的命令应用。
  • 第五点:控件的普通事件转Command命令

第一点,WPF 命令中的四个主要概念

WPF 中的路由命令模型可分解为四个主要概念:命令、命令源、命令目标和命令绑定:

  • 命令是要执行的操作。
  • 命令源是调用命令的对象。
  • 命令目标是在其上执行命令的对象。
  • 命令绑定是将命令逻辑映射到命令的对象。

如上所述,命令其实就是ICommand接口的实现,命令源是调用命令的对象,这些对象一定会继承ICommandSource接口,而命令绑定就像是一座桥梁,它将命令与逻辑代码建立一种映射,这座桥梁就是CommandBinding。最后,命令如何与命令源建立绑定?当然就是Binding对象了。

较形象的总结一下命令的路线:ICommandSource命令源(或控件的事件)->绑定一个ICommand命令->CommandBinding桥梁->真正的业务逻辑代码块。

下一节,我们以这个路线先介绍命令源。

——重庆教主 2023年10月11日

copyright @重庆教主 WPF中文网 联系站长:(QQ)23611316 (微信)movieclip (QQ群).NET小白课堂:864486030 | 本文由WPF中文网原创发布,谢绝转载 渝ICP备2023009518号-1