Async loading is harder than it looks


Previously, we talked about the requirement for handling lost devices and putting CanvasControl in charge of resource creation policy. We’d just added the CreateResources event and were planning how to celebrate having solved the device lost problem…
And this is where real-life sneaks in and spoils the party.  It turns out that not everything is a nice self-contained piece of test code.  Sometimes it’s useful to be able to load resources from disk. In WinRT all IO operations follow the async pattern.  This leads to code like:
CanvasBitmap bitmap;

public MainPage()
{
    this.InitializeComponent();
}

async void Canvas_CreateResources(CanvasControl sender, object args)
{
    bitmap = await CanvasBitmap.LoadAsync(sender, "hello.png");
}

void Canvas_Draw(CanvasControl sender, CanvasDrawEventArgs args)
{
    args.DrawingSession.DrawImage(
        bitmap, 
        new Rect(0, 0, sender.ActualWidth, sender.ActualHeight));
}


This fails because at the point that DrawImage is called, bitmap is null.  But how can that be?  The control raises CreateResources and then raises Draw, so bitmap must have been set to the loaded image, right?
This is where “async” kneels down behind us and “await” pushes us backwards onto the floor.  When we raise CreateResources we have no way of knowing that it’s an async method – there’s no such thing as an async event handler in WinRT (or .NET).  So, as far as we’re concerned, Canvas_CreateResources returns immediately, when in actual fact it has arranged to perform the “assign bitmap to the value returned by LoadAsync” operation after the load completes.  We merrily raise the Draw event thinking that CreateResources has completed, and we fall over because it hasn’t.
Anyone running into this quickly fixes it by checking that bitmap is non-null or adding an isLoaded flag:

bool isLoaded;
async void Canvas_CreateResources(CanvasControl sender, object args)
{
    bitmap1 = await CanvasBitmap.LoadAsync(sender, "hello.png");
    bitmap2 = await CanvasBitmap.LoadAsync(sender, "goodbye.png");
    isLoaded = true;
}

void Canvas_Draw(CanvasControl sender, CanvasDrawEventArgs args)
{
    if (!isLoaded) return;

    args.DrawingSession.DrawImage(
        bitmap1, 
        new Rect(0, 0, sender.ActualWidth, sender.ActualHeight));
    args.DrawingSession.DrawImage(bitmap2);
}    


So that problem’s solved then: and we’re done.  (Yes, we really thought we were done at this point).

Bugs


Shawn didn’t like how repetitive our code was becoming.  Everything using CanvasControl and loading images ended up adding an “isLoaded” flag.  So some more time was spent looking into ways we could improve it, or at least trying to decide if it could be improved. Along the way it dawned on us that the code above actually has multiple bugs in it:
The first one is quite noticeable: the control doesn’t redraw itself after the bitmap has been loaded.  This is because the control doesn’t know that it’s ready to be drawn, and so needs the app to tell it:

bool isLoaded;
async void Canvas_CreateResources(CanvasControl sender, object args)
{
    bitmap1 = await CanvasBitmap.LoadAsync(sender, "hello.png");
    bitmap2 = await CanvasBitmap.LoadAsync(sender, "goodbye.png");
    isLoaded = true;
    canvas.Invalidate(); 
}


The second one manifests when the device is recreated and CreateResources is called – the first draw after the new device is created will use bitmaps left over from the old device! This is because the second async CreateResources operation is still executing, yet the isLoaded flag has already been set to true. The fix here is simple:

bool isLoaded;
async void Canvas_CreateResources(CanvasControl sender, object args)
{
    isLoaded = false;
    bitmap1 = await CanvasBitmap.LoadAsync(sender, "hello.png");
    bitmap2 = await CanvasBitmap.LoadAsync(sender, "goodbye.png");
    isLoaded = true;
    canvas.Invalidate();
}


The third bug is more fundamental: what happens if one of the CanvasBitmap.LoadAsync calls generates a device lost exception?  We’ve already established that this code is running outside of CanvasControl’s try/catch.  In fact, nobody is ready to catch this and the app will raise the UnhandledException event.  Are we back to apps having to handle this themselves?
The fourth bug is related – CanvasControl could conceivably start multiple chains of overlapping resource creation async operations overwriting each other in confusing ways.  Since it doesn’t know when a previous async operation has completed it’ll quite happily start a new one while the previous one is still executing.
So we have more to do, but it isn’t obvious what we should do.  In the next part we’ll look at some of the ways we didn’t do it…


Comments (0)

Skip to main content