I don't think using global variables is a good idea in this case. Here's how I would do it by adding cancellation logic to my AsyncOp
class from a related question. This code also implements the IProgress
pattern and throttles the ViewModel updates.
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
namespace Wpf_21611292
{
/// <summary>
/// Cancel and restarts an asynchronous operation
/// </summary>
public class AsyncOp<T>
{
readonly object _lock = new object();
Task<T> _pendingTask = null;
CancellationTokenSource _pendingCts = null;
public Task<T> CurrentTask
{
get { lock (_lock) return _pendingTask; }
}
public bool IsPending
{
get { lock (_lock) return _pendingTask != null && !_pendingTask.IsCompleted; }
}
public bool IsCancellationRequested
{
get { lock (_lock) return _pendingCts != null && _pendingCts.IsCancellationRequested; }
}
public void Cancel()
{
lock (_lock)
{
if (_pendingTask != null && !_pendingTask.IsCompleted && !_pendingCts.IsCancellationRequested)
_pendingCts.Cancel();
}
}
public Task<T> Run(
Func<CancellationToken, Task<T>> routine,
CancellationToken token = default,
bool startAsync = false,
bool continueAsync = false,
TaskScheduler taskScheduler = null)
{
Task<T> previousTask = null;
CancellationTokenSource previousCts = null;
Task<T> thisTask = null;
CancellationTokenSource thisCts = null;
async Task<T> routineWrapper()
{
// await the old task
if (previousTask != null)
{
if (!previousTask.IsCompleted && !previousCts.IsCancellationRequested)
{
previousCts.Cancel();
}
try
{
await previousTask;
}
catch (Exception ex)
{
if (!(previousTask.IsCanceled || ex is OperationCanceledException))
throw;
}
}
// run and await this task
return await routine(thisCts.Token);
};
Task<Task<T>> outerTask;
lock (_lock)
{
previousTask = _pendingTask;
previousCts = _pendingCts;
thisCts = CancellationTokenSource.CreateLinkedTokenSource(token);
outerTask = new Task<Task<T>>(
routineWrapper,
thisCts.Token,
continueAsync ?
TaskCreationOptions.RunContinuationsAsynchronously :
TaskCreationOptions.None);
thisTask = outerTask.Unwrap();
_pendingTask = thisTask;
_pendingCts = thisCts;
}
var scheduler = taskScheduler;
if (scheduler == null)
{
scheduler = SynchronizationContext.Current != null ?
TaskScheduler.FromCurrentSynchronizationContext() :
TaskScheduler.Default;
}
if (startAsync)
outerTask.Start(scheduler);
else
outerTask.RunSynchronously(scheduler);
return thisTask;
}
}
/// <summary>
/// ViewModel
/// </summary>
public class ViewModel : INotifyPropertyChanged
{
string _width;
string _text;
public string Width
{
get
{
return _width;
}
set
{
if (_width != value)
{
_width = value;
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Width)));
}
}
}
public string Text
{
get
{
return _text;
}
set
{
if (_text != value)
{
_text = value;
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Text)));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
/// <summary>
/// MainWindow
/// </summary>
public partial class MainWindow : Window
{
ViewModel _model = new ViewModel { Text = "Starting..." };
AsyncOp<DBNull> _asyncOp = new AsyncOp<DBNull>();
CancellationTokenSource _workCts = new CancellationTokenSource();
public MainWindow()
{
InitializeComponent();
this.DataContext = _model;
this.Loaded += MainWindow_Loaded;
this.SizeChanged += MainWindow_SizeChanged;
}
void MainWindow_SizeChanged(object sender, SizeChangedEventArgs e)
{
_asyncOp.Run(WorkAsync, _workCts.Token);
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
_asyncOp.Run(WorkAsync, _workCts.Token);
}
async Task<DBNull> WorkAsync(CancellationToken token)
{
const int limit = 200000000;
var throttle = TimeSpan.FromMilliseconds(200);
// update ViewModel's Width
_model.Width = $"Width: {this.Width:#.##}";
// update ViewModel's Text using IProgress pattern
// and throttling updates
IProgress<int> progress = new Progress<int>(i =>
{
_model.Text = $"{(double)i / (limit - 1)* 100:0.}%";
});
var stopwatch = new Stopwatch();
stopwatch.Start();
// do some CPU-intensive work
await Task.Run(() =>
{
int i;
for (i = 0; i < limit; i++)
{
if (stopwatch.Elapsed > throttle)
{
progress.Report(i);
stopwatch.Restart();
}
if (token.IsCancellationRequested)
break;
}
progress.Report(i);
}, token);
return DBNull.Value;
}
}
}
XAML:
<Window x:Class="Wpf_21611292.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<TextBox Width="200" Height="30" Text="{Binding Path=Width}"/>
<TextBox Width="200" Height="30" Text="{Binding Path=Text}"/>
</StackPanel>
</Window>
It uses async/await
, so if you target .NET 4.0, you'd need Microsoft.Bcl.Async
and VS2012+. Alternatively, you can convert async/await
to ContinueWith
, which is a bit tedious, but always possible (that's more or less what the C# 5.0 compiler does behind the scene).