0

I need to populate a column in a DataGridView with a thumbnail image. I would like to load the DataGridViewImageCell.Value asynchronously as it does take some time to download the images.

This solution loads the images asynchronously, but it appears to prevent the UI thread from performing other tasks (I assume because the application's message queue is filled with the .BeginInvoke calls).

How can this be accomplished yet still allow the user to scroll through the grid while images are being downloaded?

private void LoadButton_Click(object sender, EventArgs e)
{
    myDataGrid.Rows.Clear();

    // populate with sample data...
    for (int index = 0; index < 200; ++index)
    {
        var itemId = r.Next(1, 1000);
        var row = new DataGridViewRow();

        // itemId column
        row.Cells.Add(new DataGridViewTextBoxCell 
            { 
                ValueType = typeof(int), 
                Value = itemId 
            });

        // pix column
        row.Cells.Add(new DataGridViewImageCell 
            { 
                ValueType = typeof(Image),
                ValueIsIcon = false 
            });

        // pre-size height for 90x120 Thumbnails
        row.Height = 121; 

        myDataGrid.Rows.Add(row);

        // Must be a "better" way to do this...
        GetThumbnailForRow(index, itemId).ContinueWith((i) => SetImage(i.Result));
    }
}

private async Task<ImageResult> GetThumbnailForRow(int rowIndex, int itemId)
{
    // in the 'real world' I would expect 20% cache hits.
    // the rest of the images are unique and will need to be downloaded
    // emulate cache retrieval and/or file download
    await Task.Delay(500 + r.Next(0, 1500));

    // return an ImageResult with rowIndex and image
    return new ImageResult 
        {
            RowIndex = rowIndex,
            Image = Image.FromFile("SampleImage.jpg") 
        };
}

private void SetImage(ImageResult imageResult)
{
    // this is always true when called by the ContinueWith's action
    if (myDataGrid.InvokeRequired)
    {
        myDataGrid.BeginInvoke(new Action<ImageResult>(SetImage), imageResult);
        return;
    }

    myDataGrid.Rows[imageResult.RowIndex].Cells[1].Value = imageResult.Image;
}

private class ImageResult
{
    public int RowIndex { get; set; }
    public Image Image { get; set; }
}
Tony
  • 1,986
  • 2
  • 25
  • 36
  • You could throw the task for 1 image and when this task ends load the next and again and again till the last image. – McNets Oct 26 '16 at 20:04
  • I wonder if there isn't an answer? It would seem as though any solution would send messages to the UI thread, choking its ability to process user messages. I can test that by increasing my "emulated" delay. – Tony Oct 26 '16 at 20:51
  • Test Results -- I just increased my emulated delay to 5000-7500 mS and I was able to navigate the grid for ~5s and then it was locked up as the images were populated. Hmmmm. – Tony Oct 26 '16 at 21:00
  • http://stackoverflow.com/questions/25009437/running-multiple-async-tasks-and-waiting-for-them-all-to-complete – McNets Oct 26 '16 at 21:03
  • @mcNets Thanks. I'm not sure why I didn't do this sooner (I know better), but I think this issue is more of a release vs debug issue. When I run the above code in release mode, I can navigate the grid fine. I always forget the debugger does 'odd things' with threads at times. – Tony Oct 26 '16 at 21:06

2 Answers2

1

Methods like ContinueWith() are since the introduction of async-await fairly out-of-date. Consider using real async-await

Every now and then your thread has to wait for something, wait for a file to be written, wait for a database to return information, wait for information from a web site. This is a waste of computing time.

Instead of waiting, the thread could look around to see it if could do something else, and return later to continue with the statements after the wait.

Your function GetThumbNail for row simulates such a wait in the Task.Delay. Instead of waiting, the thread goes up it's call stack to see it its caller is not awaiting for the result.

You forgot to declare your LoadButton_Click async. Therefore your UI isn't responsive.

To keep a UI responsive while an event handler is busy, you have to declare your event handler async and use awaitable (async) functions whenever possible.

Keep in mind:

  • a function with a await should be declared async
  • every async function returns Task instead of void and Task<TResult> instead of TResult
  • The only exception to this is the event handler. Although it is declared async, it returns void.
  • if you await a Task the return is void; if you await a Task<TResult> the return is TResult

So your code:

private async void LoadButton_Click(object sender, EventArgs e)
{
    ...

    // populate with sample data...
    for (int index = 0; index < 200; ++index)
    {
        ...
        ImageResult result = await GetThumbnailForRow(...);
    }
}

private async Task<ImageResult> GetThumbnailForRow(int rowIndex, int itemId)
{
    ...
    await Task.Delay(TimeSpan.FromSeconds(2));
    return ...;
}

Now whenever the await in your GetThumbnailForRow is met, the thread goes up its call stack to see if the caller is not awaiting the result. In your example the caller is awaiting, so it goes up its stack to see... etc. Result: whenever your thread isn't doing anything your user interface is free to do other things.

However you could improve your code.

Consider to start loading the thumbnail as at the beginning or your event handler. You don't need the result immediately and there are other useful things to do. So don't await for the result, but do those other things. Once you need the result start awaiting.

private async void LoadButton_Click(object sender, EventArgs e)
{
    for (int index = 0; index < 200; ++index)
    {
        // start getting the thumnail
        // as you don't need it yet, don't await
        var taskGetThumbNail = GetThumbnailForRow(...);
        // since you're not awaiting this statement will be done as soon as
        // the thumbnail task starts awaiting
        // you have something to do, you can continue initializing the data
        var row = new DataGridViewRow();
        row.Cells.Add(new DataGridViewTextBoxCell 
        { 
            ValueType = typeof(int), 
            Value = itemId 
        });
        // etc.
        // after a while you need the thumbnail, await for the task
        ImageResult thumbnail = await taskGetThumbNail;
        ProcessThumbNail(thumbNail);
    }
}

If getting thumbnails is independently waiting for different sources, like waiting for a web site and a file, consider starting both functions and await for them both to finish:

private async Task<ImageResult> GetThumbnailForRow(...)
{
    var taskImageFromWeb = DownloadFromWebAsync(...);
    // you don't need the result right now
    var taskImageFromFile = GetFromFileAsync(...);
    DoSomethingElse();
    // now you need the images, start for both tasks to end:
    await Task.WhenAll(new Task[] {taskImageFromWeb, taskImageFromFile});
    var imageFromWeb = taskImageFromWeb.Result;
    var imageFromFile = taskImageFromFile.Result;
    ImageResult imageResult = ConvertToThumbNail(imageFromWeb, imageFromFile);
    return imageResult;
}

Or you could start getting all thumbnails without await and await for all to finish:

List<Task<ImageResult>> imageResultTasks = new List<Task<ImageResult>>();
for (int imageIndex = 0; imageIndex < ..)
{
    imageResultTasks.Add(GetThumbnailForRow(...);
}
// await for all to finish:
await Task.WhenAll(imageResultTasks);
IEnumerable<ImageResult> imageResults = imageResulttasks
    .Select(imageResultTask => imageResultTask.Result);
foreach (var imageResult in imageResults)
{
    ProcesImageResult(imageResult);
}

If you have to do some heavy calculations, without waiting for something, consider creating an awaitable async function to do this heavy calculation and let a separate thread do these calculations.

Example: the function to convert the two images could have the following async counterpart:

private Task<ImageResult> ConvertToThumbNailAsync(Image fromWeb, Image fromFile)
{
    return await Task.Run( () => ConvertToThumbNail(fromWeb, fromFile);
}

An article that helped me a lot, was Async and Await by Stephen Cleary

The analogy to prepare a meal described in this interview with Eric Lippert helped me to understand what happens when your thread encounters an await. Search somewhere in the middle for async-await

Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116
  • Using your first suggestion, it appears as though the rows/images are loaded sequentially. In fact the behavior is worse than before. I noticed that the call to SetImage (what you called ProcessImageResult) is always called on the UI thread. Whereas in my solution it is first called on a non-UI thread and then .BeginInvoke'd onto the UI thread. This allows the rest of the grid (sans pix) to populate. – Tony Oct 27 '16 at 12:22
  • I ran your sample on release, and I noticed I can navigate around the grid while it loads, but an entire row (and its image) are loaded one at a time. IOW, the grid rows slowly populate row by row but during this time you can scroll up/down through the grid. In my solution the entire grid (without pix) is populated and then the images slowly appear. I can't decide if that is better or not. – Tony Oct 27 '16 at 12:29
  • You are right, by default async-await always runs on the same context, in your case UI. See link to Stephen Cleary's article. The advantage is that you don't need `Invoke`, because you know the context is the UI context. Disadvantage is that if this thread is doing a lot of calculations, your UI is dead during the periods of calculations. If your calculations take a lot of time without await, consider `Task.Run` to do the calculations async. I think best UI is showing wait cursor while loading instead of the table until everything is loaded. After all: while loading operator does nothing. – Harald Coppoolse Oct 27 '16 at 12:49
  • That is something to consider, what is the best UI experience. I had assumed looking at a "full" gird without photos was "better" than waiting for the grid (with photos) to fill completely. But if my users would always wait for photos (TBD), then it really doesn't matter how I load the pictures. – Tony Oct 27 '16 at 13:23
  • If you think that the opertor selects the fifth photo, while the UI is still loading photos 10 t 50, then better show photos as soon as possible and allow selection of photos while loading. This means that somehow you have to stop loading photos as soon as operator selected one. For this do Task.Run with a cancellationTokenSource or use real working thread for the loading, also with cancellationTokenSource – Harald Coppoolse Oct 27 '16 at 14:08
  • I ended up going with a Task.Run solution. Within the LoadButton_Click's for-loop, I set the image to a temp image found in the app's resources ("Pending Image"). Then I created a dictionary of rowIndex and itemId. After the grid is populated (which happens quickly), I Task.Run a separate process to load, and set, the images. Good call on the cancellation token. I'll add that and cancel if any other grid action is performed. Thanks again for the information. – Tony Oct 27 '16 at 14:33
0

Start by making your event handler async:

private async void LoadButton_Click(object sender, EventArgs e)

Then change this line:

GetThumbnailForRow(index, itemId).ContinueWith((i) => SetImage(i.Result));

to:

var image = await GetThumbnailForRow(index, itemId);
SetImage(image);
sellotape
  • 8,034
  • 2
  • 26
  • 30