DevExpress MVVM Framework. Asynchronous Commands.

OTHER RELATED ARTICLES:

  1. Getting Started with DevExpress MVVM Framework. Commands and View Models.
  2. DevExpress MVVM Framework. Introduction to Services, DXMessageBoxService and DialogService.
  3. DevExpress MVVM Framework. Interaction of ViewModels. IDocumentManagerService.
  4. DevExpress MVVM Framework. Introduction to POCO ViewModels.
  5. DevExpress MVVM Framework. Interaction of ViewModels. Messenger.
  6. DevExpress MVVM Framework. Using Scaffolding Wizards for building Views.
  7. DevExpress MVVM Framework. Data validation. Implementing IDataErrorInfo.
  8. DevExpress MVVM Framework. Using DataAnnotation attributes and DevExpress Fluent API.
  9. DevExpress MVVM Framework. Behaviors.
  10. DevExpress MVVM Framework. TaskbarButtonService, ApplicationJumpListService and NotificationService.
  11. THIS POST: DevExpress MVVM Framework. Asynchronous Commands.
  12. DevExpress MVVM Framework. Converters.


In one of my previous posts, we reviewed two ICommand implementations: DelegateCommand and DelegateCommand<T>. Today, we’ll examine two more implementations: AsyncCommand and AsyncCommand<T>.

These commands may be useful when running time-consuming operations on a separate thread without freezing the UI. For instance, if you need to execute a calculation, you can bind a button to an AsyncCommand. When an end-user clicks the button, the command starts the calculation task and the button remains disabled. When the task finishes, the button is re-enabled.

Creating AsyncCommands

The AsyncCommand and AsyncCommand<T> are created similarly to the regular DelegateCommands. There are two constructors: one constructor accepts an Execute delegate parameter; the second constructor accepts Execute and CanExecute delegate parameters. The Execute delegate should return a Task object. Both pairs of asynchronous command constructors support the UseCommandManager optional parameter (set to True by default) that determines whether the CommandManager is used to raise the CanExecuteChanged event.

AsyncCommand<string> asyncCommand = new AsyncCommand<string>(Calculate, CanCalculate, true);
Task Calculate(string parameter) {
    return Task.Factory.StartNew(CalculateCore);
}
bool CanCalculate(string parameter) {
    //...
}
void CalculateCore() {
    //...
}

Running AsyncCommands

You can bind AsyncCommands in the same manner as ICommands.

<Button Content="..." Command="{Binding AsyncCommand}"/>
<Button Content="..." Command="{Binding AsyncCommand}" CommandParameter="..."/>

Preventing simultaneous execution

The AsyncCommand and AsyncCommand<T> classes provide an IsExecuting property. While the command task is executing, IsExecuting is True and the AsyncCommand.CanExecute method always returns False regardless of the contents of your CanExecute delegate. This feature disables controls bound to the command until the pending command task is finished.

You can explicitly disable this behavior by setting the AsyncCommand.AllowMultipleExecution property to True. In which case, the AsyncCommand.CanExecute method returns a value based on your provided CanExecute delegate.

Canceling command executions

If necessary, you can implement a mechanism to cancel a running command. The asynchronous commands have an IsCancellationRequested property you can check from the task Action.

AsyncCommand = new AsyncCommand(Calculate);
Task Calculate() {
    return Task.Factory.StartNew(CalculateCore);
}
void CalculateCore() {
    for(int i = 0; i <= 100; i++) {
        if(AsyncCommand.IsCancellationRequested) return;
        Progress = i;
        Thread.Sleep(TimeSpan.FromSeconds(0.1));
    }
}

In this case, you can cancel command execution with AsyncCommand.CancelCommand. This command sets the AsyncCommand.IsCancellationRequested property to True, so you can stop calculation in the Execute delegate.

<StackPanel Orientation="Vertical">
    <ProgressBar Minimum="0" Maximum="100" Value="{Binding Progress}" Height="20"/>
    <Button Content="Calculate" Command="{Binding AsyncCommand}"/>
    <Button Content="Cancel" Command="{Binding AsyncCommand.CancelCommand}"/>
</StackPanel>

You can use the AsyncCommand.CancellationTokenSource property if you need more control over the cancellation process. For instance:

AsyncCommand = new AsyncCommand(Calculate);
Task Calculate() {
    return Task.Factory.StartNew(CalculateCore, AsyncCommand.CancellationTokenSource.Token).
        ContinueWith(x => MessageBoxService.Show(x.IsCanceled.ToString()));
}
void CalculateCore() {
    for(int i = 0; i <= 100; i++) {
        AsyncCommand.CancellationTokenSource.Token.ThrowIfCancellationRequested();
        Progress = i;
        Thread.Sleep(TimeSpan.FromSeconds(0.1));
    }
}

AsyncCommands and IDispatcherService

Sometimes, it’s necessary to report progress on the task to the main thread. This can be done by using the IDispatcherService (see Introduction to Services for more details).

IDispatcherService DispatcherService { get { ... } }
void CalculateCore() {
    ...
    DispatcherService.BeginInvoke(() => {
        ...
    });
    ...
}

AsyncCommands in POCO

POCO provides the capability to automatically create AsyncCommands based on public functions which return a Task object (the function also needs to be defined parameterless or with a single parameter). To access the IsExecuting and IsCancellationRequested command properties, you can use the DevExpress.Mvvm.POCO.POCOViewModelExtensions class that provides the GetAsyncCommand method. Below is an example of a POCO ViewModel that provides the CalculateCommand AsyncCommand.

[POCOViewModel]
public class ViewModel {
    public virtual int Progress { get; set; }
    public Task Calculate() {
        return Task.Factory.StartNew(CalculateCore);
    }
    void CalculateCore() {
        for(int i = 0; i <= 100; i++) {
            if(this.GetAsyncCommand(x => x.Calculate()).IsCancellationRequested) return;
            Progress = i;
            Thread.Sleep(TimeSpan.FromSeconds(0.1));
        }
    }
}

 

8 comment(s)
Neven Zovko

Alexander, thanks for posting this. Very cool indeed!

Two things:

1. What exactly is the purpose of the POCOViewModelAttribute?

I have tried your sample code with and without this attribute, but couldn't observe any differences. In both cases the view model worked as expected.

2. I've rewritten your sample code using async/await. The code below works as expected, just curious if there is anything else I should have taken care of when working with async/await.

public class ViewModel

{

   public virtual int Progress { get; set; }

   public async Task Calculate() {

       var calculateAsyncCommand = this.GetAsyncCommand(x => x.Calculate());

       for (var i = 0; i <= 100; i++) {

           if (calculateAsyncCommand.IsCancellationRequested) {

               return;

           }

           this.Progress = i;

           // await Task.Delay(TimeSpan.FromSeconds(0.1));

           await Task.Delay(TimeSpan.FromSeconds(0.1)).ConfigureAwait(false);

       }

   }

}

15 August, 2014
Alexander (DevExpress Support)

Hi Neven,

Thank you for your feedback.

Indeed, the POCOViewModel attribute is not needed. In my sample, I only used it to emphasize that ViewModel is a POCO class that should be created with the ViewModelSource helper. Nonetheless, this attribute is needed in case you wish to implement the IDataErrorInfo interface with POCO. It was described in one of our previous posts: community.devexpress.com/.../devexpress-mvvm-framework-data-validation-implementing-idataerrorinfo.aspx

As for the async/await feature, there are no specifics of using it. This approach just allows you to await the Calculation task when you call it from code.

18 August, 2014
Neven Zovko

Great, thx!

18 August, 2014
Noufal Aboobacker 1

How AsyncCommand implementation can be tested?

If we try to call Execute then it will immediately return to the caller as the command is executing asynchronously.

1 January, 2018
Alexander (DevExpress Support)

Hi Noufal,

Your calculation function returns a Task object, which provides the Wait method. You can call it in your test.

If you need further assistance, please contact our support center (www.devexpress.com/.../Create).

4 January, 2018
Edgar Duee

I find the example code above to be just too terse and confusing. It is extremely confusing, in a tutorial, to name a variable (of type AsyncCommand) "AsyncCommand", as seems to be happening above. This is so obviously the one name that should NOT have been used!! This is like teaching a novice how to write code by declaring a variable as: int Int;

The line:

AsyncCommand.CancellationTokenSource.Token.ThrowIfCancellationRequested();

makes no sense until you realise that AsyncCommand is the variable name (as well as the type).

Also, I can find no reference to IDispatcherService in the link given.

It is also unclear how (or if, or when) this relates to the AsyncCommandAttribute.

The example here:

www.devexpress.com/.../mvvm-poco-async-commands-samples

is disappointing - almost content-free and the attribute is commented out!

A real-life example (not using the POCO form) would be much appreciated. In my situation, I want to call the method:

public async Task<Blah> GetBlah(string s, CancellationToken t)

which gets data via a Web API call.

21 May, 2018
Alexander (DevExpress Support)

Hi Edgar,

I agree with you. We will fix this issue in our documentation:

documentation.devexpress.com/.../Asynchronous-Commands

If you need any assistance with AsyncCommands, please contact our support team:

www.devexpress.com/.../Create

21 May, 2018
Ernie Salazar

This is a very nice implementation of an async ICommand.  But, as mentioned, the documentation is lacking.  Not just online but in the code itself - there is no XML documentation.

For example, I was having trouble with unit tests and using an AutoResetEvent and knowing when the command was finished.  But it turns out the command has a .Wait(timeout) which solves the problem.  Thats great but the only way I found and understood it was decompiling the source code.  The link about does not mention it.  A simple xml tag would go a long way.

20 June, 2018

Please login or register to post comments.