DataGridViewImageCell异步图像加载

时间:2016-10-26 19:56:54

标签: c# winforms datagridview async-await

我需要使用缩略图图像填充DataGridView中的列。我想加载DataGridViewImageCell.Value 异步,因为下载图片需要一些时间。

此解决方案异步加载图像,但似乎阻止UI线程执行其他任务(我假设因为应用程序的消息队列中填充了.BeginInvoke调用)。

如何实现这一目标仍允许用户在下载图像时滚动网格?

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; }
}

2 个答案:

答案 0 :(得分:1)

像ContinueWith()这样的方法是因为async-await的引入已经过时了。考虑使用real async-await

你的线程必须等待某事,等待文件写入,等待数据库返回信息,等待来自网站的信息。这是浪费计算时间。

而不是等待,线程可以环顾四周看看它是否可以做其他事情,并在稍后返回以继续等待后的语句。

您的函数GetThumbNail for row在Task.Delay中模拟这样的等待。线程上升到它的调用堆栈,而不是等待它看到它的调用者没有等待结果。

您忘记声明LoadButton_Click异步。因此,您的用户界面无响应。

要在事件处理程序繁忙时保持UI响应,您必须声明事件处理程序异步并尽可能使用等待(异步)函数。

请记住:

  • 具有await的函数应声明为async
  • 每个异步函数都返回Task而不是voidTask<TResult>而不是TResult
  • 唯一的例外是事件处理程序。虽然它被声明为异步,但它返回void。
  • 如果等待任务,则返回无效;如果等待Task<TResult>,则返回TResult

所以你的代码:

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 ...;
}

现在只要满足GetThumbnailForRow中的await,线程就会调高其调用堆栈以查看调用者是否在等待结果。在您的示例中,调用者正在等待,因此它会在堆栈中查看...等等。结果:无论何时您的线程没有执行任何操作,您的用户界面都可以自由地执行其他操作。

但是,您可以改进代码。

考虑开始加载缩略图,如开头或事件处理程序。您不需要立即获得结果,还有其他有用的事情要做。所以不要等待结果,而是做其他事情。一旦你需要结果开始等待。

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);
    }
}

如果获取缩略图是独立等待不同的来源,例如等待网站和文件,请考虑启动这两个功能并等待它们两者完成:

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;
}

或者你可以在没有等待的情况下开始获取所有缩略图并等待所有人完成:

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);
}

如果你必须做一些繁重的计算,而不是等待什么,考虑创建一个等待的异步函数来执行这个繁重的计算,并让一个单独的线程进行这些计算。

示例:转换两个图像的函数可能具有以下异步对应项:

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

一篇帮助我很多的文章是Async and Await by Stephen Cleary

this interview with Eric Lippert中描述的准备膳食的类比帮助我理解当你的线程遇到等待时会发生什么。在中间某处搜索async-await

答案 1 :(得分:0)

首先将事件处理程序设为异步:

private async void LoadButton_Click(object sender, EventArgs e)

然后改变这一行:

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

为:

var image = await GetThumbnailForRow(index, itemId);
SetImage(image);