Introduction
This article shows a small example on how to use the Async/Await pattern in a simple WPF-based application, and how the user can Cancel the current started thread by means of a CancellationToken
.
Using the Code
We will first start to show how the demo application works, and next we will go through the technical implementation details in a step by step manner.
The User Interface
The application is quite simple. The user fills in the full path where the text file resides, clicks the Get button and the content of the textfile is loaded into the main screen (if found), else an error messagebox will be shown. To prove that the processing (read) of the textfile executes async
, the user can click the "+" button while processing the input file. This will show an integer value augmenting with 1 in the main-screen. While processing the file, a % processing message is shown in the main screen. Finally, to show the use of CancellationTokens
when the user "hits" the Cancel button, the running thread (task) will be interrupted and a cancellation message will be shown to the user!
Before Moving to the Code Behind ...
Remark 1: Use of Regions ...
IMHO, it is always a good idea to group similar code into regions, I usually use next regions:
- Private Storage: Variable declarations
- C'tor: Constructor code
- Private Interface: All
private
methods, subdivided between UIEventhandlers
and ApplicationLogic
- Public Interface: All code accessible from outside (+ eventually
Protected
and Internal
sections if needed...).
Remark 2: Writing Code in UI-Code-Behind
For the sake of simplicity... all event-handling and application-logic in this sample has been written in the code-behind of the main-form. Although this is ok for demo-purposes, it should be omitted in production coding. Instead, all application and event-handling logic should be extracted from the UI code behind and put in so-called View-Model classes, adapting the MVVM (Model-View-ViewModel) pattern.
Explaining the Code-Behind
Private Storage
#region Private Storage
private int _counter;
CancellationTokenSource _cts;
#endregion Private Storage
The _counter
variable will be used to update the counter when the user clicks the + button. This is just mentioned to show the reader that the UI-interface stays responsive while the background thread is running (task which is executing the read-async after the user activated the Get
button).
Get File Content event-handling Method
private async void buttonGetFile_Click(object sender, RoutedEventArgs e)
{
try
{
_cts = new CancellationTokenSource();
textBlockResult.Text = string.Empty;
labelPlus.Content = string.Empty;
labelProgress.Content = string.Empty;
buttonGetFile.IsEnabled = false;
textBlockResult.Text = await GetFileContentAsync(textBoxFileName.Text, _cts.Token);
}
catch (OperationCanceledException exCancel)
{
MessageBox.Show(exCancel.Message);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
finally
{
buttonGetFile.IsEnabled = true;
}
}
This method will call a method which reads data from a file in an async
manner. In the call to GetFileContentAsync(...)
, which reads the content of a file on disk, send the CancellationTokenSource.Token
property of _cts
as an argument. The Token
property propagates a cancellation message if cancellation is requested. Add a catch
block that displays a message if the user chooses to cancel the file-read operation. While this IO bound background thread is running, the UI is still responsive to user input (click the + button to prove this).
UI Stays Responsive During Execution of Background Thread
private void buttonPlus_Click(object sender, RoutedEventArgs e)
{
labelPlus.Content = _counter++.ToString();
}
During the execution of the background-thread (started when user provided a correct URL to a phsycial file on local disk and started the IO bound retrieval process by hitting the Get button in the UI), UI stays responsive and user can click the + button which will augment the counter.
The Implementation of the async Method
private async Task<string> GetFileContentAsync(string fileName, CancellationToken ct)
{
for(int i = 0; i <= 100; i++)
{
if (!ct.IsCancellationRequested)
{
await Task.Delay(50);
this.Dispatcher.Invoke(() => SetProgress(i.ToString(), fileName));
}
}
using (StreamReader reader = new StreamReader(fileName))
{
if (!ct.IsCancellationRequested)
{
string fileContent = await reader.ReadToEndAsync();
return fileContent;
}
throw new OperationCanceledException
($"File-read-async of {fileName} has been canceled by user !");
}
}
private void SetProgress(string progress, string fileName)
{
labelProgress.Content = $"file {fileName} - {progress} % processed ...";
}
private void buttonCancel_Click(object sender, RoutedEventArgs e)
{
if (_cts != null)
_cts.Cancel();
}
The code is quit straightforward, the method provides a fileName
and CancellationToken
as input argument will return a Task of string as return value. Once the Task
is completed, control will be returned to the calling method (buttonGetFile_Click
) and the result will be shown in the UI. Due to the async
executing context, the UI will stay responsive for the user todo other stuff mainwhile the background process runs ... If the user hits the Cancel button, then the ongoing thread will be canceled. Remark: Because the async reading process doesn't take ling to complete, I've mimick a long-during-process by introducing a delay. Also note that we inform the UI-Thread on each interval. We need to execute the this.Dispatcher.Invoke(...)
method because our running background process is running on a different thread than the UI-thread.
Points of Interest
Please note that the above mentioned example is an IO bound async process, this, in contrast to CPU-bound async processes may be handled differently, more specifically we should (in contrast to CPU bound) try to give the thread back to the thread-pool while executing the IO intensive tasks. This way, the thread can be re-used by the windows-scheduler to handle different operations. To keep things simple, I did not include this behavoir in this simple example, but please check (among-others) next URL on this concept: return-task-threads-back-to-the-thread-pool.