How to stop C# async methods

Scenario: a WPF application which when the user presses a button goes off and runs something asynchronously, so that the user is still able to interact with the user interface without it freezing. Not only that, I want to be able to stop the asynchronous operation on receipt of another button press so that it exits graciously.

The is where CancellationToken structure comes in, that is used to propagates notifications that operations should be cancelled.

Some useful StackOverflow resources on this subject can be found here:

https://stackoverflow.com/questions/15614991/simply-stop-an-async-method

https://stackoverflow.com/questions/7343211/cancelling-a-task-is-throwing-an-exception

Some instructions to implement this in a WPF / MVVM kind of environment are given:

Step 1: Create a WPF application

Step 2: Include event-handling classes

Specifically this means classes to implement ICommand

RelayCommand.cs

using System;
using System.Windows.Input;

namespace AsyncCancel
{
   public class RelayCommand<T> : ICommand
   {
      private readonly Predicate<T> _canExecute;
      private readonly Action<T> _execute;

      public RelayCommand(Action<T> execute)
         : this(execute, null)
      {
         _execute = execute;
      }

      public RelayCommand(Action<T> execute, Predicate<T> canExecute)
      {
         if (execute == null)
            throw new ArgumentNullException(nameof(execute));
         _execute = execute;
         _canExecute = canExecute;
      }

      public bool CanExecute(object parameter)
      {
         return _canExecute == null || _canExecute((T) parameter);
      }

      public void Execute(object parameter)
      {
         _execute((T) parameter);
      }

      public event EventHandler CanExecuteChanged
      {
         add { CommandManager.RequerySuggested += value; }
         remove { CommandManager.RequerySuggested -= value; }
      }
   }

   public class RelayCommand : ICommand
   {
      private readonly Predicate<object> _canExecute;
      private readonly Action<object> _execute;

      public RelayCommand(Action<object> execute)
         : this(execute, null)
      {
         _execute = execute;
      }

      public RelayCommand(Action<object> execute, Predicate<object> canExecute)
      {
         if (execute == null)
            throw new ArgumentNullException("execute");
         _execute = execute;
         _canExecute = canExecute;
      }

      public bool CanExecute(object parameter)
      {
         return _canExecute == null || _canExecute(parameter);
      }

      public void Execute(object parameter)
      {
         _execute(parameter);
      }

      // Ensures WPF commanding infrastructure asks all RelayCommand objects whether their
      // associated views should be enabled whenever a command is invoked 
      public event EventHandler CanExecuteChanged
      {
         add
         {
            CommandManager.RequerySuggested += value;
            CanExecuteChangedInternal += value;
         }
         remove
         {
            CommandManager.RequerySuggested -= value;
            CanExecuteChangedInternal -= value;
         }
      }

      private event EventHandler CanExecuteChangedInternal;

      public void RaiseCanExecuteChanged()
      {
         CanExecuteChangedInternal.Raise(this);
      }
   }
}

EventRaiser.cs

using System;

namespace AsyncCancel
{
   public static class EventRaiser
   {
      public static void Raise(this EventHandler handler, object sender)
      {
         handler?.Invoke(sender, EventArgs.Empty);
      }

      public static void Raise<T>(this EventHandler<EventArgs<T>> handler, object sender, T value)
      {
         handler?.Invoke(sender, new EventArgs<T>(value));
      }

      public static void Raise<T>(this EventHandler<T> handler, object sender, T value) where T : EventArgs
      {
         handler?.Invoke(sender, value);
      }

      public static void Raise<T>(this EventHandler<EventArgs<T>> handler, object sender, EventArgs<T> value)
      {
         handler?.Invoke(sender, value);
      }
   }
}

EventArgs.cs

using System;

namespace AsyncCancel
{
   public class EventArgs<T> : EventArgs
   {
      public EventArgs(T value)
      {
         Value = value;
      }

      public T Value { get; }
   }
}

Step 3: Create a view model class

MainWindowViewModel.cs

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using AsyncCancel.Annotations;

namespace AsyncCancel
{
   public sealed class MainWindowViewModel : INotifyPropertyChanged
   {
      private CancellationTokenSource _cts;
      private ICommand _go;
      private string _text;

      public ICommand Go
      {
         get
         {
            return _go ?? (_go = new RelayCommand(
                      x => { DoStuff(); }));
         }
      }

      public string Text
      {
         get { return _text; }
         set
         {
            _text = value;
            OnPropertyChanged(nameof(Text));
         }
      }

      public event PropertyChangedEventHandler PropertyChanged;


      private async void DoStuff()
      {
         if (_cts == null)
         {
            _cts = new CancellationTokenSource();
            try
            {
               await DoSomethingAsync(_cts.Token, 1000000);
            }
            catch (OperationCanceledException)
            {
            }
            finally
            {
               _cts = null;
            }
         }
         else
         {
            _cts.Cancel();
            _cts = null;
         }
      }

      private void DoSomeCounting(int x)
      {
         for (var i = 0; i < x; i++)
         {
            var value = i.ToString();

            Text = value;
            Thread.Sleep(100);

            if (_cts != null) continue;

            Text = "";
            return;
         }
      }

      private async Task DoSomethingAsync(CancellationToken token, int size)
      {
         while (_cts != null)
         {
            token.ThrowIfCancellationRequested();
            await Task.Run(() => DoSomeCounting(size), token);
         }
      }

      [NotifyPropertyChangedInvocator]
      private void OnPropertyChanged([CallerMemberName] string propertyName = null)
      {
         PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
      }
   }
}

Step 4: Create view in MainWindow.xaml

MainWindow.xaml

<Window x:Class="AsyncCancel.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:AsyncCancel"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
   
    <Window.DataContext>
        <local:MainWindowViewModel></local:MainWindowViewModel>
    </Window.DataContext>
    
    <Grid>

        <TextBlock 
            Text="{Binding Text}"
            Height="40" Width="400" 
            TextAlignment="Left"
            VerticalAlignment="Bottom" HorizontalAlignment="Left">
        </TextBlock>

        <Button 
            Content="Go" 
            Height="30" Width="70" 
            Command="{Binding Go}" />

    </Grid>
</Window>

Step 6: Try it!

When the user the presses on Go the asynchronous operation starts and the current numeric count is displayed at the bottom of the screen. Because this operation is asynchronous the user would be free to interact with any other user interface elements while this was going on.

And on pressing Go again, the asynchronous operation is stopped and the text display is cleared:

Leave a Reply