Responsive UI with Unresponsive Data: NotifyWorker
Yes, I finally got off my SelectRecursive kick. This is penance.
The 3-box Visio Diagram
How many user interfaces do you design that follow this pattern?
Okay, almost all of them.
A frequent refrain from users (well, at least me):
Regardless of what you're doing behind-the-scenes, the interface should never hang, stutter, or lag. Ever.
Easily said, right?
Well, while hacking on one of my side projects, I ran into this problem.
I put on my "framework guy" hat and asked, "Can I formalize this into a pattern?"
My answer: NotifyWorker.
Now before I get into NotifyWorker, I should mention BackgroundWorker: a frequently used and well thought-out component supplied by our friends in the Baseclass Library (BCL) for this kind of thing.
The trick performed by BackgroundWorker is simple, but important: allow start/stop/cancel of a task on another thread. It also keeps developers sane who have to deal with strict rules about what can only be done on the UI thread.
The type of tasks for which NotifyWorker was designed fit into a special niche that I think BackgroundWorkre misses:
- Tasks that are long enough to lag the interface (0.1+ seconds) but are too short to require tracking of percent complete (so shorter than, say, 5 seconds).
- Tasks that are repeated constantly as the UI updates. Think of recalculating a math/physics model or checking a server-hosted word list for a type-a-head find control.
- Tasks where the work to be done is--in relative terms--longer than the work to update the user interface. If the task takes 0.1 seconds, but updating the 1,000 controls in your window takes 2 seconds, you should start by tweaking your architecture before using NotifyWorker.
How does it work?
(Reflector is how I learn all of my new APIs.)
The constructor takes three parameters that outline the diagram above and the workings of NotifyWorker:
- Func<bool> prework: this is a delegate that is called on the Dispatcher (UI) thread in a WPF application. Delegate passing is a trick I use a lot, and you should, too. The method does the work to take interesting parameters from the UI (control values, etc.) and puts them in a spot where NotifyWorker can safely play on a non-UI thread. If you decide there is nothing interesting to do, you can return false, telling NotifyWorker "never mind".
- Action work: this is where the magic happens. This function is called on a background thread which will leave your UI nice and responsive. Be super careful that 'work' only touches state (fields, etc) that you've safely set aside during prework.
- Action postWork: the last box in our diagram. It takes the work done in 'work' and updates the UI accordingly. This method, as well as 'preWork', synchronizes the UI and background threads, so you don't have to worry about locking.
How do you kick things off? Call NotifyNewWork. This method can be called often, if you want. Put it in the 'Changed' handler of your favorite Slider or TextBox. If NotifyWorker is idle, it will wake up and immediately call preWork. If NotifyWorker is busy, it'll loop right around to get new input values as soon as postWork is done.
A demo
Sexy, huh?
The scenario is simplistic and contrived, like so many good demos.
Both sets of UI do the same thing: take the values from two sliders and sum them. Not too complicated.
For some strange reason, the sum 'routine' takes 0.25 seconds to return a result.
Maybe it's that new math...
Eh, it's probably the Thread.Sleep(250) I put in the method.
Anyway, use your imagination. You'll notice that the 'slow' UI is, well, slower. It lags a bunch as you drag the sliders since it's calling the expensive sum method on the UI thread.
The NotifyWorker controls do much better because the expensive call is hidden off-thread.
Simple, but useful.
Odds and ends
You'll notice a LastClientExceptionEventArgs property and a ClientException event. These are designed to make it a bit easier to handle non-critical exceptions during 'work'. The demo code actually shows this off pretty well.
You'll also notice that NotifyWorker implements IDisposable. Dispose will make sure to end any background work when you're done with NotifyWorker.
Under the covers, NotifyWorker uses LockHelper and some of the pulse/monitor tricks I wrote about a few months ago.
That's about it.
Since NotifyWorker is hard-wired to the notion the Dispatcher, it lives in J832.Wpf.BackOTricksLib. The demo is in the Bag-o-Tricks app under NotifyWorker.
I'm not going to do an official release (zip with binaries/source) of the bag for this sample. It's cool, but not cool enough to warrant a whole new drop.
It also gives you an excuse to play with SVN, if you haven't already. You can always just manually download the code from my SVN share using your browser, too.
Let me know if this works well for you.
Happy multi-threaded hacking!
Note: I've had some funky auth issues with Firefox on the SVN server lately. Let me know if you have any trouble.