Introduction
In a previous article, Paulo Zemek describes some limitations of the async/await model introduced in .NET 4.5. Inspired by that, I created my own helper class, Coworker
, to make it easy to keep the user interface (UI) responsive by inserting asynchronously running code blocks calls whenever an algorithm or single method call is potentially long-running. This solution makes it really easy to switch between code that has to be run synchronously with the user interface thread and processing that is better run asynchronously in the background. This is an alternative to the await
/async
solution that works for both WinForms and WPF applications from .NET version 3.5.
Background
No one likes an application that freezes its user interface when running long-running operation, but it is still challenging to write applications that do not do that. Below is a typical, but simplified, example of an event handler that will make some operations potentially taking significant time to complete:
private btnStart2_Click(object sender, RoutedEventArgs e)
{
lboMessages.Items.Add("Operation started");
for( i = 0; i < 10; i++)
{
int result = DoLongOperation(i);
lboMessages.Items.Add("Completed " + (i + 1) * 100 / 10 "%");
}
}
The DoLongOperation
may represent either an advanced mathematical algorithm or, perhaps more common an I/O or web service call, that at least occasionally, will take significant time to perform. The basic problem with this very common approach is that all work is performed in the user interface thread, making the whole application window to freeze during the processing. In addition, none of the progress messages will be shown until the method has ended.
How do you solve this problem? The .NET Framework offers multiple option including BackgroundWorker
, BeginInvoke
on a delegate, ThreadPool
and Task
, but all of these solutions requires adding significant amounts of code to get all invocations on the correct threads. This extra plumbing code makes it much harder to read and understand the code, and the risk of introducing defects by doing this is high.
Microsoft has recognized this to be a problem too and are therefore introducing the new async
and await
keywords in .NET 4.5 and 5.0 to alleviate this situation. However, to use these keywords, you must break the interfaces of the involved methods. There are also strict rules for return values (only void
, Task
or Task<T>
allowed). In this simplified example, the code would be:
private async void btnStart2_Click(object sender, RoutedEventArgs e)
{
lboMessages.Items.Add("Operation started");
for (int i = 0; i < 10; i++)
{
int result = await DoLongOperationAsync(i);
lboMessages.Items.Add("Completed " + (i + 1) * 100 / 10 + "%");
}
}
This example is deceptively simple, as most published examples of async
/await
are, hiding the fact that the original DoLongOperation
has changed to DoLongOperationAsync
which is required to be modified to create and return a Task<int>
. In a real application, the call to DoLongOperation
may be done deeper down the call stack (e.g., in the data access layer). To introduce await
there requires several breaking changes and imposes restrictions on the calling interfaces. With the solution presented below, asynchronous blocks can easily be introduced anywhere in the call chain without breaking any interfaces.
Using the Code
Introducing the Coworker
To make it easy to program enable asynchronous background processing, I created a helper Coworker
class. The first step to enable asynchronous processing is to delegate the work to the Coworker
like this:
private void btnStart2_Click(object sender, RoutedEventArgs e)
{
Coworker.SyncBlock(() => {
lboMessages.Items.Add("Operation started");
for (int i = 0; i < 10; i++)
{
int result = DoLongOperation(i);
lboMessages.Items.Add("Completed " + (i + 1) * 100 / 10 + "%");
}
}
}
The Coworker
will run all the above code “synchronously”, i.e., still blocking the calling UI thread during the whole process. Yet we have just complicated the code and not won anything. However, now it is very easy to specify which parts to be run asynchronously, i.e., without blocking the UI thread, using an “asynchronous block” like this:
private void btnStart2_Click(object sender, RoutedEventArgs e)
{
Coworker.SyncBlock(() => {
lboMessages.Items.Add("Operation started");
for (int i = 0; i < 10; i++)
{
using( Coworker.AsyncBlock() )
{
int result = DoLongOperation(i);
}
lboMessages.Items.Add("Completed " + (i + 1) * 100 / 10 + "%");
}
}
}
The call to Coworker.AsyncBlock
will release the user interface thread leaving it to serve other requests, effectively making the calls within the block asynchronous. When the using block eventually ends, the user interface thread is captured again by the Coworker
, allowing modification of the user interface elements without cross-threading call problems. You can freely choose which code statements that should be run in non-blocking mode and use variables as normal to pass information between the blocking and non-blocking parts as you like. It would not be a problem if DoLongOperation
had an out
argument.
Temporary Switching Back to Synchronous Mode
If there is a need to execute code that (potentially) needs to modify the user interface within a AsyncBlock
, it is possible to use Coworker.SyncBlock()
to temporarily switch back to synchronous mode. This can be performed anywhere in the calling chain, exemplified by this code in the D<code>oLongOperation
operation:
private int DoLongOperation(int i)
{
Thread.Sleep(500);
if( i > 8 )
{
using( Coworker.SyncBlock() )
{
lboMessages.Items.Add(("Value is greater than 8!");
}
}
return i;
}
Using the SyncBlock
is a way for the method to tell that ”hey, I have some code that must update the user interface”.
Windows Presentation Foundation ICommand support
All examples above use event handlers as entry points. If you use ICommand
data-binding in WPF, you can actually embed the code to Coworker.SyncBlock
in a general reusable ICommand
implementation as shown in the SyncBlockCommand
in the attached demonstration code. In this way, you easily disable an asynchronous command as long it is running.
Comparison with .NET 5 async/await
First of all, the Coworker
implementation requires neither any new programming language keywords, nor the newest .NET Framework (5.0). This code runs successfully using existing .NET 3.5 and 4.0 framework. Furthermore, to convert the synchronous code to the asynchronous responsive version, no changes in the interfaces have to be done: no change of return type to Task<T>
and no requirement to refactor out the code to be run asynchronously to a separate method, adding the Async
suffix as recommended as a naming convention. You can still use methods with out
and ref
parameters, and unlike await
, you can also use AsyncBlock
in catch
and finally
statements. This means that you are free to apply the Coworker.AsyncBlock
and SyncBlock
wherever you like in the call chain, for instance in the view, view model or data access layer, without changing the calling interfaces. However, you still have to think of the possibility of concurrent access to the same shared resources within asynchronous blocks though, but the same is true for awaited methods.
Implementation Details
How Can This Work?
The Coworker
actually runs all code in a separate thread (currently obtained from the .NET ThreadPool
), but blocks the user interface code during the synchronous parts making it safe to access the UI resources during these times. In comparison, when using async
and await
keywords, execution switches between UI thread and a background thread. To enable the latter, the compiler must generate a significant amount of code to allow the execution state, including all local variables to be transferred back and forth between threads.
To complicate matters, both Windows Forms and WPF perform checks to verify that the UI resources are accessed only from the UI thread. To make Coworker
work, we have to work around these checks.
In WinForms, the cross-threading checks are only performed in debugging mode. To avoid InvalidOperationExceptions
in this mode, we must disable these checks. This is simply done by including the following line in some initializing code:
Control.CheckForIllegalCrossThreadCalls = false;
In WPF, things are a bit more complicated since all DispatcherObject
derived objects in the user interface are bound to the Dispatcher
thread they are created on. To circumvent the checkings made by the user interface objects, Coworker
temporarily changes the calling Dispatcher
’s thread binding during blocking calls, using this “hack” accessing a private
field using reflection:
private static readonly FieldInfo dispatcherThreadField =
typeof(Dispatcher).GetField("_dispatcherThread", BindingFlags.NonPublic | BindingFlags.Instance);
private object oldDispatcherThread;
private void SetDispatcher()
{
if (dispatcher != null)
{
oldDispatcherThread = dispatcherThreadField.GetValue(dispatcher);
dispatcherThreadField.SetValue(dispatcher, Thread.CurrentThread);
}
}
Due to this hack, it is not possible to use it in partial-trust applications. There is also a risk that Microsoft changes the internal implementation in future versions of .NET.
Conclusion
This article demonstrated an approach to keeping the user interface responsive without complicating the code much and not using the new async
and await
keywords. With this approach, the responsiveness can be increased in existing applications without much change of the code.
Disclaimer
This is just a proof-of-concept article. Before using it in production code, I would suggest more considerations about the usage of “hack” to access a hidden private Dispatcher
field to circumvent the WPF thread-checking and other issues due to the fact that code is actually not run on the UI thread. Code that requires to be run in a single threaded apartment (STA) must still be delegated to the UI thread itself. More testing is required.