Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cancelling AsyncRelayCommand

This is WPF (.NET Framework 4.8). I need to supply two <Button>s in the UI to start and stop a long-running background process. The start button should initially be enabled and will get disabled once the command is issued (to avoid a second click). The stop button will be disabled initially and will get enabled once the process starts running.

I'm using AsyncRelayCommand supplied in Windows Community Toolkit's MVVM library. The buttons bind to their VM through normal XAML binding. The underlying commands look like this:

private AsyncRelayCommand _StartPumpingCommand;
public AsyncRelayCommand StartPumpingCommand
{
  get
  {
    _StartPumpingCommand ??= new AsyncRelayCommand(async (CancellationToken token) =>
      {
        dispatcher.Invoke(() => _CancelPumpingCommand.NotifyCanExecuteChanged());
        await SomeTask(token);
      },
      () => {
        /* SomeTask is not running and a cancellation has not been requested  */
        return !_StartPumpingCommand.IsRunning && !_StartPumpingCommand.IsCancellationRequested;
      });

    return _StartPumpingCommand;
  }
}


private RelayCommand _CancelPumpingCommand;
public RelayCommand CancelPumpingCommand
{
  get
  {
    _CancelPumpingCommand ??= new RelayCommand(() =>
      {
        if(_StartPumpingCommand?.IsRunning??false)
          _StartPumpingCommand.Cancel();
      },
      () => _StartPumpingCommand?.IsRunning??false);

    return _CancelPumpingCommand;
  }
}

Questions:

  1. Is my implementation of the two commands correct?
  2. Does AsyncRelayCommand internally contain CancellationTokenSource magic, or do I need to create one in the VM class myself and use it in the two commands above?
  3. How do I communicate the completion of StartPumpingCommand to the CancelPumpingCommand, so that the buttons in the UI get enabled/disabled accordingly? Are we supposed to call CancelPumpingCommand.NotifyCanExecuteChanged() after SomeTask() returns? How will it behave since CancelPumpingCommand uses StartPumpingCommand.IsRunning property to decide its availability, and StartPumpingCommand would still be running, right?
like image 274
dotNET Avatar asked Oct 15 '25 02:10

dotNET


2 Answers

Here is [RelayCommand(IncludeCancelCommand = true)] attribute.

See https://github.com/CommunityToolkit/dotnet/issues/118 for details.

For example, this

[RelayCommand(IncludeCancelCommand = true)]
private Task StartPumpingAsync(CancellationToken token)
{
    // Command body.
}

should create two commands: StartPumpingCommand and StartPumpingCancelCommand. For custom implementation you may see IncludeCancelCommand or IAsyncRelayCommand.CreateCancelCommand extension.

P.S. I don't know if this functionalities are officially documented, but Windows Community Toolkit MVVM has docstrings.

like image 166
Stepan Zakharov Avatar answered Oct 17 '25 15:10

Stepan Zakharov


Figured it out. Here is the correct way of doing async commands in Community Toolkit in case it helps someone:

private AsyncRelayCommand _StartPumpingCommand;
public AsyncRelayCommand StartPumpingCommand
{
  get
  {
    _StartPumpingCommand ??= new AsyncRelayCommand(async (CancellationToken token) =>
      {
        try
        {
          await Task.Run(() => SomeTask(token), token);
        }
        catch(OperationCanceledException)
        {
          //do whatever u want to do in case of task cancellation
        }
      },
      () => !_StartPumpingCommand.IsRunning);

    return _StartPumpingCommand;
  }
}

private ICommand _CancelPumpingCommand;
public ICommand CancelPumpingCommand
{
  get
  {
    _CancelPumpingCommand ??= StartPumpingCommand.CreateCancelCommand();
    return _CancelPumpingCommand;
  }
}

private async Task SomeTask(CancellationToken token)
{
  //I'm using "await foreach" which requires C# 8 and an Nuget
  //package named `Microsoft.Bcl.AsyncInterfaces` when targeting
  //.NET Framework. You may use some other way of creating a 
  //Task here.
  await foreach(...)
  { 
    token.ThrowIfCancellationRequested();

    //do stuff
  }
}

This works perfectly correctly. My UI is totally responsive (no deadlocks) and I can cancel and restart the task any number of times. This suggests that AsyncRelayCommand internally contains task cancellation magic and needs nothing more than awaitable method to work.

like image 38
dotNET Avatar answered Oct 17 '25 15:10

dotNET