In February, I finally figured out my New Year’s Resolution. It was to build no more UIs that lock up, ever (except in exceptional circumstances where there is no other choice). It spawned this post that I’ve finally gotten around to finishing (and some of the topics I have started queuing up posts for).
It is a well known customer-service mantra we have that, any response is better than no response. If you can or cannot help someone- or you are in the midst of doing so- you should still tell them what’s going on. It builds better relationships, communication, and confidence in your team.
So it goes with UIs. Nothing is so frustrating to users as a slow UI. Here’s a quick checklist to see whether your UI behaves as it should:
- A UI should always stay responsive and never lock up.
- A UI should give feedback about what it is doing, and the more detailed feedback the better.
- The user should be able to cancel something taking too long.
The frustrating thing to me is that so many of our UIs aren’t responsive, and the only reason is laziness or ignorance. I’ve written my fair share of unresponsive UIs, but I am smarter and more experienced and I say, no more! Threading APIs are sophisticated enough where any programmer really needs to know how to write asynchronous applications. Here’s what you need to learn to create a good UI (my advice is specific to .NET/Windows but the advice should apply to most platforms). I’m going to cover each topic in some detail in later posts (and maybe add/remove/modify topics).
The Windows Message Loop
To understand why your application locks up, you need to understand Windows Messages, the Main Message Loop, and other basic threading concepts. The gist is, your Main thread is what receives messages like user input and is responsible for redrawing, so it needs to be free to act on events. So that means 1) you need it to work asynchronously, so it can act on a message in the queue and go back to doing work, and 2) most processing needs to happen on a background thread. This is a highly discussed topic and much literature is available but I’ll try to give a good summary.
Offloading Work to Background Threads
Offloading work to backgrounds threads means more than just putting long-running tasks into a BackgroundWorker. Understanding tricks for putting as much on the background as possible can make your UIs much more responsive.
This is the most basic way of keeping your UI responsive, and once you understand how to use it, can be incredibly effective and simple. I consider BackgroundWorkers almost trivial to set up after you create a few. They can be limiting, however, and they do incur some architectural overhead and you may have to make code design compromises. They need to be a tool in your toolbox, but you need to go further.
Tasks Parallel Library (TPL) and Task Parallelism
.NET 4.0 (and 3.5 with Reactive Extensions) has a System.Threading.Tasks namespace filled with all sorts of goodies. The Parallel class is excellent for data parallelism, so you can easy multithread foreach loops and LINQ queries. Task parallelism is much more sophisticated, however, and not too much has been written about it because it is complex. .NET 5.0 is going to be focused on asynchronous programming with the ‘await’ and ‘async’ keywords, which is mostly handled under the hood with the TPL. So you can find tons of good articles about task parallelism by searching for info about the .NET 5.0 CTP (community technology preview).
Lots of complexity comes with asynchrony, and especially multithreaded programming. What happens if something fails, or more pertinent to UI programming, the user wants to cancel (what’s the point of having a responsive UI if the user can’t do anything?). Having a situation where we need to be able to roll back/cancel any changes means we need a way to store how things were when the process was started. You can do this by storing state (cloning, essentially), which is difficult to implement and maintain well because it is so unclear (does everything in the object’s references need to clone? What if you have something you can’t clone), or more preferably, having data that cannot change state. To be a good asynchronous programmer, you need to be good at creating and working with immutable data. If your data is immutable, you are safe- cancellation, undos, exception recovery, etc., is trivial because you have data in a good known state that nothing can change.