2016-10-26 215 views
0

我需要用縮略圖填充DataGridView中的列。我想加載DataGridViewImageCell.Value 異步,因爲它需要一些時間來下載圖像。DataGridViewImageCell異步圖像加載

該解決方案異步加載圖像,但它似乎阻止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; } 
} 
+0

您可以爲1個圖像丟棄任務,並在此任務結束時加載下一個並一次又一次加載到最後一個圖像。 – McNets

+0

我不知道是否沒有答案?似乎任何解決方案都會將消息發送到UI線程,從而窒息其處理用戶消息的能力。我可以通過增加「模擬」延遲來測試。 – Tony

+0

測試結果 - 我只是將模擬的延遲時間增加到5000-7500 mS,我能夠在網格中導航約5秒,然後在圖像填充後鎖定。 Hmmmm。 – Tony

回答

1

方法,如ContinueWith()是因爲引入async-awa它相當過時。考慮使用真正的異步等待

現在,然後你的線程必須等待一些東西,等待文件被寫入,等待數據庫返回信息,等待來自網站的信息。這是浪費計算時間。

而不是等待,如果可以做其他事情,線程可以環顧四周以查看它,並稍後返回以在等待之後繼續執行語句。

您的函數GetThumbNail for row模擬Task.Delay中的這種等待。線程不再等待,而是調用堆棧來查看它的調用者沒有等待結果。

您忘記聲明您的LoadButton_Click異步。因此,您的用戶界面無法響應。

爲了在事件處理程序繁忙時保持UI響應,必須儘可能聲明事件處理程序異步並使用awaitable(異步)函數。

請記住:

  • 與等候函數應該異步
  • 被宣佈爲每個異步函數返回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; 
} 

或者你可以啓動讓所有的縮略圖沒有的await並等待所有完成:

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描述一頓飯幫助我理解當你的線程遇到一個等待時會發生什麼。在中間的某個地方搜索以獲得異步等待

+0

使用你的第一個建議,看起來好像行/圖像是按順序加載的。事實上,這種行爲比以前更糟糕。我注意到,在UI線程上總是調用SetImage(您稱爲ProcessImageResult)的調用。而在我的解決方案中,它首先在非UI線程上調用,然後.BeginInvoke'd到UI線程上。這允許其餘的網格(sans pix)填充。 – Tony

+0

我在發佈時運行了你的示例,並且我注意到我可以在加載時瀏覽網格,但整行(及其圖像)一次加載一個。 IOW中,網格行緩慢地逐行填充,但在此期間,您可以向上/向下滾動網格。在我的解決方案中,整個網格(沒有圖像)被填充,然後圖像緩慢出現。我不能決定這是否更好。 – Tony

+0

你是對的,默認情況下,async-await總是運行在相同的上下文中,在你的案例UI中。請參閱Stephen Cleary文章的鏈接。好處是你不需要'Invoke',因爲你知道上下文就是UI上下文。缺點是,如果此線程正在進行大量計算,則在計算期間您的用戶界面已死亡。如果您的計算需要很長時間而不等待,請考慮'Task.Run'以執行異步計算。我認爲最好的用戶界面在加載時顯示等待遊標而不是表格,直到所有內容加載完畢。畢竟:加載操作員什麼都不做。 –

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