Back to Devexpress

Asynchronous Commands

wpf-17354-mvvm-framework-commands-asynchronous-commands.md

latest13.2 KB
Original Source

Asynchronous Commands

  • Aug 05, 2022
  • 7 minutes to read

Asynchronous commands can be useful if you need to run a time-consuming operation in a separate thread without freezing the UI. For instance, if you need to calculate something, you can bind a button to an asynchronous command. When a user clicks the button, the command starts the calculation process and the button becomes disabled. When the process is done, the button is enabled.

The following asynchronous commands are available.

  • AsyncCommand<T> - Specifies a command whose Execute and CanExecute delegates accept a single parameter of type T.
  • AsyncCommand - Specifies a command whose Execute and CanExecute delegates do not have any parameters.

Creating AsyncCommands

The AsyncCommand and AsyncCommand<T> can be created similarly to the regular Delegate Commands. There are two constructors: a constructor that accepts the Execute delegate, and a second constructor that accepts Execute and CanExecute delegates. The Execute delegate should return a Task object.

The AsyncCommand constructors support the optional useCommandManager parameter (the default setting is True ) that specifies whether the CommandManager is used to raise the CanExecuteChanged event.

csharp
public MyViewModel() {
    MyAsyncCommand = new AsyncCommand<string>(Calculate, CanCalculate, true);
}

public AsyncCommand<string> MyAsyncCommand { get; private set; }
Task Calculate(string parameter) {
    return Task.Factory.StartNew(CalculateCore);
}
bool CanCalculate(string parameter) {
    //...
}
void CalculateCore() {
    //...
}
vb
Public Sub New()
    MyAsyncCommand = New AsyncCommand(Of String)(Calculate, CanCalculate, True)
End Sub

Public Property MyAsyncCommand As AsyncCommand(Of String)
Function Calculate(parameter As String) As Task
    Return Task.Factory.StartNew(AddressOf CalculateCore)
End Function
Function CanCalculate(parameter As String) As Boolean
    '...
End Function
Sub CalculateCore()
    '...
End Sub

POCO ViewModels and ViewModelBase descendants can automatically generate asynchronous commands for methods marked with the async keyword.

csharp
[AsyncCommand(UseCommandManager = false)]
public async Task Calculate() {
    for(int i = 0; i <= 100; i++) {
        Progress = i;
        await Task.Delay(20);
    }
}
vb
<AsyncCommand(UseCommandManager:=False)>
Public Async Function Calculate() As Task
    For i As Integer = 0 To 100
        Progress = i
        Await Task.Delay(20)
    Next
End Function

You can reference your asynchronous method when invalidating an auto-generated asynchronous command :

csharp
// For ViewModelBase descendants:
RaiseCanExecuteChanged(() => Calculate())

// For POCO ViewModels:
this.RaiseCanExecuteChanged(x => x.Calculate());
vb
' For ViewModelBase descendants:
RaiseCanExecuteChanged(Function() Calculate())

' For POCO ViewModels:
Me.RaiseCanExecuteChanged(Function(x) x.Calculate())

When a command with a parameter is used, you can specify any value. It will not affect the result:

csharp
[AsyncCommand(UseCommandManager = false)]
public async Task Load(int i) {
    await Task.Delay(1000);
}
//...
this.RaiseCanExecuteChanged(x => x.Load(int.MinValue));
vb
<AsyncCommand(UseCommandManager:=False)>
Public Async Function Load(ByVal i As Integer) As Task
    Await Task.Delay(1000)
End Function
' ...
Me.RaiseCanExecuteChanged(Function(x) x.Load(Int32.MinValue))

Running AsyncCommands

You can bind to AsyncCommands in the same manner as any ICommand.

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

The SimpleButton control can indicate whether an asynchronous operation is in progress. To enable this behavior, set the SimpleButton.AsyncDisplayMode property to Wait or WaitCancel:

xaml
<dx:SimpleButton Content="Async Button" 
                 AsyncDisplayMode="WaitCancel" 
                 Command="{Binding AsyncCommand}"/>
csharp
using DevExpress.Mvvm;
using System.Threading.Tasks;
// ...

public class ViewModel : ViewModelBase {
    public IAsyncCommand AsyncCommand { get; }

    public ViewModel() {
        AsyncCommand = new AsyncCommand(AsyncMethod);
    }

    async Task AsyncMethod() {
        while(!AsyncCommand.IsCancellationRequested) {
            await Task.Delay(100);
        }
    }
}
vb
Imports DevExpress.Mvvm
Imports System.Threading.Tasks
' ...

Public Class ViewModel
    Inherits ViewModelBase

    Public ReadOnly Property AsyncCommand As IAsyncCommand

    Public Sub New()
        AsyncCommand = New AsyncCommand(AddressOf AsyncMethod)
    End Sub

    Private Async Function AsyncMethod() As Task
        While Not AsyncCommand.IsCancellationRequested
            Await Task.Delay(100)
        End While
    End Function
End Class

Preventing Simultaneous Execution

The AsyncCommand and AsyncCommand<T> classes include the IsExecuting property. While the command execution task is working, this property equals True and the AsyncCommand.CanExecute method always returns False , regardless of what you implemented in the CanExecute delegate. This feature allows you to disable a control bound to the command until the previous command execution is completed.

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

Canceling Command Executions

The AsynCommands includes the IsCancellationRequested property, which you can check in the execution method to implement canceling the command execution.

csharp
public AsyncCommand MyAsyncCommand { get; private set; }

public MyViewModel() {
    MyAsyncCommand = new AsyncCommand(Calculate);
}

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

Public Sub New()
    MyAsyncCommand = New AsyncCommand(Calculate)
End Sub

Function Calculate() As Task
    Return Task.Factory.StartNew(AddressOf CalculateCore)
End Function
Sub CalculateCore()
    For i = 0 To 100
        If (myAsyncCommand.IsCancellationRequested) Then
            Return
        End If
        Progress = i
        Thread.Sleep(TimeSpan.FromSeconds(0.1))
    Next
End Sub

The AsyncCommand.IsCancellationRequested property is set to True when AsyncCommand.CancelCommand is invoked. You can bind a control to the CancelCommand as follows:

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

If you need more control of the cancellation process, use the AsyncCommand.CancellationTokenSource property. For instance:

csharp
public AsyncCommand MyAsyncCommand { get; private set; }

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

Public Sub New()
    MyAsyncCommand = New AsyncCommand(Calculate)
End Sub
Function Calculate() As Task
    Return Task.Factory.StartNew(AddressOf CalculateCore, MyAsyncCommand.CancellationTokenSource.Token).
        ContinueWith(Function(x) MessageBoxService.Show(x.IsCanceled.ToString()))
End Function
Sub CalculateCore()
    For i = 0 To 100
        MyAsyncCommand.CancellationTokenSource.Token.ThrowIfCancellationRequested()
        Progress = i
        Thread.Sleep(TimeSpan.FromSeconds(0.1))
    Next
End Sub

AsyncCommands and IDispatcherService

If you need to access the execution process from the main thread, use the IDispatcherService as follows:

csharp
IDispatcherService DispatcherService { get { ... } }
void CalculateCore() {
    ...
    DispatcherService.BeginInvoke(() => {
        ...
    });
    ...
}
vb
ReadOnly Property DispatcherService As IDispatcherService
    Get
        '...
    End Get
End Property
Sub CalculateCore() {
    ...
    DispatcherService.BeginInvoke(Sub() ...)
    ...
}

AsyncCommands in POCO

POCO ViewModels can automatically create AsyncCommands based on a public function returning a Task object (the function should have one parameter or be parameterless).

Note

POCO cannot create AsyncCommands for methods that return objects of Task<T> type.

To access the IsExecuting and IsCancellationRequested command properties, you can use the DevExpress.Mvvm.POCO. POCOViewModelExtensions class, which provides the GetAsyncCommand method. Below is an example of a POCO ViewModel that creates an async CalculateCommand for the Calculate method.

csharp
[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));
        }
    }
}
vb
<POCOViewModel>
Public Class ViewModel
    Public Overridable Property Progress As Integer
    Public Function Calculate() As Task
        Return Task.Factory.StartNew(AddressOf CalculateCore)
    End Function
    Sub CalculateCore()
        For i = 0 To 100
            If (Me.GetAsyncCommand(Function(x) x.Calculate).IsCancellationRequested) Then
                Return
            End If
            Progress = i
            Thread.Sleep(TimeSpan.FromSeconds(0.1))
        Next
    End Sub
End Class

AsyncCommands in View Models Generated at Compile Time

To add an AsyncCommand in the View Models Generated at Compile Time, create a Task method with the GenerateCommand attribute in your base View Model class.

csharp
[GenerateViewModel]
public partial class AsyncCommandsViewModel {
    [GenerateProperty]
    int _Progress;

    [GenerateCommand]
    async Task Calculate() {
        for(int i = 0; i <= 100; i++) {
            if(CalculateCommand.IsCancellationRequested) {
                Progress = 0;
                return;
            }
            Progress = i;
            await Task.Delay(20);
        }
    }
}
csharp
partial class AsyncCommandsViewModel : INotifyPropertyChanged {
    public event PropertyChangedEventHandler PropertyChanged;

    protected void RaisePropertyChanged(PropertyChangedEventArgs e) => PropertyChanged?.Invoke(this, e);

    public int Progress {
        get => _Progress;
        set {
            if(EqualityComparer<int>.Default.Equals(_Progress, value)) return;
            _Progress = value;
            RaisePropertyChanged(ProgressChangedEventArgs);
        }
    }

    AsyncCommand calculateAsyncCommand;
    public AsyncCommand CalculateAsyncCommand {
        get => calculateAsyncCommand ??= new AsyncCommand(CalculateAsync, null, false, true);
    }

    static PropertyChangedEventArgs ProgressChangedEventArgs = new PropertyChangedEventArgs(nameof(Progress));

}