In-Depth

Enhance UI Performance in WinForms

You can improve your UI in several ways, using the .NET Framework's built-in multithreading and asynchronous execution mechanisms.

Technology Toolbox: VB.NET, SQL Server 2000

You should give your apps UIs that users enjoy. Unhappy users make for unhappy programmers—and sometimes out-of-work programmers. You need to make your UIs attractive, easy to use, and responsive. Fortunately, you can do a lot to give your user interface a responsive feel without detracting from its other qualities.

You can improve your UI in three ways, using the .NET Framework's built-in multithreading and asynchronous execution mechanisms. First, you can raise events from a non-UI thread to update the controls on the UI thread. Second, you can create a simple background thread to load a DataSet, then load the DataSet into a DataGrid. Third, you can use a simple delegate to make any method asynchronous.

I've developed an application that demonstrates these three techniques. I call it DataExplorer (see Figure 1). It's a database access application—probably like most of the ones you write—and you can adapt its code for your own purposes (download the sample code here). I'll discuss the theory and practice behind DataExplorer's UI performance tricks here, starting with multithreading.

Now I won't lie and claim that multithreading's easy. However, I believe you can get up to speed on multithreading in Windows Forms apps if you absorb a little theory about processes, application domains, and the Common Language Runtime (CLR), then see how they work together in a sample app.

A "process" in Win32 refers to an executing instance of an application. Each executing app creates a new main thread that lasts the lifetime of that application instance. Each app is a process, so each instance of an app must have process isolation.

For example, each instance of Microsoft Word acts independently. When you click on Spelling and Grammar, InstanceA of Word doesn't decide to spellcheck a document running in InstanceB. Even if InstanceA tried to pass a memory pointer to InstanceB, InstanceB wouldn't know what to do with it or even where to look for it, because memory pointers only relate to the process in which they're running.

.NET Framework application domains provide security and application isolation for managed code. Several application domains can run on a single process, or thread, with the same protection apps would have running on multiple processes. Application domains reduce overhead, because you don't have to marshal calls across process boundaries if the apps need to share data. Conversely, a single application domain can run across multiple threads.

This is possible because of the way the CLR handles code execution. The Just In Time (JIT) compiler verifies code before the code executes. A successful verification process guarantees that the code doesn't perform invalid actions, such as accessing memory it's not supposed to access (doing so causes a page fault). The concept of type-safe code means your code won't violate the rules once the verifier has approved its passage from Microsoft Intermediate Language (MSIL) to portable executable (PE) code.

Most Win32 apps don't guarantee that your code won't hose my code, so each app requires process isolation. However, .NET guarantees type safety, so you can run multiple apps from multiple providers within one application domain.

Most apps benefit from running on multiple threads. The goal of creating threads to execute code is to improve performance, or, more importantly, perceived performance. A single-processor computer provides only one processor to manage all threads. In this case, the ability to create more than one thread in an application lets you give your users what they perceive as a better user interface. You've undoubtedly used applications that don't paint the form while they're processing. They appear to be locked, and they don't respond to mouse or keyboard input. Don't frustrate users like this—give them visual feedback when your code is running a long process. Better yet, enable them to cancel a long process safely. They want control over their apps.

Give Users Slick Data Access
The DataExplorer sample app demonstrates progress bars and other UI enhancements in the context of a simple database viewer application. DataExplorer lets users connect to a SQL Server database and retrieve all the databases and database objects from the server on worker threads. (Any thread not created by the main application thread is a worker thread.)

The app retrieves each table, stored procedure, and view, passing them back to the main thread to load in a TreeView. ProgressBar and StatusBar give users visual feedback on code execution status while the TreeView is loading. Users can click on a table, stored procedure, or view to see them in detail. The app responds to the user click by making an asynchronous call to load the data for the object being called.

Every data access task in DataExplorer showcases a different aspect of UI performance improvement. Every task is accomplished on worker threads, and each example is broken down into distinct code regions (see Figure 2). This helps you go right to the code for each UI technique that interests you.

For example, you call the Start method on a variable of the type System.Threading.Thread. The concept of threading is that you go off and do another task separately. The Thread's constructor uses the AddressOf operator to get the address of the procedure that will do the work for your new thread.

A simple demonstration shows how easy it is: Create two new threads and execute methods on the new threads by calling the Start method on the thread variables (see Listing 1). You can go on to create your methods as you would normally. Call these methods with the AddressOf operator, which designates the code to execute. You execute the code designated by the delegate by calling the Start method on a Thread instance. You can use a wide variety of properties and methods from the Thread class (see Table 1).

Creating threads might be easy, but you can run into trouble when you actually want to do something useful for the UI on a worker thread. Unfortunately, controls aren't thread-safe in Windows Forms. (A Form is a control as well, deriving from the System.Windows.Forms.Control class.) The lack of thread safety means you can't create a worker thread to do some long-running task and update the controls on the Form from the worker thread. You'll get a runtime error if you try to work with a control from a worker thread (see Figure 3).

You can update controls in WinForms only on the thread in which they're created. That thread owns the main window's thread handle. You can update control properties from a worker thread by switching the context from worker thread to main thread, performing some work, then returning to the worker thread to continue your long-running task. You do this by creating a delegate that executes on the main application thread. Execute the delegate by calling the Invoke or BeginInvoke method on the form or control, which in turn fires the code on the main thread.

See this in action by looking at some code that does data access on a worker thread, then passes that data back to the main thread (see Listing 2). This example demonstrates two useful functions: loading the nodes of a TreeView with data, and updating a progress bar on a form as the TreeView control is loading.

Raise Events
My first UI performance trick lets you raise events from a non-UI thread to update the controls on the UI thread. You accomplish this with a class that raises events back to the main thread, switching thread context back to the main thread to update controls on the form.

Easier said than done, but take it a step at a time. Put the code that does the heavy lifting—the data access—into a separate class file containing Public Event declarations that get raised at various points during the code's execution.

The DataExplorer project uses the TreeClass to handle calling data access methods and creating TreeNode objects to be added to the TreeView on the main form. The UpdateProgress event is raised as TreeNode objects are created in the class. This event updates ProgressBar and StatusBar on the main form. You'll need several Public Event declarations in the TreeClass (see Listing 2).

The TreeClass itself only has two methods: LoadDatabaseTree (Public), to retrieve the databases from the server; and AddTableNodes (Private), to add the tables, views, and stored procedures for every database in the LoadDatabaseTree method.

Both the LoadDatabaseTree and AddTableNodes methods raise the UpdateProgress and AddNode events (see Listing 3). The main form handles the event after it's raised, based on event handlers added through the AddHandler method. AddHandler lets you specify both the object to listen for and the event to fire when an event gets raised.

For example, the MainForm form's Connect_Click event creates an instance of the TreeClass object as well as the event handlers for events raised in the TreeClass instance, then starts the LoadDatabaseTree method on a worker thread (see Listing 4). This process would block the UI for quite a while if the code ran synchronously, without executing on a worker thread.

The LoadDatabaseTree method's events are raised back to the main form once the method starts executing on the worker thread. This process loads up the TreeView and updates ProgressBar (see Listing 5). A delegate is created to handle calling the UpdateProgress method, which then executes on the main thread.

You can switch safely from worker thread to main thread by calling the control's BeginInvoke method. BeginInvoke takes two parameters: the delegate containing the method to execute, and the arguments for the delegate's method. The thread context switches back to the main thread once you call BeginInvoke. Then the data is marshaled to the correct method on the main thread, and the control is ready for you to work with.

You can determine whether you need to use a delegate to update a control by checking a control's InvokeRequired property. The method executes synchronously on the main thread without switching thread context if InvokeRequired returns False. The method gets invoked through the delegate method if True returns (see Figure 4).

DataExplorer uses the same mechanism to add TreeView nodes as it uses to update ProgressBar. All the processor-intensive work happens on the worker thread while these methods are executing. This architecture gives users the non-blocking UI they love (see Listing 6).

A simple test lets you determine where the thread is executing, to see that each method is actually running on a different thread. Use a Console.Writeline call to output the CurrentThread.Name property to the Output window (see Figure 5).

Load DataSets Into DataGrids
Now you're ready for UI performance trick number two: creating a simple background thread to load a DataSet, then loading the DataSet into a DataGrid. This procedure solves one of the most common reasons for poor UI performance—large amounts of data loaded into a DataGrid.

Data-stuffed DataGrids bog down single-threaded systems in two ways. The task of getting the data from the database chews up time, and the task of loading that data into the grid chews up even more time.

User responses to these waits compound the problem. Nine out of 10 times, users try to reboot the system or terminate the application when it appears to stop working. They think it isn't working because the application's UI stops repainting and doesn't respond to mouse and keyboard events. They hit Ctrl-Alt-Delete; the Task Manager comes up. The Task Manager's table shows the app as "running," but they can see it isn't. They do the logical thing and hit the kill button.

You can avoid all this grief simply by retrieving data on a worker thread, then updating the DataGrid (also on a worker thread) once the data has been retrieved (see Listing 7). You're still using a worker thread to do the data-intensive work, but you aren't explicitly creating a delegate to handle the control's Invoke method. Instead, you use the MethodInvoker delegate to pass the method that handles the event you'd create normally using the Delegate keyword. It's pretty easy to use MethodInvoker, but it does have the drawback of not accepting parameters. You should use a delegate if you need to pass parameters to the method that runs on the main thread from the worker thread. Or you could call the method asynchronously and not worry about creating threads. I'll discuss this alternative more in my third UI performance solution.

You can't show progress when you're loading large amounts of data into a DataGrid. You aren't looping through any data—that's what my first trick demonstrated with the TreeView. Instead, you're simply bulk-loading into a grid. This "fire and forget" technique of loading data using a worker thread provides no user feedback and no notification of task completion. You can't give visual feedback; then again, you might not need to. It might suffice to give users the courtesy of a responsive UI—not to mention the fact that you're loading data in the background. You can build on the technique I've demonstrated here by getting notification once the DataGrid's binding method is complete.

Call Methods Asynchronously
My third and final mechanism for improving UI performance exploits the asynchronous design pattern built into the .NET Framework. This pattern uses a delegate to call a method asynchronously, sidestepping any issues that might arise with the Thread class. My first two solutions involved processing data-intensive work on a thread you create explicitly. The third solution has you using a simple delegate to invoke a method, upon which a thread is created on the Thread Pool automatically. The method passed to the delegate runs on that newly created thread (see Listing 8).

The code for this solution invokes the BeginInvoke method to create the delegate method GetTextData. Calling BeginInvoke on a control switches the thread context from a worker thread to the main UI thread. But calling BeginInvoke on the delegate method causes the method to run asynchronously on its own thread.

You can use the state of the IAsyncResult interface's object to wait for completion of the asynchronous method, and also to check the state of the executing method. The IAsyncResult interface exposes a waitable object that's available for interactions on the thread that called the asynchronous method. Use this interface to implement ProgressBar updates, or any type of visual notification for end users.

Polling the IAsyncResult object helps with some operations, as you can see. However, you might prefer to get a notification only when the asynchronous method completes. You can pass another delegate as a parameter when you call the BeginInvoke method. This delegate gets fired when you call EndInvoke on the delegate in the executing method.

Now you have three techniques for using threads and delegates to make your UIs more responsive. The third technique is the simplest; the first the most complex. So if you find these challenging, start with the last one and work your way up. On the other hand, if you want to delve deeper, I recommend looking into thread synchronization and locking shared data. You can find articles on these and related topics in Additional Resources.

Don't feel like you have to plow through these extra resources to get started writing multithreaded and asynchronous applications. The techniques I've discussed here will get you well on your way to implementing these design patterns in your own applications.

Infragistics Tracker 2004 Volume 1 and 110-page eBook

"Guidelines for Asynchronous Programming"

"Give Your .NET-based Application a Fast and Responsive UI with Multiple Threads" by Ian Griffiths

"Asynchronous Execution in Visual Basic .NET" by Brian A. Randell

"State Sanity Using Smart Clients" by Chris Sells

"Make Synchronization Automatic" by Juval Löwy

Database Design, "Implement Optimistic Concurrency," by Bob Beauchemin

Q&A, "Call WinForms on Multiple Threads," by Juval Löwy

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube