2

I have seen a similar question being asked here but it does not seem quite right for my scenario.

We have a UI that can execute a request and if the user wants to execute the request again (with a different query parameter) we want to abandon the initial request, ignore its response and only use the latest requests response.

At the moment I have:

private readonly IDataService _dataService;
private readonly MainViewModel _mainViewModel;

private CancellationTokenSource _cancellationTokenSource;

//Constructor omitted for brevity

public async void Execute()
{
    if (_cancellationTokenSource != null)
    {
        _cancellationTokenSource.Cancel();
    }

    _cancellationTokenSource = new CancellationTokenSource();

    try
    {
        string dataItem = await _dataService.GetDataAsync(_mainViewModel.Request, _cancellationTokenSource.Token);
        _mainViewModel.Data.Add(dataItem);
    }
    catch (TaskCanceledException)
    {
        //Tidy up ** area of concern **
    }
}

This seems to function well and I have a nice and responsive UI but I have a scenario that concerns me:

  1. A request is made by the user
  2. The user makes a new request which cancels the original request
  3. The new request returns before the original cancelled request throws its exception populating the UI with the currently required data
  4. The exception is thrown and clean up occurs overwriting the new requests output

This may be very rare but I can see it as a possibility unless my understanding of this is wrong.

Is there any way to ensure that if a Task is cancelled via a cancellation token request and a new Task is started the cancellation happens before the new task is started/returns execution without blocking the UI thread?

Any reading to expand my understanding on this would be most appreciated.

Community
  • 1
  • 1
Adam
  • 2,123
  • 4
  • 17
  • 25

1 Answers1

3

Is there any way to ensure that if a Task is cancelled via a cancellation token request and a new Task is started the cancellation happens before the new task is started/returns execution without blocking the UI thread?

Any reading to expand my understanding on this would be most appreciated.

First, some related reading and questions, as requested:

If I understood your question correctly, your major concern is that the previous instance of the same task may update the UI (or ViewModel) with obsolete results, once it has completed.

To make sure this does not happen, use ThrowIfCancellationRequested with the corresponding token before your're going to update the UI/model, everywhere you do that. Then it would not matter if the most recent instance of the task completes before the previous older one. The older task will reach the ThrowIfCancellationRequested point before it might have a chance to do anything harmful, so you don't have to await the older task:

public async void Execute()
{
    if (_cancellationTokenSource != null)
    {
        _cancellationTokenSource.Cancel();
    }

    _cancellationTokenSource = new CancellationTokenSource();
    var token = _cancellationTokenSource.Token.

    try
    {
        string dataItem = await _dataService.GetDataAsync(
            _mainViewModel.Request, 
            token);

        token.ThrowIfCancellationRequested();

        _mainViewModel.Data.Add(dataItem);
    }
    catch (OperationCanceledException)
    {
        //Tidy up ** area of concern **
    }
}

A different concern is what to do in the situation when the previous task fails with anything else than TaskCanceledException. This can too happen after the newer task has completed. It is up to you to decide whether to ignore this exception, re-throw it, or do anything else:

try
{
    string dataItem = await _dataService.GetDataAsync(
        _mainViewModel.Request, 
        token);

    token.ThrowIfCancellationRequested();

    _mainViewModel.Data.Add(dataItem);
}
catch (Exception ex)
{
    if (ex is OperationCanceledException)
        return

    if (!token.IsCancellationRequested)
    {
        // thrown before the cancellation has been requested,
        // report and re-throw
        MessageBox.Show(ex.Message);
        throw;
    }

    // otherwise, log and ignore
}
Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • 1
    Would you not want to handle the OperationCanceledException also? – paparazzo Mar 16 '14 at 13:28
  • @Blam, of course you're right, I initially misunderstood you. Yes, `OperationCanceledException` should be used everywhere instead of `TaskCancelledException`. I overlooked that when copied the OP's code. – noseratio Mar 16 '14 at 23:30
  • OK so this is nitpicking put I would have a catch (OperationCanceledException Ex) – paparazzo Mar 17 '14 at 00:19
  • @Blam, no worries, but why `Ex` in `catch (OperationCanceledException Ex) {}` when there's no body? – noseratio Mar 17 '14 at 01:00
  • There is no body either way. Why Catch as a general exception and then test for OperationCanceledException? – paparazzo Mar 17 '14 at 01:30
  • The point is why catch general and then test for OperationCanceledException. Catch (OperationCanceledException Ex) {} Catch {Exception Ex}. It works either way. – paparazzo Mar 17 '14 at 02:28
  • @Blam, now I see what you mean. I guess it's just a matter of taste. I prefer having a `try` followed by a single `catch`. This way I can re-use more common code inside the `catch`, like exception logging. – noseratio Mar 17 '14 at 02:32