WPFで「UriSouceプロパティに画像のURLを入れてBitmapImageを初期化する処理」を非同期で実行するとしぬ
この記事は、WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る - pierre3のブログ の続きです。
関連記事
- WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る - pierre3のブログ
- WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る (1. 検索バーの実装) - pierre3のブログ
- WPFでReactiveProperty入門 ~ Rxを使って検索結果のサムネイル画像を一括ダウンロードする - pierre3のブログ
サンプルプロジェクト github.com
目次
今回は、検索結果として取得した画像のURLから画像データを読み込んでBitmapImageを生成する部分について書くのですが、
先に言ってしまうと、試してみたけど結果うまく行きませんでした。というお話です。 BitmapImageにWeb上の画像を読み込ませるには、UriSourceプロパティを使用するのが最も簡単です。 非常に簡単で便利なのですが、この処理はUIをブロックするようなのです。 そこで、Taskにツッコんで別スレッドで実行するようにしてみます。 しかし、上記コードではImageコントロール等で表示しようとした時点で以下のエラーが発生します。 型 'System.Windows.Markup.XamlParseException' のハンドルされていない例外が PresentationFramework.dll で発生しました UIスレッド以外で生成されたUIオブジェクトはFreeze()してからでないとUIスレッドで使用できないのでした。 UIで使用する前に Freeze しておく必要があるのですが、そのタイミングも問題になります。 ダウンロードの完了を待ってからFreezeすればどうでしょうか? 試してみます。 これでうまく行きそうな気がしますが、今度はまた別の問題が発生します。 しかも、例外で落ちることもなく沈黙する最悪のパターンです。 何か手掛かりは無いか、VisualStudio(2015)の診断ツールで「イベント」を確認すると、何やら謎の例外がスローされて直後にキャッチされているのが分かります。 どうやら、 この EndInig()メソッドの内部で 色々試してみましたが、今回のようなアプローチの仕方ではうまく行きそうにありません。 「UriSource を使った一連の処理は、UIスレッド以外で実行しないようにしましょう。」 では、非同期で処理できないの?というわけではなく、別の方法があります。 以下の様に、画像データを先にダウンロードしておき、MemoryStream でBitmapImageに流してやればOKです。 Web検索等で調べると、このようなやり方が一般的のようですね。 しかし、これにもまだ問題があります。
上のはじめに
BitmapImageの初期化にUriSourceを使うとUIがブロックされる
UriSourceプロパティに画像のURLを指定するだけで、画像データのダウンロードとBitmapへのデコードをBitmapImage自身がやってくれます。public BitmapImage CreateBitmapImage(Uri sourceUri)
{
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = sourceUri;
bitmap.EndInit();
return bitmap;
}
// 以下の様にコンストラクタにURIを渡すのと同じ
// var bitmap = new BitmapImage(sourceUri);
実際に、上記のコードを使って複数の画像を連続してダウンロードすると、すべての画像が生成されるまでUIの表示が更新されず、操作もできない状態になります。
(ダウンロード完了を待たずにEndInit()を抜けて、DownloadCompletedイベントで完了通知を受け取るようになっているので、ダウンロード自体はバックグラウンドで行われているように見えるのですが)非同期にしてみる
public Task<BitmapImage> CreateBitmapImageAsync(Uri sourceUri)
{
return Task.Run(()=> {
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = sourceUri;
bitmap.EndInit();
return bitmap;
}
}
DependencySource は、DependencyObject と同じ Thread 上で作成する必要があります。
例えば、以下の様にEndInit() の後に Freeze しようとしても、この時点では画像のダウンロードが完了していないので、Freezeできません。public Task<BitmapImage> CreateBitmapImageAsync(Uri sourceUri)
{
return Task.Run(()=> {
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = sourceUri;
bitmap.EndInit();
bitmap.Freeze(); //初期化が終わっていないのでFreezeできません!!
return bitmap;
}
}
ダウンロード完了を待ってからFreezeする
ダウンロードの完了は DownloadCompleted イベントで受け取れます。
また、ダウンロード中に発生した例外は、DownloadFailed イベントで補足できます。private ObservableCollection images = new ObservableCollection();
private Task CreateBitmapImage(Uri sourceUri)
{
return Task.Run(()=> {
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = sourceUri;
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.DownloadCompleted += (sender,args) => {
var image =(BitmapImage)sender;
image.Freeze(); //Freezeしてから
images.Add(image); //Binding Sourceのコレクションに追加
};
bitmap.DownloadFailed += (sender,args) => {
//失敗したらここに来る(はず)
Trace.WriteLine($"DL Failed. [{args.ErrorException}]");
};
bitmap.EndInit();
}
}
UIスレッド以外で実行すると謎の沈黙
System.Deployment.Application.InvalidDeploymentException
なる例外が発生して、尚且つそれが握りつぶされているのが原因のようです。InvalidDeploymentException
ってなんなんでしょう? 例外発生時の呼び出し履歴を見てみます。System.Deployment.dll!System.Deployment.Application.ApplicationDeployment.CurrentDeployment.get()
System.Deployment.dll!System.Deployment.Application.ApplicationDeployment.IsNetworkDeployed.get()
PresentationCore.dll!MS.Internal.AppModel.SiteOfOriginContainer.SiteOfOriginForClickOnceApp.get()
PresentationCore.dll!MS.Internal.SecurityHelper.ExtractUriForClickOnceDeployedApp()
PresentationCore.dll!MS.Internal.SecurityHelper.BlockCrossDomainForHttpsApps(System.Uri uri = {不明})
PresentationCore.dll!System.Windows.Media.Imaging.BitmapDownload.BeginDownload(System.Windows.Media.Imaging.BitmapDecoder decoder = {不明}, System.Uri uri = {不明}, System.Net.Cache.RequestCachePolicy uriCachePolicy = {不明}, System.IO.Stream stream = {不明})
PresentationCore.dll!System.Windows.Media.Imaging.LateBoundBitmapDecoder..ctor(System.Uri baseUri = {不明}, System.Uri uri = {不明}, System.IO.Stream stream = {不明}, System.Windows.Media.Imaging.BitmapCreateOptions createOptions = {不明}, System.Windows.Media.Imaging.BitmapCacheOption cacheOption = {不明}, System.Net.Cache.RequestCachePolicy requestCachePolicy = {不明})
PresentationCore.dll!System.Windows.Media.Imaging.BitmapDecoder.CreateFromUriOrStream(System.Uri baseUri = {不明}, System.Uri uri = {不明}, System.IO.Stream stream = {不明}, System.Windows.Media.Imaging.BitmapCreateOptions createOptions = {不明}, System.Windows.Media.Imaging.BitmapCacheOption cacheOption = {不明}, System.Net.Cache.RequestCachePolicy uriCachePolicy = {不明}, bool insertInDecoderCache = {不明})
PresentationCore.dll!System.Windows.Media.Imaging.BitmapImage.FinalizeCreation()
PresentationCore.dll!System.Windows.Media.Imaging.BitmapImage.EndInit()
LateBoundBitmapDecoder
が生成されて、そのコンストラクタ内でBitmapDownload.BeginDownload()
が実行され、 SecurityHelper.BlockCrossDomainForHttpsApps()
メソッド内でClickOnce Deployed がなんちゃら・・・・
うーん、よくわかりません。
これ以上の深追いはやめておきます。UriSourceを使ったBitmapImageの初期化はUIスレッドで
別のアプローチ
public async Task<BitmapImage> DownloadImageAsync(string url)
{
using (var web = new HttpClient())
{
var bytes = await web.GetByteArrayAsync(url).ConfigureAwait(false);
using (var stream = new MemoryStream(bytes))
{
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.StreamSource = stream;
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.EndInit();
bitmap.Freeze();
return bitmap;
}
}
}
(だったら最初からそうしろってことですが、結果がダメでもいろいろ試してみることに意義があるという事で)DownloadImageAsync()
を実行するとメモリリークを引き起こすのですが、長くなってきたので続きは次回!