Smarter Asynchronous Data Loading

Smarter Asynchronous Data Loading

Smarter Asynchronous Data Loading

Author: Fredrik Kalseth An entry about windows forms Publication date 23. September 2007 13:23
Imagine you have a Windows Forms application that has a dialog for sending email messages. The ‘Send Message’ dialog has a button that opens a ‘Find Recipient’ dialog, which lets the user select a recipient from a rather large address book. It takes the data access layer several seconds to come up with the data for this dialog. Hopefully, warning lamps should go off in your head at seeing the following code:
private void FindRecipient_Load(object sender, EventArgs e)
{
_recipientsList.DataSource = AddressBook.GetRecipients();
}
Sure, it works – but since we are loading the data in the same thread as the UI, we’re effectively blocking the message pump which makes the application seemingly hang for the time it takes GetRecipients() to return the data. If this method takes 5 seconds, 10 seconds or even a few minutes to return, the user would be forgiven for thinking your application has crashed. We can surely do better. Lets move the loading of data into a separate thread, and let the user know that it may take a while:
private void FindRecipient_Load(object sender, EventArgs e)
{
BackgroundWorker worker = new BackgroundWorker();
PleaseWait pleaseWait = new PleaseWait();
worker.DoWork += new DoWorkEventHandler(
delegate(object s, DoWorkEventArgs args)
{
args.Result = AddressBook.GetRecipients();
});
worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(
delegate(object s, RunWorkerCompletedEventArgs args)
{
_recipientsList.DataSource = args.Result;
pleaseWait.Dispose();
});
worker.RunWorkerAsync();
pleaseWait.ShowDialog();
}
Here, we’re using the BackgroundWorker class to load the data asynchronously. That’s a huge improvement, and in many scenarios it will be good enough. But lets take it one step further… What if we could deduce that the user is likely to soon open the dialog? Then we could start loading the data even before the user requests it, having it ready in time for when it is needed. In this example, we can confidently assume that when the user opens the ‘New Message’ dialog, he will shortly after want to select a recipient. Why not start loading the address book data right away?
private void NewMessage_Load(object sender, EventArgs e)
{
// here, we start populating the cache asynchronously because we know the
    // user is likely to click the 'Find' button soon. 
    AddressBook.PopulateCacheAsync(); 
}
Now, as soon as the parent dialog opens we start loading the data asynchronously. If the user now spends a few seconds typing in a title for his message, and then clicks the ‘Find’ button to find a recipient, the data will already have had time to load the data, and the FindRecipient dialog shows up almost instantly. However, to make this work properly we need to synchronise the PopulateCacheAsync and GetRecipients methods on AddessBook – because if the user clicks the ‘Find’ button before PopulateCacheAsync has finished populating the cache, then it and GetRecipients will likely get in a tangle. Luckilly, this is quite easy to sort out using the double-checked locking pattern:
public static class AddressBook
{
private static readonly int _noOfRecipients = 500;
static Action<int> _reportProgress = null;
private static IList<string> _cache = null;
private static object _cacheLock = new object();
private static IEnumerable<string> GetMockData()
{
for (int i = 0; i < _noOfRecipients; i++)
{
yield return "Recipient #" + i;
Thread.Sleep(10); // sleep for 10ms each loop to simulate a long-running operation
        }
}
public static void PopulateCacheAsync()
{
ThreadPool.QueueUserWorkItem(
delegate
            {
PopulateCacheCore();
});
}
private static void PopulateCacheCore()
{
if (null == _cache) // if the cache has not already been loaded, populate it
        {
lock (_cacheLock)
{
if (null == _cache) // the cache may have been popualted while we were waiting to get the lock, so check again
                {
_cache = new List<string>(GetMockData());
}
}
}
}
public static IList<string> GetRecipients()
{
PopulateCacheCore();
return _cache;
}
}
And thats it! We’ve radically improved the perceived performance of our application by being a bit clever about when we load our data. I’ve included a demo solution that includes the above three scenarios, so you can get a feel for how they perform. The downloadable code also goes even further and adds a progres bar to the ‘please wait’ dialog.

This post is also available in: Norsk bokmål