Neutral Scent

App developments & Gadgets

リスト画像を遅延ロード&分離ストレージにキャッシュする

どうも、最近ListBoxのテンプレートはBlendを使うよりXAMLを手書きした方が早くなってきた方、kaorunです。
WP7のSilverlightで画像を多数ロードするアプリを作成していると、ロードの遅さにイライラっと来ることになります。3G回線が遅いのはしょうがないですよね。
なので、いちいち画像をロードせずIsolatedStorage(分離ストレージ)にファイルをダウンロードしてからリストボックス等へ表示するためのヘルパーを書いてみました。
核心部分はこんな感じ、



public void DownloadFile(Uri uri, Action<string> callback)
{
string filename = FileDownloader.CachePath + "/" + Path.GetFileName(uri.LocalPath);

using(IsolatedStorageFile iso = IsolatedStorageFile.GetUserStoreForApplication())
{
if (iso.FileExists(filename))
{
callback(filename);
return;
}
}

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
Observable.FromAsyncPattern<WebResponse>(request.BeginGetResponse, request.EndGetResponse)()
.Select(res => res.GetResponseStream())
.ObserveOnDispatcher()
.Subscribe(src =>
{
using (IsolatedStorageFile iso = IsolatedStorageFile.GetUserStoreForApplication())
{
if (!iso.DirectoryExists(FileDownloader.CachePath))
iso.CreateDirectory(FileDownloader.CachePath);
if (!iso.FileExists(filename))
{
using (var dst = iso.CreateFile(filename))
{
byte[] buffer = new byte[src.Length];
src.Read(buffer, 0, (int)src.Length);
dst.Write(buffer, 0, (int)src.Length);
dst.Close();
src.Close();
}
}
}
callback(filename);
}
, e =>
{
Debug.Assert(false, e.Message);
});
}

Rxでファイルを取得してきて、IsolatedStorageに保存します。
ファイル名が重複すればスキップするし、バッファ・例外処理もてきとうなので、そのあたりはお好みで。
呼ぶ側は、


public class Picture : INotifyPropertyChanged
{
private FileDownloader loader = new FileDownloader();

public Picture()
{
this.loader.ImageLoaded += new FileDownloader.ImageLoadedHandler(loader_ImageLoaded);
}

void loader_ImageLoaded(ImageSource image, EventArgs e)
{
this.image = image;
}

public void RequestImage(Uri uri)
{
this.loader.RequestImage(uri);
}

private ImageSource imageVal;
public ImageSource image
{
get { return imageVal; }
set
{
if (value != imageVal)
{
imageVal = value;
NotifyPropertyChanged("image");
}
}
}

...
}

こんな感じ。
ItemViewModelを作成時にダミーのimageを入れておけば遅延ロード中のプレースホルダーも簡単。詳しくはサンプルプロジェクトを参照の事。
Bindingするにあたってプロジェクトコンテンツの画像ファイルとIsolatedStorageの関係はどうなっているのか? と思ったらUriオブジェクトのUriKindで切り替えるだけでした。簡単ですね。
クラス全体:



using System;
using System.Net;
using System.Windows.Media;
using System.IO;
using System.IO.IsolatedStorage;
using Microsoft.Phone.Reactive;
using Microsoft.Phone;
using System.Diagnostics;

namespace ImageLoaderSample1.Utils
{
public class FileDownloader
{
public delegate void ImageLoadedHandler(ImageSource image, EventArgs e);
public delegate void FileDownloadCompletedHandler(string filename, EventArgs e);
public readonly static string CachePath = "/cache";

public event ImageLoadedHandler ImageLoaded;
public event FileDownloadCompletedHandler FileDownloadCompleted;

public void InvokeImageLoaded(ImageSource image)
{
if (this.ImageLoaded != null)
ImageLoaded(image, new EventArgs());
}

public void InvokeFileDownloadCompleted(string filename)
{
if (this.FileDownloadCompleted != null)
FileDownloadCompleted(filename, new EventArgs());
}

public void RequestImage(Uri uri)
{
DownloadFile(uri, f => InvokeImageLoaded(LoadImageFromIsolatedStorage(f)));
}

public void DownloadFile(Uri uri)
{
DownloadFile(uri, filename => InvokeFileDownloadCompleted(filename));
}

public void DownloadFile(Uri uri, Action<string> callback)
{
string filename = FileDownloader.CachePath + "/" + Path.GetFileName(uri.LocalPath);

using(IsolatedStorageFile iso = IsolatedStorageFile.GetUserStoreForApplication())
{
if (iso.FileExists(filename))
{
callback(filename);
return;
}
}

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
Observable.FromAsyncPattern<WebResponse>(request.BeginGetResponse, request.EndGetResponse)()
.Select(res => res.GetResponseStream())
.ObserveOnDispatcher()
.Subscribe(src =>
{
using (IsolatedStorageFile iso = IsolatedStorageFile.GetUserStoreForApplication())
{
if (!iso.DirectoryExists(FileDownloader.CachePath))
iso.CreateDirectory(FileDownloader.CachePath);
if (!iso.FileExists(filename))
{
using (var dst = iso.CreateFile(filename))
{
byte[] buffer = new byte[src.Length];
src.Read(buffer, 0, (int)src.Length);
dst.Write(buffer, 0, (int)src.Length);
dst.Close();
src.Close();
}
}
}
callback(filename);
}
, e =>
{
Debug.Assert(false, e.Message);
});
}

private ImageSource LoadImageFromIsolatedStorage(string imageName)
{
try
{
using (IsolatedStorageFile iso = IsolatedStorageFile.GetUserStoreForApplication())
{
using (IsolatedStorageFileStream stream = iso.OpenFile(imageName, FileMode.Open, FileAccess.Read))
{
return PictureDecoder.DecodeJpeg(stream);
}
}
}
catch (Exception ex)
{
Debug.Assert(false, ex.Message);
return null;
}
}
}
}

サンプルプロジェクト: ImageLoaderSample1.zip
リポジトリ: https://bitbucket.org/kaorun/kaorun_samples/src/c2ec642f938a/WP7/ImageLoaderSample1/
Flickrの新着からcatタグの付いた写真のATOMフィードを取得して、ViewModelのPicturesに割り当て、ListBoxに{Binding}しています。
例によって、根本的にアプローチが間違ってるとか、もっとスマートなやり方をご存じの方がおられましたら随時ツッコミをお願いします。