When a Task
is cancelled an OperationCanceledException
exception is thrown. The catch block that catches this exception should be the only place where you create a new instance of CancellationTokenSource
, right after you disposed the cancelled instance. The CancellationTokenSource
can only be used once (once the CancellationTokenSource.IsCancellationRequested
was set to true
by calling Cancel()
).
The Task
can only be cancelled if CancellationToken.ThrowIfCancellationRequested()
is called and cancellation was requested. This means the observer of the CancellationToken
is responsible to call CancellationToken.ThrowIfCancellationRequested()
every time the running operation allows to get aborted but as often as possible, in order to cancel as soon the cancellation was requested and always before allocating resources.
So it only makes sense to pass a reference of the current CancellationToken
to every method that is called during the execution of the cancelable Task
. Also always make sure that all async
methods return a Task
or Task<T>
(except for event handlers which require to return void
based on the event
delegate signature). The following example will work as intended.
MainWindow.xaml
<Window>
<Window.DataContext>
<CheckListDetailViewModel />
</Window.DataContext>
<Button>
<Button.Style>
<Style>
<Setter Property="Command" Value="{Binding LoadChecklistTemplateCommand}" />
<Setter Property="Content" Value="Load Template" />
<!-- Set the Button's Command to CancelCommand once the download has started -->
<Style.Triggers>
<DataTrigger Binding="{Binding IsBusy}" Value="True">
<Setter Property="Content" Value="Cancel" />
<Setter Property="Command" Value="{Binding CancelCommand}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</Window>
CheckListDetailViewModel.cs
public CancellationTokenSource CancellationTokenSource { get; set; }
private bool isBusy;
public bool IsBusy
{
get => this.isBusy;
set
{
this.isBusy = value;
OnPropertyChanged();
}
}
public ICommand LoadChecklistTemplateCommand { get; set; }
public ICommand CancelDownloadCommand => new DelegateCommand(CancelDownload, () => true);
public CheckListDetailViewModel(IAuditInspectionDataService auditInspectionDataService)
{
this.CancellationTokenSource = new CancellationTokenSource();
_auditInspectionDataService = auditInspectionDataService;
LoadChecklistTemplateCommand = new DelegateCommand(OnLoadTemplate, CanLoadTemplate).ObservesProperty(() => ChecklistItems.Count);
}
// Cancel Task instance
public void CancelDownload()
{
this.CancellationTokenSource.Cancel();
}
private async Task OnLoadTemplateAsync()
{
if (this.IsBusy)
{
return;
}
CancellationToken cancellationToken = this.CancellationTokenSource.Token;
this.IsBusy = true;
try
{
var items = await Task.Run(() =>
{
cancellationToken.ThrowIfCancellationRequested();
return this._auditInspectionDataService.GetCheckList(cancellationToken, InspectionType.InspectionTypeId);
}, cancellationToken);
this.ChecklistItems = new ObservableCollection<ChecklistItem>(items);
}
catch (OperationCanceledException)
{
// CancellationTokenSource can only be used once. Therefore dispose and create new instance
this.CancellationTokenSource.Dispose();
this.CancellationTokenSource = new CancellationTokenSource();
this.ChecklistItems = new ObservableCollection<ChecklistItem>();
}
this.IsBusy = false;
}
AuditInspectionDataService.cs
// Member of IAuditInspectionDataService
public void GetCheckList(CancellationToken cancellationToken)
{
using (OleDbConnection connection = new OleDbConnection("Provider=Microsoft.ACE.OLEDB.12.0;Data Source=\"myfile.xls\";Extended Properties=\"Excel 12.0;HDR=YES;IMEX=1\""))
{
OleDbCommand command = new OleDbCommand("SELECT * FROM [Sheet1$]", connection);
connection.Open();
OleDbDataReader reader = command.ExecuteReader();
int partitionSize = 50;
int linesRead = 0;
while (reader.Read())
{
if (++linesRead == partitionSize)
{
cancellationToken.ThrowIfCancellationRequested();
llnesRead = 0;
}
Console.WriteLine(reader[0].ToString());
}
reader.Close();
}
}