In this post I will explain how to create a background worker task that updates UI elements in the background when triggered. Every time the update is triggered, the previous task is canceled and a new task is started.
This can be used for example to immediately show an overview of data items in your application. The details of the data items are loaded from disk or a database and will update in the background. Every time the user triggers a refresh that requires the items to be reloaded, the loading of items in the background is restarted.
The UI is updated from the background task without freezing the application. This it improves the user experience and takes some load from the main threat. As the previous task is canceled, there is no unnecessary work done in the background. No fire and forget.
Example
The main window of the example will look like this:
You can download the example project at the end of this post.
There are some items displayed in a DataGrid. In the example, those items are just created in memory, but in a real world example, we would have loaded them from disk or from a database.
When the button is clicked, the background task is started that loads the details of all the items in a loop. The loading of the details will take significantly longer than loading of the general item information, which is already available at startup.
Model
The example uses the MVVM pattern. Let’s start in the model.
Here we have a FileItem class as representation of a file on the disk. The Load() method simulates the loading of the file details by putting the current thread to sleep.
public void Load() { // Simulate long loading time by putting the thread to sleep between 1 and 3 seconds. int timeout = 1000 + Convert.ToInt32(2000 * _random.NextDouble()); Thread.Sleep(timeout); }
ViewModel
Next, we have a ViewModel class FileViewModel, which is mainly a wrapper class for the Model. Additionally, it has a Status property that will inform the user about the current status of the file. This property is displayed in the second column of our grid.
public class FileViewModel : ViewModelBase { private readonly FileItem _item; private string _status; public FileViewModel(string fileName) { _item = new FileItem(fileName); Status = $"FileViewModel for {_item.FileName} created."; } public string Status { get => _status; private set { _status = value; OnPropertyChanged(nameof(Status)); } } public string FileName => _item.FileName; public void Load() { Status = $"Loading file {_item.FileName}..."; _item.Load(); Status = $"File {_item.FileName} load completed."; } }
After creation of the ViewModel, the status is set to a default value telling the user that the VM is ready. When the loading starts, the user is informed that the file is being loaded and after the loading is completed, the file status changes accordingly.
Here are the relevant parts of the main ViewModel:
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private Task _backgroundTask = Task.CompletedTask; public ObservableCollection<FileViewModel> Items { get; } = new ObservableCollection<FileViewModel>(); public ICommand LoadCommand { get; }
In the main window of the example, there is a ListBox whose ItemsSource property is bound to the Items property of the ViewModel. This is an ObservableCollection of FileViewModels. The Button command is bound to the LoadCommand property of the ViewModel.
In the main ViewModel, we define a field that will hold a reference to the background task. In addition to that, we also define a field holding a CancellationTokenSource. This is used later to cancel the running background task.
Background Task
I will explain a bit more in detail what happens when the LoadCommand is executed:
private void LoadExecute() { // Cancel the previous task _cancellationTokenSource.Cancel(); try { // Wait until task is completed; (1) _backgroundTask.Wait(); } catch (AggregateException aggregateException) { // Ignore AggregateExceptions aggregateException.Handle(x => true); } // Dispose the existing token source and create a new one. (2) _cancellationTokenSource.Dispose(); _cancellationTokenSource = new CancellationTokenSource(); CancellationToken cancellationToken = _cancellationTokenSource.Token; _backgroundTask = Task.Run(() => // (3) { for (int i = 0; i < Items.Count; i++) { FileViewModel item = Items[i]; if (!cancellationToken.IsCancellationRequested) { item.Load(); } } }, cancellationToken); }
First, the previous task is cancelled by calling the Cancel() method on the CancellationTokenSource, if it is still running (1). We will wait until the task is completed before continuing creating a new task. As it is expected, we will catch an AggregateException.
Then, the existing CancellationTokenSource is disposed and a new CancellationTokenSource is created and assigned to the local field (2).
Then a new background task is started using the actual token of the CancellationTokenSource. Within that task, we loop through all item ViewModels and call their Load() method if the task was not canceled before (3). This is done checked by checking the IsCancellationRequested property of the CancellationToken.
Some notes:
- The CancellationToken is not passed to the action of Task.Run(), it is only available in the action because it is defined inline.
- In a real world example, the item ViewModels would probably have a status that is checked before calling the Load() method to prevent updating the data if it is not necessary.
- In this example, after restarting the background task we have to wait until the loading of the file currently being processed is completed. To cancel the task in between, you can also pass the CancellationToken to the Load() method.