Click here to Skip to main content
15,867,453 members
Articles / Desktop Programming / Windows Forms

Keep Your User Interface Responsive Easily Using a Coworker

Rate me:
Please Sign up or sign in to vote.
4.92/5 (12 votes)
8 Jul 2012CPOL7 min read 40.2K   1.1K   32   8
An alternative approach to the new .NET async/await keywords to program asynchronously commands to make your user interface more responsive.

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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Sweden Sweden
Henrik Jonsson is a Microsoft Professional Certified Windows Developer (MCPD) that currently works as an IT consultant in Västerås, Sweden.

Henrik has worked in several small and large software development projects in various roles such as architect, developer, CM and tester.

He regularly reads The Code Project articles to keep updated about .NET development and get new ideas. He has contributed with articles presenting some useful libraries for Undo/Redo, Dynamic Linq Sorting and a Silverlight 5 MultiBinding solution.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Member 110436458-Jul-15 22:59
Member 110436458-Jul-15 22:59 
GeneralMy vote of 5 Pin
Herre Kuijpers10-Mar-14 22:01
Herre Kuijpers10-Mar-14 22:01 
GeneralMy vote of 5 Pin
Manoj Kumar Choubey15-Jan-13 22:01
professionalManoj Kumar Choubey15-Jan-13 22:01 
QuestionMy vote of 5 Pin
nandixxp12-Jan-13 3:36
nandixxp12-Jan-13 3:36 
good idea!
GeneralMy vote of 5 Pin
Pete O'Hanlon9-Jul-12 7:18
subeditorPete O'Hanlon9-Jul-12 7:18 
QuestionA possibly confusing sample Pin
Pete O'Hanlon9-Jul-12 1:48
subeditorPete O'Hanlon9-Jul-12 1:48 
AnswerRe: A possibly confusing sample Pin
Henrik Jonsson9-Jul-12 9:42
Henrik Jonsson9-Jul-12 9:42 
GeneralRe: A possibly confusing sample Pin
Pete O'Hanlon9-Jul-12 10:07
subeditorPete O'Hanlon9-Jul-12 10:07 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.