目录

WPF-ICommand(RelayCommand)

在 WPF 中,MVVM(Model-View-ViewModel)模式的核心之一就是 命令,而 ICommand 接口就是实现这个核心功能的基础。为了让命令与 UI 的交互变得简洁而又灵活,RelayCommand(或者叫做 DelegateCommand)成为了一个非常常见的封装类,它是 ICommand 接口的具体实现。

一、ICommand

1.ICommand 的定义

在WPF中,ICommand是命令(Command)的接口,它负责封装业务逻辑的执行,并于UI控件进行解耦。

public interface ICommand
{
    event EventHandler CanExecuteChanged;
    bool CanExecute(object parameter)
    void Execute(object parameter)
}

解释:

CanExecute:此方法用于判断命令是否可以执行。通常,这个方法返回一个布尔值,如果返回 true,命令按钮或其它触发元素将是可用的;如果返回 false,它将被禁用。

Execute:命令的实际执行方法。当用户点击按钮、按下快捷键或其它触发动作时,将调用此方法执行相应的操作。

CanExecuteChanged:当命令的可执行状态发生变化时(例如,某个条件满足/不满足),就会触发这个事件,通知UI更新按钮或其它控件的启用状态。

2.ICommand的工作原理

ICommand的工作流程通常如下:

  • UI控件(如按钮、菜单项等)会将自己的 Command 属性绑定到一个实现了 ICommand 接口的对象。
  • 用户交互(如点击按钮)会触发 Execute 方法的调用,而是否能够执行命令由 CanExecute 方法的返回值决定。
  • 如果命令的执行条件发生变化,调用 CanExecuteChanged 事件通知 UI 控件重新评估命令是否可执行,从而更新控件的状态。

二、RelayCommand

RelayCommand 是对 ICommand 接口的一个常见封装,通过委托(Action/Func)解耦UI与业务逻辑,避免在ViewModel中直接依赖具体命令类。

标准RelayCommand

适用同步、无参数命令

public class RelayCommand : ICommand
{
    private readonly Action<object> _execute;
    private readonly Func<object, bool> _canExecute;

    

    // 构造函数接受执行命令和判断是否能执行命令的委托
    public RelayCommand(Action<object> execute, Func<object, bool> canExecute=null)
    {
        _execute = execute ?? throw new ArgumentNullException(nameof(execute));
        _canExecute = canExecute;
    }

    // 判断命令是否能执行
    public bool CanExecute(object parameter)
    {
        // 如果没有提供 CanExecute 委托,则默认总是返回 true。
        return _canExecute?.Invoke(parameter) ?? true;
    }

    // 执行命令
    public void Execute(object parameter)
    {
        // 执行传入的委托
        _execute(parameter);
    }
    
    // 在高频状态变化(例如每秒几十次)或想完全掌控命令刷新机制时可以不使用CommandManeger,不使用时仅需删除花括号及其内的内容。
    public event EventHandler?  CanExecuteChanged
    {
        add => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -= value;
    }

    // 触发 CanExecuteChanged 事件
    public void RaiseCanExecuteChanged()
    {
        CommandManager.InvalidateRequerySuggested();
    }
}

Action _execute:执行命令的委托。在按钮点击时,会调用这个委托来执行实际的操作。

Func<object, bool> _canExecute:判断命令是否可以执行的委托。在按钮是否可点击时,会调用这个委托来检查是否满足执行条件。

CanExecuteChanged:ICommand的事件,用于通知UI控件命令的可执行状态变化。通常,在某些条件改变时会手法触发这个事件。

三、RelayCommand 和 ICommand 在 MVVM 中的作用

1.MVVM 模式下的命令职责

在 MVVM 中,命令(ICommand)主要有两个职责:

  • 封装视图模型中的逻辑:UI 交互(如按钮点击)会触发命令,命令会调用视图模型中的方法处理业务逻辑。
  • 与 UI 控件解耦:命令的使用使得视图层和逻辑层解耦,视图只关心命令的触发,而无需了解命令的具体实现。

2..RelayCommand 如何在 MVVM 中应用

在 MVVM 模式中,我们将 RelayCommand 通常用于视图模型(ViewModel)中,来响应用户的交互事件。

示例:

public class MyViewModel : INotifyPropertyChanged
{
    private string _inputText;

    public string InputText
    {
        get => _inputText;
        set
        {
            _inputText = value;
            OnPropertyChanged();
            ((RelayCommand)SubmitCommand).RaiseCanExecuteChanged();  // 手动触发 CanExecuteChanged 事件
        }
    }

    public ICommand SubmitCommand { get; }

    public MyViewModel()
    {
        // 创建 RelayCommand,并传入执行和判断是否可以执行的逻辑
        SubmitCommand = new RelayCommand(OnSubmit, CanSubmit);
    }

    private void OnSubmit(object parameter)
    {
        // 提交操作
        MessageBox.Show($"Submitted: {InputText}");
    }

    private bool CanSubmit(object parameter)
    {
        // 如果 InputText 为空则返回 false
        return !string.IsNullOrEmpty(InputText);
    }

    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

3.绑定到 UI 控件

<Button Content="Submit" Command="{Binding SubmitCommand}" />
<TextBox Text="{Binding InputText}" />

解释:

  • SubmitCommand:视图模型中暴露的命令属性,它是 RelayCommand 的实例。
  • OnSubmit:当按钮被点击时,会调用 OnSubmit 方法。
  • CanSubmit:当按钮的可用性需要更新时,CanSubmit 方法会决定按钮是否启用
  • 命令绑定:在 Button 上,我们将 Command 属性绑定到 SubmitCommand(类型为 RelayCommand)上。
  • CanExecute:当 SubmitCommand 被绑定到 Button 上时,CanExecute 会自动触发,来判断按钮是否可点击。CanExecute 的返回值由 CanSubmit 方法决定(根据 InputText 是否为空来判断)。
  • Execute:当用户点击按钮时,Execute 方法会自动被调用,触发 OnSubmit 方法,执行提交逻辑。

手动触发 CanExecuteChanged:

在 OnPropertyChanged 方法中,调用了 RaiseCanExecuteChanged(),它会触发 CanExecuteChanged 事件。这是必要的,因为 RelayCommand 需要知道何时重新评估 CanExecute,通常这是由于绑定的属性(如 InputText)发生变化时触发的。如果你不主动触发 CanExecuteChanged,按钮的启用状态就不会自动更新。

RelayCommand 的 CanExecute 和 Execute 工作流程:

  • CanExecute:每当命令绑定的上下文发生变化时(例如,InputText 改变),WPF 会检查 CanExecute 是否返回 true,并据此启用或禁用按钮。CanExecute 并不会自动被调用,你必须手动触发 RaiseCanExecuteChanged() 来更新它的状态。
  • Execute:当用户点击按钮时,Execute 会触发,执行 OnSubmit 方法。

为什么不显式调用 CanExecute 和 Execute?

  • WPF 命令机制:在 MVVM 中,ICommand 实现会与 UI 控件(如按钮)绑定,而 UI 控件会负责调用 CanExecute 和 Execute。你不需要手动调用这些方法,绑定和 UI 的交互会自动完成。
  • 数据绑定的工作机制:ICommand 的一个重要特性是它与数据绑定结合。当控件触发命令时,UI 控件本身会自动触发 CanExecute 和 Execute,无需你显式地调用它们。

CanExecuteChanged 事件的自动管理:

  • CanExecuteChanged 事件的作用是通知 UI 控件命令的可执行状态已更改,控件需要更新自身的启用/禁用状态。
  • 你不需要显式地订阅 CanExecuteChanged 事件,WPF 会自动处理这个事件的订阅和触发。当命令的可执行状态(如绑定属性的变化)发生变化时,框架会自动通知 UI 控件更新其状态。

RaiseCanExecuteChanged 的作用:

  • 虽然你不需要手动订阅 CanExecuteChanged 事件,但你仍然需要手动触发 RaiseCanExecuteChanged 事件,以便通知 UI 控件更新 CanExecute 的执行状态。
  • 这通常发生在命令的执行条件(比如 InputText)变化时。在这种情况下,RaiseCanExecuteChanged 会手动触发 CanExecuteChanged 事件。
  • 举例来说,当 InputText 的值改变时,RaiseCanExecuteChanged 会通知 UI 更新 CanExecute 状态,从而更新按钮的启用/禁用状态。

四、AsyncRelayCommand

在一些场景中,需要执行异步操作,比如发起网络请求或数据库查询。为了支持这种场景,可以通过扩展 RelayCommand 来实现异步命令。

public class AsyncRelayCommand : ICommand
{
    private readonly Func<object?, Task> _execute;
    private readonly Func<object, bool> _canExecute;
    // 亦或使用 Predicate<object?>? _canExecute;

    private bool _isExecuting;

    public AsyncRelayCommand(Func<object, Task> execute, Func<object, bool> canExecute = null)
    {
        _execute = execute ?? throw new ArgumentNullException(nameof(execute));
        _canExecute = canExecute;
    }

    public bool CanExecute(object? parameter) => 
        !_isExecuting && (_canExecute?.Invoke(parameter) ?? true);

    public async void Execute(object? parameter)
    {
        if (!CanExecute(parameter))
        {
            return;
        }
        
        try
        {
            _isExecuting = true;
            CommandManager.InvalidateRequerySuggested();
            
            await _execute(parameter);
        }
        catch (Exception ex)
        {
            // 此处可替换为日志系统
            MessageBox.Show(ex.Message);
        }
        finally
        {
            _isExecuting = false;
            CommandManager.InvalidateRequerySuggested();
        }
    }
    
    public event EventHandler CanExecuteChanged
    {
        add => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -=value;
    }
}

解释:

  • 异步执行:Execute 方法现在接受一个 Task,因此可以执行异步操作。
  • UI 不会被阻塞:异步操作不会阻塞 UI 线程,可以让 UI 保持响应

五、RelayCommand

订阅全部委托给了 CommandManager.RequerySuggested

using System;
using System.Windows.Input;

public class RelayCommand<T> : ICommand
{
    private readonly Action<T?> _execute;
    private readonly Func<T?, bool> _canExecute;
    // 亦或使用 Predicate<object?>? _canExecute;

    // 构造函数,接收命令执行和可执行条件的逻辑
    public RelayCommand(Action<T> execute, Func<T, bool> canExecute = null)
    {
        _execute = execute ?? throw new ArgumentNullException(nameof(execute));
        _canExecute = canExecute;
    }

    // ICommand 接口的 CanExecute 方法
    public bool CanExecute(object? parameter)
    {
        if (parameter is T t)
        {
            return _canExecute?.Invoke(t) ?? true;
        }
        
        if (parameter == null && default(T) == null)
        {
            return _canExecute?.Invoke(default) ?? true;
        }
        
        return false;
    }

    // ICommand 接口的 Execute 方法
    public void Execute(object parameter)
    {
        if (parameter is T t)
        {
            _execute(t);
        }
        else if (parameter == null && default(T) == null)
        {
            _execute(default);
        }
    }

    // CanExecuteChanged 事件
    public event EventHandler? CanExecuteChanged
    {
        add => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -= value;
    }

    // 通知命令是否可以执行
    public void RaiseCanExecuteChanged()
    {
        CommandManager.InvalidateRequerySuggested();
    }
}
  1. Action _execute:执行命令时调用的逻辑。
  2. Func<T, bool> _canExecute:决定命令是否可执行的逻辑。如果没有提供,则默认返回 true,表示命令总是可执行的。
  3. CanExecuteChanged:用于通知命令绑定的状态是否发生变化,通常与 CommandManager.RequerySuggested 结合,自动管理命令是否可执行。
  4. RaiseCanExecuteChanged:这个方法允许你手动触发 CanExecuteChanged 事件,从而更新命令的可执行状态。

自管理订阅列表:

public class RelayCommand : ICommand
{
    private readonly Action<object> _execute;
    private readonly Func<object, bool> _canExecute;

    public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
    {
        _execute = execute ?? throw new ArgumentNullException(nameof(execute));
        _canExecute = canExecute;
    }

    public bool CanExecute(object parameter)
    {
        return _canExecute?.Invoke(parameter) ?? true;
    }

    public void Execute(object parameter)
    {
        _execute(parameter);
    }

    // 自己维护的后备委托字段
    private EventHandler _canExecuteChanged;

    public event EventHandler CanExecuteChanged
    {
        add
        {
            _canExecuteChanged += value;
            CommandManager.RequerySuggested += value;
        }
        remove
        {
            _canExecuteChanged -= value;
            CommandManager.RequerySuggested -= value;
        }
    }

    // 只触发本命令的订阅者,不影响其他命令
    public void RaiseCanExecuteChanged()
    {
        _canExecuteChanged?.Invoke(this, EventArgs.Empty);
    }
}

六、ICommand 的最佳实践

1.使用 RelayCommand 来实现 MVVM

RelayCommand 可以帮助你简化命令的实现,不需要手动编写 ICommand 的实现,只需传递委托。

2.避免直接在 ViewModel 中编写逻辑

尽量避免在 ViewModel 中写大量的 UI 控件交互代码,保持逻辑与 UI 分离。

3.适当使用 CanExecute:

在需要时启用或禁用按钮时,可以使用 CanExecute 来动态判断命令是否可用。

4.支持异步操作

使用异步命令(如 AsyncRelayCommand)来支持 UI 交互中的异步操作。

七、总结

  • ICommand 是命令的核心接口,负责封装业务逻辑并与 UI 控件交互。
  • RelayCommand 是 ICommand 的一个常见实现,它将命令的执行和可执行性逻辑委托给外部的委托,简化了命令的实现。
  • 在 MVVM 模式 中,RelayCommand 能帮助我们把 UI 控件的行为与视图模型中的逻辑解耦,从而保持代码的清晰和可维护性。