2016-10-26 7 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秒間グリッドをナビゲートすることができました。その後、画像が入力されるとロックされました。うーん。 – Tony

答えて

1

方法があるためです

private async void LoadButton_Click(object sender, EventArgs e) 

は、この行を変更しますasync-awaの導入それはかなり古くなった。実際の非同期待ちの使用を検討してください

いつでもスレッドは何かを待たなければなりません、ファイルが書き込まれるのを待って、データベースが情報を返すのを待って、Webサイトからの情報を待ちます。これは計算時間の無駄です。

待機中ではなく、何か他のことができる場合はスレッドを見て回って、待機後にステートメントを続行することができます。

行のGetThumbNail関数は、Task.Delayでそのような待機をシミュレートします。待機するのではなく、スレッドは呼び出しスタックを呼び出して呼び出し元が結果を待っていないことを確認します。

あなたはLoadButton_Click asyncを宣言するのを忘れました。したがって、あなたのUIは応答しません。

イベントハンドラがビジー状態のときにUIを応答し続けるには、イベントハンドラを非同期として宣言し、可能な限り待ち時間のある(非同期)関数を使用する必要があります。

に注意してください:

  • すべての非同期機能がTask代わりのvoidTask<TResult>の代わりTResult
  • これに対する唯一の例外はイベントですが返さ
  • 非同期(async)のawaitを持つ関数を宣言する必要がありますハンドラ。非同期と宣言されていますが、voidを返します。
  • タスクを待っている場合は、返品は無効です。今すぐあなたのGetThumbnailForRow内のawaitが満たされるたびに

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

    、スレッドは、発信者がいないかどうかを確認するために、そのコールスタックを上がる:あなたはTask<TResult>を待つ場合、戻りはTResult

だからあなたのコードがあります結果を待っています。あなたの例では、呼び出し側が待ち受けているので、スタックを見て...などとなります。結果:スレッドが何もしていないときはいつでも、ユーザインタフェースは自由に行うことができます。

ただし、コードを改善することができます。

サムネールの読み込みを開始時やイベントハンドラの開始時に開始することを検討してください。あなたは即座に結果を得る必要はなく、他にも役に立つことがあります。だから結果を待つのではなく、他のことをしてください。一度あなたが結果を待って開始する必要があります。

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

をあなたには、いくつかの重い計算を行う必要がある場合は、何かを待たずにcreatiを考えますこの重い計算を行うために待たされる非同期関数を使い、別のスレッドにこれらの計算をさせてください。

例:2枚の画像を変換する機能は、次の非同期対応持つことができる:

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

に私をたくさん助けた記事を、this interview with Eric Lippertで説明食事を準備するAsync and Await by Stephen Cleary

類推ましたあなたのスレッドが待っているときに何が起こるのか理解するのを助けました。途中で非同期待ちのために検索

+0

最初の提案を使用すると、行/イメージが順番に読み込まれるように見えます。実際、この行動は以前よりも悪化しています。私はSetImage(あなたがProcessImageResultと呼んだもの)の呼び出しがUIスレッドで常に呼び出されることに気付きました。私のソリューションでは、最初は非UIスレッドで呼び出され、次に.BeginInvokeはUIスレッドに呼び出されます。これにより、残りのグリッド(sans pix)にデータを取り込むことができます。 – Tony

+0

私はあなたのサンプルをリリース時に走らせました、そして、ロードしている間グリッドの周りを移動することができますが、行全体(とそのイメージ)が一度に1つずつ読み込まれることに気付きました。 IOWでは、グリッドの行はゆっくりと行単位で取り込まれますが、この間はグリッドを上下にスクロールできます。私のソリューションでは、グリッド全体(pixなし)にデータが入力され、画像がゆっくりと表示されます。私はそれが良いかどうかを判断することはできません。 – Tony

+0

そうですが、デフォルトでは、async-awaitは常にケースのUIで同じコンテキストで実行されます。 Stephen Clearyの記事へのリンクを参照してください。利点は、コンテキストがUIコンテキストであることを知っているので、 'Invoke'は必要ないということです。欠点は、このスレッドが大量の計算を行っている場合、計算期間中のUIが死んでいることです。あなたの計算が待たずに時間がかかる場合は、 'Task.Run'を使って非同期計算を行うことを検討してください。私は最良のUIは、テーブルの代わりにすべてがロードされるまで、ロード中に待機カーソルを表示していると思う。結局、ローディング演算子は何もしません。 –

0

スタート:

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

へ::ContinueWith()のような

var image = await GetThumbnailForRow(index, itemId); 
SetImage(image); 
関連する問題