August 20, 2014
Hot Topics:
RSS RSS feed Download our iPhone app

Maintaining a Responsive UI

  • August 5, 2003
  • By Jason Clark
  • Send Email »
  • More Articles »

Jason Clark is with Wintellect

I would venture a guess that nearly every Windows user has used an application that hangs when it performs lengthy operations. Applications that communicate with databases or do other network operations such as e-mail are particularly susceptible to this phenomenon. To be clear, most applications that behave this way are not actually hung; instead it is merely the user interface of the application that has become unresponsive. Of course, to the end-user and to the OS a frozen UI is hard to distinguish from a hung application. In this article we are going to look at how to address this problem in your Windows Forms Applications.

The solution to this problem is to never do a lengthy operation on the same thread that is managing your UI. A simple solution, in concept, this approach does come with a few idiosyncrasies. So here is the plan: in this article we will start by building an application that exhibits the problem, and then we will improve upon the code in two steps until we have a complete solution. So without further ado, let's go.

using System;
using System.Threading;
using System.Windows.Forms;
using System.Drawing;

class App {
   public static void Main() {
      Application.Run(new ResponsiveForm());
   }
}

// Form-derived class
class ResponsiveForm : Form {

   // Couple of fields to hold references to controls
   TextBox textbox;
   Button button;

   // .ctor to set up the form with child controls
   public ResponsiveForm() {

      // Create a textbox
      this.textbox = new TextBox();      
      this.textbox.Size = new Size(100, 24);
      this.textbox.Location = new Point(10, 10);
      this.Controls.Add(this.textbox);

      // Create a button with event handler
      this.button = new Button();
      this.button.Text = "Do Processing...";
      this.button.Size = new Size(100, 24);
      this.button.Location = new Point(10, 40);
      this.button.Click += new EventHandler(this.OnButtonClick);
      this.Controls.Add(this.button);
   }

   // When the button clicks count up to the number in the textbox
   void OnButtonClick(Object sender, EventArgs args) {
      try {

         Int32 num = Int32.Parse(this.textbox.Text);
         // Call a method that does work that takes time
         this.DoLengthyOperation(num); 

      } catch (FormatException) {
         MessageBox.Show(this, 
            "Enter numeric text in TextBox", "Error");
      }
   }

   // Method does arbitrarily lengthy operation
   void DoLengthyOperation(Int32 num) {
      for (Int32 index = 0; index < num; index++) {
         Console.WriteLine(index); // WriteLine the count
         Thread.Sleep(100);
      }
   }
}

Figure 1 ResponsiveUIPart1.cs -- An Unresponse Application

The code in Figure 1 is a complete application that presents a UI with a button and a text box. The user enters a number into the text box, and clicks the button. The application processes for a length of time relative to the number entered into the text box. The processing is actually a loop that writes a value to the console, so to see the full effect you should build the application in Figure 1 as a console application.

Build Note — To build all four stages of code in this article, unzip the code file into a directory. Then run the .build.bat file, or open the .sln file in the sln subdirectory, and build-all from Visual Studio .NET v1.1. Either approach will place four executables in the same directory as the source code. The executables are named ResponsiveUIPart1.exe — ResponsiveUIPart4.exe.

Try building and running the application in Figure 1. Type the number 200 into the text box and press the button. You will see that the application goes unresponsive for about twenty seconds. And yet if you watch the console window that accompanies the UI you can see that processing is still occurring. The application is actually doing exactly what it is designed to do. Unfortunately, it is designed to have an unresponsive UI while doing lengthy processing.

What is the Problem?

Ok, so our part-1 application is not responsive when the user kicks-off a lengthy operation; but why? The reason is this: Windows communicates UI events, such as mouse clicks, to applications via Window Messages. Messages must be actively retrieved and dispatched by application code in a fairly simple loop that has come to be known as a Message Pump. If for any reason the application stops pumping messages, even briefly, then the applications UI becomes unresponsive until the pump resumes.

Many programming environments, such as Visual Basic 6.0 and earlier, shield the programmer from the details of pumping messages, but they are actually true of every Windows program written since Windows v1.0. If the concept of pumping Window Messages is a new one to you, then probably two questions sprung to mind. First, where is the message pump in the program in Figure 1? Second, what is it about the program in Figure 1 that causes the pump to stop processing messages?

Let's tackle the first question. Source code line 8 in Figure 1, is the one and only line of code in Main and it reads like so:

Application.Run(new ResponsiveForm());

The Run method of the System.Windows.Forms.Application class in the class library implements a message pump. And when you create a Windows Forms-based application, your application lives its entirely life pumping messages inside of Application.Run.

Ok, so this is where our application pumps messages, but what about question 2? What, exactly, is it about our application that causes the message pump to stop? Well here it is. When the message pump dispatches a method, it does this using the same thread that is pumping messages. The process of dispatching a message involves calling code to handle the message. Long-story short: OnButtonClick is an example of a method that handles messages dispatched by the message pump, and until it returns, the pump is waiting on application code.

That is the problem in a nutshell; now let's start solving the problem.

The Solution Has Some Problems

The code in Figure 2 will remain responsive to the user, regardless of operations that it is performing. In fact, the problem is that the application is too responsive to the user. Build the code in Figure 2 and give it a run and see.

using System;
using System.Threading;
using System.Windows.Forms;
using System.Drawing;

class App {
   public static void Main() {
      Application.Run(new ResponsiveForm());
   }
}

// Form-derived class
class ResponsiveForm : Form {

   // Couple of fields to hold references to controls
   TextBox textbox;
   Button button;

   // .ctor to set up the form with child controls
   public ResponsiveForm() {

      // Create a textbox
      this.textbox = new TextBox();      
      this.textbox.Size = new Size(100, 24);
      this.textbox.Location = new Point(10, 10);
      this.Controls.Add(this.textbox);

      // Create a button with event handler
      this.button = new Button();
      this.button.Text = "Do Processing...";
      this.button.Size = new Size(100, 24);
      this.button.Location = new Point(10, 40);
      this.button.Click += 
                 new EventHandler(this.OnButtonClick);
      this.Controls.Add(this.button);
   }

   // When the button clicks count up to the number 
   // in the textbox
   void OnButtonClick(Object sender, EventArgs args) {
      try {
 
         Int32 num = Int32.Parse(this.textbox.Text);
         // Call asynchronously method that does work 
         // that takes time
         WaitCallback doWork = 
            new WaitCallback(this.DoLengthyOperation);
         ThreadPool.QueueUserWorkItem(doWork, num);

      } catch (FormatException) {
         MessageBox.Show(this, 
            "Enter numeric text in TextBox", "Error");
      }
   }

   // Method does arbitrarily lengthy operation
   void DoLengthyOperation(Object param) {
      Int32 num = (Int32) param;
      for (Int32 index = 0; index < num; index++) {
         Console.WriteLine(index); // WriteLine the count
         Thread.Sleep(100);
      }
   }
}

Figure 2 ResponsiveUIPart2.cs -- An Response Application, With Problems

The lines in Figure 2 shown in red are the lines that were updated from the previous incarnation of the application. The gist is that the DoLenghtyOperation method is now being executed on a background thread from the thread pool. This leaves the UI thread free to return to the message pump to pump more messages for the application. However, this also has the side-effect of allowing the user to initiate multiple lengthy operations at once, which usually is not the preferred operation for the application. Try it with the code in Figure 2; run the application, enter 200 into the text box, and then press the button several times. You will see each operation working blindly alongside any other processing, each operation stepping on any others' toes all along.

Imagine that rather than count, the DoLengthyOperation method is doing a SQL query which can take up to a minute to process. It's great to keep the UI responsive; however it isn't great to let the user initiate more then one query. That would not benefit the user, and meanwhile the server then must serve multiple requests, all but one of which are unnecessary and will have their results ignored completely.

Again, we need a solution, and this time the solution comes in the form of disabling and re-enabling certain portions of the UI of the application before and after lengthy operations are performed. But there is a bit more to it then just that, so again, let's look at some changes to our application.

using System;
using System.Threading;
using System.Windows.Forms;
using System.Drawing;

class App {
   public static void Main() {
      Application.Run(new ResponsiveForm());
   }
}

// Form-derived class
class ResponsiveForm : Form {

   // Couple of fields to hold references to controls
   TextBox textbox;
   Button button;

   // .ctor to set up the form with child controls
   public ResponsiveForm() {

      // Create a textbox
      this.textbox = new TextBox();      
      this.textbox.Size = new Size(100, 24);
      this.textbox.Location = new Point(10, 10);
      this.Controls.Add(this.textbox);

      // Create a button with event handler
      this.button = new Button();
      this.button.Text = "Do Processing...";
      this.button.Size = new Size(100, 24);
      this.button.Location = new Point(10, 40);
      this.button.Click += 
             new EventHandler(this.OnButtonClick);
      this.Controls.Add(this.button);
   }

   // When the button clicks count up to the number 
   // in the textbox
   void OnButtonClick(Object sender, EventArgs args) {
      try {
 
         Int32 num = Int32.Parse(this.textbox.Text);

         SetDoingLengthyOperation(true);

         // Call asynchronously method that does work 
         // that takes time
         WaitCallback doWork = 
            new WaitCallback(this.DoLengthyOperation);
         ThreadPool.QueueUserWorkItem(doWork, num);

      } catch (FormatException) {
         MessageBox.Show(this, 
            "Enter numeric text in TextBox", "Error");
      }
   }

   // Method does arbitrarily lengthy operation
   void DoLengthyOperation(Object param) {
      Int32 num = (Int32) param;
      for (Int32 index = 0; index < num; index++) {
         Console.WriteLine(index); // WriteLine the count
         Thread.Sleep(100);
      }      

      // Re-enable UI
      this.SetDoingLengthyOperation(false);
   }

   // Enables and disables UI, also makes sure it runs 
   // on UI thread
   void SetDoingLengthyOperation(Boolean working) {
      if (this.InvokeRequired) { // Make sure we run on UI thread
         // Create a delegate to self
         HelperDelegate setDoingLengthyOperation = 
            new HelperDelegate(this.SetDoingLengthyOperation);
         // Roll arguments in an Object array
         Object[] arguments = new Object[]{working};
         // "Recurse once, onto another thread"
         this.Invoke(setDoingLengthyOperation, arguments);
         return; // return;
      }

      // If this is executing then the call occured on the 
      // UI thread so we can freely access controls
      this.textbox.Enabled = !working;
      this.button.Enabled = !working;
   }

   delegate void HelperDelegate(Boolean working);
}

Figure 3 ResponsiveUIPart3.cs — An Response Application, With Problems

The code in Figure 3 shown in red reflects changes to the part-2 incarnation. You would think that disabling the UI before starting the operation on the background thread, and then re-enabling the UI in the background thread would be simple. But alas, with Windows forms, the background thread should not be the thread to do the re-enabling, so this introduces a wrinkle.





Page 1 of 2



Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Sitemap | Contact Us

Rocket Fuel