Archive - WindowsPhone7
.NETの標準シリアライザ(XML/JSON)の使い分けまとめ
- C# Silverlight WindowsPhone7 - 11.12/10
今年もAdvent Calendarの季節がやってきましたね。去年は私はC#とJavaScriptで書きましたが、今年はC#とSilverlightでやります。というわけで、この記事はSilverlight Advent Calendar 2011用のエントリです。前日は@posauneさんのSilverlightのListBoxでつくるいんちきHorizontalTextBlock でした。
今回の記事中のサンプルはSilverlight 4で書いています。が、Silverlight用という体裁を持つためにDebug.WriteLineで書いているというだけで、Silverlightらしさは皆無です!えー。.NET 4でもWindows Phone 7でも関係なく通じる話ですねん。
シリアライザを使う場面
概ね3つではないでしょうか。外部で公開されているデータ(APIをネット経由で叩くとか)をクラスに変換する。これは 自分の管理外→プログラム での片方向です。内部で持っているデータ(クラスのインスタンス)を保存用・復元用に相互変換する。これは プログラム←→自分の管理内 での双方向です。最後に、内部で持っているデータを公開用に変換する。これは プログラム→外部 での片方向。
目的に応じてベストな選択は変わってきます。こっから延々と長ったらしいので、まず先に結論のほうを。
- 外部APIを叩く→XML/XmlSerializer, JSON/DataContractJsonSerializer
- オブジェクトの保存・復元用→DataContractSerializer
- 外部公開→さあ?
外部公開のは、Silverlightの話じゃないので今回はスルーだ!XStreamingElementで組み上げてもいいし、何でもいいよ!WCFのテンプレにでも従えばいいんぢゃないでしょーか。
XmlSerializer
古くからあるので、シリアライザといったらこれ!という印象な方も多いのではないでしょうか。その名の通り、素直にXMLの相互変換をしてくれます。
// こんなクラスがあるとして // (以降、断り書きなくPersonが出てきたらこいつを使ってると思ってください) public class Person { public string Name { get; set; } public int Age { get; set; } }
// データ準備 var data = new Person { Name = "山本山", Age = 99 }; var serializer = new XmlSerializer(typeof(Person)); using (var ms = new MemoryStream()) { serializer.Serialize(ms, data); // シリアライズ // 結果確認出力 var xml = Encoding.UTF8.GetString(ms.ToArray(), 0, (int)ms.Length); Debug.WriteLine(xml); ms.Position = 0; // 巻き戻して…… var value = (Person)serializer.Deserialize(ms); // デシリアライズ Debug.WriteLine(value.Name + ":" + value.Age); // 山本山:99 }
// 出力結果のXML <?xml version="1.0" encoding="utf-8"?> <Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <Name>山本山</Name> <Age>99</Age> </Person>
素直な使い勝手、素直な出力。いいですね。さて、しかし特に外部APIを叩いて手に入るXMLは名前PascalCaseじゃねーよ、とか属性の場合どうすんだよ、という場合も多いでしょう。細かい制御にはXmlAttributeを使います。
[XmlRoot("people")] public class People { [XmlElement("count")] public int Count { get; set; } [XmlArray("persons")] [XmlArrayItem("person")] public Person[] Persons { get; set; } } [XmlRoot("person")] public class Person { [XmlElement("name")] public string Name { get; set; } [XmlAttribute("age")] public int Age { get; set; } }
// データ準備 var data = new People { Count = 2, Persons = new[] { new Person { Name = "山本山", Age = 99 }, new Person { Name = "トマト", Age = 19 } } }; var xml = @" <people> <count>2</count> <persons> <person age=""14""> <name>ほむ</name> </person> <person age=""999""> <name>いか</name> </person> </persons> </people>"; var serializer = new XmlSerializer(typeof(People)); // シリアライズ using (var ms = new MemoryStream()) { serializer.Serialize(ms, data); Debug.WriteLine(Encoding.UTF8.GetString(ms.ToArray(), 0, (int)ms.Length)); } // デシリアライズ using (var sr = new StringReader(xml)) { var value = (People)serializer.Deserialize(sr); foreach (var item in value.Persons) { Debug.WriteLine(item.Name + ":" + item.Age); } } // 出力結果のXMLは↑に書いたXMLと同じようなものなので割愛
ちょっと属性制御が面倒ですが、それなりに分かりやすく書けます。他によく使うのは無視して欲しいプロパティを指定するXmlIgnoreかしら。さて、そんな便利なXmlSerializerですが、XML化するクラスに制限があります。有名所ではDictionaryがシリアライズできねえええええ!とか。小細工して回避することは一応可能ですが、そんな無理するぐらいなら使うのやめたほうがいいでしょう、シリアライザは別にXmlSerializerだけじゃないのだから。
というわけで、XmlSerializerの利用シーンのお薦めは、ネットワークから外部APIを叩いて手に入るXMLをクラスにマッピングするところです。柔軟な属性制御により、マッピングできないケースは(多分)ないでしょう。いや、分かりませんが。まあ、ほとんどのケースでは大丈夫でしょう!しかし、LINQ to XMLの登場により、手書きで変換するのも十分お手軽なってしまったので、こうして分かりにくい属性制御するぐらいならXElement使うよ、というケースのほうが多いかもしれません。結局、XML構造をそのまま映すことしかできないので、より細かく変換できたほうが良い場合もずっとあって。
実際、私はもう長いことXmlSerializer使ってない感じ。LINQ to XMLは偉大。
DataContractSerializer
割と新顔ですが、もう十分古株と言ってよいでしょう(どっちだよ)。XmlSerializerと同じくオブジェクトをXMLに変換するのですが、その機能はずっと強力です。Dictionaryだってなんだってシリアライズできますよ、というわけで、現在では.NETの標準シリアライザはこいつです。
// データ準備 var data = new Person { Name = "山本山", Age = 99 }; var serializer = new DataContractSerializer(typeof(Person)); using (var ms = new MemoryStream()) { serializer.WriteObject(ms, data); // シリアライズ // 結果確認出力 var xml = Encoding.UTF8.GetString(ms.ToArray(), 0, (int)ms.Length); Debug.WriteLine(xml); ms.Position = 0; // 巻き戻して…… var value = (Person)serializer.ReadObject(ms); // デシリアライズ Debug.WriteLine(value.Name + ":" + value.Age); // 山本山:99 }
<Person xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/SilverlightApplication34"><Age>99</Age><Name>山本山</Name></Person>
とまあ、使い勝手はXmlSerializerと似たようなものです。おお、出力されるXMLは整形されていません。整形して出力したい場合は
// 出力を整形したい場合はXmlWriter/XmlWriterSettingsを挟む using (var ms = new MemoryStream()) using (var xw = XmlWriter.Create(ms, new XmlWriterSettings { Indent = true })) { serializer.WriteObject(xw, data); xw.Flush(); var xml = Encoding.UTF8.GetString(ms.ToArray(), 0, (int)ms.Length); Debug.WriteLine(xml); }
<?xml version="1.0" encoding="utf-8"?> <Person xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/SilverlightApplication34"> <Age>99</Age> <Name>山本山</Name> </Person>
さて、結果をXmlSerializerと見比べてみるとどうでしょう。名前空間が違います。SilverlightApplication34ってありますね。これは、私がこのXMLを出力するのに使ったSilverlightプロジェクトの名前空間です。ワシのConsoleApplicationは221まであるぞ(整理しろ)。さて、ではこのXMLをデシリアライズするのに、別のアプリケーション・別のクラスで使ってみるとどうでしょう?
namespace TestSilverlightApp { public class Person { public string Name { get; set; } public int Age { get; set; } } public partial class MainPage : UserControl { public MainPage() { InitializeComponent(); var xml = @"<?xml version=""1.0"" encoding=""utf-8""?> <Person xmlns:i=""http://www.w3.org/2001/XMLSchema-instance"" xmlns=""http://schemas.datacontract.org/2004/07/SilverlightApplication34""> <Age>99</Age> <Name>山本山</Name> </Person>"; var serializer = new DataContractSerializer(typeof(Person)); using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(xml))) { // System.Runtime.Serialization.SerializationExceptionが起こってデシリアライズできない // 名前空間 'http://schemas.datacontract.org/2004/07/TestSilverlightApp' の要素 'Person' が必要です。 // 名前が 'Person' で名前空間が 'http://schemas.datacontract.org/2004/07/SilverlightApplication34' の 'Element' が検出されました。 var value = (Person)serializer.ReadObject(ms); } } } }
デシリアライズ出来ません。対象オブジェクトが名前空間によって厳密に区別されるからです。じゃあどうするのよ!というと、属性で名前空間を空、という指示を与えます。
// DataContract属性をクラスにつけた場合は // そのクラス内のDataMember属性をつけていないプロパティは無視される [DataContract(Namespace = "", Name = "person")] public class Person { [DataMember(Name = "name")] public string Name { get; set; } [DataMember(Name = "age")] public int Age { get; set; } }
// こんなプレーンなXMLも読み込める var xml = @" <person> <age>99</age> <name>山本山</name> </person>"; var serializer = new DataContractSerializer(typeof(Person)); using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(xml))) { var value = (Person)serializer.ReadObject(ms); Debug.WriteLine(value.Name + ":" + value.Age); }
属性面倒くせー、ですけれど、まあしょうがない。そうすれば外部からのXMLも読み込めるし、と思っていた時もありました。以下のようなケースではどうなるでしょうか?Personクラスは↑のものを使うとして。
// こんなさっきと少しだけ違うXMLがあるとして var xml = @" <person> <name>山本山</name> <age>99</age> </person>"; var serializer = new DataContractSerializer(typeof(Person)); using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(xml))) { var value = (Person)serializer.ReadObject(ms); Debug.WriteLine(value.Name + ":" + value.Age); // 結果は??? }
これは出力結果は「山本山:0」になります。Ageが0、つまり復元されませんでした。なぜかというと、XMLを見てください。nameが先で、ageが、後。DataContractSerializerは規程された順序に強く従います。DataMember属性のOrderプロパティで順序を与えるか、与えない場合はアルファベット順(つまりAgeが先でNameが後)となります。この辺はデータ メンバーの順序に書かれています。
と、いうような事情から、DataContractSerializerを外部XMLからの受け取りに使うのはお薦めしません。XmlSerializerなら順序無視なので大丈夫です。いや、普通は順序が変わったりなどしないだろう!と思わなくもなくもないけれど、意外とデタラメなのじゃないか、基本的にはお外からのデータが何もかも信用できるわけなどないのだ、とうがってしまい(TwitterのAPIとか胡散臭さいのを日常的に触っていると!)、厳しいかなって、思ってしまうのです。
しかし、オブジェクトの保存・復元用にはDataContractSerializerは無類の強さを発揮します。例えば設定用のクラスを丸ごとシリアライズ・デシリアライズとかね。iniにして、じゃなくてフツーはXMLにすると思いますが、それです、それ。Dictionaryだってシリアライズできるし、引数なしコントラクタがないクラスだってシリアライズできちゃうんですよ?
// とある引数なしコンストラクタがないクラス [DataContract] public class ToaruClass { [DataMember] public string Name { get; set; } public ToaruClass(string name) { Name = name; } }
var toaru = new ToaruClass("たこやき"); var serializer = new DataContractSerializer(typeof(ToaruClass)); using (var ms = new MemoryStream()) { serializer.WriteObject(ms, toaru); // シリアライズできるし ms.Position = 0; var value = (ToaruClass)serializer.ReadObject(ms); // デシリアライズできる Debug.WriteLine(value.Name); // たこやき }
ただし、対象クラスにDataContract属性をつけてあげる必要はあります。つけてないとシリアライズもデシリアライズもできません。
ちなみに何でコンストラクタがないのにインスタンス化出来るんだよ!というと、System.Runtime.Serialization.FormatterServices.GetUninitializedObjectを使ってインスタンス化しているからです(Silverlightの場合はアクセス不可能)。こいつはコンストラクタをスルーしてオブジェクトを生成する反則スレスレな存在です、というか反則です。チートであるがゆえに、対象クラスにはDataContract属性をつける必要があります。コンストラクタ無視してもいいよ、ということを保証してあげないとおっかない、というわけです。(GetUninitializedObjectメソッド自体は別に属性は不要で何でもインスタンス化できます、typeof(void)ですらインスタンス化できます、無茶苦茶である)
なお、このGetUninitializedObjectが使われるのはDataContract属性がついているクラスのみです。DataContract属性がついていなければ、普通のコンストラクタが呼ばれるし、逆にDataContract属性がついていると、例え引数をうけないコンストラクタがあったとしても、GetUninitializedObject経由となりコンストラクタは無視されます。DataContract属性を付ける時はコンストラクタ内でシリアライズで復元できない副作用のある処理をすべきではない。ということに注意してください。
また、.NET 4版ではprivateプロパティの値も復元できるのですが、Silverlightの場合は無理のようです。ということでフル.NETなら不変オブジェクトでもサクサク大勝利、と思ってたのですが、Silverlightでの不変オブジェクトのシリアライズ・デシリアライズは不可能のようです。保存したいなら、保存専用の代理のオブジェクトを立ててやるしかない感じでしょうかね。
そんなわけで微妙な点も若干残りはしますが、オブジェクトを保存するのにはDataContractSerializerがお薦めです。
DataContractとSerializable
シリアライズ可能なクラス、の意味でDataContract属性をつけているわけですが、じゃあSerializable属性は?というと、えーと、SerializableはSilverlightでは入っていなかったりするとおり、過去の遺物ですね。なかったということで気にしないようにしましょう。
DataContractJsonSerializer
今時の言語はJSONが簡単に扱えなきゃダメです。XMLだけ扱えればいい、なんて時代は過ぎ去りました。しかしC#は悲しいことに標準では……。いや、いや、SilverlightにはSystem.Jsonがありますね。しかし.NET 4にはありません(.NET 4.5とWinRTには入ります)。いや、しかし.NET 4にはDynamicJsonがあります(それ出していいならJSON.NETがあるよ、で終わりなんですけどね)。が、Windows Phone 7には何もありません。ああ……。
とはいえ、シリアライザならば用意されています。DataContractJsonSerializerです。
// データ準備 var data = new Person { Name = "山本山", Age = 99 }; var serializer = new DataContractJsonSerializer(typeof(Person)); using (var ms = new MemoryStream()) { serializer.WriteObject(ms, data); // シリアライズ // 結果確認出力 var xml = Encoding.UTF8.GetString(ms.ToArray(), 0, (int)ms.Length); Debug.WriteLine(xml); // {"Age":99,"Name":"山本山"} ms.Position = 0; // 巻き戻して…… var value = (Person)serializer.ReadObject(ms); // デシリアライズ Debug.WriteLine(value.Name + ":" + value.Age); // 山本山:99 }
使い勝手はDataContractSerializerと完全に一緒です。ただし、違う点が幾つか。名前空間が(そもそもJSONで表現不可能なので)なくなったのと、順序も関係なく復元可能です。
var json1 = @"{""Name"":""山本山"",""Age"":99}"; var json2 = @"{""Age"":99,""Name"":""山本山""}"; var serializer = new DataContractJsonSerializer(typeof(Person)); using (var ms1 = new MemoryStream(Encoding.UTF8.GetBytes(json1))) using (var ms2 = new MemoryStream(Encoding.UTF8.GetBytes(json2))) { var value1 = (Person)serializer.ReadObject(ms1); var value2 = (Person)serializer.ReadObject(ms2); Debug.WriteLine(value1.Name + ":" + value2.Age); Debug.WriteLine(value2.Name + ":" + value2.Age); }
というわけで、随分とDataContractSerializerよりも使い勝手が良い模様。いい話だなー。さて、難点は出力されるJSONの整形が不可能です。DataContractSerializerではXmlWriterSettingsで行えましたが、DataContractJsonSerializerではそれに相当するものがありません。というわけでヒューマンリーダブルな形で出力、とはならず、一行にドバーっとまとめて吐かれるのでかなり苦しい。
もう一つ、これは本当に大したことない差なのでどうでもいいのですが、DataContractSerializerのほうが速いです。理由は単純でDataContractSerializerに一枚被せる形でDataContractJsonSerializerが実装されているから。その辺の絡みで.NET 4にはJsonReaderWriterFactoryなどがあって、これを直に触ってJSON→XML変換をするとLINQ to XMLを通したJSONの直接操作が標準ライブラリのみで可能なのですが、Silverlight/Windows Phone 7では残念なことに触ることができません。
外部APIを叩いて変換する際に、シリアライズはお手軽で便利であると同時に、完全に同一の形のオブジェクトを用意しなければならなくて、かったるい側面もあります。LINQ to XML慣れしていると特に。そういった形でJSONを扱いたい場合、WP7ではJson.NETを使う、しかありません。使えばいいんぢゃないかな、どうせNuGetでサクッと入れられるのだし。
とはいえまあ、そう言うほど使いづらいわけでもないので、標準のみでJSONを扱いたいという場合は、DataContractJsonSerializerが第一にして唯一の選択肢になります。
JavaScriptSerializer
.NET Framework 4.0 Client Profileでは使えないのですが、FullならばSystem.Web.Extensionを参照することでJavaScriptSerializerが使えます。もはや完全にSilverlightと関係ないのでアレですが、少し見てみましょう。
var serializer = new JavaScriptSerializer(); var target = new { Name = "ほむほむ", Age = 14 }; var json = serializer.Serialize(target); // stringを返す
Serializeで文字列としてのJSONを返す、というのがポイントです。それと、シリアライザ作成時にtypeを指定しません。また、匿名型もJSON化することが可能です(これはDataContractSerializerでは絶対無理)。ただし、コンストラクタのないクラスのデシリアライズは不可能です。
中々使い勝手がいいですね!で、これは、リフレクションベースの非常に素朴な実装です。だから匿名型でもOKなんですねー。ちょっとした用途には非常に楽なのですが、Client Profileでは使えないこともありますし(ASP.NETで使うために用意されてる)、あまり積極的に使うべきものではないと思います。ちなみに、一時期ではObsoleteになっていてDataContractJsonSerializer使え、と出ていたのですが、またObsoleteが外され普通に使えるようになりました。やはり標準シリアライザとしてはDataContractJsonSerializerだけだと重すぎる、ということでしょうか。
バイナリとか
別にシリアライズってXMLやJSONだけじゃあないのですね。サードパーティ製に目を向ければ、色々なものがあります。特に私がお薦めなのはprotobuf-net。これはGoogleが公開しているProtocol Buffersという仕様を.NETで実装したものなのですが、とにかく速い。めちゃくちゃ速い。稀代のILマスターが書いているだけある恐ろしい出来栄えです。SilverlightやWP7版もあるので、Protocol Buffersの本来の用途というだけなく、幅広く使えるのではかとも思います。
もう一つは国内だと最近目にすることの多いMessagePack。以前に.NET(C#)におけるシリアライザのパフォーマンス比較を書いたときは振るわないスコアでしたが、最近別のC#実装が公開されまして、それは作者によるベンチMessagePack for .NET (C#) を書いたによると、protobuf-netよりも速いそうです。
Next
というわけでSilverlight枠でいいのか怪しかったですが、シリアライザの話でした。次は@ugaya40さんのWeakEventの話です。引き続きチェックを。あ、あと、Silverlight Advent Calendarはまだ埋まってない(!)ので、是非是非参加して、埋めてやってください。申し込みはSilverlight Advent Calendar 2011から。皆さんのエントリ、待ってます。どうやらちょうど今日Silverlight 5がリリースされたようなので、SL5の新機能ネタとかいいんじゃないでしょうか。
SL/WP7のSilverlight Unit Test Frameworkについて少し深く
- C# Rx Silverlight WindowsPhone7 - 11.09/23
の、前に少し。DynamicJsonとAnonymousComparerをNuGetに登録しました。どちらも.csファイル一個のお手軽クラスですが、NuGetからインストール可能になったことで、より気楽に使えるのではかと思います。機能説明は省略。
そして、昨日の今日ですがChaining AssertionをSilverlight Unit Test Frameworkに対応させました。リリースのバージョンは1.6.0.1ということで。NuGetではChainingAssertion-SLとChainingAssertion-WP7になります。
Silverlight Unit Test Framework
Silverlightで使う場合は(WP7じゃなくてね、という意味です)、一応Silverlight Toolkitに同梱という話ではあるのですが、テンプレートなどの用意が面倒くさいので、NuGet経由で入れるのが最も楽のようです。Install-Package Silverlight.UnitTestで。
まず、Silverlightアプリケーションを新規作成。Webサイトでのホストはなしでいいです。それとブラウザで実行させる必要もないので、プロジェクトのプロパティからOut of Browserに変更してしまいましょう。次に、NuGetからInstall-Package Silverlight.UnitTest。これでライブラリの参照と、ApplicationExtensions.cs(イニシャライズ用拡張メソッド)、UnitTest.cs(テスト用テンプレ)が追加されているはずです。次にApp.xaml.csのStartupを以下のように書き換えます。
private void Application_Startup(object sender, StartupEventArgs e) { // this.StartTestRunnerDelayed(); this.StartTestRunnerImmediate(); }
StartTestRunnerDelayedはテストランナー起動時に実行オプション(指定属性のもののみ実行するなど)を選択可能にするもの、Immediateはすぐに全テストを実行する、というものです。どちらかを選択すればOK。それで、とりあえず実行(Ctrl+F5)してみれば、テストランナーが立ち上がって、デフォテンプレに含まれるUnitTest.csのものが実行されているんじゃないかしらん。あとは、それを適宜書き換えていけばよし。なお、テンプレのテストクラスはSilverlightTestを継承していますが、これは必ずしも継承する必要はありません。後述しますが、Asynchronousのテストを行いたいときは必須ですが、そうでないならば、普通にMSTestでの場合と同じように、[TestClass]と[TestMethod]属性がついているものがテスト対象になっています。
なお、MainPage.xaml/.xaml.csは不要なので削除してしまってOK。StartTestRunnerによって、参照DLLのほうに含まれるxamlが呼ばれているためです。
WP7の場合。
一応NuGetにも用意されてるっぽい(silverlight.unittest.wp7)んですが、動きませんでした。ので、今のところ手動で色々用意する必要があります。詳しくはWindows Phone 7用の単体テストツール? その2「使ってみた」 - かずきのBlog@Hatenaに全部書いてあるのでそちらを参照のことということで。参照するためのDLLを拾ってくる→App.xaml.cs、ではなくてMainPage.xaml.csを書き換える、という、Silverlight版とやることは一緒なのですけどね。こういう状況なのはMangoのSDKがベータだったからとかなんとかのせいだとは思うので、近いうちに解決するのではかと、楽観視したいところです。
Chaining Assertionを使ってみる
Chaining Assertion ver 1.6.0.0の解説で紹介した失敗結果が丁寧に表示されるよー、をチェックしてみませう。
// こんなクラスがあるとして public class Person { public int Age { get; set; } public string FamilyName { get; set; } public string GivenName { get; set; } } [TestClass] public class ToaruTest { [TestMethod] public void PersonTest() { // こんなPersonがあるとすると var person = new Person { Age = 50, FamilyName = "Yamamoto", GivenName = "Tasuke" }; // こんな風にメソッドチェーンで書ける(10歳以下でYamadaTarouであることをチェックしてます) // 実際の値は50歳でYamamotoTasukeなので、このアサーションは失敗するでしょう person.Is(p => p.Age <= 10 && p.FamilyName == "Yamada" && p.GivenName == "Tarou"); } }
はい、ちゃんと表示されます。Chaining Assertionを使うと、メソッドチェーンスタイルで、実際の値.Is(期待値の条件)というように、 簡潔な記述でテストを書くことが出来るのがうりです。また、失敗時には、この場合personの値を詳細に出力してくれるので、何故失敗したのかが大変分かりやすい。もし、普通に書くと以下のようになりますが、
// もし普通に書く場合 var person = new Person { Age = 50, FamilyName = "Yamamoto", GivenName = "Tasuke" }; Assert.IsTrue(person.Age <= 10); Assert.AreEqual("Yamada", person.FamilyName); Assert.AreEqual("Tarou", person.GivenName);
まず、Assert.IsTrueでは失敗時にperson.Ageの値を出してくれないので、確認が面倒です。また、この場合、Personが正しいかをチェックしたいわけなので、FamilyNameやGivenNameも同時に判定して欲しいところですが、Ageを判定した時点で失敗のため、そこでテストは終了してしまうため、FamilyNameやGivienNameの実際の値を知ることは出来ません。
などなどの利点があるので、Chaining Assertionはお薦めです!この記事はSilverlight Unit Test Frameworkの紹介の体をとっていますが、実態はChaining Assertionの宣伝記事ですからね(キリッ
非同期テストをしてみる
Silverlightといったら非同期は避けて通れない。というわけで、Silverlight Unit Test Frameworkには非同期をテストできる機構が備わっています。[Asynchronous]というように、Asynchronous属性をつければそれだけでOK。と、思っていた時もありました。実際に試してみると全然違って、独特なシステムのうえにのっかっていて、かなり面倒くさかった……。
準備。まず、非同期テストをしたいクラスはSilverlightTestクラスを継承します。そしてAsynchronous属性をつけます。すると、そのテストメソッドはTestCompleteが呼ばれるか例外を検知するまでは、終了しなくなります。というわけで、こんな感じ。
[TestClass] public class ToaruTest : SilverlightTest { [TestMethod] [Asynchronous] public void AsyncTest() { var req = WebRequest.Create("http://www.google.co.jp/"); req.BeginGetResponse(ar => { try { req.EndGetResponse(ar) .ResponseUri.ToString() .Is("http://www.google.co.jp/"); } catch (Exception ex) { EnqueueCallback(() => { throw ex; }); // 例外はテスト用スレッドに投げる必要がある return; } // ↓は定型句なので、EnqueueTestComplete(); という単純化されたのが用意されている EnqueueCallback(() => TestComplete()); // 何事もなければ終了でマーク }, null); } }
このUnitTestの非同期は、独自のスレッドモデル(のようなもの)で動いていて、Dispatcherのようなキューにたいしてアクションを放り投げてあげる必要があります。別スレッドからUIスレッドは触れないように、「成功(TestComplete)」か「失敗(例外発生)」を伝えるには、EnqueueCallbackを経由しなければなりません。この辺はDispatcher.BeginInvokeするようなもの、と考えるといいかもしれません。
上のは少し原理に忠実にやりすぎた。まるごとEnqueueCallbackしてしまえばスレッドを意識する必要性は少しだけ減ります。
[TestMethod, Asynchronous] public void AsyncTest() { var req = WebRequest.Create("http://www.google.co.jp/404"); //404なので例外出してくれる req.BeginGetResponse(ar => { EnqueueCallback(() => req.EndGetResponse(ar) .ResponseUri.ToString() .Is("http://www.google.co.jp/")); EnqueueTestComplete(); }, null); }
といっても、これは非常に単純なケースなだけであって、複雑なケースを書くとどんどん泣きたくなっていくでしょう……。一応、Enqueueには他にEnqueueConditionalという、条件式がtrueになるまで待機し続けるというものが用意されているので、若干制御はできなくもないんですが、あんまりできるとは言い難い仕組みがあります。詳しくは述べませんというか、別に使いやすいシステムじゃないのでどうでもいいです。
Rxを使ってみる
結果・もしくは例外を別のスレッドシステムに投げる。どこかで聞いたことあるような。ここでティンと来るのはReactive ExtensionsのObserveOnDispatcherです。Dispatcher.BeginInvokeのかわりにEnqueueCallback。丸っきりそっくり。なので、ObserveOnTestQueueのようなメソッドが作れれば、非常に使い勝手がいいんじゃないか。と思い浮かぶわけです。
と、浮かんだ人は実に素敵な発想力を持っていますね。浮かんだのは私じゃなくて海外の人です。はい。Writing asynchronous unit tests with Rx and the Silverlight Unit Testing Framework | Richard Szalayに、実装が書かれています。
そのRxによるScheduler実装を使うと(WP7版なのでSystem.ObservableとMicrosoft.Phone.Reactiveも参照してください)
[TestMethod, Asynchronous] public void AsyncTest() { var req = WebRequest.Create("http://www.google.co.jp/"); Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse,req.EndGetResponse)() .ObserveOnTest(this) .Subscribe(r => r.ResponseUri.ToString().Is("http://www.google.co.jp/"), () => TestComplete()); }
EnqueueCallbackの管理がなくなり、非常に簡単に記述できました。Rxのスケジューラのシステムの柔軟さの賜物ですね。これはRxの素晴らしい応用例だと本当に感動しました。Richard Szalayさんに乾杯。それと、私がこの記事を知ったのはInfoQ: Rx と Silverlight で非同期テストを記述するからなので、紹介したInfoQと、そして翻訳した勇 大地さんにも大変感謝します。
Silverlightの場合
Richard SzalayさんのコードはWP7のMicrosoft.Phone.Reactiveのためのものなので、Silverlight用Rxの場合はそのままでは動きません。はい。残念ながら、WP7版RxとDataCenter版Rxとでは、互換性がかなり崩壊しているので、そのまま動くことなんてないんです。悲しいですねえ……。これに関しては銀の光と藍い空: 「Rx と Silverlight で非同期テストを記述する」をWeb版にも使えるようにしたい!に書かれていますが、Silverlight用に移植してあげればよいようです。
既に、上記記事で田中さんが移植されているのですが、二番煎じに書いてみました(と、※欄で書いたものを流用です、毎回、流用させてもらっていてすみません……)
public static class TestHarnessSchedulerObservableExtensions { public static IObservable<T> ObserveOnTestHarness<T>(this IObservable<T> source, WorkItemTest workItemTest) { return source.ObserveOn(new TestHarnessScheduler(workItemTest)); } public static IDisposable RunAsyncTest<T>(this IObservable<T> source, WorkItemTest workItemTest, Action<T> assertion) { return source.ObserveOnTestHarness(workItemTest).Subscribe(assertion, () => workItemTest.TestComplete()); } } public class TestHarnessScheduler : IScheduler, IDisposable { readonly WorkItemTest workItemTest; readonly CompositeDisposable subscriptions; public TestHarnessScheduler(WorkItemTest workItemTest) { var completionSubscription = Observable.FromEventPattern<TestMethodCompletedEventArgs>( h => workItemTest.UnitTestHarness.TestMethodCompleted += h, h => workItemTest.UnitTestHarness.TestMethodCompleted -= h) .Take(1) .Subscribe(_ => Dispose()); this.subscriptions = new CompositeDisposable(completionSubscription); this.workItemTest = workItemTest; } public void Dispose() { subscriptions.Dispose(); } public DateTimeOffset Now { get { return DateTimeOffset.Now; } } public IDisposable Schedule<TState>(TState state, DateTimeOffset dueTime, Func<IScheduler, TState, IDisposable> action) { return Schedule(state, dueTime - Now, action); } public IDisposable Schedule<TState>(TState state, TimeSpan dueTime, Func<IScheduler, TState, IDisposable> action) { if (subscriptions.IsDisposed) return Disposable.Empty; workItemTest.EnqueueDelay(dueTime); return Schedule(state, action); } public IDisposable Schedule<TState>(TState state, Func<IScheduler, TState, IDisposable> action) { if (subscriptions.IsDisposed) return Disposable.Empty; var cancelToken = new BooleanDisposable(); workItemTest.EnqueueCallback(() => { if (!cancelToken.IsDisposed) action(this, state); }); subscriptions.Add(cancelToken); return Disposable.Create(() => subscriptions.Remove(cancelToken)); } }
Richard Szalayさんのコードが非常に素晴らしく、あらゆるケースへのキャンセルに対して完全に考慮されているという感じなので、そのまま持ってきました。実際のところ、テスト用なので「例外発生/TestCompleteが呼ばれる」で実行自体が終了してしまうわけなので、こうもギチギチに考えなくてもいいのではかなー、とか緩いことを思ってしまいますが、まあ、よく出来ているならよく出来ているままに使わさせてもらいます。
メソッド名は、ObserveOnTestHarnessに変更しました。ObserveOnTestだけだと何かイマイチかなー、と思いまして。それと、時間のスケジューリングは、NotSupportedではなくて、EnqueueDelayというのものがあるので、それを使うことにしてみました。それと、ObserveOn -> Subscribe -> onCompletedにTestCompleteが定形文句なので、それらをひとまとめにしたRunAsyncTestを追加。こんな風に書けます。
var req = WebRequest.Create("http://www.google.co.jp/444"); Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse)() .RunAsyncTest(this, res => res.ResponseUri.ToString().Is("http://www.google.co.jp/"));
定形文句が減る、つまりうっかりミスで書き忘れて死亡というのがなくなる、というのはいいことです。
通常のMSTestの場合
ところで、もしSilverlight/WP7固有の機能は使っていなくて、WPFでも利用出来るようなコードならば、コードをリンク共有の形でWPF側に持っていってしまって、そこでテスト実行してしまうと非常に楽です。まず第一に、MSTestやNUnitなどの通常のテストフレームワークが使えるため、Visual Studio統合やCIが簡単に行えます。第二に、非同期のテストが(Rxを使った場合)更に簡単になります。
[TestMethod] public void AsyncTest() { var req = WebRequest.Create("http://www.google.co.jp/"); var result = Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse)() .First(); // First()で同期的に待機して値が取れる。複数の場合はToEnumerable().ToArray()で。 result.ResponseUri.ToString().Is("http://www.google.co.jp/"); }
FirstやToEnumerable.ToArrayにより、同期的に待機することが出来るので、簡単にテストすることができます。通常のコードは同期的待機はすべきではないのですが、こうしたユニットテストの場合は便利に使えます。
じゃあSilverlightのユニットテストでも待機できるのはないか?というと、それはできません。理由はWindows Phone 7で同期APIを実現するたった つの冴えないやり方で書いたのですが、WebRequestなどのネットワーク問い合わせは、一度Dispatcherに積まれて、現在のメソッドを抜けた後に実行開始されるので、テスト実行スレッドで同期的に待って値を取り出すことは不可能なのです。
こういった細部の違いもあるので、コード共有してMSTestでチェックするのは楽でいいのですが、やはりSilverlight/WP7の実際の環境で動かされるユニットテストのほうも必要不可欠かなー、と。どこまでやるか、にもよりますが。
まとめ
Chaining Assertionは便利なので是非試してみてね!
なお、Rxを使うとTestScheduler(時間を好きなように進められる)やITestableObserver(通知の時間と値を記録できる)といった、イベント/非同期のテストを強力に支援する仕組みが備わっているので、それらと併用することで、より簡単に、もしくは今までは不可能だったことを記述できるようになります。それはまた後日そのうち。
SL/WP7のテストは、本当はIDE統合されてるといいんですけどねー。まあ、エミュレータ動かさなければならないので、しょうがないかな、というところもありますけれど。その辺も次期VisualStudioでは改善されるのかされないのか、怪しいところです。現在DeveloperPreviewで出ているVS11は、特に何も手をつけられてる感じがしないので、そのままな可能性はなきにしもあらず。どうなるかしらん。async/awaitが入ることだし、色々変わってくるとは思うんですけれど。
Rxでのイベント変換まとめ - FromEvent vs FromEventPattern
- C# Rx WindowsPhone7 - 11.07/06
Reactive Extensionsの機能の一つに.NETにおけるイベントをIObservable<T>に変換する、というものがあります。Bridging with Existing .NET Events。そして、そのためのメソッドがFromEventでした。ところが最近のRxでは二つ、FromEventとFromEventPatternが用意されています。この差異は何なのでしょうか?
結論としては、過去のRx(このサイトの古い記事や他のサイトの過去の記事などで触れられている)やWindows Phone 7でのFromEventはFromEventPatternに改名されました。後続にEventPatternという(object Sender, TEventArgs EventArgs)を持つ.NETのイベントの引数そのものを渡すものです。そして、空席になったFromEventに新しく追加されたFromEvent(紛らわしい!)は、EventArgsだけを送ります。それ以外の差異はありません。
つまるところFromEventは FromEventPattern.Select(e => e.EventArgs) ということになります。なら、それでいいぢゃん、何も混乱を生む(WP7のFromEventがFromEventPatternである、というのは致命的よねえ)ことはないよ、とは思うのですが、パフォーマンスの問題でしょうかね。確かに、Senderは必要なく使うのはEventArgsだけの場合が多い。それなのに、毎回EventPatternを生成していたり、Selectというメソッド呼び出しが入るのは無駄です。
そもそもインスタンスに対してFromEventで包むということは、クロージャでsenderは変数としていつでもどこでも使えてしまうのですよね、そもそも、そもそも。そういう意味でも送られてくるのはEventArgsだけでいいのであった。というわけで、基本的にはFromEventでいいと思います。
FromEventPatternについて
では、改めてFromEventPatternを復習します(WP7の人はFromEventで考えてください)。Observable.FromEventPattern(TEventArgs) Method (Object, String) (System.Reactive.Linq)にサンプルコードがあるのですけれどね。そうそう、MSDNのリファレンスには、一部のメソッド/一部のオーバーロードにはサンプルコードがあります。全部ではないのがミソです、見て回って発掘しましょう。まあ、というわけで、とりあえずそのFileSystemWatcherで。
// FileSystemWatcherは指定フォルダを監視して、変化があった場合にイベントを通知します // 例えばCreatedイベントはファイルが作成されたらイベントが通知されます var fsw = new FileSystemWatcher(@"C:\", "*.*") { EnableRaisingEvents = true }; // FromEventPatternその1、文字列でイベント名指定 Observable.FromEventPattern<FileSystemEventArgs>(fsw, "Created") .Subscribe(e => Console.WriteLine(e.EventArgs.FullPath)); // FromEventPatternその2、静的なイベントをイベント名指定(WP7にはない) Observable.FromEventPattern<ConsoleCancelEventArgs>(typeof(Console), "CancelKeyPress") .Subscribe(e => Console.WriteLine(e.EventArgs.SpecialKey));
一番馴染み深いと思うのですが、文字列でイベント名を指定するものです。その2のほうはあまり見ないかもしれませんが、静的イベントに対しての指定も可能です。これら文字列指定によるメリットは、比較的シンプルであること。デメリットは、リフレクションを使うので若干遅い・スペルミスへの静的チェックが効かない・リファクタリングが効かない、といった、リフレクション系のデメリットそのものとなります。
リフレクションしかないの?というと、勿論そんなことはありません。
// FromEventPatternその3、EventHandlerに対する変換 var current = AppDomain.CurrentDomain; Observable.FromEventPattern(h => current.ProcessExit += h, h => current.ProcessExit -= h) .Subscribe(e => Console.WriteLine(e.EventArgs)); // FromEventPatternその4、EventHandler<T>に対する変換 Observable.FromEventPattern<ContractFailedEventArgs>( h => Contract.ContractFailed += h, h => Contract.ContractFailed -= h) .Subscribe(e => Console.WriteLine(e.EventArgs.Message)); // FromEventPatternその5、独自イベントハンドラに対する変換 Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>( h => new FileSystemEventHandler(h), h => fsw.Created += h, h => fsw.Created -= h) .Subscribe(e => Console.WriteLine(e.EventArgs.FullPath));
イベントの登録と削除を行うためのラムダ式を渡してやります。その3とその4は比較的分かりやすいのではないでしょうか。その5の第一引数が謎いのですが、これはconversionです。C#の型システムの都合上、そのまんまだと独自イベントハンドラを処理出来ないので、型を変換してやる必要があるという定型句。
数あるFromEventPatternのオーバーロードの中で、一番多く使うのはその5だと思います。何故なら、C#のイベントは独自イベントハンドラになっていることが多いから。はっきしいって、最低です。EventHandler<T>を使ってくれてさえいれば、こんな苦労はしなくて済むというのに。独自イベントハンドラは100害あって一利なし。え、WPFとか.NET標準がイベントハンドラは独自のものを使ってる?それは、WPFが悪い、.NET設計の黒歴史、悪しき伝統。
それと、もはや独自デリゲートも最低です。FuncやActionを使いましょう。C#のデリゲートはメソッドの引数や戻り値が一致していようが、型が違ったら別のものとして扱われます。そのことによる不都合は、↑で見たように、あるんです。極力ジェネリックデリゲートを使いましょう。そうすれば、こんな腐った目に合わなくても済みます。
ところで、その5は、もう少しだけ記述が短くなります。
// FromEventPatternその5、第一引数別解、こう書くと短くて素敵 Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>( h => h.Invoke, h => fsw.Created += h, h => fsw.Created -= h) .Subscribe(e => Console.WriteLine(e.EventArgs.FullPath));
h.Invoke。というのは、割とhそのものなわけですが、しかしInvokeと書くことで型が変換されます。この辺はコンパイラの都合上のマジックというか何というか。そういうものだと思えばいいのではかと。その5のスタイルで書くときは、この書き方をすると良いと思います。で、まだオーバーロードがあって
// その6 Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>( h => fsw.Created += h, h => fsw.Created -= h) .Subscribe(e => Console.WriteLine(e.EventArgs.FullPath));
conversionが不要で書けたりもします。一見素晴らしい、のですが、これ、中でなにやってるかというとconversionに相当するものをリフレクションで生成してるだけだったりして。そのため、なるべくconversionを使うオーバーロードのほうを使ったほうがよいでしょう。h => h.Invokeを書くだけですしね。このオーバーロードは紛らわしいだけで存在意義が不明すぎる。
FromEventについて
と、長々と見てきましたが、ではFromEventのほうも。
// FromEvent Observable.FromEvent<FileSystemEventHandler, FileSystemEventArgs>( h => (sender, e) => h(e), h => fsw.Created += h, h => fsw.Created -= h) .Subscribe(e => Console.WriteLine(e.FullPath)); // FromEventPatternその5(比較用) Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>( h => (sender, e) => h(sender, e), h => fsw.Created += h, h => fsw.Created -= h) .Select(e => e.EventArgs) .Subscribe(e => Console.WriteLine(e.FullPath));
というわけで、FromEventPatternのその5に近いわけですが、conversionでEventArgsしか渡していない、という点が差異ですね。なので、後続にはsenderが伝わってこず、EventArgsしか通りません。まあ、senderは、↑の例ですとfswでどこでも使えるので、そもそも不要なわけで、これで良いかと思います。
ところでFromEventも色々なオーバーロードがあるにはあるんですが、私の頭では存在意義が理解できなかったので無視します。挙動とかは理解したんですが、なんというか、存在する必要性、有効な利用法がさっぱり分からなかったのです……。まあ、多分、あんま意味ないと思うので気にしないでもいいかと。
拡張メソッドに退避させよう
FromEventにせよFromEventPatternにせよ、長いです。長い上に定型句です。なので、拡張メソッドに退避させると、スッキリします。例えば、今まで見てきたFileSystemWatcherだったら
// .NETのFromEventなら IObservable<TEventArgs> // .NETのFromEventPatternなら IObservable<EventPattern<TEventArgs>> // WP7のFromEventなら IObservable<IEvent<TEventArgs>> // を返す拡張メソッド群を用意する。 // 命名規則はイベント名AsObservableがIntelliSenseの順序的にお薦め public static class FileSystemWatcherExtensions { public static IObservable<FileSystemEventArgs> CreatedAsObservable(this FileSystemWatcher watcher) { return Observable.FromEvent<FileSystemEventHandler, FileSystemEventArgs>( h => (sender, e) => h(e), h => watcher.Created += h, h => watcher.Created -= h); } public static IObservable<FileSystemEventArgs> DeletedAsObservable(this FileSystemWatcher watcher) { return Observable.FromEvent<FileSystemEventHandler, FileSystemEventArgs>( h => (sender, e) => h(e), h => watcher.Deleted += h, h => watcher.Deleted -= h); } public static IObservable<RenamedEventArgs> RenamedAsObservable(this FileSystemWatcher watcher) { return Observable.FromEvent<RenamedEventHandler, RenamedEventArgs>( h => (sender, e) => h(e), h => watcher.Renamed += h, h => watcher.Renamed -= h); } public static IObservable<FileSystemEventArgs> ChangedAsObservable(this FileSystemWatcher watcher) { return Observable.FromEvent<FileSystemEventHandler, FileSystemEventArgs>( h => (sender, e) => h(e), h => watcher.Changed += h, h => watcher.Changed -= h); } }
var fsw = new FileSystemWatcher(@"C:\", "*.*") { EnableRaisingEvents = true }; // 例えば、ただ変更をロギングしたいだけなんだよ、という場合の結合 // FromEventを外出ししていることによって、すっきり書ける Observable.Merge( fsw.CreatedAsObservable(), fsw.DeletedAsObservable(), fsw.ChangedAsObservable(), fsw.RenamedAsObservable()) .Subscribe(e => Console.WriteLine(e.ChangeType + ":" + e.Name));
といった形です。また、普通に+-でのイベント以外のものへの登録も可能です。例えば
// LayoutRootはWPFの一番外枠の<Grid Name="LayoutRoot">ということで。 Observable.FromEvent<MouseButtonEventHandler, MouseButtonEventArgs>( h => (sender, e) => h(e), h => LayoutRoot.AddHandler(UIElement.MouseDownEvent, h), h => LayoutRoot.RemoveHandler(UIElement.MouseDownEvent, h)) .Subscribe(e => Debug.WriteLine(e.ClickCount));
こんな形のものもObservable化が可能です。
イベントの解除
Subscribeの戻り値はIDisposableで、Disposeを呼ぶことでイベントが解除されます。
// アタッチ var events = Observable.Merge( fsw.CreatedAsObservable(), fsw.DeletedAsObservable(), fsw.ChangedAsObservable(), fsw.RenamedAsObservable()) .Subscribe(e => Console.WriteLine(e.ChangeType + ":" + e.Name)); // デタッチ(合成などをしていて、元ソースが複数ある場合も、すべて解除されます) events.Dispose();
Rxのこの仕組みは、従来に比べて圧倒的にイベントの解除がやりやすくなっていると思います。
まとめ
非同期の説明ばかりしてきていて、イベントはすっかり置き去りだったことを、まずはゴメンナサイ。少し前からFromEvent周りは大きな仕様変更が入ったわけですが、ようやくまともに解説できました。基本中のキの部分であるここが、過去のリソースがそのまま適用出来ないという最悪の自体に陥っていたので、とりあえずこれで何とか、でしょうかどうでしょうか。
小さなこととはいえ、WP7との互換性が絶えているのが痛いのですが、その辺どうにかならなかったのかねー、とは思います。けれど、このEventArgsだけ送るFromEvent自体は良いと思います。 .Select(e => e.EventArgs) が定型句だったので、こういった変更は喜ばしい限り。それと、今まで思っていた、ぶっちゃけラムダ式とかRxでイベント登録するならsenderって不要じゃね?に対する答え(その通りで、完全に不要)を出してくれたのが嬉しい。
さて、変換できるのはいいけれど、じゃあどこで使うのがいいの?という話がいつもありません。次回は、時間周りと絡めて、その辺のお話が出来ればと思いますが、いつも次回予告が達成されたことはないので、別のことを書くでしょう←ダメぢゃん。
ReactiveOAuth ver.0.4 - Twitpic(OAuth Echo)対応
- C# Rx WindowsPhone7 - 11.06/23
ver.0.4になりました。少し前に0.3.0.1をこっそり出していたので、それを含めて0.3からの差分は、「対象Rxのバージョンが現在最新の1.0.10605(Stable)」に、というのと「Realmが含まれていると認証が正しく生成出来なかったバグの修正」と、「TwitpicClientサンプルの追加」になります。バグのほうは本当にすみません……。Twitterでしかテストしてない&TwitterはRealm使わないため、全然気づいていなくて。ダメですねホント。
OAuth Echo
TwitpicはOAuth Echoという仕組みでTwitterと連携した認証をして、画像を投稿できます。詳しくはUsing OAuth Echo | dev.twitter.comやTwitPic Developers - API Documentation - API v2 » uploadにありますが、よくわかりませんね!Twitpicに画像を投稿、というわけでTwitpicのAPIにアクセスするわけですが、その際のヘッダにTwitterに認証するためのOAuthのヘッダを付けておくと、Twitpic側がTwitterに問い合せて認証を行う。という仕組みです、大雑把に言って。
ただのOAuthとはちょっと違うので、今までのReactiveOAuthのOAuthClientクラスは使えない。けれど、認証用ヘッダの生成は同じように作る。というわけで、ここはReactiveOAuthにひっそり用意されているOAuthBaseクラスを継承して、Twitpic専用のTwitpicClientクラスを作りましょう。
が、作るのもまた少し面倒なので Sample/TwitpicClient/TwitpicClient.cs に作成したのを置いておきました。ファイルごとコピペってご自由にお使いください。.NET 4 Client Profile, Silverlight 4, Windows Phone 7の全てに対応しています。
Windows Phone 7でのカメラ撮影+投稿のサンプル
TwitpicClient.cs の解説は後でやりますが、その前に利用例を。WP7でカメラ撮影+投稿をしてみます。CameraCaptureTaskの利用法に関しては CameraCaptureTaskを使ってカメラで静止画撮影を行う – CH3COOH(酢酸)の実験室 を参考にさせて頂きました。TwitterのAccessTokenの取得に関しては、ここでは解説しませんので neue cc - ReactiveOAuth - Windows Phone 7対応のOAuthライブラリ を参照ください。
// CameraCaptureTaskのCompletedイベント void camera_Completed(object sender, PhotoResult e) { if (e.TaskResult == TaskResult.OK) { // 撮影画像(Stream)をバイト配列に格納 var stream = e.ChosenPhoto; var buffer = new byte[stream.Length]; stream.Read(buffer, 0, buffer.Length); // key, secret, tokenは別に設定・取得しておいてね new TwitpicClient(ConsumerKey, ConsumerSecret, accessToken) .UploadPicture(e.OriginalFileName, "from WP7!", buffer) .ObserveOnDispatcher() .Catch((WebException ex) => { MessageBox.Show(new StreamReader(ex.Response.GetResponseStream()).ReadToEnd()); return Observable.Empty<string>(); }) .Subscribe(s => MessageBox.Show(s), ex => MessageBox.Show(ex.ToString())); } }
new TwitpicClient(キー, シークレット, アクセストークン).UploadPicture(ファイル名, メッセージ, 画像) といった風に使います。戻り値はIObservable<string>で結果(投稿後のURLとか)が返ってくるので、あとは好きなように。投稿に失敗した場合は、WebExceptionが投げられるので、それを捉えてエラーメッセージを読み取ると開発には楽になれそうです。
TwitpicClient.cs
以下ソース。Sample/TwitpicClient/TwitpicClient.cs と同じですが、自由にコピペって使ってください。大事なことなので2回言いました。このコード自体はTwitpicに特化してありますが、認証部分のヘッダを少しと画像アップロードを変更する部分を弄れば、他のOAuth Echoサービスにも対応させることができると思います。
using System; using System.Linq; using System.Text; using System.Net; using System.IO; #if WINDOWS_PHONE using Microsoft.Phone.Reactive; #else using System.Reactive.Linq; #endif namespace Codeplex.OAuth { public class TwitpicClient : OAuthBase { const string ApiKey = ""; // set your apikey readonly AccessToken accessToken; public TwitpicClient(string consumerKey, string consumerSecret, AccessToken accessToken) : base(consumerKey, consumerSecret) { this.accessToken = accessToken; } private WebRequest CreateRequest(string url) { const string ServiceProvider = "https://api.twitter.com/1/account/verify_credentials.json"; const string Realm = "http://api.twitter.com/"; var req = WebRequest.Create(url); // generate oauth signature and parameters var parameters = ConstructBasicParameters(ServiceProvider, MethodType.Get, accessToken); // make auth header string var authHeader = BuildAuthorizationHeader(new[] { new Parameter("Realm", Realm) }.Concat(parameters)); // set authenticate headers req.Headers["X-Verify-Credentials-Authorization"] = authHeader; req.Headers["X-Auth-Service-Provider"] = ServiceProvider; return req; } public IObservable<string> UploadPicture(string filename, string message, byte[] file) { var req = CreateRequest("http://api.twitpic.com/2/upload.xml"); // choose xml or json req.Method = "POST"; var boundaryKey = Guid.NewGuid().ToString(); var boundary = "--" + boundaryKey; req.ContentType = "multipart/form-data; boundary=" + boundaryKey; return Observable.Defer(() => Observable.FromAsyncPattern<Stream>(req.BeginGetRequestStream, req.EndGetRequestStream)()) .Do(stream => { using (stream) using (var sw = new StreamWriter(stream, new UTF8Encoding(false))) { sw.WriteLine(boundary); sw.WriteLine("Content-Disposition: form-data; name=\"key\""); sw.WriteLine(); sw.WriteLine(ApiKey); sw.WriteLine(boundary); sw.WriteLine("Content-Disposition: form-data; name=\"message\""); sw.WriteLine(); sw.WriteLine(message); sw.WriteLine(boundary); sw.WriteLine("Content-Disposition: form-data; name=\"media\"; filename=\"" + filename + "\""); sw.WriteLine("Content-Type: application/octet-stream"); sw.WriteLine("Content-Transfer-Encoding: binary"); sw.WriteLine(); sw.Flush(); stream.Write(file, 0, file.Length); stream.Flush(); sw.WriteLine(); sw.WriteLine("--" + boundaryKey + "--"); sw.Flush(); } }) .SelectMany(_ => Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse)()) .Select(res => { using (res) using (var stream = res.GetResponseStream()) using (var sr = new StreamReader(stream, Encoding.UTF8)) { return sr.ReadToEnd(); } }); } } }
認証ヘッダ作成はConstructBasicParametersとBuildAuthorizationHeaderというprotectedメソッドで行います。わけわかんないよね…気持ち悪いよね…。使いにくいメソッドです、すみません、私もそう思います。そういうものだと思って、見ないふりしてもらえれば幸いです。
コードの大半を占めているのは画像を投稿するためのmultipart/form-dataのもので、これはもうOAuth Echo関係ない話、で、面倒ぃ。特にWP7での非同期だと涙が出る。POSTはBeginGetRequestStreamとBeginGetResponseの二つの非同期メソッドをセットで使う必要があるため、コードがごちゃごちゃするのです。
しかしReactive Extensionsを使えばあら不思議!でもないですが、ネストがなくなって完全に平らなので、結構普通に読めるのではないでしょうか?(ストリーム書き込みのコード量が多いのは、これは同期でやっても同じ話なので)。例外処理も利用例のところで見たように、Catchメソッドをくっつけるだけ。実に色々とスッキリします。
Rxがあれば非同期POSTも怖くない。
やっていることは単純で、FromAsyncPatternでBegin-Endを変換。StreamへのWriteは後続への射影はなく、対象(Stream)に対しての副作用(書き込み)のみなのでDo、RequestStream->Responseへの切り替えはSelectMany、Responseから結果のStringへの変換はSelect、と、お決まりの定型メソッドに置き換えていっただけです。この辺はパターンみたいなものなので、これやるにはこのメソッドね、というのを覚えてしまえばそれでお終いです。
Stream読み書きは非同期にしないの?
StreamにもBeginReadとかBeginWriteとかありますものね。しかし、しません(キリッ。理由は死ぬほど面倒だからです。やってみると分かりますが想像以上に大変で、おまけに何とか実現するためにはRxでのチェーンを大量に重ねる必要がありオーバーヘッドがバカにならない……。なので、わざわざやるメリットも全くありません。
一応、ReactiveOAuthのOAuthClientは、そこも非同期でやってますが、わざわざ頑張った意味があったかは、かなり微妙なところ。実装は Internal/AsynchronousExtensions.cs にあるので参照ください。それと、この AsynchronousExtensions.cs はReactive Extensionsで非同期処理を簡単にで言った「拡張メソッドのすゝめ」を実践したものでもあります。WebRequestはプリミティブすぎて扱い難いので、Rxに特化したうえで簡単に扱えるようにDownloadStringやUploadValueなどといったメソッドを拡張してあります。便利だと思いますので、こちらも TwitpicClient.cs と同様に、ファイルごと自由にコピペって使ってやってください。
まとめ
ReactiveOAuthを公開する目的に、「これが入り口になってRxの世界を知ってもらえると嬉しい」というのもあったのですが、WP7開発で利用してもらったりと、その目的は少しは達成出来たかもで、良かった良かった。ちょっと練りたりなかったり、未だにバグがあったり(本当にごめんなさい!)と至らない点も多いですが、今後も改善していきますのでよろしくお願いします。
はぢめてのWindows Phone 7でのデータベース(Linq to Sql)
- C# Misc WindowsPhone7 - 11.05/30
Windows Phone 7に新しいSDKが来ました!9月頃リリースという話のMangoアップデート(Windows Phone 7.1)対応SDK。まだベータですが色々触れます。そしてついにデータベースが搭載されました。というわけで軽く触ってみました。
Code First
フツーだとSQLを書いてデータベースの定義を用意しなければならないところですが、WP7でのデータベースプログラミングにおいて、SQLは不要です。と、いうよりも、そもそも使えません。データベース本体(SQLCE)や、データベースにSQLを発行するクラス(ADO.NET)はMicrosoft.Phone.Data.Internalに格納されており、Internalという名のとおり、外から触ることは出来ません。ではどうするか、というと、WP7ではデータベースはLinq to Sqlを介して操作します。
じゃあテーブル定義どうするの、リレーションどうするの、というと、それはクラスから生成します。クラスを書いて、ある程度DB的に属性を付与して、CreateDatabaseとすれば、それらのテーブルを持ったデータベースが生成されます。まずコードを。あ、そうそう、System.Data.Linqの参照が別途必要です。
public class Meibo : DataContext { public Meibo() : base("isostore:/Meibo.sdf") // connection string { } public Table<Person> Persons { get { return GetTable<Person>(); } } } [Table] public class Person { [Column(IsPrimaryKey = true, IsDbGenerated = true)] public int Id { get; set; } [Column(CanBeNull = false)] public string Name { get; set; } [Column] public int Age { get; set; } }
名前と年齢のあるPersonというテーブルを持つ、Meiboというデータベースを定義しました。データベースはDataContextを継承し、各Tableを持つプロパティを。そしてテーブルはTable属性とColumn属性を。IsPrimaryKeyは主キー、IsDbGeneratedは自動連番、CanBeNullはnull非許可です。Columnを付けて回るのが面倒くさそうですが、まぁ単純明快ではありますねん。
接続文字列は基底クラスに渡す形で。保存場所はIsolatedStorage内です。どうせ場所固定で弄らないでしょ?と思うので、上の例では直接定義しちゃっていますが、弄りたい場合はその辺調整で。ちなみにisostore:/です。isostore://にするとダメです(最初うっかり引っかかった)。
実際に使う場合は
// 初回実行時はデータベースを作る、これはapp.xaml.csに書いておくといい using (var db = new Meibo()) { if (!db.DatabaseExists()) { db.CreateDatabase(); } } // Insertの例 var meibo = new Meibo(); var person1 = new Person { Name = "ほげほげ", Age = 20 }; var person2 = new Person { Name = "ふがふが", Age = 15 }; var person3 = new Person { Name = "たこたこ", Age = 23 }; meibo.Persons.InsertOnSubmit(person1); // Insertする meibo.Persons.InsertAllOnSubmit(new[] { person2, person3 }); // 複数の場合 meibo.SubmitChanges(); // SubmitChangesまではDBへの挿入はされていない // Selectの例(Ageが20以上のものを抽出) var query = meibo.Persons.Where(p => p.Age >= 20); foreach (var item in query) { MessageBox.Show(item.Id + ":" + item.Name + ":" + item.Age); }
というわけで、DBの存在を全く意識せず自然に書けます。実に素晴らすぃー。
リレーション
リレーションも勿論張れます。例はマクドナルドのバーガーの価格表で。地域で価格が違うので、Burger(バーガー名)、Price(値段)、Place(地域)の3つをBurger-Price-Placeで関連付けてきませう。
public class McDonald : DataContext { public McDonald() : base("isostore:/McD.sdf") { } public Table<Burger> Burgers { get { return GetTable<Burger>(); } } public Table<Price> Prices { get { return GetTable<Price>(); } } public Table<Place> Places { get { return GetTable<Place>(); } } } [Table] public class Burger { [Column(IsPrimaryKey = true, IsDbGenerated = true)] public int Id { get; set; } [Column] public string Name { get; set; } [Association(Storage = "_Prices", OtherKey = "BurgerId")] public EntitySet<Price> Prices { get { return this._Prices; } set { this._Prices.Assign(value); } } private EntitySet<Price> _Prices = new EntitySet<Price>(); } [Table] public class Price { [Column(IsPrimaryKey = true, IsDbGenerated = true)] public int Id { get; set; } [Column] public int BurgerId { get; set; } [Column] public int PlaceId { get; set; } [Column] public int Value { get; set; } [Association(IsForeignKey = true, Storage = "_Burger", ThisKey = "BurgerId")] public Burger Burger { get { return _Burger.Entity; } set { _Burger.Entity = value; } } private EntityRef<Burger> _Burger = new EntityRef<Burger>(); [Association(IsForeignKey = true, Storage = "_Place", ThisKey = "PlaceId")] public Place Place { get { return _Place.Entity; } set { _Place.Entity = value; } } private EntityRef<Place> _Place = new EntityRef<Place>(); } [Table] public class Place { [Column(IsPrimaryKey = true, IsDbGenerated = true)] public int Id { get; set; } [Column] public string Name { get; set; } [Association(Storage = "_Prices", OtherKey = "PlaceId")] public EntitySet<Price> Prices { get { return this._Prices; } set { this._Prices.Assign(value); } } private EntitySet<Price> _Prices = new EntitySet<Price>(); }
ぎゃー。面倒くさ。本来のLinq to Sqlではデータベースが先にあって、そこから機械生成でこれを作るんですが、コードを先で作るのはちょっと骨が折れます。Entity Framework Code Firstは、コードを先に作るのが大前提だけあって書きやすいように色々調整してある感じですが、WP7/Linq to Sqlは、本当にただただ手で書きますというわけで全くイケてない。
さて、リレーションはAssociation属性でつけます。また、多を辿る場合はEntitySet、一を辿る場合はEntityRefのプロパティを用意します。これがまた面倒くさ……。たいしたことはない機械的作業ですが、自動プロパティで済ませられないとウザったいことこの上なく。コードスニペットでも用意しますかねえー。
しかし苦労するだけの価値は、あります!
まずデータを用意しなきゃということでInsertを。
// Insertの例 var mcd = new McDonald(); var hamburger = new Burger() { Name = "ハンバーガー" }; var blt = new Burger() { Name = "ベーコンレタストマト" }; var kanto = new Place() { Name = "関東" }; var qshu = new Place() { Name = "九州" }; var hokkaido = new Place() { Name = "北海道" }; var prices = new[] { new Price { Burger = hamburger, Place = kanto, Value = 100 }, new Price { Burger = hamburger, Place = qshu, Value = 150 }, new Price { Burger = hamburger, Place = hokkaido, Value = 160 }, new Price { Burger = blt, Place = kanto, Value = 250 }, new Price { Burger = blt, Place = qshu, Value = 230 }, new Price { Burger = blt, Place = hokkaido, Value = 220 } }; mcd.Places.InsertAllOnSubmit(new[] { kanto, qshu, hokkaido }); mcd.Burgers.InsertAllOnSubmit(new[] { hamburger, blt }); mcd.Prices.InsertAllOnSubmit(prices); mcd.SubmitChanges();
リレーションを軽やかに片付けて、挿入してくれます。実に自然でイイ!それに、こういうののinsert文手書きはカッタルイですからねえ。更にSelectは
var mcd = new McDonald(); // 関東のバーガーのNameとPriceを抽出 var query = mcd.Burgers.Select(b => new { b.Name, Price = b.Prices.First(p => p.Place.Name == "関東").Value }); // ハンバーガー:100, ベーコンレタス:250 foreach (var item in query) { MessageBox.Show(item.Name + ":" + item.Price); } // なお、IQueryable<T>をToStringすると手軽に発行されるSQLが確認出来る // もう一つの手はDataContext.Logから取ること MessageBox.Show(query.ToString());
コード上にjoinはないけど、発行されるSQLはjoinしています。手動でjoinすることも可能ですが、基本的にはオブジェクト間をドットで辿って操作します。その方が自然に書けるし、何より、楽ですもの。
DataContextのDispose
usingで括ってあげるのが礼儀正しいわけですが、WP7では実際どう考えるべきだろう。サンプル見てると、CreateDatabaseやSchemaUpdateではusingで囲んでますが、そうでない普通の操作ではコードビハインド内でDataContext使い回してるんですね。基本的にIsolatedStorageに隔離されているわけだし、画面外に出るときだけ切った繋げたすればいいのかなあ、といったふうに思いましたがどうなのでしょ。
.NET版との差分
ほとんど.NET版のLinq to Sqlと同じなのですが、若干追加があります。一つはデータベースのスキーマのアップデート。Microsoft.Phone.Data.Linq Namespace名前空間の参照で、DataContextにCreateDatabaseSchemaUpdaterが追加されます。これにより、アップデートなどによるテーブル構造の変化にも対応出来ます。もう一つは IndexAttribute Class。
これらは、通常Linq to Sqlが用いられていたデータベースからのクラス自動生成じゃなく、クラスからのデータベース生成になったことにより、テーブル作りに足りていなかった面の補足と見れるかな。また、その逆で.NET版でサポートされているけれど、WP7版にはないものも幾つかあります。詳しいリストはMSDNのLINQ to SQL Support for Windows Phoneを見ればいいんじゃないかな、ということで。
学習リソース
若干の差異はあるとはいえ、Linq to SqlはLinq to Sqlなので、MSDN - LINQ to SQLを見るのが良いでしょう。また、慣れない間はWP7版ではなく.NET版で、ConsoleApplicationで挙動をあらかた確認しておいたほうが、スムースに行くかとは思います。属性貼ったりは、結構面倒だし罠もあるところですからね……。
まとめ
諸君らの愛したLinq to Sqlは死んだ!何故だ!そうしてEntity Frameworkに置き換えられる運命を辿ったLinq to Sqlですが、ここにきて華麗に復活するとは誰も予想だにしなかったところで、こういう展開は面白い。そして生SQLが使えないのは英断。縛りではあるのですが、Phoneでのアプリケーションの9.9割は、生SQLを必要とすることはないのではないか、とも。
生SQL触れるだろうと思ってWP7版も作るぜ!な勢いで用意していたDbExecutorのWP7版は永劫さようならになってしまいましたががが。DbExecutorはDbExecutorで、もう少し機能追加しますがー。
ところでMangoで、他に追加されたクラスを少し。System.Reflection.Emitが追加されました。これはILを直弄りして動的コード生成するためのクラスですが、WP7でIL生成とかヤラネーヨ。というわけでもなくはなく実は有益。シリアライザの高速化のために動的コード生成は常套手段となっているので、自分は直に使わなくても、普通にメリットは大きく。例えばJSON.NETのJSONシリアライズ/デシリアライズは、WP7版だけリフレクションを直に使ったもので見たところ遅そうでしたが、恐らく次からは.NET版と同じく動的コード生成になり、高速化されるでしょう。ORマッパーなどもそうです。そう、Linq to SqlでもMetaAccessorクラスなどの辺りを覗いてみれば、ILをEmitしているコードが見えます。
そういえばLambdaExpressionもCompile出来るようになりました。が、AssignやLoopなどは搭載されていません、ぐぬー。コード生成したい人はExpressionTreeでお手軽、ではなく、まだまだILGeneratorでEmit頼りしかなさそうです。更に言えばExpressionVisitorも入っていませんね。SL4に近くなったけれどSL4とは言えない、WP7はWP7としか言いようのないAPIになってまいりました。
Linq と Windows Phone 7(Mangoアップデート)
- C# Rx WindowsPhone7 - 11.04/14
今冬に予定されている大型アップデート(Mango)に向けて、MIX2011で大量に情報出てきました。色々ありましたが、その中でも目を引いたのがデータベース対応とQueryable対応。データベースは恐らくSQL Server Compact 4.0相当で、これはいいものだ。ただ、生SQL文かあ、という思いもあり。他のやり方ないのかなあ。オブジェクトDBとか?などと夢見てしまうけれど、高機能なエンジン(SQLCE4)がある以上は、それをそのまま使ったほうがいいのは自明で。
インピーダンスミスマッチを解消する、そのためのLinqでしょ!と、ああ、そうだね。でもLinq to Sqlはないし、Linq to Entitiesが乗っかるわけもないし、策はなく普通に生SQLの気もするなあ。どうでしょうね。どうなるのでしょうね。で、Queryable対応も発表された、というかデモられていました。
これでDB+Linqがあることが保証されたから安泰。何のQueryProviderと見るべきかしら。この辺、不透明なので、情報入り次第しっかり追っかけます。
Queryableの難点は、見た目はスッキリしてますが、コンパイル後の結果は式木のお化けになる+実行時は式木のVisitというわけで、軽い処理とは言い難いところなのですが、軽いとか重いとか気にしないし!じゃあなくて、MangoではGC改善など性能向上も入るようなので、十分なパフォーマンスは確保できそうです。あと、ユーザーエクスペリエンスを損なうボトルネックは結局そこじゃあないんだよ、的なこともありそうだし。
例えば現行のWP7のListBoxは致命的に重くて、純正アプリは軽いのにユーザー作成アプリではクソ重く、純正だけネイティブでチートかよゴルァ、という勢いだったんですが(Mangoで改善されるそうです!)、これの要因の一つは画像の読み込み周りがアレだったことにあるようで……。
CocktailFlowという素敵な見た目のアプリがあるのですが、これはその辺の問題に対処するためか、事前にLoadingを取って、完全に読み込み終わってからしか表示しないようになってます。でも、それはそれでネットワーク読みに行ってるわけでもないのにLoadingが長くて、微妙だなあ、と。
MIX11でのRx
二つセッションがありました。一つはLinq in Actionの著者(最初期に出たものですが、良い内容でしたね)によるWP7でのRx活用例。
マイクのボリュームが不安定で聴きにくいですー。話はPull(Enumerable)とPush(Observable)の比較から入ってますね。MoveNextして、MoveNextして、MoveNextして、ええ、ああ、メソッド探訪第7回:IEnumerable vs IObservableの辺りで書きましたね~。それのEnumerbaleサイドのリライト版であるLINQの仕組みと遅延評価の基礎知識の続きとして「Rxの仕組み」を書きたいのですが書くと思ってもう3ヶ月も経ってる、うぐぐ……。
話がそれた。んで、話の序盤は結構退屈です。IEnumerator vs IObserverとかつまらないです、って、あ、私もやってたっけ……。話の流れを上手くつながないと何か唐突なのね。さて、デモはサイコロ転がしを完成させていくというもの。どんどんコードがメソッドチェーンのお化けになっていく様はどこかで見たことある(笑)。最初は、重たい処理を、ObserveOn(Scheduler.ThreadPool)とするだけで実行スレッドを切り替えて、CPUバウンドの処理を簡単に同期→非同期に変換してUIをブロックしないというデモ。ちなみにObserveOn(Scheduler.ThreadPool)じゃなくて、これはToObservableの時点でScheduler渡したほうがいいんじゃあないのかなあ。まあ、大して違いはないといえばないですがー。
次は加速センサーをRxに変換して、TimestampをつけてShakeされたことを検出するというもの。生のセンサーイベントをRxで事前に加工して扱いやすくする、という手ですね。私もUtakotohaの再生イベントでそれやったよ!(いちいち対抗心燃やして宣伝しなくてよろし)。ObservableだとMockに置き換えやすいんだよ、という話もね。気づいたらメソッドチェーンのお化けになっているのも。Linq使いの末路はみんなこんなのになってしまうという証明が!
セッションはもう一つ。RxチームのBart De SmetによるRxJSのセッション。MIX2011はIE10と合わせてHTML5推しもありましたしね。
最初の20分は、ObservableとObserverの購読概念などについて簡単な説明。あとRange,FromArray,Timerといった生成子。Pullとの比較がない、つまり”Linq”とは全く言わないのがJavaScript向けですね。喩えもHTML5のGeolocation APIが、などなど新鮮。そして、だからこそ分かりやすいかもしれません。その後はいきなりGeolocation APIをラップする自作Observableを作ろうが始まって難易度が、まあLevel300だものね。実用的な話ではあるし、Observableを作るということは仕組みを理解するという話でもあるので、こういうのも良い流れ。ほか、KeyUpを拾ってネットワークを問い合せてのリアルタイム検索(event+asynchronousの合成例)など。これはハンズオン資料でもお馴染みの例ですが、順に追ってデモしてもらえるとThrottleの意味は凄く分かりやすいですね。Switchも強力だけど分かりづらさを持つので、こうして丁寧に解説してもらえると、いいね。ともかく、概念としてC#もJavaScriptも超えたところにあるので、考え方、コード例はC#/WP7でも使えます。良いセッションだと思いますので見るのお薦め。
そういえばで、Async CTPが更新されていて、一部変更があったので、RxとAsyncとのブリッジライブラリ(Rx-AsyncLinq)が動かなくなったと思われるので、それに合わせて近いうちにRxもアップデートありそうな気がします。RxJSもここずっと更新とは遠かったのですが、こうしてセッションもあったことだし、大型アップデートはあってもおかしくないかな。
Async CTPはufcppさんの記事Async CTP Refreshに詳しく書かれていますが、ライセンス形態がご自由にお使いください(※但し自己責任)へと変更、日本語版でも入るようになった、WP7対応、といった具合に、WP7開発でも今すぐ投下しても大丈夫だ問題ないになったのは大きい。
他言語からWP7開発のために来たので、C#の経験があまりなくてLinqがイマイチ分からない。でも非同期ダルい。という人には、Rxよりも、Asyncのほうがずっと扱いやすく応えてくれるので、なし崩し的に投入してしまっていい気がしますねー。勿論、ある程度は大きな変更に対応していく気は持ってないとダメですが。
ある程度Linqが分かる人なら(別にそんな大仰なスキルを要求するわけじゃなく、SelectしてWhereして、Linq便利だな~、ぐらいでいいです!)Rxを学ぶのもお薦め。基本的にはLinqに沿っているので、学習曲線はそんなにキツくないです。特に、非同期をラップしてWhereしてSelectしてSubscribe、ぐらいならすぐ。他のは、徐々に覚えればいいだけで。
まとめ
WP7はRx標準搭載で、ただでさえLinq濃度が高かったというのに、Queryable搭載でLinq天国に!もはやWP7が好きなのかLinqが好きなのかよくわかりませんが(多分、後者です)、ともかく最高のプラットフォームなのは間違いない。
ところでDay1ではnugetが割とフィーチャーされていたのですが、その背景、よくみると……
右のほうにlinq.jsが!すんごく嬉しい。
Utakotoha - Windows Phone 7用の日本語歌詞表示アプリケーション
- C# WindowsPhone7 - 11.04/09
マーケットプレイスに通り、公開されました。フリーです(下で述べますがソースも公開しています)。再生中の音楽のアーティスト・タイトルを元に自動で検索して、歌詞を表示する、というものです。海外製の同種アプリとの違いは、対象が日本の曲ということになります。下記バナー、もしくはマーケットプレイス検索”utakotoha”でどうぞ。アプリケーション名は例によってヒネりなしで「歌詞」からそのままとりました。うたのことば。Utakata TextPadと名前が似ていますが偶然の一致です(ほんと)
スクリーンショットでは格好つけてズームインして隠蔽していますが、スクリーンショット用詐欺なだけで、実際はこんな感じです。
ようは、単純にWebBrowserにgoo歌詞を出しているだけです。ダブルタップでエリアに沿って幅一杯の拡大してくれるので、誤魔化し的にはまぁまぁ。WebBrowserを経由する理由は、APIが提供されているわけでもないので、権利関係考えるとこうでないとマズいかな、といったところで。goo歌詞を選んだ理由も直リンが許可されていたからです。
あと、WP7はテーマに黒背景のDarkと白背景のLightがあるわけですが、goo 音楽の背景は強制白で、どうもミスマッチなわけですね。アプリ自体を完全に白背景にしてしまうのもいいかな、とは思ったんですが、ブラウザのCSSを書き換えて黒背景にするという手を(ネタとして)取ってみました。オプションでOFFに出来るというか、デフォルトはOFFです。ズームすれば違和感はないんですが、画面いっぱいに広がってるものでは、微妙度極まりない。
ほか、Twitterに再生中楽曲を投稿する機能があります。
ソースコード
CodePlex上で公開しています。単機能アプリですので、コード規模は小さめです。あまりしっかりとはしてませんが、ユニットテストなども書いてあります。
WP7のReactive Extensions実践例サンプル、のつもりで書いたので、Rxを全面的に使っています。むしろRx縛りと言ってもいいぐらいにRxのみでやるのを無駄に貫いています。意味がなくてただ複雑化しただけの箇所多数。UIと絡めて使うのがイマイチ分からず振り回されてますねー。とはいえ、バッチリはまった部分も勿論あり。というわけで、Windows Phone 7においてReactive Extensionsがどのような場所で使えるのか、というのをコードとともに解説していきます。
コードはWindows Phone 7向けですが、通常のSilverlightでもWPFでも適用できる話なので、WP7関係ないからー、と言わず、Rxに興味ありましたら、眺めてみてください。
Linq to Event
Rxの特徴の一つはイベントのLinq変換です。Linq化して何が嬉しいかというと、柔軟なフィルタリングが可能になることです。WP7では、センサーからのデータ処理やタッチパネルなど、様々な箇所で威力を発揮すると思われます。UtakotohaではMediaPlayerの再生情報の変化に対してRx化とフィルタリングを施しました。
// Model/MediaPlayerStatus.cs public class MediaPlayerStatus { public MediaState MediaState { get; set; } public ActiveSong ActiveSong { get; set; } public static MediaPlayerStatus FromCurrent() { return new MediaPlayerStatus { MediaState = MediaPlayer.State, ActiveSong = MediaPlayer.Queue.ActiveSong }; } public static IObservable<MediaPlayerStatus> ActiveSongChanged() { return Observable.FromEvent<EventArgs>( h => MediaPlayer.ActiveSongChanged += h, h => MediaPlayer.ActiveSongChanged -= h) .Select(_ => MediaPlayerStatus.FromCurrent()); } public static IObservable<MediaPlayerStatus> MediaStateChanged() { return Observable.FromEvent<EventArgs>( h => MediaPlayer.MediaStateChanged += h, h => MediaPlayer.MediaStateChanged -= h) .Select(_ => MediaPlayerStatus.FromCurrent()); } // (省略)すぐ下に... }
Rx化は、ただFromEventをかますだけです。また、その際に、IEventではなく、本当に使う情報にだけ絞ったもの(この場合はMediaStateとActiveSong)をSelectで変換しておくと色々とコードが書きやすくなるのでお薦めです。
さて、「柔軟なフィルタリング」とは何か。ただデータを間引くだけなら、イベントの先頭でif(cond) return;を書けばいいだけです。RxではLinq化により、(自分自身を含めた)イベント同士の合成と、時間軸を絡めた処理が可能になります。これにより、従来一手間だった処理がたった一行で、連続した一塊のシーケンスとして処理することが可能になりました。
Utakotohaでも、生のイベントをそのまま扱わず、ある程度間引いて加工したものを渡しています。
/// <summary>raise when ActiveSongChanged and MediaState is Playing</summary> public static IObservable<Song> PlayingSongChanged(int waitSeconds = 2, IScheduler scheduler = null) { return ActiveSongChanged() .Throttle(TimeSpan.FromSeconds(waitSeconds), scheduler ?? Scheduler.ThreadPool) // wait for seeking .Where(s => s.MediaState == MediaState.Playing) .Select(s => new Song(s.ActiveSong.Artist.Name, s.ActiveSong.Name)); }
「再生中かつx秒間(デフォルトは2秒)新しいイベントが発生しなかった最新のものだけを流す」というものです。どういう意味かというと、連続で楽曲をスキップした時。Utakotohaでは再生曲の変更に合わせて、自動で歌詞検索をしますが、連続スキップにたいしても全て裏で検索に走っていたらネットワークの無駄です。なので、2秒間だけ間隔を置いて連続スキップされていない、と判断した曲のみを歌詞検索するようにしています。
こういう処理は、地味だけど必ず入れなければならない、けれど面倒くさい。でも、Rxを使えばなんてことはなく、Throttleで一撃です。Timer動かしたりDateTimeを比較したりなんて、もうしなくていいんだよって。
/// <summary>raise when MediaState Pause/Stopped -> Playing</summary> public static IObservable<Song> PlayingSongActive() { return MediaStateChanged() .Zip(MediaStateChanged().Skip(1), (prev, curr) => new { prev, curr }) .Where(a => (a.prev.MediaState == MediaState.Paused || a.prev.MediaState == MediaState.Stopped) && a.curr.MediaState == MediaState.Playing) .Select(s => new Song(s.curr.ActiveSong.Artist.Name, s.curr.ActiveSong.Name)); }
こちらは、状態が停止->再生になったことを検知するというもの。停止->再生でも自動検索を走らせたいので。
この source.Zip(source.Skip(1), /* merge */) は、一見奇妙に見えるかもしれませんが、ある種のイディオムです。一つ先(Skip(1))の値と合流するということは、Skip(1)時の値を基準にすると、現在値と一つ前の値で合流させることができる、ということになります。それにより、一つ前の状態を参照して停止->再生を検知しています。
過去の値を参照するには、他に、Scan(Aggregateの列挙版、そう考えると現在値と一つ前の値が使えることのイメージつくでしょうか?)やBufferWithCount(Listでバッファを持つ、第二引数でずらす範囲を指定可能)など、幾つかやり方がありますが、このZip(Skip(1))が最も扱いやすいところ。ただし、通常のものとSkipしたものとで、二つ分Subscribeされるということは留意したほうがいいかもしれません。そのことが問題になるケースもあるので、キャッシュを使う(Pulish(xs=>xs.Zip(xs.Skip(1)))など回避策を頭に入れておくと良いケースもあります。
Linq to Asynchronous
Rxのもう一つの大きな特徴は非同期のLinq変換です。そうすることにより、コールバックの連鎖で扱いにくかった非同期が、一本の流れに統一されます。
Utakotohaでは、歌詞の検索部分が代表的です。歌詞検索の背景ですが、goo歌詞のものを表示しているのだからgoo歌詞の検索を呼んでやるかなあ、と思ったのですが、それは色々マズいので、Bing Apiのサイト内検索を経由して、表示しました。Bing Apiについては若干苦労話もあるので、いつかまた。
// Model/Bing/BingRequest.cs public IObservable<SearchWebResult> Search(params SearchWord[] keywords) { var req = WebRequest.Create(BuildUrl(keywords)); return Observable.Defer(() => req.GetResponseAsObservable()) .Select(res => { var serializer = new DataContractJsonSerializer(typeof(SearchWebStructure)); using (var stream = res.GetResponseStream()) { return (SearchWebStructure)serializer.ReadObject(stream); } }) .SelectMany(x => (x.SearchResponse.Web.Results != null) ? x.SearchResponse.Web.Results : Enumerable.Empty<SearchWebResult>()); }
割とあっさり。Bing APIならびにJSONに関しては、以前Windows Phone 7でJSONを扱う方法について(+ Bing APIの使い方)として書きました。ほとんどそのままです。SearchResponse.Web.Resultsは、配列なので、SelectManyで分解してやります(nullの場合は空シーケンスを流す)。すると、後続に繋げるのが非常にやりやすくなります。実際に
// Model/Song.cs public IObservable<SearchWebResult> SearchLyric() { return new BingRequest() .Search(MakeWord(Artist), MakeWord(Title), LyricSite, Location, Language) .Where(sr => sr.Url.EndsWith("index.html")) .Do(Clean); }
といったように、Searchから更に続いて、若干のフィルタリングが入っています。これを使う場面では、勿論、更にチェーンが続きます。といったように、Rxは一つの流れを構築するわけですが、それらを徹底的に分解・分割して適切な場所への配置・組み合わせが可能になっています。もし、通常の非同期処理のようなコールバックの連鎖だったら、組み合わせは大変でしょう(だから、その場だけで処理したくなって、ネストが嵩んでしまう)
Orchestrate and Coordinate
Rx全体の特徴として、また、他の非同期を扱うライブラリと最も異なる、しかし重要な点として、中に流れるデータを区別しません。非同期もイベントもタイマーもオブジェクトシーケンスも、全て同列に扱います。それはどういうことかというと、データの種別を超えて合成処理が可能になるということです。つまり、Rxは、あらゆるデータソースを統合する基盤といえます。上のAsynchronousの例でも、非同期のWebResponseが、SelectMany以降はオブジェクトシーケンスに摩り替わっていました。
Utakotohaでは、歌詞の表示部分で色々なデータを混ぜあわせる処理を行っています。
// View/MainPage.xaml.cs LyricBrowser.NavigatedAsObservable() .Where(ev => ev.EventArgs.Uri.AbsoluteUri.Contains(GooLyricUri)) .SelectMany(ev => { // polling when can get attribute return Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(3)) .ObserveOnDispatcher() .Select(_ => (ev.Sender as WebBrowser).SaveToString()) .Select(s => Regex.Match(s, @"s.setAttribute\('src', '(.+?)'")) .Where(m => m.Success) .Take(1); }) .Select(m => WebRequest.Create(GooUri + m.Groups[1].Value).DownloadStringAsync()) .Switch() .Select(s => Regex.Replace(s.Trim(), @"^draw\(|\);$", "")) .Where(s => !string.IsNullOrEmpty(s)) .ObserveOnDispatcher() .Subscribe(jsonArray => { // insert json array to html LyricBrowser.InvokeScript("eval", @" var array = " + jsonArray + @"; var sb = []; for(var i = 0; i < array.length; i++) sb.push(array[i]); document.getElementById('lyric_area').innerHTML = sb.join('<br />')"); // (省略) }, e => MessageBox.Show("Browser Error")) .Tap(disposables.Add);
歌詞の表示といっても、WebBrowserに表示しているこということは、歌詞のURLを渡すだけです。それだけで済むはずでした。が、……妙に複雑怪奇です。歌詞のURLが渡されて表示が完了してからが起点として(Navigated)、処理を開始しています。
何故こうなったか。WP7が現在積んでるIEは7。ついでにFlashはまだ未対応で見れません。それとこれとがどう関係あるかというと、歌詞表示に関係あります。日本の歌詞サイトは大抵はコピペ禁止のために右クリック禁止、だけでなく、Flashで表示していたりします。それじゃあ手が出せない。では今回利用しているgoo歌詞は、というと、少し変わっていてHTML5 Canvasに描画しています。勿論Canvasは古いブラウザじゃ動かないので、互換用JavaScriptも挟んでいるよう。uuCanvas.js を使っているようですが、環境貧弱なWP7版のIE7じゃあ、土台動きませんでした。
このまんまじゃあ歌詞が表示出来なくて困ったわけですが、幸いgoo歌詞はJSONPで歌詞データを別途取得してCanvasにデータを流しているようなので、HTMLからJSONPの発行先を割り出して、歌詞データを頂いてしまえば問題ない(この時点で規約的にはグレーな気が)。生の歌詞データが手に入ってしまっ……。こいつをキャッシュするようにしてオフラインでも見れるといった機能を提供してあげられれば幸せになれるのですが、そういうのは利用規約に違反、してますね、明らかに。
じゃあどうするか。手元には歌詞の表示されていないブラウザ上のHTMLと、歌詞データがある。よし、じゃあブラウザにこちらからはめ込んでやればいいんじゃなイカ?
WP7ではWebBrowserのDOMは触れません。DOMを外から触ってappendChildしてサクッと終了、というわけにはいかず。ただ、外部から干渉出来る口が一つだけ用意されています。それがInvokeScript。外から実行関数を指定して、戻り値を受け取れます(DOMは無理なので、Stringで貰うのが無難)。ならば、evalして外から実行関数自体を注入してやれば、何だって出来る。どうにも馬鹿らしい気もしますが、このぐらいしか手がないのでShoganai(なお、予めWebBrowserのプロパティでスクリプト実行を許可しておかないと例外が出ます)。
Timer and Polling
イベント(Navigated)→タイマー(SaveToString)→非同期(DownloadStringAsync)という直列の合成でした。直列の合成を行うメソッドはSelectMany(もしくはSelect+Switch、両者には若干の違いがあるのですが、それに関しては後日説明します)で、Rxの中でも頻繁に使うことになるメソッドです。
ところで、タイマーが唐突なのですが、何故タイマーを仕込んでいるのか。どうもNavigated直後にSaveToString(WebBrowser内のHTMLを文字列化)だと、タイミング次第で上手く抽出できないことが多かったので(JSで色々処理されてる影響かな?)、必要なJSONPの書かれた属性が取れるまでSaveToStringをリトライするようにしました。つまり、ポーリング(定期問い合わせ)です。
ポーリングは普通だと面倒くさいはずなんですが、Rxだと恐ろしく簡単に書ける上に、こうして通常の処理の流れと合成することが可能になっているのが何よりも強力です。.NET Frameworkには幾つものTimerクラスがあって、何を使えばいいのかと戸惑ってしまうところがありますが、答えは出ました。Observable.Timerがベスト。大変扱いやすい。
これで、本来Canvasのあった領域にテキストデータとして歌詞を表示させられました。全く違和感のない、完璧なハメコミ合成。無駄なコダワリです。地味すぎて一手間かけてることなんてさっぱり分からない。だがそれがいい。……いや、ちょっと悲しい。それにしてもでしかし、Canvas, Flash対応になればこんなやり方は不要になるわけで、今年後半のアップデートでのIE9搭載が待ち遠しい。
Unit Testing
Silverlight向けのユニットテスト環境って、全然ない。ブラウザ上のSilverlightで動かす、というタイプは幾つかありますが、Visual Studioと統合された形のでないと、そんなのアタシが許さない。
TDDするわけでも、熱心にテスト書くわけでも、特段カバレッジを気にするわけでもない私ですが、テストが書けることは重要視しています。何故かというと、メソッドの動作確認が最も素早く行えるから。テスト(が出来ること)に何を期待しているかというと、確認したいんです、メソッドの動作を。手軽に、コンソールアプリを書く感覚で、素早く。処理をコンソールアプリにコピペって、もしくは並走してコンソールアプリを立てながら開発していたりなどを以前よくしたのですが、それを単体テストのメソッド部分に書けば、動作確認のついでに、テストまで手に入るので、それは素敵よね?と。
(単体)テストが第一の目的ではない、(動作確認)テストが目的なのだ。だから、テストフレームワークはVisual Studioと完全な統合を果たしていなければならない。ショートカットでIDE内のウィンドウで即座に実行。スピードが大事。また、シームレスなデバッグ実行への移行も。故にMSTestを選択するのである(キリッ
などなどはその辺にしておいて、それはともかくで、素直にフル.NET Frameworkで動くMSTestを使います。一応Silverlightのアセンブリはフル.NETからも参照可能のようなので、普通にテストプロジェクトを立ててDebug.dll を参照してやる(プロジェクト参照は警告出るので)という手も使えなくはなさそうなのですが、完全な互換を持つコアライブラリは全体のごく一部で、それ以外を使っていると普通に実行時例外でコケるなど、正直使えないと私は判断しました。よって、アセンブリ参照でやるのは諦め。プロジェクト参照で警告が出る理由も分かりました、あまり実用的な機能ではない……。
代わりにWP7プロジェクトとテストプロジェクトの間に、.NET4ライブラリプロジェクトを立てて、「追加->既存の項目->リンクとして追加」で、フルフレームワークとWP7間で.csファイルを共有してやります(Viewは勿論共有出来ないので、基本はModelのみ)。
勿論、コードレベルで互換が取れていないと動きません。そんなわけで、少なくともModelに関してはWPF/SL/WP7で共通で使いまわせるように意識して作りたいところです。移植性というだけじゃなく、MSTestの恩恵を受けれるので。ただまあ、無理な部分は無理で諦めちゃってもいいとは思います(SLのほうにだけあるクラスとかもありますから)。非同期のテストなどは、幸いRxを使っていれば非常に簡単なので、バシバシ書いちゃいましょう。
// Utakotoha.Test/SongTest.cs [TestMethod] [Timeout(3000)] public void SearchLyric() { var song = new Song("吉幾三", "俺ら東京さ行ぐだ"); var array = song.SearchLyric().ToEnumerable().ToArray(); array.Count().Is(1); array.First().Title.Is("俺ら東京さ行ぐだ 吉幾三"); array.First().Url.Is("http://music.goo.ne.jp/lyric/LYRUTND1127/index.html"); }
ToEnumerableしてToArrayするだけです!非同期のテストなんて怖くない。
Portable Library Tools CTPという、各環境で互換性の取れるライブラリが作成出来るもの、なども出てきているので、リンクで追加とかいう間抜け(そして面倒)なことじゃなく、プロジェクトごと分離して、Modelは互換で生成、というのが将来的には良いやり方になるかなあ、などと思っています。そういう事情から、私は移植性とは関係ない立場からも、Portable Library Toolsの発展に期待しています。
なお、アサーションはChaining Assertion使っています。ドッグフードドッグフード。というかもう、必需品なので、これないと書けない、書きたくない……。
Mocking Event with Moles and Rx
さて、このやり方の利点として、PexやMolesが使えます(Pexは一応SLをサポートしたものの、Molesは依然としてSL未サポート)。Moles(Microsoft Researchが提供するモックフレームワーク、フリー)の乗っ取り機構は強力なので、テスト可能範囲が大幅に広がります。詳しくはRx + MolesによるC#での次世代非同期モックテスト考察をどうぞ。
今回Linq to Eventで紹介したPlayingSongActiveは、以下のようにテストしています。MediaPlayer周りはXNAなので、Test側の参照DLLとしてXNA Game Studio v4.0のMicrosoft.Xna.Framework.dllを参照。そしてMolesで乗っ取り。
// Utakotoha.Test/MediaPlayerStatusTest.cs private MediaPlayerStatus CreateStatus(MediaState state, string artist, string name) { return new MediaPlayerStatus { MediaState = state, ActiveSong = new Microsoft.Xna.Framework.Media.Moles.MSong { NameGet = () => name, ArtistGet = () => new MArtist { NameGet = () => artist } } }; } [TestMethod, HostType("Moles")] public void PlayingSongActiveTest() { // event invoker var invoker = new Subject<MediaPlayerStatus>(); MMediaPlayerStatus.MediaStateChanged = () => invoker; // make target observable var target = MediaPlayerStatus.PlayingSongActive().Publish(); target.Connect(); // at first, stopped using (target.VerifyZero()) { invoker.OnNext(CreateStatus(MediaState.Stopped, "", "")); } // next, playing using (target.VerifyOnce(song => song.Is(s => s.Title == "song" && s.Artist == "artist"))) { invoker.OnNext(CreateStatus(MediaState.Playing, "artist", "song")); } // pause using (target.VerifyZero()) { invoker.OnNext(CreateStatus(MediaState.Paused, "", "")); } // play again using (target.VerifyOnce(song => song.Is(s => s.Title == "song2" && s.Artist == "artist2"))) { invoker.OnNext(CreateStatus(MediaState.Playing, "artist2", "song2")); } }
元のコード自体がイベント発火部分はRxで包んであるので、そのイベント発火だけを差し替え。SubjectとはイベントのRxでの表現。OnNextでイベント発火の代用が可能になっています。これで、任意のイベント(今回はMediaPlayerなので、再生停止であったり再生開始であったり)を発行して、その結果の挙動を確認しています。
VerifyなんたらはIObservableへの自前拡張メソッドで、発火されたか/回数の検証です。「イベントは発生したけれどフィルタリングされて値が届かなかった」ことの、フィルタリングが正常に出来たかの確認って、そのままだと難しい。如何せんSubscribeまで届いてくれないということですから。そのため、その辺を面倒みてくれるものを用意しました。
// Utakotoha.Test/Tools/ObservableVerifyExtensions.cs /// <summary>verify called count when disposed. first argument is called count.</summary> public static IObservable<T> Verify<T>(this IObservable<T> source, Expression<Func<int, bool>> verify) { var count = 0; return source .Do(_ => count += 1) .Finally(() => { var msg = verify.Parameters.First().Name + " = " + count + " => " + verify.Body; Assert.IsTrue(verify.Compile().Invoke(count), "Verifier " + msg); }); } /// <summary>verify called count when disposed. first argument is called count.</summary> public static IDisposable VerifyAll<T>(this IObservable<T> source, Expression<Func<int, bool>> verify, Action<T> onNext = null) { return source.Verify(verify).Subscribe(onNext ?? (_ => { })); } /// <summary>verify not called when disposed.</summary> public static IDisposable VerifyZero<T>(this IObservable<T> source) { return source.VerifyAll(i => i == 0); } /// <summary>verify called once when disposed.</summary> public static IDisposable VerifyOnce<T>(this IObservable<T> source, Action<T> onNext = null) { return source.VerifyAll(i => i == 1); }
usingによるスコープを抜けるとFinallyで検証が入ります。RxでイベントをラップするとIDisposableになる、そのことの利点が生きてきます。
今後の改善
Pivotのヘッダーのデザインがどうも間抜け(マージンの取り方が変だし文字サイズも違和感あり)なのが気になってるので、変えたいです。HeaderTemplateの編集の仕方がよくわからずで放置なのですけれど、ゆったり紐解けば出来るでしょう。多分。
レジュームへの配慮が全くなくて、別画面にいくと真っ白になるのがビミョい。WebBrowserが絡むので完全な復元は無理だから、いっかー、とか思ったのが半分はあるのですが、いやまてその理屈はオカシイ。ので、ちょっと何とかさせないとですね。
xaml.csのコードが全体的にマズい。特にOAuth認証の部分はありえない強引さなのでとっとと変更。あと、もう少し適切な分割。MVVMはわからんちん。
Settingsが何か変。IsolatedStorageSettingsというか、その内部のDataContractSerializerの都合というか。これだ!というやり方ないかしら。今のやり方は、非常に間抜け。
ブラウザ画面黒背景はビミョーなので、設定でアプリ全体を白背景に変更するオプションを入れるのもいいかなー。それとアプリ起動時は楽曲を再生中でも自動検索しないのだけど、自動検索してくれたほうが嬉しいかなー。など、細かい点では色々考えること、追加することあります。
まとめ
上手く決まった部分しか解説してないので、実際のコードは残念ながらスパゲッティです:) というか、UI絡みのコードってほとんど書いたことないので、経験の無さが如実に現れていて苦すぃ。WP7で勝手がわからないというもの若干はありますが、それ以前の問題がかなり。サンプルアプリとしても、もう少し良くしたいので、コードは徐々に洗練させていきたいですます。
アプリとしては、まあまあいい出来というか、実用品として悪くないフィーリングだと思うのですが、どうでしょうか?画面周りは、このレイアウトで決まるまで何度も試して投げてを繰り返してこれに落ち着きました。実際に作りながら、試しながらでないとこういうの決められないよね。コードに関してもそうだけど。ともあれ、Pivotいいよねー。やはりWP7といったらPivot。
そんなわけで、RxはWP7開発のお供として欠かせない代物なので、是非使ってみてください。また、WP7で欠かせないということはSilverlightで欠かせないということであり、Silverlightで欠かせないということはWPFでも欠かせないということでも、あったりなかったりするので、Rxによるプログラミングパラダイムの変化を是非とも楽しんでみてください。Linq to Anything!
一記事に収めるため少々駆け足気味だったので、なにか不明な点、質問などありましたら気楽にコメントどうぞ。突っ込みも勿論歓迎です。
Windows Phone 7でJSONを扱う方法について(+ Bing APIの使い方)
- C# Rx WindowsPhone7 - 11.03/31
C#と親和性の高いデータ形式はXMLです。何と言ってもLinq to Xmlが強力です。また、SOAPも悪くない、というのもVisual Studioの自動生成が効くので何も考えずともホイホイ使えます。ではJSONは、というと、これは割と扱いづらいところがあるのが正直なところ。しかしWindows Phone 7においては、JSONを選択すべきでしょう。なにせ、モバイル機器。ネットワークがとても貧弱。データは小さいに越したことはない。XMLとJSONとでは、雲泥の差です。
WPFではJsonReaderWriterFactory(と、内部にそれを用いたDynamicJson)、SilverlightではSystem.Jsonなどが用意されていますが、WP7には一切ありません。じゃあどうするかといえば、シリアライザを使います。WP7ではDataContractJsonSerializerが標準で用意されている(WPF, SLにもあります)ので、それを使ってデシリアライズしてJSONをオブジェクトに変換するのが基本戦略となります。
外部ライブラリ、Json.NETを使うという手も勿論ありますが。
BingからのJSONの取得
何はともあれ、サンプル題材のJSONを拾ってきましょう。Webからの取得というと、最近はいつもTwitterのPublic Timelineでマンネリ飽き飽きなので、別のものを。WP7なので、Bing APIを使いましょう!Bing APIはIDを取得しないと使えないのでサンプル的にどうよ、というところもありますが、IDの取得は簡単(ほんとワンクリックです)だしWP7と親和性の高いAPIでもあるので、これを機に、試しに取ってみるのも良いのではと思います。画像検索、翻訳など色々種類があるのですが、今回はWeb検索(sources=web)にします。
// 標準WP7テンプレのMainPage.xaml.csにベタ書き const string AppId = ""; // AppIdは登録してください Uri CreateQuery(params string[] words) { // countなどは変数で置き換えれるようにするといいのではと思います、ここでは固定決め打ちですが var query = "?Appid=" + AppId + "&query=" + Uri.EscapeUriString(string.Join(" ", words)) + "&sources=web" + "&version=2.0" + "&Market=ja-jp" + "&web.count=20" + "&web.offset=0"; return new Uri("http://api.search.live.net/json.aspx" + query); } public MainPage() { InitializeComponent(); var wc = new WebClient(); Observable.FromEvent<DownloadStringCompletedEventHandler, DownloadStringCompletedEventArgs>( h => h.Invoke, h => wc.DownloadStringCompleted += h, h => wc.DownloadStringCompleted -= h) .ObserveOnDispatcher() .Subscribe(e => { var json = e.EventArgs.Result; // ダウンロード結果(json文字列) MessageBox.Show(json); }); wc.DownloadStringAsync(CreateQuery("地震")); }
json.aspxにクエリ文字列をつけてGETするだけなので割とお手軽。クエリ文字列がゴチャゴチャして分かりづらいのですが、基本的に弄るのはqueryとweb.countぐらいかな、と思います。BingのReferenceは、生成元のクラス構造がまんま掲示されているだけで、恐ろしく分かりづらいので、適当にサンプルから当たりをつける感じで。
非同期通信の実行はReactive Extensions(Rx)で行います。Windows Phone 7では標準で入っているのでSystem.ObservableとMicrosoft.Phone.Reactiveを参照に加えてください。非同期通信を生でやるなんてありえませんから!Rx利用を推奨します。
得られるJSONは下記のものです。
{ "SearchResponse": { "Version": "2.0", "Query": { "SearchTerms": "地震" }, "Web": { "Total": 88, "Offset": 0, "Results": [ { "Title": "地震情報 - Yahoo!天気情報", "Description": "Yahoo!天気情報は、市区町村の天気予報、世界の天気...", "Url": "http://typhoon.yahoo.co.jp/weather/jp/earthquake/", "DisplayUrl": "typhoon.yahoo.co.jp/weather/jp/earthquake", "DateTime": "2011-03-29T19:11:00Z" }, { "Title": "地震情報 :: ウェザーニュース", "Description": "最新の地震の震度、震源地、震度分布を速報で届けます...", "Url": "http://weathernews.jp/quake/", "DisplayUrl": "weathernews.jp/quake", "DateTime": "2011-03-28T09:10:00Z" }, // 配列上なので幾つも... ] } } }
JSONに関してはJSON ViewerをVisual StudioのVisualizerに組み込むとかなり快適にプレビュー出来るようになります。が、カスタムVisualizerはWP7では実行出来ないのでテキストで見て、スタンドアロンのものにコピペってのを実行ですね、しょんぼり。
DataContractJsonSerializer
では、JSONをオブジェクトに変換しましょう。基本的には、1:1に対応するクラスを作るだけ。必要に応じて System.Runtime.Serializationの参照を加えDataContract, DataMember属性なども加えればよし。
public class BingWebRoot { public SearchResponse SearchResponse { get; set; } } public class SearchResponse { public string Version { get; set; } public Query Query { get; set; } public Web Web { get; set; } } public class Query { public string SearchTerms { get; set; } } public class Web { public int Total { get; set; } public int Offset { get; set; } public Results[] Results { get; set; } } public class Results { public string Title { get; set; } public string Description { get; set; } public string Url { get; set; } public string DisplayUrl { get; set; } public string DateTime { get; set; } }
JSONは、JavaScriptのオブジェクトとほぼ同一の記述ですが、ようするに{}になっている部分はクラスで、[]になっている部分は配列で、置き換えていけばいい、ということで。難しくはないのですが、面倒くさいには大変面倒くさい。なお、JSONの構造を全部記述する必要はなく、必要なものだけでも構いません、例えばVersionやQueryはいらないから省くとか、全然アリです。
そして、 System.ServiceModel.Web を参照設定に加え、DataContractJsonSerializerを使います。
var wc = new WebClient(); Observable.FromEvent<OpenReadCompletedEventHandler, OpenReadCompletedEventArgs>( h => h.Invoke, h => wc.OpenReadCompleted += h, h => wc.OpenReadCompleted -= h) .ObserveOnDispatcher() .Subscribe(e => { using (var stream = e.EventArgs.Result) { var serializer = new DataContractJsonSerializer(typeof(BingWebRoot)); var result = (BingWebRoot)serializer.ReadObject(stream); MessageBox.Show(result.SearchResponse.Web.Results[0].Title); } }); wc.OpenReadAsync(CreateQuery("地震"));
デシリアライズはReadObject、シリアライズはWriteObjectで行います。基本はstreamを渡すだけでオブジェクトの出来上がり。
with Reactive Extensions
ですが、まあ、Resultsが欲しいだけなのにSearchResponse.Web.Resultsは長げーよ、とか、DateTimeがstringでイヤだー、とか色々あります。そういう場合はJSONとのマッピング用のクラスとは別に、アプリケーション側で使うクラスを別に立ててやればいいんぢゃないかしら。
public class SearchResults { public string Title { get; set; } public string Url { get; set; } public DateTime DateTime { get; set; } public override string ToString() { return DateTime + " : " + Title + " : " + Url; } }
TitleとUrlとDateTimeしかいらない!という具合で。これを、今度はWebRequestを使って書くと
var req = WebRequest.Create(CreateQuery("ほむほむ")); Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse)() .Select(r => { using (var stream = r.GetResponseStream()) { var serializer = new DataContractJsonSerializer(typeof(BingWebRoot)); return (BingWebRoot)serializer.ReadObject(stream); } }) .SelectMany(x => x.SearchResponse.Web.Results) .Select(x => new SearchResults { DateTime = DateTime.Parse(x.DateTime), Title = x.Title, Url = x.Url }) .ObserveOnDispatcher() .Subscribe(x => { // 加工は全部終わってるのでここで色々自由に処理 Debug.WriteLine(x); });
となります。最初のSelectは非同期の結果、次のSelectManyではResults[]、つまり普通の配列を平坦化して、以降は普通のLinqのようなコレクション処理をしています。
非同期リクエストとオブジェクトのコレクション処理が、完全にシームレスに溶け込んでいます。これが、RxがLinqとして存ることの真価の一つです。記述が統一され、かつ限りなくシンプルになる。Rxは非同期が、イベントが、時間が、簡単に扱えます。でも、本当の真価は単独で使うというだけでなく、それらが全てPush型シーケンスに乗っていることで、統合することが可能だというところにあります。
でも、むしろ分かりにくい?ふむむ……。慣れの問題、などというと全く説得力がなくてアレですが、しかし、慣れです。記述がシンプルになり、柔軟性と再利用性が増していることには間違いないわけで、後は一度全て忘れてLINQの世界に飛び込んでしまえばいいと思うんだ。
Linqは各処理の単位が細分化されている(Selectは射影、Whereはフィルタ)ことも特徴ですが、これは思考の再利用可能性を促します。非同期->オブジェクト配列=SelectManyなど、単純な定型パターンに落とし込めます。C#はもとより強力なIntelliSenseにより、ブロックを組み立てるかの如きなプログラミングを可能にしていますが、Linqでは、それが更に先鋭化されていると見れます。
まとめ
これも現在製作中のWP7アプリからの一部です。最近Bing API利用に切り替えたので。無駄に汎用化して作りこみつつきりがないので適度なところできりあげつつ。ユニットテスト作ってあったので移行自体は幸いすんなりいった。良かった良かった。テスト大事。
Bing APIの前は諸事情あってGoogleからのスクレイピングでした。スクレイピングはグレーだろうということで代替案をずっと探していて、何とかBingに落ち着きました。最初はどうにも使い物にならない、と思ったのですが、検索パラメータを色々変えて、ある程度望む結果が出るようにはなったかな、と。Bingは結構癖があって、調整大変ですね。その話は後日、WP7アプリが完成したときにでも……。
コード的にはスクレイピングのほうも割と凝ってたんですけどねー、バッサリとゴミ箱行き。復活することは、ないかな。もったいないけどしょうがない。いつかそのうち紹介する日は、来るかも来ないかも。
そんなわけで延々と足踏みしていて実装は相変わらず一歩も進んでませんが(!) 順調に制作は進行中なので乞うご期待。いやほんと。
Widows Phone 7でアプリケーション名やバージョン番号をバインドする方法
- C# WindowsPhone7 - 11.03/28
アプリケーション名の表示や、about画面に表示したいであろうバージョン番号、どうします?直書きのstringやリソースから?それもいいのですけれど、せっかくプロジェクトのプロパティ(AssemblyInfo.cs)で、アプリケーション名やバージョン番号を設定しているわけだから、そこから利用できたほうがいいですよね。
というわけで、これらの情報はアセンブリから取得しましょう。
public class AssemblyInfoData { public static readonly AssemblyInfoData ExecutingAssembly = new AssemblyInfoData(Assembly.GetExecutingAssembly()); public string FileName { get; private set; } public string Version { get; private set; } public string FileVersion { get; private set; } public string Title { get; private set; } public string Description { get; private set; } public string Configuration { get; private set; } public string Company { get; private set; } public string Product { get; private set; } public string Copyright { get; private set; } public string Trademark { get; private set; } public string Culture { get; private set; } public AssemblyInfoData(Assembly assembly) { var assemblyName = new AssemblyName(assembly.FullName); FileName = assemblyName.Name; Version = assemblyName.Version.ToString(); FileVersion = GetAttributeName<AssemblyFileVersionAttribute>(assembly, a => a.Version); Title = GetAttributeName<AssemblyTitleAttribute>(assembly, a => a.Title); Description = GetAttributeName<AssemblyDescriptionAttribute>(assembly, a => a.Description); Configuration = GetAttributeName<AssemblyConfigurationAttribute>(assembly, a => a.Configuration); Company = GetAttributeName<AssemblyCompanyAttribute>(assembly, a => a.Company); Product = GetAttributeName<AssemblyProductAttribute>(assembly, a => a.Product); Copyright = GetAttributeName<AssemblyCopyrightAttribute>(assembly, a => a.Copyright); Trademark = GetAttributeName<AssemblyTrademarkAttribute>(assembly, a => a.Trademark); Culture = GetAttributeName<AssemblyCultureAttribute>(assembly, a => a.Culture); } private string GetAttributeName<T>(Assembly assembly, Func<T, string> selector) where T : Attribute { var attr = assembly.GetCustomAttributes(typeof(T), true).Cast<T>().FirstOrDefault(); return (attr == null) ? "" : selector(attr); } }
FileName, Versionはnew AssemblyNameに渡してから(WPFだとGetNameで直に取れるのですが、Silverlightだとセキュリティ違反で例外が飛ぶためこうする必要がある)、それ以外の値はカスタム属性から取得できます。また、任意のAssemblyの情報をコンストラクタに投げて取得出来るようになっていますが、どうせ必要なのは実行アセンブリの情報だけでしょ?ってことで、public staticなフィールドにExecutingAssemblyのデータを公開するようにしています。なので、コードからは、 AssemblyInfoData.ExecutingAssembly.Version とアクセスするだけで、簡単に取得できます。
でも、コードから欲しいということはほとんどなくて、UIに表示するためだけに欲しいのですよね、こういう情報は。バインディングしましょう!まず、こんなクラスを用意します。
public class AssemblyInfoDataBindingHelper { public AssemblyInfoData Value { get { return AssemblyInfoData.ExecutingAssembly; } } }
何故これが必要かというと、WPFの場合は{x:static}でstatic変数もバインド出来るのですが、Silverlight/WP7ではバインド出来ないためです。いやあ、カッコ悪いですね、{x:static}欲しいですね、まあ、ないものはしょうがない。
次にApp.xamlのApplication.Resourcesの中に
<Application.Resources> <!-- Applicationのところで xmlns:local="ネームスペース" を宣言しておく--> <local:AssemblyInfoDataBindingHelper x:Key="AssemblyInfoData"/> </Application.Resources>
と書いてリソースを登録。準備はこれで完了で、あとはバインドするだけ。
<TextBlock x:Name="ApplicationTitle" Text="{Binding Value.Title, Source={StaticResource AssemblyInfoData}}" Style="{StaticResource PhoneTextNormalStyle}"/> <TextBlock x:Name="PageTitle" Text="{Binding Value.Version, Source={StaticResource AssemblyInfoData}}" Style="{StaticResource PhoneTextTitle1Style}" />
と、以上です。これで下のような感じに
表示されました。ApplicationTitleにTitleは分かりますがPageTitleにVersionは丸っきりイミフ。ボタンは、何となく寂しいから置いただけで意味はないです気にしないで。
AssemblyInfoDataクラスのコードは完全に独立して使い回しが効くので、コピペってどうぞご自由にお使いください。煮るなり焼くなり……、パブリックドメインで。
まとめ
といったのは、一応、今製作中のWP7アプリの一部です。順調に制作は遅れまくり。うむむ。今月中といきたかったのだけど、まーだずれ込みそう。その前は二月中のつもりだったのだけど、Chaining Assertionが思いの外引っ張りすぎて手を付けてる余裕がなかった。とにかく、4月中頃までには、マーケットプレイスで公開したいなあ。あとソースコードも公開します(というか既に製作中のがこっそり公開されてます)。Reactive Extensionsの実践例として、ただたんに非同期で使うというだけじゃなく、こういうケースで使える、コードはこうなる。というサンプルとして役立てればいいな、という思いで書いてますので、適当に待っていてください。
人に見せるためのコード、というのを念頭に置きすぎていて、同じ場所のコードの修正ばかり繰り返していてアプリ全体としては一歩も製作が進まないという超鈍足状態に陥ってますが(コード書きの遅さに定評のある私です(キリッ)、でも、書きなおす度に確実によくなっていく実感はあるので、最終的にそこそこ見せれるコードになるのではないかと思っています。少なくとも、部分的には面白い内容になるはずです。
ReactiveOAuth ver.0.2.0.0
- C# Rx WindowsPhone7 - 11.01/23
ReactiveOAuthを更新しました。今回よりNuGetに対応したのでReactiveOAuth、もしくはReactiveOAuth-WP7で入れられます。あとSilverlightもサポートしました。そして、盛大にバグってたのを修正しました。UrlEncodeをそのまんまの使ったのでRFC2396でRFC3986じゃないから「!」とかが含まれた文章を投稿すると死ぬ、という。あまりにも限りなく初歩ミスで、死ぬほど反省します……。
おまけに、この辺りがマズいのは出した当初から薄々感づいていたのですが、「あとで直す」とか思って延々と今まで放置していたので、もう本当に本当にごめんなさい。リリース用にzip固めるスクリプトも書いた(fsxで)し、ディレクトリ周りも整理したしで、負担なくリリースしてける態勢を整えたので、もう放置しません、今後他のプロジェクトでも。本当に、今回はあまりにも酷かったのでなんともかんともです。
一応、通信部分を以前書いたReactive Extensions用のWebRequest拡張メソッドに載せ替えたりしたりなど、中身も変えたんですが、うーん。解説する気力が折れたぽ。
とりあえずドッグフードな体制を整えるためにXboxInfoほげほげにとっとと積んでしまうかな……。
Windows Phone 7で同期APIを実現するたった つの冴えないやり方
- C# Rx Silverlight WindowsPhone7 - 10.10/14
Windows Phone 7が発表されました。中々に素晴らしい仕上がりに見えます。米国では来月発売と非常に順調そうですが、日本では…… ローカライズが非常に難しそうに見えました。発売されること自体は全然疑っていませんが、問題は、米国で達成出来ているクオリティをどこまで落とさず持ってこれるか。日本語フォントや日本語入力、今一つなBing Map、但し日本は除くなZune Pass。本体だけではなく、周辺サービスも持ってきて初めてWindows Phone 7の世界が完成する。ということを考えると、大変難しそう。
その辺はMicrosoft株式会社に頑張ってもらうとして、一開発者的には淡々とアプリ作るだけでする。というわけで、標題のお話。WP7というかSilverlightと、そして例によっていつもの通り、Rxの話です。
問題です。以下のコードの出力結果(Debug.WriteLineの順序)はどうなるでしょうか。
// 何も変哲もないボタンをクリックしたとする void Button_Click(object sender, RoutedEventArgs e) { Debug.WriteLine("start"); // 10秒以内にレスポンスが来るとする var req = WebRequest.Create("http://bing.com/"); req.BeginGetResponse(ar => Debug.WriteLine("async"), null); Thread.Sleep(10000); // 10秒待機 Debug.WriteLine("end"); }
答えは後で。
Dispatcher.BeginInvokeとPriority
Dispatcherとは何ぞやか。について説明するには余白が狭すぎる。ので軽くスルーしてコードを。Dispatcher.BeginInvokeは通常は別スレッドから単発呼び出しが多いですが、UIスレッド上でDispatcher.BeginInvokeを呼ぶとどうなるでしょう?
// (WPF)何も変哲もないボタンをクリックしたとする void Button_Click(object sender, RoutedEventArgs e) { Debug.WriteLine("start"); Dispatcher.BeginInvoke(new Action(() => Debug.WriteLine("normal1")), DispatcherPriority.Normal); Dispatcher.BeginInvoke(new Action(() => Debug.WriteLine("background")), DispatcherPriority.Background); Dispatcher.BeginInvoke(new Action(() => Debug.WriteLine("normal2")), DispatcherPriority.Normal); Debug.WriteLine("end"); }
結果は、start->end->normal1->normal2->backgroundです。なおDispatcherPriorityはWPFでは設定可能ですが、Silverlightでは設定不可で、内部的には全てBackgroundになります。挙動は以下の図のようになっています。
一番上のブロックが現在実行中メソッド。下のがDispatcher。BeginInvokeで実行キューに優先度付きで突っ込まれて、現在実行中のメソッドが終了したら、キューの中のメソッドが順次、優先度順に実行されます。といったイメージ。
問題の答え
冒頭の問題の答えは、WPFではstart->async->endの順。Silverlight(WP7も含む)ではstart->end->asyncの順になります。ええ。WPFとSilverlightで挙動が違うのです!今更何をっていう識者も多そうですが(Silverlightももう4だしねえ)私ははぢめて知りました。はまった。BeginGetResponseはWPFでは(というか普通の.NET環境では)そのまま別スレッド送りで実行されますが、Silverlightでは一旦Dispatcherに突っ込まれた後に実行されるのですねー、といったような雰囲気(なので一つ前でDispatcher.BeginInvokeがどうのという話を挟みました)。
Silverlightでは、BeginGetResponseはすぐには実行されない。それを踏まえて次へ。
非同期 to 同期
非同期を同期に変換してみましょう。Reactive Extensions for .NET (Rx)で。
void Button_Click(object sender, RoutedEventArgs e) { // 非同期を同期に変換! var req = WebRequest.Create("http://bing.com/"); var response = Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse).Invoke() .First(); Debug.WriteLine(response.ResponseUri); }
Rxで非同期を包むと長さ1のReactiveシーケンスとなるので、Firstを使うと同期的に値を取り出せる、という話を前回の記事 Rxを使って非同期プログラミングを簡単に でしました。そして実際、上のコードはWPFでは上手く動きます。きっちりブロックして値を取り出せる。勿論、それならGetResponseを使えよという話ではありますが。
では、Silverlight(勿論WP7でも)では、というと…… 永久フリーズします。理由は、BeginGetResponseはDispatcherに積まれた状態なので、現在実行中のメソッドを抜けない限りは動き出さない。Firstは非同期実行が完了するまでは現在実行中のメソッドで待機し続けるので、結果として、待機しているので実行が始まらない=実行完了は来ない→永遠に待機。になります。
結論としては、UIスレッド上で同期的に待つことは不可能です。代替案としてはThreadPoolで丸々包んでしまうということもなくはない。
void Button_Click(object sender, RoutedEventArgs e) { ThreadPool.QueueUserWorkItem(_ => { var req = WebRequest.Create("http://bing.com/"); var response = Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse) .Invoke() .First(); Debug.WriteLine(response.ResponseUri); }); }
こうすれば、BeginGetResponseが発動するので問題なく待機して値を取り出せます。でも、これじゃあ全然嬉しくもない話で全く意味がない。Rxで包んでいる状態ならば、.Subscribeでいいぢゃん。ということだし。
// 非同期を同期に、そんなことは幻想なのでこう書くのがベストプラクティス void Button_Click(object sender, RoutedEventArgs e) { var req = WebRequest.Create("http://bing.com/"); Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse) .Invoke() .Subscribe(res => Debug.WriteLine(res.ResponseUri)); }
素直に、普通にReactive Extensionsを使うのが、一番簡単に書けます。息を吸うように、ごく自然にそこにあるものとしてRxを使おう。
Delegate.BeginInvokeのこと
相違点はまだあります。普通の.NET環境ではDelegateのBeginInvokeで非同期実行できますが、Silverlightにはありません。やってみるとNotSupportedExceptionが出ます。じゃあFuncにラップしてみるとどうだろう?
void Button_Click(object sender, RoutedEventArgs e) { Debug.WriteLine("start"); Action action = () => Debug.WriteLine("action"); Func<AsyncCallback, object, IAsyncResult> wrappedBeginInvoke = action.BeginInvoke; wrappedBeginInvoke.Invoke(ar => Debug.WriteLine("async"), null); Debug.WriteLine("end"); }
WPFではstart->end->action->async。Silverlightではstart->NotSupportedExceptionの例外。ここまではいいんです。Windows Phone 7でこのコードを試すと、例外出ません。何故か実行出来ます。SilverlightではBeginInvokeは出来ないはずなのに!そして、その実行結果はstart->action->end。つまり、非同期じゃない。BeginInvokeじゃない。Invokeとして実行されてる。意味がさっぱりわかりません。
不思議!不思議すぎたので、MSDNのWindows Phone 7 Forumで聞いてみましたが、良い返答は貰えず。とりあえず、怪しい挙動をしているのは間違いないので、これはやらないほうが無難です。勿論、通常こんなこと書きはしないと思うのですが、RxのFromAsyncPatternをDelegateに対して使おうとするとこうなりますので注意。Delegateの非同期実行したい場合はFromAsyncPetternじゃなくてToAsyncを使いましょう。
void Button_Click(object sender, RoutedEventArgs e) { Debug.WriteLine("start"); // Observable.ToAsync(()=>{})でもいいし、すぐにInvokeするならObservable.Start(()=>{})も有用 Action action = () => Debug.WriteLine("action"); action.ToAsync().Invoke().Subscribe(_ => Debug.WriteLine("async")); Debug.WriteLine("end"); }
こうすることで、Rxは内部でBeginInvokeではなくThreadPoolを使うので、問題は起こらずWPFと同じ結果が得られます。
まとめ
同期的に書くほうが分かりやすいには違いないし、また、非同期が苦痛なのもその通り。でも、非同期を同期に、なんて考えない方がいい。AutoResetEventなどを駆使して擬似的に再現出来たとしても、やっぱ無理ありますし、非同期のメリットを犠牲にしてまでやるものではない。確かに非同期をそのまま扱うのは苦痛だけれど、Rxを使えば緩和される。むしろ慣れれば同期的に書くよりも利点が見えてくるぐらい。無理に同期に変換しようとしないでRxを覚えよう。が、結論です。
でもドキュメント全然ないし日本語の話なんて皆無で難しいって? そうですねえ、そうかもですねえ……。このブログも全然順序立ってなくて、思い立ったところから書いてるだけで分かりづらいことこの上ないし。うむむ……。でも、Rxの機能のうち非同期周りの解説に関してはほとんど出せているはずなので、読みにくい文章ですが、目を通してもらえればと思います。
もしつまづくところがあれば、Twitterで「Reactive Extensions」を投稿文に含めてくれれば、Twitter検索経由で見つけて反応します。(「Rx」だと検索結果が膨大になるので反応出来ません……)。検索を見てるワードとしては、他に「Linq」なども高確率で反応しにいきます←逆に怖いって?すみませんすみません。
「C#」が検索キーワードに使えたらいいんですけどねえ。「Scala」とか「JavaScript」は常時見てるんですが、かなり活況に流れているんですよ。そういうの見てると、Twitter上のC#な話も漏らさず見たい・参加したいと思ってしまうわけで。
ReactiveOAuth - Windows Phone 7対応のOAuthライブラリ
- C# Rx WPF WindowsPhone7 - 10.09/12
Windows Phone 7用のOAuth認証ライブラリを作成し、公開しました。他のライブラリに比べての特徴は、非同期APIしか用意されていないWindows Phone 7での利用を念頭に置き、Reactive Extensions(Rx)をフル活用しているという点です。そもそもWindows Phone 7対応のOAuthライブラリが少ないので、特にWindows Phone 7開発者は是非どうぞ。
Windows Phone 7専用というわけでもないですが(Console/WPFで使えるよう、DLLとサンプルコードを用意してあります)、Windows Phone 7以外では別途Rxのインストールが必要です。Windows Phone 7環境では最初から入っているのでRxのインストール不要。Silverlight用は、コードコピペで別プロジェクト立てるだけで動くと思うんですが、確認取るのが面倒だった(クロスドメインがー)ので、そのうちに。
ところでそもそもRxって何、という人は Reactive Extensions入門 + メソッド早見解説表 をどうぞ。
何故Rxを使うのか
ReactiveOAuthの説明に入る前に、何故Rxを使うのかということを少し。理由は簡単で、非同期プログラミングは大変だから。論より証拠で、WebRequestのPOSTを全てBegin-Endパターンで構築してみましょう。
var req = (HttpWebRequest)WebRequest.Create("http://google.co.jp/"); // dummy req.Method = "POST"; req.BeginGetRequestStream(ar => { var stream = req.EndGetRequestStream(ar); stream.BeginWrite(new byte[10], 0, 10, _ar => { stream.EndWrite(_ar); req.BeginGetResponse(__ar => { var res = req.EndGetResponse(__ar); var resStream = res.GetResponseStream(); var s = new StreamReader(resStream).ReadToEnd(); Console.WriteLine(s); }, null); }, null); }, null);
コールバックの連鎖とはこういうことであり、大変酷い。冗談のようだ。こんなに面倒なら、こんなに苦しいのなら、非同期などいらぬ!しかし現実問題、Silverlightには、Windows Phone 7には、非同期APIしか搭載されていません。Begin-Endが強要される世界。理由は分かる。ユーザーエクスペリエンスの為でしょう。モバイル機器は性能が貧弱だから重い処理はいけない、と言うけれど、大事なのは処理が重いか否かではなく、体感。UIを止めさえしなければ、不快感を与えることはない。だから、強制的に非同期操作のみとした。
けれど、それで開発難しくなったり面倒になってしまってはいけない。非同期処理が簡単に出来れば……。その答えが、Rx。「簡単に出来るから」「開発者が幸せで」「全てが非同期になり」「ユーザーも幸せになる」。楽しい開発って大事だよね。本当にそう思っていて。開発者が不幸せで、コードに愛がなければ良いものなんて生まれやしないんだって、本当に思っていて。
// Rxならこう書ける(...AsObservableは拡張メソッドとして別途定義) req.GetRequestStreamAsObservable() .SelectMany(stream => stream.WriteAsObservable(new byte[10], 0, 10)) .SelectMany(_ => req.GetResponseAsObservable()) .Select(res => new StreamReader(res.GetResponseStream()).ReadToEnd()) .Subscribe(Console.WriteLine); // SelectManyが苦手ならばクエリ構文という手もあります var query = from stream in req.GetRequestStreamAsObservable() from _ in stream.WriteAsObservable(new byte[10], 0, 10) from res in req.GetResponseAsObservable() select new StreamReader(res.GetResponseStream()).ReadToEnd(); query.Subscribe(Console.WriteLine);
ネストが消滅して、メソッドチェーンの形をとって非同期が同期的のように書けるようになります。利点は他にもあって、様々な操作(合成・射影・抽出・待機などなど)が可能になる、ということもありますが、それはまたそのうち。
ところで、最初のコールバックの連鎖って何だか見覚えのあるような雰囲気ありませんか?JavaScriptで。そう、XmlHttpRequestであったりsetTimeoutであったりの連鎖と同じです。RxのJavaScript版、RxJSではJavaScriptでのそれらのネストを殺害することが可能です。興味があれば、そちらも是非試してみてくださいな。
ReactiveOAuthとは?
OAuthであっても、ようするにWebRequestなわけです。GET/POSTしてResponseを取ってくるということにかわりはない。そんなわけで、ネットワーク通信してResponseを取ってくる部分を片っ端から全てIObservableにしたのがReactiveOAuthです。Rxにべったり依存したことで、良くも悪くも、他のOAuthライブラリとは全く毛色の違う仕上がりになっています。
とはいっても、とにかく「簡単に書けること」にこだわりを持ってデザインしたので、利用自体は簡単ですし、Rxの知識もそんなに必要ありません。普通に取得するなら、SelectとSubscribeしか使わないので、全然安心です!まあ、できれば、これが入り口になってRxの世界を知ってもらえると嬉しいなあ、という皮算用もあったりですが。
使いかた1. AccessToken取得
デスクトップアプリケーション上でのOAuthの仕組みを簡単に説明すると、RequestToken取得→認証URL表示→PINコード入力→PINコード+RequestTokenを使ってAccessToken取得。という形になっています。まず、ユーザー名・パスワードの代わりとなるものはAccessTokenです。これを取得して、API呼び出しの時に使い、また、パスワード代わりに保存するわけです。そのRequestTokenやPINコードはAccessTokenを取得するための一時的な認証用キーということです。
AccessToken取得までにはOAuthAuthorizerクラスを使います。
// グローバル変数ということで。 const string ConsumerKey = "consumerkey"; const string ConsumerSecret = "consumersecret"; RequestToken requestToken; AccessToken accessToken; private void GetRequestTokenButton_Click(object sender, RoutedEventArgs e) { var authorizer = new OAuthAuthorizer(ConsumerKey, ConsumerSecret); authorizer.GetRequestToken("http://twitter.com/oauth/request_token") .Select(res => res.Token) .ObserveOnDispatcher() .Subscribe(token => { requestToken = token; var url = authorizer.BuildAuthorizeUrl("http://twitter.com/oauth/authorize", token); webBrowser1.Navigate(new Uri(url)); // navigate browser }); }
GetRequestTokenとBuildAuthorizeUrlを使い、RequestTokenの取得と、内蔵ブラウザに認証用URLを表示させました。
private void GetAccessTokenButton_Click(object sender, RoutedEventArgs e) { var pincode = PinCodeTextBox.Text; // ユーザーの入力したピンコード var authorizer = new OAuthAuthorizer(ConsumerKey, ConsumerSecret); authorizer.GetAccessToken("http://twitter.com/oauth/access_token", requestToken, pincode) .ObserveOnDispatcher() .Subscribe(res => { // Token取得時のレスポンスには、Token以外に幾つかのデータが含まれています // Twitterの場合はuser_idとscreeen_nameがついてきます // ILookup<string,string>なので、First()で取り出してください UserIdTextBlock.Text = res.ExtraData["user_id"].First(); ScreenNameTextBlock.Text = res.ExtraData["screen_name"].First(); accessToken = res.Token; // AccessToken }); }
画像は認証が全て終わった時の図になっています。RequestTokenとPinCodeをGetAccessTokenメソッドに渡すだけです。これでAccessTokenが取得できたので、全ての認証が必要なAPIにアクセス出来るようになりました。
使いかた2. APIへのGet/Post
ここからはConsoleApplicationのサンプルコードで説明。
var client = new OAuthClient(ConsumerKey, ConsumerSecret, accessToken) { Url = "http://api.twitter.com/1/statuses/home_timeline.xml", Parameters = { { "count", 20 }, { "page", 1 } }, ApplyBeforeRequest = req => { req.Timeout = 1000; req.UserAgent = "ReactiveOAuth"; } }; client.GetResponseText() .Select(s => XElement.Parse(s)) .Run(x => Console.WriteLine(x.ToString()));
IObservable<T>連鎖の最後のメソッドとしてRunを使うと、同期的になります。通常はSubscribeで非同期にすると良いですが、コンソールアプリケーションなどでは、同期的な動作のほうが都合が良いでしょう。
OAuthClientを作成し、オブジェクト初期化子でURL、パラメータ(コレクション初期化子が使えます)を設定したら、GetResponseTextを呼ぶだけ。あとはIObservable<string>になっているので、Linqと同じように操作していけます。
ApplyBeforeRequestではリクエストが発行される前に、生のHttpWebRequestが渡されるので(!)、TimeoutやUserAgentなど細かい設定がしたい場合は、ここにラムダ式を埋めてください。
では、POSTは?
new OAuthClient(ConsumerKey, ConsumerSecret, accessToken) { MethodType = MethodType.Post, Url = "http://api.twitter.com/1/statuses/update.xml", Parameters = { { "status", "PostTest from ReactiveOAuth" } } }.GetResponseText() .Select(s => XElement.Parse(s)) .Run(x => Console.WriteLine("Post Success:" + x.Element("text")));
POSTの場合はMethodTypeにMethodType.Postを指定します(デフォルトがGETなので、GETの場合は指定の省略が可)。それ以外はGETと同じです。Urlとパラメータ指定して、GetResponse。
ストリーミングもいけます
OAuthClientには3つのメソッドがあります。GetResponseは生のWebResponseを返すもので細かい制御をしたい時にどうぞ。GetResponseTextはStreamReaderのReadToEndで応答をテキストに変えたものを返してくれるもので、お手軽です。そのままXElement.Parseとかに流すと楽ちん。そして、GetResponseLinesはReadLineで一行ずつ返してくれるもの、となっています。GetResponseTextとGetResponseLinesは型で見ると両方共IObservable<string>なため戸惑ってしまうかもですが、前者は流れてくるのは一つだけ、後者は行数分だけ、となります。
GetResponseLinesはStreamingAPIで使うことを想定しています。とりあえず、WPF用のサンプルを見てください。
var client = new OAuthClient(ConsumerKey, ConsumerSecret, accessToken) { Url = "http://chirpstream.twitter.com/2b/user.json" }; // streamingHandleはIDisposableで、これのDisposeを呼べばストリーミング停止 streamingHandle = client.GetResponseLines() .Where(s => !string.IsNullOrWhiteSpace(s)) // filter invalid data .Select(s => DynamicJson.Parse(s)) .Where(d => d.text()) // has text is status .ObserveOnDispatcher() .Subscribe( d => StreamingViewListBox.Items.Add(d.user.screen_name + ":" + d.text), ex => MessageBox.Show(ReadWebException(ex))); // エラー処理
ストリーミングAPIを使うにあたっても、何の面倒くささもなく、至って自然に扱えてしまいます!Dynamicを使ったJsonへの変換にはDynamicJson - C# 4.0のdynamicでスムーズにJSONを扱うライブラリを、また、RxとStreamingAPIとの相性については過去記事C#とLinq to JsonとTwitterのChirpUserStreamsとReactive Extensionsを見てください。
そうそう、それとネットワーク通信で起こったエラーのハンドリングが、SubscribeのOnErrorに書くだけで済むというのもRxを使って嬉しいことの一つです。
実装について
構造は全体的に@ugaya40さんのOAuthAccessがベースになっています(パク……)。そもそもにTwitterTL to HTMLのOAuth対応した時に、OAuthAccessを使って、あー、OAuth周りってRxに乗せると快適になるなー、とか思ったのが作ろうとした最初の動機だったりもして。非常に感謝。
WebClient風の、認証がOAuthなだけのベタなWebRequestラッパーという感じなので、特別なところはありません。インターフェイスとかなくて、本当にただのベタ書き。特に奇をてらってるところはないんですが、ストリーミングAPIで使うために用意したGetResponseLinesは個人的には笑えたり。
var req = WebRequest.Create(Url); return Observable.Defer(() => req.GetRequestStreamAsObservable()) .SelectMany(stream => stream.WriteAsObservable(postData, 0, postData.Length)) .SelectMany(_ => req.GetResponseAsObservable()); .Select(res => res.GetResponseStream()) .SelectMany(s => Observable.Using(() => new StreamReader(s), sr => Observable.Repeat(sr))) .TakeWhile(sr => !sr.EndOfStream) .Select(sr => sr.ReadLine());
うわー……。利用者としては、ただのIObservable<string>としか見えないので、前段階でこんなにチェーンが繋がってるだなんてこと、気にする必要は全くないんですけどねー。これがベストな書き方だとは全然思えないので、誰かアドバイス欲すぃです。
まとめ
RxというとLinq to Events、イベントのLinq化という方向が目につきますが、今回は非同期のLinq化のほうにフォーカスしました。何というか、実に、素晴らしい!asynchronus programming is hard、と、思っていた時もありました。今や私達にはRxがある。恐れることはなにもない。
今回WPFとWindows Phone 7(Silverlight)でサンプルを作ったのですが、コードがコピペで、完全な互換性もって動いちゃうんですね。WPFで書いてSilverlightに持っていく時に、ああ、BeginGetResponseに書き換えなきゃ…… みたいなことが起こらない。最初から非同期で統一することで、全部ライブラリがネットワーク周りを吸収してくれる。非同期→同期にするのも簡単だし(RunやToEnumerableを使えばいい)、そもそも、Rxの土台に乗っている方が、普通に同期的に書くよりもむしろ楽だったりします。
個人的には、Windows Phone 7のローンチに粗製乱造Twitterアプリを送り込むという野望があるんですがねえー。いや、粗製乱造にするつもりはないんですが、機能をザックリと削って、一般性を無視して「私が使うシチュエーションで私が使いやすいような」アプリを出したいなーと思ってます。構想はあって、そこそこ尖った個性ある内容になる予定なので一部の人がフィットしてくれればいいな、と。多くの人に目に触れては欲しいのでローンチのタイミングは外したくない。問題はWorldのローンチタイミングに合わせてもJapanだと実機がないってことですね!開発機欲しい(お金は払いますからどうかー)。
と、思ってたのですがサンプル作りで、あきらかーなXAML知識のなさ(ていうか何も知りません)が露呈したので、ローンチに間にあわせるとか寝言すぎるのですが。せめてJapanのローンチまでにはそれなりな技量を身につけたいところです。
Windows Phone 7 + Reactive ExtensionsによるXml取得
- C# Rx Silverlight WindowsPhone7 - 10.07/19
Windows Phone 7にはReactive Extensionsが標準搭載されていたりするのだよ!なんだってー!と、いうわけで、Real World Rx。じゃないですけれど、Rxを使って非同期処理をゴニョゴニョとしてみましょう。ネットワークからデータ取って何とかする、というと一昔前はRSSリーダーがサンプルの主役でしたが、最近だとTwitterリーダーなのでしょうね。というわけで、Twitterリーダーにします。といっても、ぶっちゃけただたんにデータ取ってリストボックスにバインドするだけです。そしてGUI部分はSilverlightを使用してWindows Phone 7でTwitterアプリケーションを構築 - @ITのものを丸ごと使います。手抜き!というわけで、差分としてはRxを使うか否かといったところしかありません。
なお、別に全然Windows Phone 7ならでは!なことはやらないので、WPFでもSilverlightでも同じように書けます。ちょっとしたRxのサンプルとしてどうぞ。今回は出たばかりのWindows Phone Developer Tools Betaを使います。Windows Phone用のBlendがついていたりと盛り沢山。
Xmlを読み込む
とりあえずLinq to XmlなのでXElement.Load(string uri)ですね。違います。そのオーバーロードはSilverlightでは使えないのであった。えー。なんでー。とはまあ、つまり、同期系APIの搭載はほとんどなくて、全部非同期系で操作するよう強要されているわけです。XElement.Loadは同期でネットワークからXMLを引っ張ってくる→ダウンロード時間中はUI固まる→許すまじ!ということのようで。みんな大好きBackgroundWorkerたん使えばいいぢゃない、みたいなのは通用しないそうだ。
MSDNにお聞きすれば方法 : LINQ to XML で任意の URI の場所から XML ファイルを読み込むとあります。ネットワークからデータを取ってくるときはWebClient/HttpWebRequest使えというお話。
では、とりあえず、MainPage.xamlにペタペタと書いて、MessageBox.Showで確認していくという原始人な手段を取っていきましょう。XElementの利用にはSystem.Xml.Linqの参照が別途必要です。
public MainPage() { InitializeComponent(); var wc = new WebClient(); wc.OpenReadCompleted += (sender, e) => { var elem = XElement.Load(e.Result); // e.ResultにStreamが入ってる MessageBox.Show(elem.ToString()); // 確認 }; wc.OpenReadAsync(new Uri("http://twitter.com/statuses/public_timeline.xml")); // 非同期読み込み呼び出し開始 }
別に難しいこともなくすんなりと表示されました。簡単なことが簡単に書けるって素晴らしい。で、WebClientのプロパティをマジマジと見ているとAllowReadStreamBufferingなんてものが。trueの場合はメモリにバッファリングされる。うーん、せっかくなので完全ストリーミングでやりたいなあ。これfalseならバッファリングなしってことですよね?じゃあ、バッファリング無しにしてみますか。
var wc = new WebClient(); wc.AllowReadStreamBuffering = false; // デフォはtrueでバッファリングあり、今回はfalseに変更 wc.OpenReadCompleted += (sender, e) => { try { var elem = XElement.Load(e.Result); // ここで例外出るよ! } catch (Exception ex) { // Read is not supporeted on the main thread when buffering is disabled. MessageBox.Show(ex.ToString()); } };
例外で死にました。徹底して同期的にネットワーク絡みの処理が入るのは許しません、というわけですね、なるほど。じゃあ別スレッドでやるよ、ということでとりあえずThreadPoolに突っ込んでみた。
wc.OpenReadCompleted += (sender, e) => { ThreadPool.QueueUserWorkItem(_ => { try { var elem = XElement.Load(e.Result); MessageBox.Show(elem.ToString()); // 今度はここで例外! } catch(Exception ex) { // Invalid cross-thread access. Debug.WriteLine(ex.ToString()); } }); };
読み込みは出来たけど、今度はMessageBox.Showのところで、Invalid Cross Thread Accessで死んだ。そっか、MessageBoxもUIスレッドなのか。うーむ、世の中難しいね!というわけで、とりあえずDispatcher.BeginInvokeしますか。
wc.OpenReadCompleted += (sender, e) => { ThreadPool.QueueUserWorkItem(_ => { var elem = XElement.Load(e.Result); Dispatcher.BeginInvoke(() => MessageBox.Show(elem.ToString())); }); };
これで完全なストリームで非同期呼び出しでのXmlロードが出来たわけですね。これは面倒くさいし、Invoke系の入れ子が酷いことになってますよ、うわぁぁ。
Rxを使う
というわけで、非Rxでやると大変なのがよく分かりました。そこでRxの出番です。標準搭載されているので、参照設定を開きMicrosoft.Phone.ReactiveとSystem.Observableを加えるだけで準備完了。
var wc = new WebClient { AllowReadStreamBuffering = false }; Observable.FromEvent<OpenReadCompletedEventArgs>(wc, "OpenReadCompleted") .ObserveOn(Scheduler.ThreadPool) // ThreadPoolで動かすようにする .Select(e => XElement.Load(e.EventArgs.Result)) .ObserveOnDispatcher() // UIスレッドに戻す .Subscribe(x => MessageBox.Show(x.ToString())); wc.OpenReadAsync(new Uri("http://twitter.com/statuses/public_timeline.xml"));
非常にすっきり。Rxについて説明は、必要か否か若干悩むところですが説明しますと、イベントをLinq化します。今回はOpenReadCompletedイベントをLinqにしました。Linq化すると何が嬉しいって、ネストがなくなることです。非常に見やすい。更にRxの豊富なメソッド群を使えば普通ではやりにくいことがいとも簡単に出来ます。今回はObserveOnを使って、どのスレッドで実行するかを設定しました。驚くほど簡単に、分かりやすく。メソッドの流れそのままです。
FromAsyncPattern
WebClientだけじゃなく、ついでなのでHttpWebRequestでもやってみましょう。(HttpWebRequest)WebRequest.Create()死ね、といつも言ってる私ですが、SilverlightにはWebRequest.CreateHttpでHttpWebRequestが作れるじゃありませんか。何ともホッコリとします。微妙にこの辺、破綻した気がしますがむしろ見なかったことにしよう。
var req = WebRequest.CreateHttp("http://twitter.com/statuses/public_timeline.xml"); req.AllowReadStreamBuffering = false; req.BeginGetResponse(ar => { using (var res = req.EndGetResponse(ar)) using (var stream = res.GetResponseStream()) { var x = XElement.Load(res.GetResponseStream()); Dispatcher.BeginInvoke(() => MessageBox.Show(x.ToString())); } }, null);
非同期しかないのでBeginXxx-EndXxxを使うのですが、まあ、結構面倒くさい。そこで、ここでもまたRxの出番。BeginXxx-EndXxx、つまりAPM(Asynchronus Programming Model:非同期プログラミングモデル)の形式の非同期メソッドをラップするFromAsyncPatternが使えます。
var req = HttpWebRequest.CreateHttp("http://twitter.com/statuses/public_timeline.xml"); req.AllowReadStreamBuffering = false; Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse) .Invoke() // 非同期実行開始(Invoke()じゃなくて()でもOKです、ただのDelegateなので) .Select(res => XElement.Load(res.GetResponseStream())) .ObserveOnDispatcher() .Subscribe(x => MessageBox.Show(x.ToString()));
ラップは簡単で型として戻り値を指定してBeginXxxとEndXxxを渡すだけ。あとはそのまま流れるように書けてしまいます。普通だと面倒くさいはずのHttpWebRequestのほうがWebClientよりも素直に書けてしまう不思議!FromAsyncPatter、恐ろしい子。WebClient+FromEventは先にイベントを設定してURLで発動でしたが、こちらはURLを指定してから実行開始という、より「同期的」と同じように書ける感じがあって好き。WebClient使うのやめて、みんなHttpWebRequest使おうぜ!(ふつーのアプリのほうでは逆のこと言ってるのですががが)
ところで、非同期処理の実行開始タイミングはInvokeした瞬間であって、Subscribeした時ではありません。どーなってるかというと、ぶっちゃけRxは実行結果をキャッシュしてます。細かい話はまた後日ちゃんと紹介するときにでも。
バインドする
GUIはScottGu氏のサンプルを丸々頂いてしまいます。リロードボタンを押したらPublicTLを呼ぶだけ、みたいなのに簡略化してしまいました。
<Grid x:Name="LayoutRoot" Background="Transparent"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <Button Grid.Row="0" Height="72" Width="200" Content="Reload" Name="Reload"></Button> <ListBox Grid.Row="1" Name="TweetList" DataContext="{Binding}"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <Image Source="{Binding Image}" Height="73" Width="73" VerticalAlignment="Top" /> <StackPanel Width="350"> <TextBlock Text="{Binding Name}" Foreground="Red" /> <TextBlock Text="{Binding Text}" TextWrapping="Wrap" /> </StackPanel> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid>
あとは、ボタンへのイベント設定と、Twitterのクラスを作る必要があります。
public class TwitterStatus { public long Id { get; set; } public string Text { get; set; } public string Name { get; set; } public string Image { get; set; } public TwitterStatus(XElement element) { Id = (long)element.Element("id"); Text = (string)element.Element("text"); Name = (string)element.Element("user").Element("screen_name"); Image = (string)element.Element("user").Element("profile_image_url"); } } public partial class MainPage : PhoneApplicationPage { public MainPage() { InitializeComponent(); Reload.Click += new RoutedEventHandler(Reload_Click); // XAMLに書いてもいいんですけど。 } void Reload_Click(object sender, RoutedEventArgs e) { var req = HttpWebRequest.CreateHttp("http://twitter.com/statuses/public_timeline.xml"); req.AllowReadStreamBuffering = false; Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse) .Invoke() .Select(res => XElement.Load(res.GetResponseStream())) .Select(x => x.Descendants("status").Select(xe => new TwitterStatus(xe))) .ObserveOnDispatcher() .Subscribe(ts => TweetList.ItemsSource = ts); } }
実行するとこんな具合に表示されます。簡単ですねー。ただ、これだとリロードで20件しか表示されないので、リロードしたら継ぎ足されるように変更しましょう。
イベントを合成する
継ぎ足しの改善、のついでに、一定時間毎に更新も加えよう。基本は一定時間毎に更新だけど、リロードボタンしたら任意のタイミングでリロード。きっとよくあるパターン。Reload.Click+=でハンドラ足すのはやめて、その部分もFromEventでObservable化してしまいましょう。そして一定時間毎のイベント発動はObservable.Timerで。
// 30秒毎もしくはリロードボタンクリックでPublicTimeLineを更新 Observable.Merge( Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(30), Scheduler.NewThread).Select(_ => (object)_), Observable.FromEvent<RoutedEventArgs>(Reload, "Click").Select(_ => (object)_)) .SelectMany(_ => { var req = HttpWebRequest.CreateHttp("http://twitter.com/statuses/public_timeline.xml"); req.AllowReadStreamBuffering = false; return Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse)(); }) .Select(res => XStreamingReader.Load(res.GetResponseStream())) .SelectMany(x => x .Descendants("status") .Select(xe => new TwitterStatus(xe)) .Reverse()) // 古い順にする .Scan((before, current) => before.Id > current.Id ? before : current) // 最後に通した記事よりも古ければ通さない(で、同じ記事を返す) .DistinctUntilChanged(t => t.Id) // 同じ記事が連続して来た場合は何もしないでreturn .ObserveOnDispatcher() .Subscribe(t => TweetList.Items.Insert(0, t)); // Insertだって...
流れるようにメソッド足しまくるの楽しい!楽しすぎて色々足しすぎて悪ノリしている感が否めません、とほほ。解説しますと、まず一行目のMerge。これは複数本のイベントを一本に統一します。統一するためには型が同じでなければならないのですが、今回はTimer(long)と、Click(RoutedEventArgs)なのでそのままでは合成出来ません。どちらも発火タイミングが必要なだけでlongもRoutedEventArgsも不必要なため、Objectにキャストしてやって合流させました。
こういう場合、Linq to Objectsなら.Cast<object>()なんですよね。Castないんですか?というと、一応あるにはあるんですが、実質無いようなもので。というわけで、今のところキャストしたければ.Select(=>(object))を使うしかありません。多分。もっとマシなやり方がある場合は教えてください。
続いてSelectMany。TimerもしくはClickは発火のタイミングだけで、後ろに流すのはFromAsyncPatternのデータ。こういった、最初のイベントは発火タイミングにだけ使って、実際に流すものは他のイベントに摩り替える(例えばマウスクリックで発動させて、あとはマウスムーブを使うとか)というのは定型文に近い感じでよく使うことになるんじゃないかと思います。SelectMany大事。
XMLの読み込み部は、せっかくなので、こないだ作ったバッファに貯めこむことなくXmlを読み込めるXStreamingReaderを使います。こんな風に、XMLを読み取ってクラスに変換する程度ならXElement.Loadで丸々全体のツリーを作るのも勿体無い。XStreamingReaderなら完全ストリーミングでクラスに変換出来ますよー。という実例。
その下は更にもう一個SelectMany。こっちはLinq to Objectsのものと同じ意味で、IEnumerableを平たくしています。で、ScanしたDistinctUntilChangedして(解説が面倒になってきた)先頭にInsert(ちょっとダサい)。これで古いものから上に足される = 新しい順番に表示される、という形になりました。XAML側のListBoxを直に触ってInsertとか、明らかにダサい感じなのですが、まあ今回はただのサンプルなので見逃してください。
RxのMergeに関しては、後日他のイベント合流系メソッド(CombineLatest, Zip, And/Then/Plan/Join)と一緒に紹介したいと思っています。合流系大事。
まとめ
驚くほどSilverlightで開発簡単。っぽいような印象。C#書ける人ならすぐにとっかかれますねー。素晴らしい開発環境だと思います。そして私は同時に、Silverlight全然分かってないや、という現実を改めて突きつけられて参ってます。XAMLあんま書けない。Blend使えない。MVVM分からない。モバイル開発云々の前に、基本的な技量が全然欠けているということが良く分かったし、それはそれで良い収穫でした。この秋なのか冬なのかの発売までには、ある程度は技術を身につけておきたいところです。
そしてそれよりなにより開発機欲すぃです。エミュレータの起動も速いし悪くないのですが、やっぱ実機ですよ、実機!配ってくれぇー。