在MVVM中使用DelegateCommand的异步CanExecute方法

时间:2019-09-21 13:13:25

标签: c# wpf mvvm icommand delegatecommand

我有一个简单的DelegateCommand类,如下所示:

public class DelegateCommand<T> : System.Windows.Input.ICommand where T : class
{
    public event EventHandler CanExecuteChanged;

    private readonly Predicate<T> _canExecute;
    private readonly Action<T> _execute;

    public DelegateCommand(Action<T> execute) : this(execute, null)
    {
    }

    public DelegateCommand(Action<T> execute, Predicate<T> canExecute)
    {
        this._execute = execute;
        this._canExecute = canExecute;
    }

    public bool CanExecute(object parameter)
    {
        if (this._canExecute == null)
            return true;

        return this._canExecute((T)parameter);
    }

    public void Execute(object parameter)
    {
        this._execute((T)parameter);
    }


    public void RaiseCanExecuteChanged()
    {
        this.CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }
}

我正在使用GalaSoft.MvvmLight进行验证,通常我会在View构造函数中做类似的事情:

this.MyCommand = new DelegateCommand<object>(o => {
   //Do execute stuff
}, o => 
{
   //Do CanExecute stuff
   var validateResult = this.Validator.ValidateAll();
   return validateResult.IsValid;
});

public DelegateCommand<object> MyCommand { get; }

当我进行以下简单的验证检查时,这一切都很好:

this.Validator.AddRequiredRule(() => this.SomeProperty, "You must select.......");

但是现在我需要一个验证方法来执行长时间运行的任务(在我的情况下是WebService调用),因此当我想做这样的事情时:

this.Validator.AddAsyncRule(async () =>
{
    //Long running webservice call....
    return RuleResult.Assert(true, "Some message");
});

,因此声明如下命令:

this.MyCommand = new DelegateCommand<object>(o => {
   //Do execute stuff
}, async o => 
{
   //Do CanExecute ASYNC stuff
   var validateResult = await this.Validator.ValidateAllAsync();
   return validateResult.IsValid;
});

我有点不高兴,因为标准的ICommand实现似乎无法处理异步情况。

无需过多考虑,您似乎可以重新编写DelegateCommand类以支持此类功能,但是我已经看过Prism处理此https://prismlibrary.github.io/docs/commanding.html的方式,但是似乎它们也不支持异步CanExecute方法。

那么,有没有办法解决这个问题?还是在尝试使用ICommand从CanExecute运行Async方法方面有根本的突破?

2 个答案:

答案 0 :(得分:1)

Delegatecommand满足ICommand接口。您不仅可以更改内容的签名,而且仍然可以使用。它还需要在UI线程上执行此操作,因此您无法对其进行线程化。

请记住,只要有用户交互,视图中的所有命令都可以执行。即使您可以使谓词异步,它也可能多次受到性能影响。

在这种情况下,要做的是使CanExecute快速返回布尔值。

将代码封装在其他地方,例如在Task中。异步调用该代码,然后将结果返回给布尔。然后在您的委托命令上提高canexecutechanged,以便读取该布尔值。

您可能还希望最初将bool设置为false并在Action中检查其值。这样,用户将无法重复单击绑定到该按钮的按钮。

根据正在进行的输入量以及视图中可能包含的这些内容,您可能需要考虑采取步骤,以便仅在自上次运行以来数据发生更改时才运行昂贵的过程。

由于您已经有一个可以执行的谓词,因此可以更改您的委托命令实现以添加此保护布尔并将其公开。

注意

当需要进行复杂且昂贵的验证时,经常使用另外两种方法。

1)在输入属性时对其进行验证。这样会分散验证,因此验证会在用户填写字段时发生,并且可能在他准备单击该提交按钮之前就已经完成。

2)让用户点击“提交”,但此时进行任何(更昂贵的)检查,然后报告验证失败。这样可以保证在他们点击提交时只有一张支票。

答案 1 :(得分:1)

Andy's answer很不错,应该接受。 TL; DR版本是“您不能使CanExecute异步”。

我将在这里就问题的这一部分回答更多:

  

在尝试使用ICommand从CanExecute运行Async方法时,有根本的突破吗?

是的,肯定有。

从您使用的UI框架的角度考虑这一点。当OS要求它绘制屏幕时,框架必须显示UI,并且必须立即显示它。绘制屏幕时,没有时间进行网络呼叫。该视图必须能够随时显示。 MVVM是一种模式,其中ViewModels是用户界面的逻辑表示,并且视图和VM之间的数据绑定意味着ViewModels需要立即且同步地将其数据提供给视图。因此,ViewModel属性必须是常规数据值。

CanExecute是命令的一个奇怪设计的方面。从逻辑上讲,它充当数据绑定属性(但带有参数,这就是为什么我认为将其建模为方法的原因)。当OS要求UI框架显示其窗口,并且UI框架要求其View渲染(例如)按钮时,并且View询问ViewModel该按钮是否被禁用时,必须立即并同步返回结果。

换句话说,UI本质上是同步的。您需要采用不同的UI模式,以将同步UI代码与异步活动结合起来。例如,用于异步加载数据的“正在加载...” UI,或者用于异步验证的禁用带有帮助文本的按钮直到未验证(如本问题所述),或者带有故障通知的基于队列的异步请求系统