BlackWaspTM

This web site uses cookies. By using the site you accept the cookie policy.This message is for compliance with the UK ICO law.

Parallel and Asynchronous
.NET 4.0+

Attaching Continuation Tasks to the UI Thread

Parallel tasks can be used in Windows Forms and Windows Presentation Foundation applications to run slow processes without blocking the user interface thread. However, this presents a problem, as both systems prevent other threads from updating controls.

Thread Affinity

Rich client applications developed using .NET technologies, such as Windows Forms and Windows Presentation Foundation (WPF) have the concept of thread affinity. A single user interface (UI) thread is responsible for controlling on-screen updates and is the only thread permitted to make changes to the properties of the controls that appear in forms or windows. This has benefits for the operation of the application, as information critical to the user interface can be held by the thread and always be accessible.

One of the problems with this restriction becomes apparent when you are developing multi-threaded or parallel applications. It is common practice to initialise a form when it first loads. For some forms the initialisation process may include reading data from a file, database or on-line service, or can involve complex calculations that take a long time to run. Similarly, some actions that the user performs can initiate slow processes. If these activities execute on the UI thread, the user interface will become unresponsive; all controls stop functioning, it may be impossible to move or resize the window and the operating system may inform the user that the program has "stopped responding". This can leave the user wondering whether the application has crashed and gives the impression that your software is unstable.

The solution is to execute long-running processes within parallel tasks. This places the tasks on separate threads, ensuring that the UI thread is not blocked and the user interface remains responsive. However, if the user interface should be updated when parallel tasks complete, you need a way to ensure that the updates are performed using the UI thread. Any attempts to update controls from within parallel tasks will cause a cross-threading exception.

To demonstrate, create a Windows Forms project, add a button named "TestButton" to the automatically generated form and add the following code to the button's Click event.

private void TestButton_Click(object sender, EventArgs e)
{
    TestButton.Enabled = false;

    var backgroundTask = new Task(() =>
    {
        Thread.Sleep(5000);
        TestButton.Enabled = true;  // Causes cross-threading exception
    });
    backgroundTask.Start();
}

Here the button is disabled when clicked, before a parallel task is executed. The parallel task pauses for five seconds to simulate a slow process. After the pause the task tries to re-enable the button. This fails as the change to the button's Enabled property is invalid from any thread other than the UI thread.

Scheduling Tasks on the UI Thread

The solution to the above problem is to ensure that any changes to the user interface controls occur on the UI thread. We can do this splitting parallel tasks into two tasks. The first task runs on a non-UI thread and performs the operation that is slow to execute. The second task is used to update the UI controls. We can create the second task as a continuation of the first to ensure that it executes after the complex process completes. We can also provide the task with a task scheduler that ensures that it executes on the UI thread. By only performing user interface updates in the second task we can ensure that it executes quickly and does not stop the application from responding.

To obtain a task scheduler that runs tasks on the UI thread we need to use the TaskScheduler class' FromCurrentSynchronizationContext static method. This method must be called from the UI thread. The returned object can be passed to the second parameter of the ContinueWith method that creates the continuation task. This is shown in the updated code below, which no longer throws cross-threading exceptions.

private void TestButton_Click(object sender, EventArgs e)
{
    TestButton.Enabled = false;
    var uiThreadScheduler = TaskScheduler.FromCurrentSynchronizationContext();

    var backgroundTask = new Task(() =>
    {
        Thread.Sleep(5000);
    });

    var uiTask = backgroundTask.ContinueWith(t =>
    {
        TestButton.Enabled = true;
    }, uiThreadScheduler);

    backgroundTask.Start();
}
1 January 2012