WPFでReactiveProperty入門 ~ Rxを使って検索結果のサムネイル画像を一括ダウンロードする

この記事は、WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る - pierre3のブログ の続きです。

関連記事

  1. WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る - pierre3のブログ
  2. WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る (1. 検索バーの実装) - pierre3のブログ

サンプルプロジェクト

github.com

今回は、Bing画像検索を実行し、検索結果からサムネイル画像を取得して一覧表示する処理について、Model側に焦点を当てて見てみたいと思います。

目次

Modelの概要

今回は、Model側の処理がメインになりますので、Model側の主要クラスについて簡単にまとめておきます。

クラス 説明
Bing.BingSearchContainer Microsoftが提供するBing Search APIのプロキシクラス
Bing.ImageResult Bing Search APIが返す画像検索結果のクラス。検索結果の画像1枚の情報が格納されている
WebImage 検索結果の画像1枚を表すクラス。Bing.ImageResultに格納されている画像URLから画像データをダウンロード後、BitmapImageとして保持する
WebImageStore WebImageのコレクションを管理するクラス。画像検索を実行し、検索結果からWebImageを生成してコレクションに保持する
WebImageHelper WebImage、WebImageStoreクラスを補助するメソッドを提供するクラス

http://pierre3net.azurewebsites.net/Content/image/Models.svg

Bing Search API プロキシクラス

Bing Search API をAzure Market Placeから購入(無料含む)すると、AzureポータルからBing Searchのプロキシクラス(BingSearchContainer.csファイル)をダウンロードして使用することが出来ます。

今回はこれをそのまま使用させて頂きました。

BingSearchContainer

このプロキシクラスは、WCFデータサービスの仕組みを利用しており、System.Data.Services.Client.DataServiceContextを継承したBingSearchContainerクラスを用いて検索を行います。

画像検索の実行には、Image() メソッドを使用します。 引数で検索条件等を色々指定できるのですが、とりあえずは第1引数に検索ワードを入れたら後はnullでOKです。

※ パラメータの詳細は、下記リンクの"Optional Parameters" の項目で確認できます。 https://msdn.microsoft.com/en-us/library/dd250942.aspx

//Bing Image検索プロキシクラス
public class BingSearchContainer : System.Data.Services.Client.DataServiceContext
{
    //画像検索用のクエリを発行
    public DataServiceQuery<ImageResult> Image(String Query, String Options, 
        String Market, String Adult, Double? Latitude, Double? Longitude, String ImageFilters)
    {
        DataServiceQuery<ImageResult> query;
        query = base.CreateQuery<ImageResult>("Image");
        //...(中略)...
        return query;
    }
}

Image()メソッドは、画像検索用クエリを表す DataServiceQuery<ImageResult> を返却します。
DataServiceQuery<ImageResult>Execute()メソッドでクエリを実行すると、検索結果がIEnumerable<ImageResult>のかたちで返却されます。

var bing = new Bing.BingSearchContainer(new Uri("https://api.datamarket.azure.com/Bing/search/"));
bing.Credentials = new NetworkCredential("accountKey", accountKey);
//画像検索用のクエリ(DataServiceQuery<Bing.ImageResult>)を発行
var query = bing.Image(searchWord, null, null, null, null, null, null);
//検索実行
IEnumerable<T> result = query.Execute();

ImageResult

画像検索結果が格納されるImageResultクラスは以下のプロパティを持ちます

//画像検索の結果を格納するクラス
public class ImageResult
{
    public Guid ID { get; set; }
    public String Title { get; set; }
    public String MediaUrl { get; set; }
    public String SourceUrl { get; set; }
    public String DisplayUrl { get; set; }
    public Int32? Width { get; set; }
    public Int32? Height { get; set; }
    public Int64? FileSize { get; set; }
    public String ContentType { get; set; }
    public Thumbnail Thumbnail { get; set; }
}
public class Thumbnail
{
    public String MediaUrl { get; set; }
    public String ContentType { get; set; }
    public Int32? Width { get; set; }
    public Int32? Height { get; set; }
    public Int64? FileSize { get; set; }
}

MediaUrlに画像データをダウンロードするためのURLが格納されています。
また、画像サイズを縮小したサムネイル用の画像データを取得することが可能で、その場合はThumbnailプロパティのMediaUrlを使用します。

検索結果のシーケンスからサムネイル画像をまとめて非同期ダウンロードする。

IEnumerable<ImageResult> として返される全ての検索結果(最大50件)のサムネイル画像を一度にダウンロードするのですが、(当然ながら)UIをブロックしないよう非同期で行う必要があります。

Reactive Extensions で非同期ダウンロード

上記のような非同期操作の実装はいくつか考えられますが、今回はReactive Extensionsを使用した方法で実装を試してみたいと思います。

IEnumerable<ImageResult> から IObservable<ImageResult> に変換

IE から IO への変換は、ToObservable() 拡張メソッドを使用します。

今回のサンプルでは、検索ワードを渡すとBing画像検索を実行して結果をIObservable<ImageResult>で返すヘルパーメソッドWebImageHelperクラスに用意しています。

ここでは、DataServiceQuery<ImageResult>をExecute()して得られるIEnumerable<ImageResult>ToObservable()IObserbable<ImageResult> に変換たものをreturn しています。

static class WebImageHelper
{
    //Bing Image 検索を実行して 結果を IObservable<T> で返す
    public static IObservable<Bing.ImageResult> SearchImageAsObservable(
        string searchWord, string accountKey, int skip, int top)
    {
        var bing = new Bing.BingSearchContainer(
            new Uri("https://api.datamarket.azure.com/Bing/search/"));
        bing.Credentials = new NetworkCredential("accountKey", accountKey);
        var query = bing.Image(searchWord, null, null, null, null, null, null);
        query = query.AddQueryOption("$skip", skip);
        query = query.AddQueryOption("$top", top);
        try
        {
            IEnumerable<T> result = query.Execute();
            return result.ToObservable();
        }
        catch (Exception e)
        {
            return Observable.Throw<Bing.ImageResult>(e);
        }
    }
}

例外をIObserbable<T> に乗せるには Observable.Throw<T>()

検索実行中に発生した例外は、そのままでは IO<T> の一連の流れの中では補足できませんが、 Observable.Throw<T>() メソッドを使用することで、例外をIObservable<T> として扱うことが可能になります。

上記の例では、検索実行中に発生した例外は IObservable<ImageResult> で包まれて、購読側のOnError()メソッドで補足できるようになります。

SelectMeny()で非同期メソッドを実行する

Rxで流れてきた検索結果オブジェクトImageResult を基にサムネイル画像のダウンロードを行う処理は、以下のメソッドを用いて行います。

ここでは、ImageResult オブジェクトを引数にWebImageインスタンスを生成後そのDownLoadThumbnailAsync()メソッドでサムネイル画像のダウンロードを非同期に行います。戻り値はTask<WebImage> となります。

public class WebImageStore : IDisposable
{
    //検索結果オブジェクト(ImageResult)でWebImageのインスタンスを生成後、
    //ImageResultに格納されているサムネイル画像のURLから画像データをダウンロードする
    private async Task<WebImage> CreateWebImageAsync(Bing.ImageResult bingResult)
    {
        var image = new WebImage(bingResult, logger);
        await image.DownLoadThumbnailAsync();
        return image;
    }
}

Rxの中で非同期メソッドを実行するには、SelectMeny() を使用します。
SelectMeny()に渡すデリゲートをasync にして、その中で await CreateWebImageAsync() とします。

こうすることで、CreateWebImageAsync() メソッドは非同期かつ並列的に実行されます。実行の結果は、完了した順に後続へ渡ります。

この一連の処理をSubscribe()すると、OnNext()にサムネイルのダウンロードが完了したWebImageオブジェクトが渡ってくるので、これを ObservableCollection に逐次追加して行きます。

以下に、実装例を示します。(実際には進捗通知等の処理が入り、もう少し複雑になっていますが、ここでは説明のため簡略化しています)

public class WebImageStore : IDisposable
{
    private IDisposable disposable;
    private ObservableCollection<WebImage> imagesSource;
    public ReadOnlyObservableCollection<WebImage> readonlyImages;
    public IReadOnlyObservableCollection<WebImage> Images {get {return readonlyImages;}}

    public void DownloadSearchResult(string searchWord)
    {
        disposable = WebImageHelper.SearchImageAsObservable(searchWord, bingAccountKey)
            .SelectMany(async bingResult => await CreateWebImageAsync(bingResult))
            .Where(webImage => webImage?.Thumbnail != null)
            .Subscribe(
                onNext: webImage =>
                {
                    //ObservableCollection に追加
                    imagesSource.Add(webImage);
                },
                onError: e =>
                {
                    //SearchImageAsObservable()内でObservable.Throw()した例外はここで補足
                    logger.Error("画像の検索に失敗しました。", e);
                },
                onCompleted: () =>
                {
                    logger.Info("検索が完了しました。");
                });
    }
}

まとめ

これで、検索を実行して、結果をサムネイルの一覧で表示するための枠組みが出来ました。
次回は、URLからサムネイルの画像データをダウンロードする処理と、そのデータを元にBitmapImageオブジェクトを生成する処理の詳細について書きたいと思います。