Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Improving ListBox and ListView responsiveness

0.00/5 (No votes)
7 Oct 2015 1  
Detect and correct unresponsive forms

Introduction

Many articles have been written about the advantages of moving CPU bound tasks from the user interface (UI) thread to a background thread. But what if the background threads are creating the problem via a high message rate? A high rate of message displays to the UI will create response problems that mirror a CPU intensive task running in the UI thread. It is even possible all interaction with the form may be blocked until the high volume of data is reduced or stopped. The GuiMonitor class solves the problem by dynamically monitoring the form's responsiveness.

The GuiMonitor class works with ListBox and ListView controls and has an option that allows you to customize usage for any control. Your code must be modified to use the GuiMonitor class.

Background

Window forms receive events from the message queue which is also known as the message pump. Mouse clicks and key presses are examples of events as are timer events and Invoke calls from non-UI threads. If the user interface thread is too busy, for any reason, events on the message queue will queue up thus delaying events. If event handling is delayed too long, the form is perceived as unresponsive.

The .NET framework supports three timer classes: System.Windows.Forms.Timer, System.Timers.Timer and System.Threading.Timer. The latter two timer events execute in a worker thread obtained from the common language runtime thread pool. Code running in a worker thread can not act upon UI components. System.Windows.Forms.Timer executes in the application's UI thread where UI control modification is allowed. Furthermore, events raised by the System.Windows.Forms.Timer are processed only when the GUI thread is not busy. These characteristics make the System.Windows.Forms.Timer almost perfect for determining form responsiveness.

You can read more about the differences between the timers at

Monitoring Form Responsiveness

The GuiMonitor class takes advantage of the timer event being delivered via the message queue. The form's responsiveness is measured by scheduling a timer event to run approximately ten times per second. The UI is deemed to be responsive as long as timer events scheduled by the GuiMonitor class occur within the expected time ranges. If two consecutive timer events are missed, responsiveness is deemed to be unacceptable and corrective action is taken. Corrective action either changes the operational mode of the UI control or requests a brief pause from routines writing to the UI. The GuiMonitor has four operational modes: normal, accelerated, delay and extended delay.

Normal mode does nothing more than monitor the timer events scheduled by the GuiMonitor. When two consecutive timer events are missed, the GuiMonitor class will shift to accelerated mode.

Accelerated mode provides the ability to handle brief periods of high data volume. In accelerated mode, a BeginUpdate() function is issued for the ListBox or ListView control. This BeginUpdate() function temporarily stops displaying messages which will allow the processing of many more messages per second than when the control is in normal display mode. If scheduled timer events are being met, an EndUpdate() function is issued and normal mode is resumed. However, if timer events continue to be missed, further corrective action is needed so an EndUpdate() command is issued to the control and the GuiMonitor class enters the delay mode.

In the delay mode, the GuiMonitor class will issue a request asking for a brief delay in the routines producing the data. If scheduled events are being met, the ListBox or ListView control will return to normal mode with no delays requested. Otherwise, the GuiMonitor class will enter the extended delay mode.

In extended delay mode, the delay period will will be periodically increased until scheduled timer events are being met at which point the ListBox or ListView control will be returned to normal display mode with no requested delay times.

The Sample Code

Download the source code for a working example of the GUI monitor class in action. The example allows you to try ListBox and ListView controls with and without monitoring and/or automatic scrolling.

Without the GuiMonitor, the form will "freeze" with a high number of messages and multiple threads. You can easily see the freeze or the lack thereof by moving your cursor over the various buttons. The buttons will quickly change color with a responsive screen. Pressing either of the "Test response" buttons will display a message just under the buttons. These actions will be delayed for non-responsive screens.

"Maximum random delay" allows you to simulate random delivery times for messages. A positive number will cause a delay of the specified milliseconds between each message. A negative number generate a random delay between zero and the positive value of the specified number of milliseconds.

The "Turn message display off" button toggles between message displaying or not. It illustrates how fast the accelerated mode is.

"Autoscroll", "Maximum random delay" and "Turn message display off" may be changed while a test is running.

The more writer threads you start in the example, the more likely you will create a non-responsive form, especially on multi-core machines. Your form should never be non-responsive when the GUI monitor is activated.

Using the code

The GuiMonitor class fully supports the ListBox or ListView controls via two constructors. A third constructor performs monitoring only. This allows the programmer to provide support for any other type of form control or to provide customized handling of the ListBox or ListView controls.

I will first describe how to use the ListBox and ListView constructors and then explain the more generic constructor.

ListBox and ListView Constructors

This example uses a ListBox but almost the same code applies equally well to a ListView. Begin by copying the GuiMonitor class into your test program.

Now, in your test program, define the object and start the monitor.

using RAMSystems;
GuiMonitor guiMonitor = new GuiMonitor(listBox1);
guiMonitor.Enabled = true;		//start responsiveness monitoring

If automatic scrolling is being used, inform the guiMonitor object when scrolling is turned on or off. For example:

guiMonitor.AutoScroll = AutoScrollCheckBox.Checked;

Inform the guiMonitor object when the GUI control is updated. The GuiMonitor.TotalMessages property must be updated when the UI is updated otherwise the UI response time can not be checked. Any value may be used for the update.

listBox1.Items.Add(text);
guiMonitor.TotalMessages = listBox1.Items.Count;

Delay and extended delay modes require cooperation from threads writing to the control. There are two techniques. Both techniques assume a delegate has been set up and initialized.

private delegate int DelegateSendText(string textLine);
private DelegateSendText m_DelegateSendText;
m_DelegateSendText = this.SendText;

The first example illustrates a caller that honors the pause request.

private int SendText(string textLine)
{   //use this version if the caller will honor the returned pause value
    if (this.InvokeRequired)
    {   //this code block runs in a work thread
        try { this.Invoke(m_DelegateSendText, textLine); }
        catch (Exception) { }
    }
    else
    {   //this code block runs in the GUI thread
        listBox1.Items.Add(textLine);
        if (guiMonitor.AutoScroll)
            listBox1.TopIndex = listBox1.Items.Count - 1;
        guiMonitor.TotalMessages = listBox1.Items.Count;
    }
    return guiMonitor.PauseValue;
} //ends private void SendText(...
// your code running in a worker thread
private void WriterThread(string myTextMessage)
{
  int pause = SendText(myTextMessage);
  if(0 != pause)		//was a delay requested?
    Thread.Sleep(pause);	// yes
}

The second example illustrates a caller that does not honor the pause request. The pause request is honored in the SendText function.

private delegate void DelegateSendText(string textLine);
private DelegateSendText m_DelegateSendText;
m_DelegateSendText = this.SendText;

private void SendText(string textLine)
{ //use this version if the caller does not honor the returned pause value
  if (this.InvokeRequired)
    { //this code block runs in a work thread
      try { this.Invoke(m_DelegateSendText, textLine); }
      catch (Exception) { }
      if (guiMonitor.PauseValue > 0)
         Thread.Sleep(guiMonitor.PauseValue);
    }
    else
    {  //this code block runs in the GUI thread
       listBox1.Items.Add(textLine);
       if (guiMonitor.AutoScroll)
          listBox1.TopIndex = listBox1.Items.Count - 1;
       guiMonitor.TotalMessages = listBox1.Items.Count;
    }
} //ends private void SendText(...
// your code running in a worker thread
private void WriterThread(string myTextMessage)
{
  SendText(myTextMessage);
}

Do not issue the Thread.Sleep in the GUI thread!

The update to TotalMessages is very important. The GuiMonitor uses this call to check for missed timer events. You may stop the monitor when updates to the GUI are complete. It is not an error to leave the monitor running but the 100 millisecond timer continues to run resulting in some light overhead. Use the following command to stop monitoring.

guiMonitor.Enabled = false;

You may monitor or customize the delay values by adding a GuiMonitor.DelegateNotifyModePauseValueChange delegate to the constructor's parameter list. The delegate is called when either the mode and/or pause value changes. This allows you to coordinate or influence other controls on the form. You may change the value of the PauseValue property while in the GuiMonitor.DelegateNotifyModePauseValueChange function but not the mode.

You may monitor or customize the delay values by adding a GuiMonitor.DelegateNotifyModePauseValueChange delegate to the constructor's parameter list. The delegate is called when either the mode and/or pause value changes. This allows you to coordinate or influence other controls on the form. You may change the value of the PauseValue property while in the GuiMonitor.DelegateNotifyModePauseValueChange function but not the mode.

int  myPauseValue = 0;	//calculated delay time
guiMonitor = new GuiMonitor(listBox1, this.HandlePauseValueChanged );
// your code …
private void HandlePauseValueChanged(GuiMonitor.TrafficMode newMode, 
	int newValue)
{
   switch (newMode)
   {
       case GuiMonitor.TrafficMode.Normal:
       case GuiMonitor.TrafficMode.Accelerated:
           myPauseValue = newValue;
           break;
       case GuiMonitor.TrafficMode.Delayed:
          myPauseValue = guiMonitor.PauseValueMilliseconds + (workerThreads.Count * 2);
           break;
       case GuiMonitor.TrafficMode.ExtendedDelay:
           myPauseValue +=  guiMonitor.IncreasedDelaySinceExtended);
           break;
   }
}

The Generic Constructor

GuiMonitor myGuiMonitor = new GuiMonitor(ModeSwitchRoutine);

The generic constructor is useful if the high CPU usage is from a control not supported by the GuiMonitor class. The GuiMonitor class will handle the scheduling and monitoring of timer events and call the function you provided via the constructor when an operating mode change is needed. Your routine should interpret the suggested operating mode to alleviate the delay.

The DataGridView, GridView and DataTable classes are commonly used and would be perfect candidates for the GuiMonitor except for the problem with BeginLoadData() / EndLoadData(); checks for identical data already in the data tables fail when issued during the load operation. Microsoft states the BeginLoadData() function "Turns off notifications, index maintenance, and constraints while loading data.". I suggest ignoring the "Accelerated" mode and going directly to the "Delayed" mode if using "LoadData" operations with a data table.

The sample code uses a status toolbar to demonstrate the generic constuctor. Realistically, the update of the status line was so fast I was never able to cause a delay problem.

Other ListBox / ListView functions

Use the GuiMonitor version of BeginUpdate / EndUpdate and SuspendLayout / ResumeLayout when it is known in advance that a large number of changes will be made to a control. Monitoring is temporarily suspended between the begin and end calls.

Additional Class Properties

performance/statistical properties that may be of interest are:

EnteredAcceleratedMode		number of times accelerated mode was entered
EnteredDelayedMode		number of times delays were requested
IncreasedDelaySinceExtended	number of times a delay increase was requested

Points of Interest

Checking response time ten times per second was a guess on my part; too short an interval slows down the user's program while too long an interval is ineffective. One tenth of a second seemed to be a reasonable compromise.

My original project supported only a ListBox. The ListBox is capable of holding and displaying more than 64K items but manually scrolling to the end of the list was problematic. Run the demo program in ListBox mode with message display off and auto scroll on. Produce over 64K of messages and turn the message display on. The display will show you the last entry as expected until you use the scroll bar to position to the first message and then attempt to reposition to the last message. The ListView did not have a problem until the number of items in the list reached 100 million. I consider this to be more a scrollbar problem but it definitely impacks the design of your display.

History

October, 2015 Initial release

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here