Maxim Gershkovich
Maxim Gershkovich

Reputation: 47199

Async CanExecute method using DelegateCommand in MVVM

I have a simple DelegateCommand class that looks like this:

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);
    }
}

I am using GalaSoft.MvvmLight for validation and normally I would just do something like this in the View constructor:

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 all works great when I have a simple validation check like:

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

but now I need a validation method that executes a long running task (in my case a WebService call) so when I want to do somthing like this:

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

and therefore declare the command like this:

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

I'm in a bit of a pickle because the standard ICommand implementation doesn't appear to deal with async scenarios.

Without too much thought it seems that you could potentially re-write the DelegateCommand class to support such functionality but I have looked at the way that Prism deals with this https://prismlibrary.github.io/docs/commanding.html, However it seems that they also DO NOT support async CanExecute methods.

So, is there a way around this problem? Or is there something fundamentally broken in trying to run an Async method from CanExecute using ICommand?

Upvotes: 1

Views: 1508

Answers (2)

Stephen Cleary
Stephen Cleary

Reputation: 457302

Andy's answer is great and should be accepted. The TL;DR version is "you can't make CanExecute asynchronous".

I'm just going to answer more on this part of the question here:

is there something fundamentally broken in trying to run an Async method from CanExecute using ICommand?

Yes, there definitely is.

Consider this from the perspective of the UI framework you're using. When the OS asks it to paint the screen, the framework has to display a UI, and it has to display it now. There's no time for network calls when painting the screen. The view must be able to be displayed immediately at any time. MVVM is a pattern where the ViewModels are a logical representation of the user interface, and the data binding between views and VMs means that ViewModels need to provide their data to the views immediately and synchronously. Therefore, ViewModel properties need to be regular data values.

CanExecute is a weirdly-designed aspect of commands. Logically, it acts as a data-bound property (but with an argument, which is why I think it was modeled as a method). When the OS asks the UI framework to display its window, and the UI framework asks its View to render (e.g.) a button, and the View asks the ViewModel whether the button is disabled, the result must be returned immediately and synchronously.

To put it another way: UIs are inherently synchronous. You'll need to adopt different UI patterns to marry the synchronous UI code with asynchronous activities. E.g., "Loading..." UIs for asynchronously loading data, or disable-buttons-with-help-text-until-validated for asynchronous validation (as in this question), or a queue-based system of asynchronous requests with failure notifications.

Upvotes: 3

Andy
Andy

Reputation: 12276

Delegatecommand is satisfying the ICommand interface. You can't just change the signature of stuff and it'll still work. It also needs to do it's thing on the UI thread so you can't thread it.

Bear in mind that all commands in a view will have canexecute checked whenever there is a user interaction. Even if you could make the predicate async then it could well be hit numerous times with performance implications.

The thing to do in this case is to make CanExecute quickly return the value of a bool.

Encapsulate your code elsewhere, say in a Task. Call that code asynchronously and return the result to the bool. Then raise canexecutechanged on your delegatecommand so the value of that bool is read.

You probably also want to set that bool false initially and check it's value inside your Action. That way the user can't click a button bound to it repeatedly.

Depending on the amount of input going on and how many of these things you might have in a view you might want to consider taking steps so the expensive process is only run whilst the data has changed since last run.

Since you already have a canexecute predicate you could alter your delegatecommand implementation to add this guard bool and make it public.

Note

When complicated expensive validation is necessary there are two other approaches often used.

1) Validate properties as they're entered. This spreads out the validation so it happens as the user fills fields and is probably done before he's ready to click that submit button.

2) Let the user hit submit but do any (further expensive) checks at that time and report validation failures then. This guarantees there's only the one check when they hit submit.

Upvotes: 1

Related Questions