前回まででおよその形は出来上がりました。まぁいまどきのアプリってSilverlightとかだわなぁってことでSL、WP7から使えるようにライブラリをこさえてみようと思ったんですがうまくいかない。
WebClientで同期でとっちゃってるんでそりゃ駄目だわな。非同期に書き換えるとして。。。IEnumerableで返せん。。。Linq構文あきらめてただのWebClientの非同期サンプルにする?それは負けだろう。
…Rxだな。。。
とはいえRXで非同期をなんてサンプルはどこにでもある話。やるなら噂で聞いたIQbservableかなぁと漠然と思いながらneueさんのRxのライブラリ紹介とかを見ながらお勉強。どうせなら同じソースで動かしたいよねぇってことでRxを落としてくる。が、ここで問題発生。どうもWP7にはIQbservableがなさそうな予感。。。色々悩んだ結果以前の部分でIEnumerableを返してたのでTの部分を非同期リクエストの戻りであるIObservableを内包した形で返すことに。合わせてResult周りの型を変更。GourmetResult : ResultBase>な感じに。
ではプロジェクトのほうを整えることに。Coreに切りだしていた部分はそのまま使うのでリンクで追加。あ、そうそうWP7にはExpressionVisitorがないんですがMSDNのサイトにソースが全部あるので追加。プロバイダ周りは書き換えるのでコピーで追加しました。全体像してはこんな感じ。

PagingQueryProviderを書き換えていくことにします。肝としてはWebClientで動機リクエストしてたところをIObservable化するところってことですが@ITにずばりな感じの入門記事があったのでそれを参考に。
第1回 Reactive Extensionsの概要と利用方法
WebClientにDownloadStringAsyncってのが追加されてIObservableで返ってきますのでその後パースしてってところは前と同じ。
var req = WebRequest.Create(executeUrl);
var o = req.DownloadStringAsync().Select(_ =>
{
var element = XDocument.Parse(_);
var ns = element.Root.GetDefaultNamespace();
//エラーチェック
var error = element.Descendants(ns + "error").FirstOrDefault();
if (error != null) throw new InvalidQueryException(new Error(ns, error).Message);
//クエリー条件にマッチする、検索結果の全件数
this.limit = int.Parse(element.Descendants(ns + "results_available").FirstOrDefault().Value);
//このXMLに含まれる検索結果の件数クエリー条件にマッチする、検索結果の全件数
this.returnedCount = int.Parse(element.Descendants(ns + "results_returned").FirstOrDefault().Value);
//検索結果の開始位置
var resultStart = int.Parse(element.Descendants(ns + "results_start").FirstOrDefault().Value);
var collection = element.Descendants(ns + name);
//取得件数が指定されているなら補正する
if (takeCount.HasValue)
{
var endLimit = takeCount.Value + start;
this.limit = Math.Min(endLimit, this.limit);
}
return collection.Select(x => innerBuilder(x)).ToArray();
});
こんな感じです。パース後にDescendantsするとXElement[]になるんですが途中部分でサクッと変換はやりにくいので要素一個のファクトリメソッドを受け取るようにしておきました。(FuncなinnnerBuilderってやつです)これでIObservableって形になるのでこれを前回の結果のIEnumerableを内包した結果で返します。
継続部分をとる必要がありますが以前はIEnumerableだったので普通にConcatでした。今回はIObservableとIEnumerable>をつないで返すことに。これはEnumerable.Repeat(o, 1).Concat(GetResultCore(url))でおけ。
でとりだしの部分ですがIObservableとIEnumerable、配列が混在なのでちょっとややこしいですが分けて考えれば大丈夫。
var context = new HotpepperContext();
var query = (
from result in context.Gourmet
where result.keyword == this.textBox1.Text
select result).Take(100).First().Results.Concat();
var subscribe = (
from results in query
from shop in results
select shop)
.ObserveOnDispatcher()
.Subscribe(_ =>
{
count++;
this.textBlock1.Text = count.ToString();
this.listBox1.Items.Add(_);
});
こうなります。前半は以前と同様IQeryableを使ってるところ。IEnumerableを含んだ(Resultsがそれ)結果がひとつ返るのでFirstしてResultsをConcat。この操作でIEnumerable>がIObservableに平滑化されます。このまま続けて書いてもいいのですがここまではIQueryable、これからはIObservableという違いとT[]で返るのでSelectManyがわかりやすいように分けてクエリ構文で記載してみました。

感触ですがかなり簡単に取れるのでいい感じ。もろもろまとめて公開予定ですがひとまずPageQueryProviderのソースだけでも乗っけときますね。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Xml.Linq;
using System.Xml;
using System.Net;
using System.Reactive.Linq;
using System.IO;
namespace LinqToHotpepper
{
///
/// ファクトリクラス
///
public static class PagingQueryProvider
{
///
/// ファクトリメソッド
///
/// 発行されたAPIキー
/// リクエストURL
/// 抽出対象のXML要素名
/// 結果セットのファクトリ
/// 結果のファクトリ
public static PagingQueryProvider Create(string apiKey, string baseUrl, string name, Func<IEnumerable<IObservable>, T1> builder, Func<XElement, T2> innerBuilder)
{
return new PagingQueryProvider(apiKey, baseUrl, name, builder, innerBuilder, null);
}
///
/// ファクトリメソッド
///
/// 発行されたAPIキー
/// リクエストURL
/// 抽出対象のXML要素名
/// 結果のファクトリ
/// 結果セットのファクトリ
/// 要素から抽出を行うFinder
public static PagingQueryProvider Create(string apiKey, string baseUrl, string name, Func<IEnumerable<IObservable>, T1> builder, Func<XElement, T2> innerBuilder, EqualsFinder finder)
{
return new PagingQueryProvider(apiKey, baseUrl, name, builder, innerBuilder, finder);
}
}
public class PagingQueryProvider : QueryProviderBase
{
/// 発行されたAPIキー
private string key;
/// リクエストURL
private string baseUrl;
/// 要素から抽出を行うFinder
private EqualsFinder finder;
/// 抽出対象のXML要素名
private string name;
/// 結果セットのファクトリ
private Func<IEnumerable<IObservable>, T1> resultBuilder;
/// 結果セットのファクトリ
private Func<XElement, T2> innerBuilder;
/// 開始件数
private int start;
/// 取得中件数
private int index;
/// 取得終了件数
private int limit;
/// 取得時の戻り件数(初回リクエスト時の戻り件数)
private int returnedCount;
/// パラメータで指定された取得件数
private int? takeCount;
/// デフォルトの取得件数
private const int DEFAULT_COUNT = 30;
///
/// コンストラクタ
///
/// 発行されたAPIキー
/// リクエストURL
/// 抽出対象のXML要素名
/// 結果のファクトリ
/// 要素から抽出を行うFinder
public PagingQueryProvider(string apiKey, string baseUrl, string name, Func<IEnumerable<IObservable>, T1> builder, Func<XElement, T2> innerBuilder, EqualsFinder finder)
{
this.start = 0;
this.key = apiKey;
this.baseUrl = baseUrl;
this.name = name;
this.resultBuilder = builder;
this.innerBuilder = innerBuilder;
this.finder = finder;
}
protected override IEnumerable GetResult(Expression expression)
{
InnermostFinder innerFinder = new InnermostFinder();
var sb = new StringBuilder(baseUrl);
sb.AppendFormat("?key={0}", key);
if (finder != null)
{
MethodCallExpression whereExpression = innerFinder.GetInnermostWhere(expression);
if (whereExpression != null)
{
LambdaExpression lambdaExpression = (LambdaExpression)((UnaryExpression)(whereExpression.Arguments[1])).Operand;
// Send the lambda expression through the partial evaluator.
lambdaExpression = (LambdaExpression)Evaluator.PartialEval(lambdaExpression);
finder.Expression = lambdaExpression.Body;
foreach (var item in finder.Results)
{
sb.AppendFormat("&{0}={1}", item.Key, item.Value);
}
}
}
MethodCallExpression skipExpression = innerFinder.GetInnermostSkip(expression);
if (skipExpression != null)
{
this.start = int.Parse(ExpressionTreeHelpers.GetValueFromExpression(skipExpression.Arguments[1]));
}
MethodCallExpression takeExpression = innerFinder.GetInnermostTake(expression);
if (takeExpression != null)
{
this.takeCount = int.Parse(ExpressionTreeHelpers.GetValueFromExpression(takeExpression.Arguments[1]));
}
var url = sb.ToString();
var executeUrl = string.Format("{0}&start={1}",url, start + 1);
if (takeCount.HasValue) executeUrl = string.Format("{0}&count={1}", executeUrl, takeCount.Value);
var req = WebRequest.Create(executeUrl);
var o = req.DownloadStringAsync().Select(_ =>
{
var element = XDocument.Parse(_);
var ns = element.Root.GetDefaultNamespace();
//エラーチェック
var error = element.Descendants(ns + "error").FirstOrDefault();
if (error != null) throw new InvalidQueryException(new Error(ns, error).Message);
//クエリー条件にマッチする、検索結果の全件数
this.limit = int.Parse(element.Descendants(ns + "results_available").FirstOrDefault().Value);
//このXMLに含まれる検索結果の件数クエリー条件にマッチする、検索結果の全件数
this.returnedCount = int.Parse(element.Descendants(ns + "results_returned").FirstOrDefault().Value);
//検索結果の開始位置
var resultStart = int.Parse(element.Descendants(ns + "results_start").FirstOrDefault().Value);
var collection = element.Descendants(ns + name);
//取得件数が指定されているなら補正する
if (takeCount.HasValue)
{
var endLimit = takeCount.Value + start;
this.limit = Math.Min(endLimit, this.limit);
}
return collection.Select(x => innerBuilder(x)).ToArray();
});
yield return resultBuilder(Enumerable.Repeat(o, 1).Concat(GetResultCore(url)));
}
///
/// 繰り返し部分の取得実体
///
///
///
private IEnumerable<IObservable> GetResultCore(string url)
{
//次回取得位置を初期化
index = start + returnedCount;
//取得ループ開始
while (true)
{
//欲しい件数が取れているなら終了
if (limit < index + 1) yield break;
var executeUrl = string.Format("{0}&start={1}", url, start + 1);
//残りカウントを取得
var leftCount = limit - (index + returnedCount);
//取得件数より小さければ取得件数を減らす
if (leftCount < returnedCount) executeUrl = string.Format("{0}&count={1}", executeUrl, leftCount);
var req = WebRequest.Create(executeUrl);
var o = req.DownloadStringAsync().Select(_ =>
{
var element = XDocument.Parse(_);
var ns = element.Root.GetDefaultNamespace();
//エラーチェック
var error = element.Descendants(ns + "error").FirstOrDefault();
if (error != null) throw new InvalidQueryException(new Error(ns, error).Message);
//クエリー条件にマッチする、検索結果の全件数
var total = element.Descendants(ns + "results_available").FirstOrDefault();
// このXMLに含まれる検索結果の件数クエリー条件にマッチする、検索結果の全件数
var returned = element.Descendants(ns + "results_returned").FirstOrDefault();
//結果がなければ終了
if (total == null || total.Value == "0" || returned == null || returned.Value == "0") Observable.Empty<XElement[]>();
var collection = element.Descendants(ns + name);
index += int.Parse(returned.Value);
return collection.Select(x => innerBuilder(x)).ToArray();
});
yield return o;
}
}
}
}
public static class WebRequestExtensions
{
public static IObservable<string> DownloadStringAsync(this WebRequest request)
{
return Observable.Defer(() => Observable.FromAsyncPattern<WebResponse>(
request.BeginGetResponse, request.EndGetResponse)()
.Select(res =>
{
using (var stream = res.GetResponseStream())
using (var sr = new StreamReader(stream))
{
return sr.ReadToEnd();
}
}));
}
}
コーディング量はそうないもののいい感じにWP7対応できました。
※ひとまず今までの一式。WP7位置情報とか簡単に取れて面白いかも。