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: