MVPとシアトルとWindows Phone (2)

WP_20151101_07_35_54_Rich今年も年末に突入しました。この記事は「Windows Phone / Windows 10 Mobile Advent Calendar 2015」の4日目の記事の本編です。(前編はこちら)

MVP Global Summitへ

WP_20151101_12_24_33_Richそんなわけで、11月第一週は、MVP Global Summitに参加すべく、米国ワシントン州・シアトル・レドモンドへと行ってきました。国外に出るのは初めてなので、色々準備が必要だったのですが、特にパスポートの受領とスーツケースの新調が大きかったです。スーツケースは以前に長期出張の時に持っていっていたものがあったのですがすっかり忘れていて、どのみちもう重いのも辛いので、軽いやつを買いました。大きさは中ぐらい?何かお土産が出来た時に入るぐらいの大きさってことで考えました。

飛行機は成田からシアトルまでのデルタ航空便でした。搭乗時間は9時間。搭乗時間が午後3時ぐらいですが、シアトルに着くと朝だそうです。機内でどんな暇つぶししようかと思い、ノートPCとか手荷物にしたのですが、とにかく乗ったらすぐに寝ないとヤヴァイ事が判明し、さっさと寝ることに。まあ、そんなに簡単に寝れる訳ないんですが…

WP_20151101_13_52_17_Richそんなこんなでシアトルに到着。周りはみんなネイティブで何言ってるか分からない… 何か乗客が流れるままに地下鉄のようなシアトル空港内の移動システムに乗り、無事に外に出てきました。ここからはツアーバスでシアトルのダウンタウンに移動して、しばし見物など。MVPのサミットセッションは翌日からなのです(前編で紹介した通り、Global Summitは全部NDAなので、残念ながらこの記事では何も紹介できません…)。

シアトルの気候は写真の通り、いつも曇り時々小雨、のような、煮え切らない天気です。なのですが、私はすごく過ごしやすく気持ちよさを感じました。気温は日本と同じぐらい。この季節だと少し肌寒い感じです。が、湿気が低いんでしょうか?それとも(名古屋に比べて)空気が良いのか、ホント快適に感じました。帰って来てからもいつも思い出すのは、この空気感ですね。ちょっと恋しいです。

WP_20151101_12_31_42_Richシアトルのダウンタウンには、有名な「スターバックス一号店」があります。見ての通り、お客さんでごった返していて、とても入れる感じではなかったのが残念です(しかもまだ現地に着いたばかりで、英語に圧倒されてて何もできなかった)。

ダウンタウンの探索はあまり時間もなく、恐る恐る徘徊してたこともあって、色々なところを見て回れなかった… ちょっと良い雰囲気の店もあったりして、シアトルに観光に行くというのは珍しいのかもしれないけど、良い所だなと思いました。


現地でプリペイドSIMを調達して使えるようにするまで

ところで、機内で(というより出発前から)心配だったことの一つが、現地で通信の確保をどうするのか、でした。参加者に話を聞いてみると:

  • 国内キャリアの完全ローミング契約:ソフトバンクに何か良い契約があるらしいです。が、うちはIIJMioなので関係ない。
  • 旅行者向けのWiFiルーターレンタル:国内に居る時に(場合によっては空港で)契約してすぐに使える。日本語が通じるうちに解決可能なので良いかも。
  • 現地でプリペイドSIMを買う:チャレンジャー

やりましたよ、チャレンジャー (;´Д`)
折角Lumia持ってるし、これはやっておかないと。それに自分で試さないと別の国の文化などモノに出来ないぃぃっっ! とか、勝手に盛り上がってました。しかし、一体どこでSIMが買えるのか、それもプリペイドSIMで、しかもLumiaに適合しなければならない。

Lumiaの仕様はここで確認できます。 例えばLumia 1520のSpecification/Cellular connectivityを見ると、

  • WCDMA network: Band 1 (2100 MHz), Band 2 (1900 MHz), Band 5 (850 MHz), Band 8 (900 MHz)
  • LTE FDD network: Band 1 (2100 MHz), Band 3 (1800 MHz), Band 7 (2600 MHz)

と書いてあります。私が知っている米国のモバイル通信業者は、AT&T・Verizon・T-Mobileぐらい。これらの事業者が対応するBandが、上記に当てはまる必要があります。とりあえずどこかでSIMを確保しなければと思って行ったのが、スーパーマーケット(?)のTARGET。しかし、ここのスマートフォンコーナーで店員捕まえて「Prepaid SIM」を連呼したら、どうも扱っていない様子。ヤヴァイ、ヤヴァイぞと思って隣の家電量販店(?)のFry’sに行ったら、ありましたよSIMが。WP_20151203_21_49_21_Pro_LI

「AT&T Prepaid SIM」の名目に、これ以外にもT-Mobileがあった気がするけど、これしかなかろう的な感じで。値段は$10でお安い。どうせ足りなかったらチャージすればいいんだし(出来るかどうかの自信はなかったが)。店の中で写真を撮ったらヤヴァイんじゃないかとか思って取らなかったのですが、これがぶら下がってる陳列棚からどうしてもこのパッケージを外せなくて、誰も見ていないかオロオロと確認しつつ、無理やり引きちぎってダッシュでレジに持って行ったのは内緒です。

WP_20151203_22_39_37_Pro_LIAT&Tだと、HSPAのband5とGSMが行けます。LTEだとかすらない。実はこれ以外にも、NEC Aterm MR04LNも準備してきました。こっちも同じくHSPAのband5とGSMが行けます。残念ながらLTEは合いませんが、とにかくメッセンジャーだけでも開通すれば、安心感がかなり違うので良しとします。それに、シアトルにはWiFiのフリースポットが沢山あります。ちょっと困っても、どこの店のスポットか分からないのですが、無料で接続できるのです(セキュリティ的に危険なのは承知の上)。だから、あくまで担保と言う事で。

AT&TのSIMは買ってみて改めて分かったことですが、契約をフレキシブルに後から決めることが出来ます。プリペイド契約の他に、一般的な月極の契約も可能で、SIMの「アクティベート」時に決める感じです。しかし、このアクティベートが問題で、電話モードにした後で特定の番号を入力し、「ZIP code」を入力します。これつまり「郵便番号」の事なんですが、当然米国在住じゃないので、ZIP codeなんてあるわけがない…

で、まだ開通していないSIMで電話番号入力させて開通させるというのも、認証とかのプロトコルってどうなってるのかなーと技術者らしい事を考えつつも、「そうだ、タブレットだとこうは行かないはずだから、別の手段があるかもしれない」と考え直し、for Tabletsという冊子を読んでみると、ありましたよやっぱり、Webでの契約方法が。

つまり、流れとして、フリーWiFiスポットとかで接続を確保して、AT&Tのサイトに行ってWebで契約すれば、ZIP code必要ないかも?

が、もっと甘かった。Webでの契約ではクレジットカードが必要なんですが、そこにはZIP codeどころか住所から入力する欄があり、しかも米国前提で州から入力するフォーム…

こりゃいよいよヤヴァイな。通信経路が確保できないと、ちょっと冒険的に街を徘徊することも出来ないかも知れない。と暗雲立ち込めてきたのですが、もう住所適当にホテルとかで入れようかどうせカードと一致してないとNGだろうけど何でPayPalに対応してないんだブツブツ… とか何とか言いながらトライしてたら、

通ってしまいました (;´Д`) 開通したスクリーンショット撮っておけば良かったなぁ。

WP_20151203_23_38_19_Pro_LIしかし住所やらZIP codeの情報は何に使ってるんだか。と言う事は初めからLumiaでZIP codeをタイプしてたら、電話も含めて使えるようになっていたのかもしれない(タブレット契約だと音声通話は出来ない)。何はともあれ、これで少しは探索めいたこともできなくもないかという事に。通信の重要性を改めて感じました。

なお、契約は$30のプランにしておきました。Auto renewをオフっておいたので、30日経過後は自動的に終了ってことで、プリペイド相当になります。ここらへんも非常にフレキシブルで進んでるなーと思います。


Microsoft Official StoreとPhone

WP_20151101_15_57_20_RichシアトルのHyatt bellvueがホテルなんですが、そのすぐそばに「Microsoft Store」があります。

ここはおひざ元らしく、発売されたばかりのSurface BookやBand2の在庫がありました。Band2は買うつもりだったのですが、残念ながら欲しいMサイズは売り切れのため、入手する事かなわず。Bookにも惹かれましたが、さすがに$3000近くの買い物をサクッとやる勇気はなく…(30万超えると損金計上出来ないというのが大きい)

このときのJapan MVPによるBook威力購買事件 (;´Д`) はこっちにまとまっているのでどうぞ。

WP_20151101_15_52_16_Richさてさて、ずらりと並んだWindows Phone。この反対側にもサードパーティ製Windows Phoneがズラリと並ぶ光景。日本では見られない景色です。この中にとあるLumiaがありました。

WP_20151203_23_23_13_Proそれがこの「Lumia 640 LTE」です。色々いっぱいっぱいで忘れてたんですが、今箱見たら$79って書いてありましたが、なんか「クソ安い」感じだったんです。togetterでもまとめ買いしている人が居たようですが、これ本当に安い。何しろContract(期間が指定される契約)ではないので、プリペイドとして使うことも出来るのにこの値段。もちろんSIMロックされていますが…

(写真手前のテーブルには「Unlocked phones」と書かれていますが、これがSIMロックなしです。もちろん高いです)

買ってすぐに使い始めたんですが、液晶の視野角はあまり広くないものの、色の具合は1520を超えているかもしれない。艶やかな感じで良いです。タッチの反応も1520ほどではありませんが、MADOSMAを超えてる気がします。エントリーモデルなのに高級感を醸し出しているのが良いです。当然ですが、AT&TのSIMが付いていたので、最初に買ったSIMは不要な感じに。まぁこうなる事が分かってたら買わないわけで、こればっかりは仕方がない。

勢いで買ってしまった感があったんですが、非常に満足感が高かった。勿論、その日のうちにWindows 10 Mobile Insider Previewに更新。ちなみにSIMロックですが、6か月経つとAT&Tのサイトでアンロックできるようになるらしいです(すぐにアンロック出来るのかと勘違いしてトライしたけど、流石にダメだった)。ほかの方法として、某SIMロック解除サイトを使うと、$15ぐらいでアンロック出来ることも判明。参考までに。


まとめ

MVP Global Summitに参加するのが主題だったんですが、初の海外旅行・シアトル良いところ・サミットセッションももちろん、偶然入手したWindows Phoneや現地SIM調達など、ものすごく盛り沢山の旅行で本当に良かったです。次回MVPを受賞しているかどうかはわかりませんが、また行ってみたいなぁと思っています。

次のカレンダーはアヤノフさんです、よろしく!

MVPとシアトルとWindows Phone (1)

WP_20151101_15_44_16_Rich今年も年末に突入しました。この記事は「Windows Phone / Windows 10 Mobile Advent Calendar 2015」の4日目の記事の前編です。(前日に公開しています。本編は予定通り明日公開です)

さて、今年私は「Microsoft MVP for .NET(今はカテゴリーが再編されたので、Visual Studio and Development Technology)」を受賞しました。MVPを受賞すると特典の一つとして、一年に一度行われる「MVP Global Summit」への参加資格が得られます。生まれてこの方、一度も日本を出たことがなかったので、この節目に行ってみようかと思いました。この記事ではその話に絡めてWindows Phoneの話をしようかと思いましたが、その前に「Microsoft MVP Award」(長いのでMSMVPと略します)についての紹介もしようかと思います。


Microsoft MVP Awardとは

MSMVPの紹介ページには、以下のような事が書いてあります。

「Microsoft Most Valuable Professional (MVP) は、自身のマイクロソフト技術に関する知識や経験を最大限に活かしながら、他のユーザーを積極的にサポートしている、非常に優れたコミュニティのリーダーです。彼らは、技術専門知識に加えて自主性と情熱を兼ね備え、マイクロソフト製品の実用的な活用方法をコミュニティやマイクロソフトと共有しています。」

コミュニティのリーダーかどうかは分かりませんが (;´Д`) コミュニティ活動を積極的に行っていることに対して、Microsoftが表彰する制度の事です。ただ、これは「受賞を目指した」り「受賞する」までは、実にどんな制度なのか分かりにくい面があると思います(実際、某フォーラムでは、MVPだからオフィシャル的な云々のような辛みのあるポストが度々発生)そこの所を端的に分かるようにしてみます。

MSMVPの表彰は、一年に一度、過去一年間に渡ってのコミュニティ活動が評価されて表彰されます

例えば、私の場合は2015.04受賞ですが、応募期間は3か月前が締め切りなので、2014年年始から2014年の年末までのコミュニティ活動が評価されて受賞したことになります。年末・3月末・6月末・9月末、が応募締め切りとなるので、狙っている方はその時期の早めにサイトをチェックすると良いでしょう。

MSMVPの応募は、自薦と他薦です

ただ、自薦が多いんじゃないかな。私も自薦応募です。自薦・他薦の比率は分かりませんが、あまり重要ではないでしょう。応募の際には、過去1年間の活動状況をシートに記述する必要があります。例えば勉強会で登壇したり、ブログ記事を書いているなどの活動記録を、あらかじめ収集しておくことをお勧めします。最初は面倒ですが、受賞後はオンラインのフォームに入れておくだけで、MVPプロフィールと連動して評価されるらしいので、楽になると思います。

「コミュニティ活動」とは何?

私が無償で勉強会やブログ記事を書いたりしていますが、これもコミュニティ活動です。一方、有償セミナーや書籍を沢山書いて出版している著者の方も受賞していたりするので、結構幅広くとらえられているようです。どのような基準で評価されるのかは、非公開であるため分かりません。受賞した場合や落選した場合でも、その理由は明らかにされません(試験と一緒ですね)。

受賞カテゴリーが決まる必要があります

少し前にカテゴリーが大幅に整理されました。MSMVPへの応募を行う場合は、これらのカテゴリーの何れか一つだけを指定する必要があります。ご自身の最も専門である分野を選ぶことになりますが、審査の過程でカテゴリーが異なると判断された場合は、別のカテゴリーで受賞する可能性があります。

MSMVPに与えられる特典とは

WP_20150414_21_04_18_Raw
多分、非受賞者からみて、ここが最もモヤモヤするところなのではないかと思います。

  • MSMVP Award Kitの受領:写真のような、立派なトロフィーや証書がもらえます。
  • MSDN Enterprise subscription / Office 365 1年分
    恐らく開発者にとっては、一番欲しいと思っているものかも知れません。MSMVPは1年間の受賞なので、次年度選考に外れてしまうと、Subscriptionは継続できません。
  • 米国本社のカテゴリー別開発チームとの、NDAベースのディスカッションの機会が得られる
    メールでスケジュールとか飛んできます。Skypeで参加するので、基本的に英語が喋れないと参加は難しいです。おまけに時差の影響があるので、ほとんどが夜中となります。日本人には敷居が高いかも…
  • MVP Global Summitへの参加権
    毎年11月ぐらいに催されているようです。期間中、米国本社(ワシントン・シアトル付近)のホテルに無料で宿泊できますが、飛行機代やそれ以外のコストは自腹でねん出する必要があります。また、ここで見聞きする技術情報はNDAベースなので、最新情報を聞いて帰ってきても、喋ることは出来ません。

NDAとは、秘密非開示契約というもので、見聞きした事は第三者に漏らしてはならないというものです。だから、どれだけNDA情報を知ったところで、twitterやfacebookでつぶやくどころかブログに書いたりオフで誰かに喋ったりなどは全く出来ません。これは、同じMSMVP受賞者同士にも当てはまります。どうですか?「それほど」強力な特典でもない気がして来ましたか? 物的な特典を期待していると、それほどでもないと思います。

誤解のもとになっている事かも

MSMVPに対する誤解の一つが、MVPというブランド的な力が良くも悪くも強いため、MVP受賞者というだけでMicrosoftの関係者と同じか、あるいはそこにきわめて近しい人のように見えてしまうことにあるのかなと思います。上に挙げたようにMSMVPの特典は良いものですが、その一方で、MVP受賞者だからとある第三者に何か便宜を図る事が出来たりとか、チートな権力を持っていたりとかは全くありません。また、受賞者は普段からMicrosoftの「製品」や「技術」をコミュニティに広げようとしているだけなので、賄賂めいた何かがあるわけでもありません。実際、広く活動している方が翌年度には落選したりすることもあります。しかも、その理由は明らかにならないのです。

だから、あまりフォーラムや掲示板で無理ゲーなことを言われても困惑する事になるわけです。

また、専門分野があるので、それ以外の事を聞かれてもまた困ってしまいます。いまやMicrosoftの技術は一人ですべてを把握するのは不可能なぐらい幅が広いです。.NET受賞者である私に「Excelのマクロのこの関数が…」と聞かれてもさっぱりわからないのです。MSMVP受賞者、というだけで超サイヤ人的に見られるのも結構つらみがあります。

追記: de:code 2016で、MVP Ask Meというキャンペーンが行われました。その時に配布されたパンフレットを公開しています。

「Windows Phone / Windows 10 Mobile Advent Calendar 2015」本編の(2)へつづく

データバインディングと非同期 – C# Advent Calendar 2014

この記事は、「C# Advent Calendar 2014」の21日目の記事です。

WPFやWindows Phoneやストアアプリでは、Windows Formsのような「コードビハインド」による実装の他に、「データバインディング」を積極的に使う実装を行う事が出来ます。というよりも、データバインディングという手法を使わないと、複雑なUIを表現するのが困難です。この記事では、その辺りと非同期がどう関係するのかを書いてみたいと思います。

(なお、例はWPFですが、ストアアプリでもほぼ同様です)


データバインディングって何?

そもそもデータバインディングとは、どういう手法でしょうか?

asyncbinding1

Windows Formsの時代、データバインディングと言うと「DataSet」クラスの内容を、「DataGridView」コントロールを使って表示させるという用途が一般的でした。

詳しい解説は「Windowsフォームにおける「データ・バインディング」」を参考にして下さい。

この手法の問題点として:

  • DataSetやその周りのクラス(DataTableなど)と、List等の限定されたクラスがターゲット。
    実際には、IListインターフェイスやIBindingSourceインターフェイスを実装したクラスであれば受け入れる事が出来ますが、殆どの場合はこれらのインターフェイスを独自実装しないで、単にDataSetクラスを使用していたと思います。
  • DataGridViewのセル内に、更に別の(コレクション)要素を配置するようなリッチな表現を、統一された方法で実現するのが困難。
    「DataGridViewのセル内に「コンボボックス」を配置するのはどうしたら良いのか?」と言うのは、定期的に発生するFAQであったと思いますが、このような拡張がスムーズに行えないのは、グリッドの表現をDataGridViewが独自に制御していたため、応用性が無く、学習効果が得られないからと思われます。
  • バインディング対象のインスタンスがコンポーネントとしてフォームに設定される必要があり、柔軟性に欠ける。
    DataGridView以外のコントロールでも、バインディング機能を使う事は出来ました。但し、バインディングしたいインスタンス(モデルクラスやコレクションなど)は、コンポーネントとしてフォームに配置される必要があったため、フォームとデータが密結合する事になり、データバインディングする事の意義が見出しにくい状態でした。また、コントロールへの直接アクセスの手法があまりに一般に広まったため、そもそもこの手法が利用可能である事も周知されませんでした。

「DataSetを用意して、単純にDataGridViewに表形式で表示」させるのであれば十分なのですが、それ以上のUI表現に踏み込むと途端に難易度が高くなり、インターフェイスが複雑化してしまいます。

また、DataSet自体がデータベースのテーブルを模倣したものであったため、階層化されたデータを容易に扱えないという問題もあります。Visual Studioのデザイナー機能によって「型付きDataSet」を生成出来るようになりましたが、結局(生の)DataSetをラップしたものに過ぎず、本来コード上でモデル型として表現されている構造から乖離してしまう事は避けられませんでした。

asyncbinding2

.NET 3.0でWPFが発表され、ここの部分に大きなメスが入りました。DataGridViewのような、特殊なクラスの特殊な機能によってバインディングを実現するのではなく、そもそもWPFの枠組みでこれを実現し、どのようなコントロールでも統一されたバインディングを使えるようにし、UIとデータを完全に分離可能にした… それが今のデータバインディングです。

図のように、「DataGridコントロール(WPF)」「TextBoxコントロール(WPF)」は、両方ともモデルクラスとのデータの送受を「バインディング式(Binding expression)」で実現しています。この手法には全く違いは無く、更にDataGridコントロールの場合は、階層化されたモデルクラスのインスタンス群をどのようにDataGridで表現するのかまでを、全てバインディング式で記述する事が出来ます。

DataGridコントロールだけではなく、リスト形式で要素を表示する「ListBoxコントロール」や、コントロール要素の配置を制御する「Panelクラス」を継承したコントロール群「DockPanelStackPanelGrid等」も、ほぼ同様の手法でデータとの関連付けが可能です。

そして、これらのコントロールを階層化して組み合わせた複雑なUIは、データバインディングの規則に従っている限り、技術的な制限なく実装する事が出来ます。

WPFでも、従来のWindows Formsのようにコントロールに直接アクセスして、ほぼ全てを制御する事も出来ますが、データバインディングを使えるようになると、かなり面倒に感じられるはずです。また、ちょっとUIが複雑化するだけで実現難易度が高くなるため、単調なUIで実現する事しか選択の余地が無くなってしまいます。

(Windows Formsが楽と言う意味ではなく、むしろ複雑なUIを実現する場合、Windows Formsでは更に難易度が高い)


データバインディングのキモ

データバインディングのキモは、コントロールの公開するプロパティと、モデルクラスの公開するプロパティの値を自動転送するところです。以下にTextBoxの例を示します。

これはXAMLの定義です:

<Window x:Class="CenterCLR.AsyncBindings.MainWindow"
		xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
		Title="MainWindow"
		Width="525"
		Height="100">
	<TextBox x:Name="sampleTextBox" />
</Window>

対応するコードビハインド:

public partial class MainWindow : Window
{
	public MainWindow()
	{
		InitializeComponent();

		// TextBoxに初期文字列を設定
		sampleTextBox.Text = "ABC";
	}
}

asyncbinding3

TextBoxのTextプロパティは、テキストボックスに入力された文字列をやり取りするプロパティです。あらかじめこのプロパティに値を設定すれば、テキストボックスの初期表示文字列となり、ユーザーが文字列を入力したり編集したりすれば、それがこのプロパティから読み取れます。ここまではWindows Formsと同様です。

ここで、Textプロパティにバインディング式を記述します。バインディングするためには、相方のプロパティが必要です。とりあえず、MainWindowクラスに直接実装しましょう。

public partial class MainWindow : Window
{
	// バインディングされる値を保持するフィールド
	private string sampleText_ = "Bound!";

	public MainWindow()
	{
		InitializeComponent();

		// バインディング対象のインスタンスを、このクラス自身に設定
		this.DataContext = this;
	}

	// バインディングされるプロパティ
	public string SampleText
	{
		get
		{
			return sampleText_;
		}
		set
		{
			sampleText_ = value;
		}
	}
}

XAMLにはこのようにバインディング式を書きます:

<Window x:Class="CenterCLR.AsyncBindings.MainWindow"
		xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
		Title="MainWindow"
		Width="525"
		Height="100">
	<!-- TextプロパティとSampleTextプロパティをバインディングする式 -->
	<TextBox Text="{Binding SampleText}" />
</Window>

XAMLのコードについて、重要な部分が二カ所あります。一つはバインディング式を記述した事です。TextBoxにはTextプロパティがあるのですが、そのプロパティに対して「{Binding SampleText}」と記述します。この中括弧+Bindingで始まる指定がバインディング式です。「SampleText」と言うのは、バインディングさせたい対象のプロパティ名です。ここではMainWindowクラスに定義した「SampleText」プロパティを指定しています。

もう一つの重要な点は、もはや「x:Name」による、コントロールへの命名が不要になっている点です。この点についてはまた後で説明します。

MainWindowクラスへの変更も説明をしておきます。MainWindowクラスにも重要な二カ所の変更点があります。一つはバインディング対象のプロパティ「SampleText」を追加した事です。これに合わせて、値を保存しておくためのフィールドも追加してあります(熱心なC#信者なら、自動実装プロパティを使う事が出来ると思うかもしれません。デバッガで動きを追えるようにするためにわざと手動で実装しています)。

もう一つはコンストラクタに追加した、「DataContext」プロパティにthisを代入するという式です。このプロパティは、XAML上でバインディング式を実行する際に、デフォルトのバインディング対象のインスタンスを特定するために使われます。ここでthisを代入しているという事は、デフォルトのバインディング対象はMainWindowクラスのインスタンスとなる、と言う事です。そして、XAML上に記述したバインディング式が「{Binding SampleText}」となっているので、結局「MainWidnowクラスのインスタンス」の「SampleTextプロパティ」が、TextBoxのTextプロパティにバインディングされる、と言う事になります。


asyncbinding4

さて、このコードを実行すると、望みの通り、バインディングが実現し、初期状態としてフィールドの値が表示されます。しかし、テキストボックスの文字列をタイプして変更しても、SampleTextプロパティのsetterに遷移しません(ブレークポイントを設定して確かめて下さい)。

これは、バインディング粒度のデフォルトが、コントロールからフォーカスが外れた時、となっているためで、サンプルではTextBoxが一つしかないためにそのタイミングが無い事によります。ここでは単純に、「一文字でも変化があれば」、と変えておきます。この指定もバインディング式だけで実現出来ます。

<Window x:Class="CenterCLR.AsyncBindings.MainWindow"
		xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
		Title="MainWindow"
		Width="525"
		Height="100">
	<TextBox Text="{Binding SampleText, UpdateSourceTrigger=PropertyChanged}" />
</Window>

asyncbinding5

XAMLのバインディング式に「UpdateSourceTrigger=PropertyChanged」と付け加えます。すると、バインディング処理の実行が、Textプロパティに変化があった時という条件となります。これで、setterにブレークポイントを張っておいて、テキストボックスで「A」をタイプするとこのようになり、バインディング式によって自動的に値が転送されて来る事が分かります。

asyncbinding6

上記のデータバインディング例を図示すると、このようになります。従来のWindows Formsのように、イベントハンドラのフックやTextBoxのTextプロパティへの直接アクセスをする事無く、値の受け渡しが出来ている事になります。おっと、忘れる所でした。これで「x:Name」によるコントロールの命名が不要になっている理由も分かりますね。プロパティ同士がバインディング式で直接転送されるので、「どのコントロールに対する操作なのか」の明示は不要となっているのです。

(ここで解説しませんが、能動的にSampleTextの値が変化する場合は、MainWindowクラスにINotifyPropertyChangedインターフェイスを実装して、プロパティの値が変化したことをWPFに通知する必要があります)

ここまで、Windows Formsっぽい手法からスタートして説明するため、バインディング対象のインスタンスをDataContextプロパティにthisを代入する事で「MainWindow」そのものにしましたが、更にコードの責務を分離するために、バインディングするクラスを独自のクラスに完全に分離することも出来ます。例えば「MainWindowViewModel」クラスを作り、DataContextにこのクラスのインスタンスを代入する事で、バインディング対象をMainWindowから切り離す事が出来ます。

// MainWindowクラスから完全に独立したクラス(ビューモデル)
public sealed class MainWindowViewModel
{
	// バインディングされる値を保持するフィールド
	private string sampleText_ = "Bound!";

	// バインディング対象のプロパティ
	public string SampleText
	{
		get
		{
			return sampleText_;
		}
		set
		{
			sampleText_ = value;
		}
	}
}

// コードビハインドが殆ど存在しないMainWindowクラス(ビュー)
public partial class MainWindow : Window
{
	public MainWindow()
	{
		InitializeComponent();

		// バインディング対象のインスタンスを、MainWindowViewModelに設定
		this.DataContext = new MainWindowViewModel();
	}
}

これが「MVVM(Model-View-ViewModel)」パターンの入り口です。


データバインディングと非同期

長い前フリでしたが (;´Д`) 本題に入ります。ストアアプリに導入された新しいWinRT APIのセットは、原則として外部リソースアクセスは全て非同期で実現しなければならないようになっています。ブログの記事や勉強会でも説明した通り、これらはC#で言う所の「async/await」を使って実現します(より深くはTaskクラスやIAsyncInfoインターフェイスを使う事ですが、本題と関係ないので省略)。

「非同期処理」と「ユーザーインターフェイス」とはどう絡むのかが本題です。この問題は古典的でもあって、一般的なウェブブラウザのフォームでも同じような問題を抱えていました。例えば、「購入」ボタンを何度もクリックされると困るので、クリック後はボタンを無効化して押せなくする等の処置は、(ブラウザとは無関係に)ウェブサーバー側で購入処理が非同期で実行されるから発生する問題でした。

あるいはもっと良い方法もあります。何度「購入」ボタンが押されても問題ないように、同じセッションが維持されていると分かる場合は、単一の要求と見なすなどです。

どちらの手法を取るにしても、「非同期処理は何も対策しないで実現することは無理」と言う事です。async/awaitのサポートによって、非同期処理の実装難易度は格段に改善されたのですが、この部分をどうするのかは、仕様や実装を行う者に任されています。

二つのパターンの背景を説明しましたが、非同期処理をユーザーインターフェイスを工夫する事によって担保する方法について、データバインディングと絡めて考えてみます。


非同期処理中にユーザーインターフェイスをロックダウンする、と言うのは、もっとも単純で効果的な方法です。真っ先に思いつくのは、Windowsクラス全体のIsEnabledをfalseに設定する事でしょう。これをいつ行えばよいでしょうか?というよりも、非同期処理をどこで開始するのか?と言う事です。

例えば、UIにボタンが配置されており、ボタンのクリックと同時に非同期処理が開始されるとします。データバインディングを使うのであれば、「ICommandインターフェイス」を実装したクラスを使って、ボタンクリックのイベントを「バインディング」するでしょう。移譲されたイベントハンドラは、以下のような実装となります。

// (これは擬似コードのため動きません。INotifyPropertyChangedの実装やCommandクラスの実装が必要です)
public sealed class MainWindowViewModel
{
	public MainWindowViewModel()
	{
		// ICommandインターフェイスを実装したクラスを使って、バインディング可能なイベントハンドラを公開する
		this.FireStart = new Command(this.OnFireStart);
	}

	// バインディング可能なイベントハンドラ(ボタンのCommandプロパティにバインディングする)
	public ICommand FireStart
	{
		get;
		private set;
	}

	// ウインドウ全体の有効・無効を制御する(MainWindowのIsEnabledプロパティにバインディングする)
	public bool IsEnabled
	{
		get;
		private set;
	}

	// Commandクラスによって、転送されるイベントの実処理
	private async void OnFireStart(object parameter)
	{
		// ユーザーインターフェイスを無効化する
		this.IsEnabled = false;
		try
		{
			// 非同期処理...
			using (var stream = await httpClient.GetStreamAsync("..."))
			{ ... }
		}
		finally 
		{
			// ユーザーインターフェイスを有効化する
			this.IsEnabled = true;
		}
	}
}

この例では、ボタンのクリックと言う「アクション」を引き金に動作するので、比較的分かりやすいと言えます。非同期プログラミングのベストプラクティスによれば、シグネチャとして「async void」と定義するのはイベントハンドラの場合だけだと述べられています。この例は本物(CLR)のイベントハンドラではありませんが、ICommandによって間接的に呼び出されるコールバックとして、広義において同じと見なしても良いと思います。

asyncbinding7

では、値を転送するデータバインディングそのものが引き金になる場合はどうでしょうか?前フリのような、テキストボックスに対する編集操作で、非同期的に処理を実行し、結果を反映する場合です。例えば、Visual Studioのインテリセンスがシンボル名の絞り込みを行う過程や、ウェブで言うなら「googleサジェスト」のような機能です。

この場合、バインディングされたプロパティに、一文字変化がある度に値が転送されてきます。転送はsetterのコールによって実行されるため、非同期処理はsetterから始める必要があります。

<Window x:Class="CenterCLR.AsyncBindings.MainWindow"
		xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
		Title="MainWindow"
		Width="525"
		Height="200"
		IsEnabled="{Binding IsEnabled}">
	<DockPanel>
		<!-- Textプロパティ同士をバインディングし、テキストボックスへの文字の入力を検知可能にする -->
		<TextBox DockPanel.Dock="Top" Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}" />

		<!-- 結果を表示するリストボックス -->
		<ListBox ItemsSource="{Binding ResultItems}">
			<ListBox.ItemTemplate>
				<DataTemplate>
					<!-- 結果を表示するテンプレート -->
				</DataTemplate>
			</ListBox.ItemTemplate>
		</ListBox>
	</DockPanel>
</Window>
// (これも同様に擬似コードです)
public sealed class MainWindowViewModel
{
	private string text_;

	public MainWindowViewModel()
	{
		this.ResultItems = new ObservableCollection<ResultModel>();
	}

	// TextBox.Textにバインディング(UpdateSourceTrigger=PropertyChangedとする)
	public string Text
	{
		get
		{
			return text_;
		}
		set
		{
			// ユーザーインターフェイスを無効化する
			this.IsEnabled = false;
			try
			{
				// 非同期処理...
				using (var stream = await httpClient.GetStreamAsync("...?q=" + text_))
				{ ... }
			}
			finally 
			{
				// ユーザーインターフェイスを有効化する
				this.IsEnabled = true;
			}
		}
	}

	public bool IsEnabled
	{
		get;
		private set;
	}

	public ObservableCollection<ResultModel> ResultItems
	{
		get;
		private set;
	}
}

このコードには仕様的にも実装的にも問題があります。仕様面では、一文字入力する度にWindowが無効化される(しかも非同期処理が完了するまで解除されない)ため、恐らくユーザー体験は最悪の物となる事です。実装的な問題としては、setter内ではawait出来ない事です。これはsetterにはasyncを指定出来ない事から来ています。

まず、実装的な問題は、実処理をメソッド抽出すれば解決します。

public sealed class MainWindowViewModel
{
	private string text_;

	public MainWindowViewModel()
	{
	}

	// TextBox.Textにバインディング(UpdateSourceTrigger=PropertyChangedとする)
	public string Text
	{
		get
		{
			return text_;
		}
		set
		{
			// 非同期メソッドを呼び出すが、待機は出来ない。
			// →Taskを返し、メソッド名に「Async」と付ける事で、setter呼び出し後は、非同期処理が継続する事がはっきりする
			var task = this.ProceedTextAsync(value);
		}
	}

	// 一般的な非同期処理のシグネチャを順守
	private async Task ProceedTextAsync(string value)
	{
		// ユーザーインターフェイスを無効化する
		this.IsEnabled = false;
		try
		{
			// 非同期処理...
			using (var stream = await httpClient.GetStreamAsync("...?q=" + value))
			{ ... }
		}
		finally
		{
			// ユーザーインターフェイスを有効化する
			this.IsEnabled = true;
		}
	}

	public bool IsEnabled
	{
		get;
		private set;
	}
}

実処理を「ProceedText」としてasync voidとする事も出来ますが、ここではより汎用的になるように(そしてベストプラクティスに従うように)、Taskクラスを返却するようにしました。setterで戻り値のTaskを無視すると、C#コンパイラが警告を発します。そのため、ダミーの変数で受けるように書いています。わざわざTaskクラスを返す事で、別の仕様要件でProceedTextAsyncを呼び出さなければならない場合にも、非同期処理の待機が可能になる余地を残す事が出来ます。

残る仕様的な問題ですが、ウインドウ全体を無効化する必要が無いのであれば、特定のコントロールに絞って無効化を実施するという方法が考えられます。例えば、検索結果に応じてリストボックスの内容を変化させる場合、テキストボックスは有効としたまま、リストボックスだけを無効化するようにすれば良いのです。その場合、ロジックの変更は必要ありません。XAMLのバインディング式をListBoxに適用します。

<Window x:Class="CenterCLR.AsyncBindings.MainWindow"
		xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
		Title="MainWindow"
		Width="525"
		Height="200">
	<DockPanel>
		<TextBox DockPanel.Dock="Top" Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}" />
		
		<!-- IsEnabledをListBoxにバインディング -->
		<ListBox ItemsSource="{Binding ResultItems}" IsEnabled="{Binding IsEnabled}">
			<ListBox.ItemTemplate>
				<DataTemplate>
					<!-- 結果を表示するテンプレート -->
				</DataTemplate>
			</ListBox.ItemTemplate>
		</ListBox>
	</DockPanel>
</Window>

非同期処理のエラーの対処

ここまでで、データバインディングで値が転送された場合の非同期処理は対処可能になりました。では、非同期処理中に発生するエラーはどのように対処すれば良いでしょうか?

そもそも、仕様的な面で非同期処理中のエラーにどう対処するかを考える必要があります。最後の例では、ユーザーがテキストボックスに何かタイプすると、非同期で検索処理を行い、結果がリストボックスに(非同期的に)表示されます。一文字タイプし、その事が原因で何らかのエラーが発生したとしても、ユーザーはもう次の文字をタイプしているかも知れませんし、ウインドウ上の別の機能を実行しているかも知れませんし、ウインドウを閉じているかも知れません。

  • エラーが発生したら、その場でメッセージボックスを表示する。
    単純なユーザーインターフェイスであれば、これでも良いと思われます。但し、複数の非同期処理を実行している可能性があり、それらが一斉に失敗すると、メッセージボックスが大量にスクリーンに表示される可能性があります。また、忘れた頃にいきなり表示される可能性もあります。
  • ログ出力のような表示領域に表示させる。
    過去に発生した内容を表示できるので、より望ましいと言えます。但し、どのメッセージが何の操作に紐づいていたのかは分かりにくくなるかも知れません。

どのような方法でも、握りつぶすのでなければ、対処方法は同じでしょう。非同期処理の実質的なコードが例外をスローする場合は、単にtry-catchで例外を補足し、エラー処理を行います。

private async Task ProceedTextAsync(string value)
{
	// ユーザーインターフェイスを無効化する
	this.IsEnabled = false;
	try
	{
		try
		{
			// 非同期処理...
			using (var stream = await httpClient.GetStreamAsync("...?q=" + value))
			{ ... }
		}
		catch (Exception ex)
		{
			// メッセージボックスを表示する
			MessageBox.Show(ex.Message, "Error occurred!", MessageBoxButton.OK, MessageBoxImage.Error);
		}
	}
	finally
	{
		// ユーザーインターフェイスを有効化する
		this.IsEnabled = true;
	}
}

注意点としては、C# 5.0ではcatch-finallyブロック内でawait出来ない事です。WPFの場合はMessageBoxの内部でメッセージポンプが実行される(Win32メッセージループが実行される)ので良いのですが、ストアアプリではawaitで待機する必要があります。そのため、多少泥臭いコードを書く必要があります(エラーを保存して一旦catchブロックから抜けてから表示するなど)。

エラー処理のコード例をお見せしましたが、結局のところ、一般的なコードと何も変わらないことが分かると思います。


ビジネスロジックの分離

仕様がより高度になるにつれ、データバインディングを実装しているクラスを分割したくなります。特にユーザーインターフェイスに対応するための固有のコードと、いわゆるビジネスロジックの分離です。リファクタリングの作法に従って、責務によってクラスを分割して行くと、MVVMで言う所のビューモデルとモデルに分割されます(うまくやればね :)。この時、前述の例で示したHttpClientによる検索処理などのビジネスロジックは、ビューモデル(元のクラス)からモデルに移動する事になります。

実装を移動する際、HttpClient.GetStreamAsyncは非同期メソッドなので、await可能でなければなりません。と言う事は、移動先のメソッドにはasyncが適用されなければならず、ベストプラクティスに従うならTaskクラスを返却するメソッドでなければなりません。結局のところ、「ProceedTextAsync」メソッドはほぼそのままの形でモデルクラスに移動する事になります。

ProceedTextAsyncがビューモデルに依存している箇所は、それぞれを以下のように対処します。

  • 結果をどう反映するか : モデルはthis.ResultItemsにはアクセス出来ない。したがって、ProceedTextAsyncは結果をまとめて(コレクションに入れるなどして)返却する。
  • IsEnabledへのアクセス : モデルは直接UIとの関係を持たないと考えると、IsEnabledの操作はビューモデルに残しておく。
  • メッセージボックスへのアクセス : モデルは直接UIとの関係を持たないと考えると、メッセージボックスの操作(例外の処理)はビューモデルに残しておく。

以下の例では、モデルクラス自身をコレクション(List)とし、UpdateAsyncメソッドでここに結果を保持するようにします。モデルの存在意義を考えてこのようにしましたが、実際の所はメソッドを単なる移譲として分離(戻り値にコレクションを返すスタティックなメソッド)した方が良かったかも知れません。

// ビジネスロジックを含むモデル(コレクション)
public sealed class BusinessLogicModel : List<ResultItem>
{
	// コレクションを更新する
	public async Task UpdateAsync(string value)
	{
		// 非同期処理...
		using (var stream = await httpClient.GetStreamAsync("...?q=" + value).
			ConfigureAwait(false))  // 以降の処理をワーカースレッドで実行
		{
			var document = SgmlReader.Parse(stream);

			// ここではUIを直接操作出来ない(ワーカースレッドなので)
			this.Clear();
			this.AddRange(
				from html in document.Elements("html")
				from body in html.Elements("body")
				// ...
				select new ResultItem { ... });
		}
	}
}

このように分離すれば、あとはビューモデルから呼び出すだけです。

private async Task ProceedTextAsync(string value)
{
	// ユーザーインターフェイスを無効化する
	this.IsEnabled = false;
	try
	{
		try
		{
			// モデルを用意して結果をフィルする
			var model = new BusinessLogicModel();
			await model.UpdateAsync(value);

			// バインディングする
			// (awaitでメインスレッドにマーシャリングされているので、UIを操作しても問題ない)
			this.ResultItems = model;
		}
		catch (Exception ex)
		{
			MessageBox.Show(ex.Message, "Error occurred!",
				MessageBoxButton.OK, MessageBoxImage.Error);
		}
	}
	finally
	{
		// ユーザーインターフェイスを有効化する
		this.IsEnabled = true;
	}
}

更なる発展

コレクションは段階的に反映したいですね。結果が認識されるたびに、1件ずつリストボックスに反映されたりすると、ユーザーインターフェイス的にとても良いと思います。

このような実装にするには、結果が1件取得される度に、バインディングしているコレクションに反映して、それがUIにも反映される必要があります。コレクションへの反映はINotifyCollectionChangedインターフェイスで通知することが出来ます。このインターフェイスはObservableCollectionクラスが実装しているので、Listの代わりにこのクラスを使うと良いでしょう。

注意点として、コレクションへの追加処理は、UIスレッド(WPFでは恐らくメインスレッド・ストアアプリではビュー毎に割り当てられているスレッド)で実行する必要があるという事です。ここにトレードオフがあります。

CPU依存性処理をワーカースレッドで効率よく処理させるために、「ConfigureAwait」メソッドを使用したくなるかもしれません。これにより、awaitの度にUIスレッドにマーシャリングされなくなるため、パフォーマンスが向上します。しかし、当然の事ながらUIスレッドにマーシャリング出来ないので、(メソッドを抜けるまでは)UIに絡む処理は実行出来ません。

ObservableCollectionを使用した場合、要素の変更時に「CollectionChanged」イベントを発火します。このイベントの発火はUIスレッドで実行しなければならないため、ObservableCollectionの操作は暗黙にUIスレッドで実行する必要があります。

前述のコード例でConfigureAwaitを使用しているのは、結果を一旦Listに保存しているので、直接的にも間接的にもUIに依存していない事が分かっているためです。このように、呼び出し元のスレッドがどこまで有効に作用するのかを考えて、ロジックを分離すると良いでしょう。

この辺りを掘り下げた資料は、以前の勉強会で発表した資料があるので、参考にどうぞ。


今年もあと少し、今日は寒くて堪りませんでした。明日はmoririringさんです。よろしく!

WinRTベースのWindows Phone内でスクレイピング – Windows Phone Advent Calendar 2014

この投稿は、Windows Phone Advent Calendar 2014 の 8日目です。

WinRTベースのWindows Phone内で、HTMLのスクレイピングを試みるネタです。とはいえ、実はこれは約1年前の年始早々に取り組んだネタの続きでもあります。

Windows PhoneもWinRTベースの環境と、続々と出荷されるWindows Phone 8ベースの端末(何故か国内では発売されていない。「何故か」w)のおかげで、すっかりWinRTも浸透してきたようです。

pronama_wp_wp_800

(写真に登場するLumia 1520とプロ生ちゃんはフィクションであり、この記事とは無関係ですw)

SgmlReader for Portable Class Library

以前に作ったスクレイピングのライブラリは、
「SgmlReader」と呼ばれるものをPortable Class Library化したものです。このライブラリは、「SGML」をパースしてXmlReader化するものですが、HTMLのDTDが添付されており、要するに「普通のHTMLパーサー」として使えるところがミソです。

移植した際に、Profile1ベース(つまり、最初期のPCLで、環境的に何でもOK)という縛りをつけたので、ストアアプリ前までの環境では、あらゆる環境で使用可能です。

pcl1

この柔軟性はかなり美味しくて、他に依存しているパッケージも存在しないため、どのようなプロジェクト(Windows Phoneに限らず)にでも安心して追加する事が出来ます。

が、WinRT環境では、PCLの設定が排他的になっており、この例のように「全部入り」にしようとしても出来ません。

そのため、WinRT用に別のPCLプロファイルを組み合わせたライブラリを追加し、SgmlReaderをバージョンアップさせようと思っていたのですが… ズルズルと今の今まで放置プレイ(困っていなかったと言うのが大きい)。

Windows Phone Advent Calendarがあるとの事だったので、ようやく重い腰を上げて取り組むことにしました。


夜明け前(前のバージョン)

Portable Class Libraryへの移植は「最大公約数」的な対応が必要です。SgmlReaderの最初の移植では、WebClientクラスを使用して直接HTTP通信を行う部分がネックとなりました。

今なら「Microsoft HTTP client libraries」という、これまたPCLで非同期な優秀なパッケージがあり、これを使えばかなりの広範囲で移植が可能なパッケージを作ることも出来たのですが、元々SgmlReader自体がシンセティック(パーサーなので、理論上、計算しかしない)なので、何にも依存しないパッケージに仕上げたかったのです。

取り組んだこと:

  • WebClientを使っている箇所を削除
  • エラーが発生する部分を修正
  • NuGetパッケージ化

ストラテジとして、WebClientの代わりにデリゲートを導入し、HTTP通信に関わる部分をDI的に挿入できるようにしようと考えていました。ところが、SgmlReaderは「SGML」であるために、DTDの外部参照を自力で解決しようとします。そこで再帰的にWebClientを使ってダウンロードを試みようとするため、この部分まで含めた対応が必要でした。

結局、初期のストリーム(ストリームと言いながらTextReaderなんですが)を与える以外にも、任意のURLからのダウンロードを可能にするデリゲートを挿入可能とし、WebClientへの依存を排除しました。これが前のバージョンです。

また、NuGetパッケージ化は「NuGet Packager」というAddinを使って行いました。NuGetのパッケージ化は面倒です。今のところ、サクッとやる方法が無く、nuspecファイルを手動で編集する必要があります。NuGet Packagerはビルド部分を自動化出来ますが、結局NuGetの構造を知らないと書けないと思います。そして、NuGet Packagerはどうも、その、MSBuildの流儀から外れているのか、ゴミファイルが散乱したりして、落ち着いて保存できないのもモヤモヤしていました。


本番

sgmlreaderstoreapp1png

そして、今回のバージョンアップです。まず、ストアアプリ向けのPCLプロジェクトを追加します。前のバージョン(Profile1)と併存させるため、わざとVisual Studio 2012で作ります。

pcl2

PCLの設定は、グレードを上げて、こんな感じに。

ソースコードは、前のプロジェクト内のソースファイルに対して「リンクで追加」を使って、参照として追加しておきます。

SgmlReaderの最初のバージョンでは、HtmlのDTD参照をフックして、組み込みリソースとしてありました。DTDの指定がHtmlとなっている場合は、Assembly.GetManifestResourceStream()を使って、これを読み取る事で、リソースメンテナンスフリーにしていたのです。

ところが、WinRT環境では、Type.Assemblyが未定義です。これはどうしたものかと思っていたら、Resharperがヒントをくれました。Typeクラスへの拡張メソッドとして「IntrospectionExtensions.GetTypeInfo()」があります。名前が意味深ですね。とにかくこれで、TypeInfoクラス経由でAssemblyクラスのインスタンスが手に入り、GetManifestResourceStream()を使う事が出来ました。

(Resharperが無かったら短時間で探せなかったかも。今見たら、TypeInfoには面白そうなアレコレがありそうだ…)

あとは、WinRT版との切り分けに、「#if NETFX_CORE」を使うだけです。こんな感じ:

#if NETFX_CORE
using (var stm = type.GetTypeInfo().Assembly.GetManifestResourceStream(name))
#else
using (var stm = type.Assembly.GetManifestResourceStream(name))
#endif

さて、ビルド出来たらNuGetへの対応です。先ほど紹介したNuGet Packagerは、どうも完成度がいまいちだったので、別の何かが無いかどうかを探してみました。何しろあれから1年です。

で、見つけたのが以下の二つ:

  • NuGet Package Project – MSの人が(個人的に)作っている拡張機能で評判は良さそうですが、いかんせんVS2013のみの対応。
  • NuBuild project system – VS2010~2013まで対応していて、素朴ではあるものの、妙な挙動が無い。

今回は2012でやっているので、どちらにしてもNuBuild project systemを選択です。

nubuild

これはNuBuildプロジェクトのダイアログですが、「Add Binaries To SubFolder」だけTrueに変えておきます。今回のパッケージには、Profile1のPCLとWinRT向けのPCLの2つのバージョンを入れ込むため、サブフォルダに配置するように指示します。あとはデフォルトのままです。

nuspecファイルはビジュアルに編集するエディタは無く、単純にxmlとして編集します。もちろん、ひな形は提供されるので、埋めていく感じです。

<?xml version="1.0" encoding="utf-16"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
	<metadata>
		<!-- Required metadata -->
		<id>CenterCLR.SgmlReader</id>
		<version>2014.12.7.3</version>
		<title>SgmlReader for Portable Class Library</title>
		<summary>SgmlReader for Portable Class Library. SgmlReader most popular usage the "HTML" parser. (It's scraper!!)</summary>
		<authors>Kouji Matsui</authors>
		<description>SgmlReader for Portable Class Library.
SgmlReader is "SGML" markup language parser, and derived from System.Xml.XmlReader in .NET CLR.
But, most popular usage the "HTML" parser. (It's scraper!!)

/* Use SgmlReader in Html parse mode. */
XDocument document = SgmlReader.Parse(stream);

Done!</description>
		<releaseNotes>2014.12.7.3:
 Add 1 line parse method.
2014.12.7.2:
 Direct handling the Stream class.
 Initial parameter is set of Html parse mode.
2014.12.7.1:
 Namespace changed "CenterCLR.Sgml".
 More easy usage, HTML parse is default mode.
 Native store app library included.
1.8.11.2014:
 Initial release.</releaseNotes>
		<projectUrl>https://github.com/kekyo/CenterCLR.SgmlReader.git</projectUrl>
		<iconUrl>https://raw.githubusercontent.com/kekyo/CenterCLR.SgmlReader/master/CenterCLR.SgmlReader.100.png</iconUrl>
		<requireLicenseAcceptance>false</requireLicenseAcceptance>
		<licenseUrl>http://opensource.org/licenses/Apache-2.0</licenseUrl>
		<copyright>Copyright (c) 2002, Microsoft Corporation; Copyright (c) 2007-2013, MindTouch; Copyright (c) 2014, Kouji Matsui</copyright>
		<tags>SgmlReader Parser Portable HtmlReader Html Scraping</tags>
		<!-- Optional metadata
		<owners></owners>
		<dependencies>
		</dependencies>
		<references></references>
		-->
	</metadata>
</package>

nubuild1

そして、NuBuildプロジェクトのReferencesにプロジェクト参照を追加します。あとはビルドすれば、Bin配下に*.nupkgが出来ているはずです。プロジェクト参照すると、きちんと依存関係も設定されます。

上記のnuspecにはfilesタグが含まれません。通常、nuspecファイルを手で作る場合は、パッケージに含める配布物をfilesタグに含めますが、そこはこのプロジェクト参照で自動的にNuBuildがやってくれます。しかも、いつもながら面倒なプラットフォーム指定文字列(「portable-net403+sl40+win+wp+Xbox40」のような)も、自動的に判定してくれます!これは本当に嬉しい。

(プラットフォーム指定文字列の一覧は、何故か物凄く探しにくいです。備忘録としてリンクを張っておきます

ビルドして生成されたnupkgをNuGetにアップロードします。アップロードの方法は、NuGetでユーザー登録するかMSアカウントでサインインしてアップロードするだけ。nuspecに書かれた定義を元に解析を行うので、特にこれと言って追加の作業はありません。

一点、アイコン参照(nuspec内のIconUrl)は、正しいURLを示していないとアイコンが表示されません。今回はGitHubで公開する事もあり、GitHub上にアイコンファイル(100x100px)を上げておいて、アイコンファイルへのRaw参照URLを指定するようにしました。


実際に使ってみる

wpad

これでNuGet出来るようになったので、使ってみましょう。パッケージIDは「CenterCLR.SgmlReader」です。

やっていることは、プロ生で登壇した時のスクレイピングネタと殆ど同じです。但し、プロ生Advent Calendar進捗ダメで書いた通り、そのままではスクレイピング出来ないので (;´Д`)、代わりにいつものネタに使う「郵便番号データ」のページを使っています。

/// <summary>
/// 指定されたURLのHTMLを読み取って解析します。
/// </summary>
/// <param name="url">URL</param>
/// <returns>コンテンツ群のURL</returns>
private static async Task<IReadOnlyList<KeyValuePair<Uri, string>>> LoadFromAsync(Uri url)
{
	using (var client = new HttpClient())
	{
		using (var stream = await client.GetStreamAsync(url).ConfigureAwait(false))
		{
			// SgmlReaderを使う
			var sgmlReader = new SgmlReader(stream);

			var document = XDocument.Load(sgmlReader);

			// 郵便番号データダウンロードのサイトをスクレイピングする
			// ターゲットは、html/body/div[id=wrap-inner]/div[id=main-box]/div[class=pad]/table/tbody/tr/td/a
			// にあるhrefとなる。
			// パースとトラバースまでワーカースレッドで実行しておく。
			return
				(from html in document.Elements("html")
					from body in html.Elements("body")
					from divWrapOuter in body.Elements("div")
					let wrapOuter = GetAttribute(divWrapOuter, "id")
					where wrapOuter == "wrap-outer"
					from divWrapInner in divWrapOuter.Elements("div")
					let wrapInner = GetAttribute(divWrapInner, "id")
					where wrapInner == "wrap-inner"
					from divMainBox in divWrapInner.Elements("div")
					let mainBox = GetAttribute(divMainBox, "id")
					where mainBox == "main-box"
					from divPad in divMainBox.Elements("div")
					let pad = GetAttribute(divPad, "class")
					where pad == "pad"
					from table in divPad.Elements("table")
					from tbody in table.Elements("tbody")
					from tr in tbody.Elements("tr")
					from td in tr.Elements("td")
					from a in td.Elements("a")
					let href = GetAttribute(a, "href")
					where href.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) == true
					let zipUrl = ParseUrl(url, href)
					where zipUrl != null
					let text = a.Value
					where string.IsNullOrWhiteSpace(text) == false
					select new KeyValuePair<Uri, string>(zipUrl, text)).
				ToList();
		}
	}
}

wpadresult

ZIPファイルへのリンクをトラバースするだけで、中身は展開していません。スクレイピングが出来る事は確認できます。


気になっていた点の改良

身も蓋もない事を言います:

「どうせ、今更SGMLなんて使わないんですw」

と言う訳で、SGMLサポートを削除… まではしませんが、Htmlのパースをもっとサクッと出来るようにしておきました。

何しろ、これ以上簡単には出来ません。1行で行けます。

// ストリームを与えてHtmlをパースし、XDocumentを返す。
XDocument document = SgmlReader.Parse(stream);

では、みなさん、楽しんでください!


SgmlReader for Portable Class Library

GitHub:Repository: CenterCLR.SgmlReader
NuGet:Packaged ID:CenterCLR.SgmlReader

AdventCalendar.StoreAppDemo1(デモ用Windows Phoneプロジェクト)

GitHub:Repository: CenterCLR.AdventCalendar.StoreAppDemo1

つぎは、icchuさん、お願いします!

BUILD 2014のWindows Phone 8.1 Word Flow世界最速タイピング

もう界隈では周知ですが、BUILD 2014のDay1 KeyNoteで、スタローン似の御仁(Joe Belfiore)から、Windows Phone 8.1の機能について発表がありました。

最も注目されたのは「cortana」と呼ばれる、iPhoneで言うなら「Siri」のライバルとなる、音声認識エージェント機能です。
ただ、cortanaはまだ日本語を理解できないし、実機のWindows Phoneでもcortanaを使う場合は、表示言語を英語に設定する必要があります。日本語対応cortanaたんに期待しましょう。

で、個人的にもっと強烈に印象を受けたのが「Word Flow」という新しいタイピング機能です。これは、ソフトウェアキーボード上を「なぞる」だけで、単語の入力が出来てしまうというものです。

Channel 9: BUILD 2014 Day1 KeyNote

build2014d1kn1

Word Flowのデモは、49分辺りからです。実際、実機+WP8.1DPでも、こんな感じで入力できます。一度慣れてしまうと、もう指を離したくなくなるぐらいのインパクトです。

また、その後のタイピングレコードの映像では、脅威的とも思える速さで入力しています。ガラケー+JK並の破壊的速さを、スマートフォンで実現出来たと言えるのではないでしょうかww

build2014d1kn2

しかし、これまた残念なことに、Word Flowは日本語キーボードでは使えません。ただし、英語キーボードに切り替えるだけで使う事は出来ます(表示言語を英語に設定する必要はありません)。

動画と実機のWord Flowの働き具合を見ている限り、トレースするキーの位置が不正確でも問題なく入力できているようです。恐らくは、トレースラインの屈曲する位置を検出して、その付近のキーをリストアップし、それらの組み合わせで最もふさわしい単語を候補としているように思います(+文脈も見ているかも)。そうであれば、日本語キーボードでは使えない理由も自明ですね。

早く日本語対応Word Flowが使いたいものです。

ToggleSwitchを分離ストレージに関連付ける

WPFのトグルスイッチは、主に設定に使用する。
これが沢山あると、

ToggleSwitch

XAMLもそうだが、トグルイベントのハンドラもそれに合わせて書かなければならない。設定項目が増えるほど、このハンドラコードをチマチマと追加しなければならず、単純ミスも多くなる。
そこで、これをビヘイビアで何とかしようという話。

アプリケーションが複雑ではない場合、設定情報を単純に分離ストレージに格納すると思う。IsolatedStorageSettings.ApplicationSettingsコレクションを使用すると、出し入れは簡単になる。
ToggleSwitchの状態は、「オン」と「オフ」だけなので、ApplicationSettingsには単純にブール値で格納すれば良い。
そこで:

public sealed class AppSettingsBehavior : Behavior<ToggleSwitch>
{
    public static DependencyProperty SettingNameProperty =
        DependencyProperty.Register(
            "SettingName",
            typeof(string),
            typeof(AppSettingsBehavior),
            new PropertyMetadata(null));

    public static DependencyProperty OnTextProperty =
        DependencyProperty.Register(
            "OnText",
            typeof(string),
            typeof(AppSettingsBehavior),
            new PropertyMetadata("On"));

    public static DependencyProperty OffTextProperty =
        DependencyProperty.Register(
            "OffText",
            typeof(string),
            typeof(AppSettingsBehavior),
            new PropertyMetadata("Off"));

    public AppSettingsBehavior()
    {
    }

    public string SettingName
    {
        get
        {
            return (string)this.GetValue(SettingNameProperty);
        }
        set
        {
            this.SetValue(SettingNameProperty, value);
        }
    }

    public string OnText
    {
        get
        {
            return (string)this.GetValue(OnTextProperty);
        }
        set
        {
            this.SetValue(OnTextProperty, value);
        }
    }

    public string OffText
    {
        get
        {
            return (string)this.GetValue(OffTextProperty);
        }
        set
        {
            this.SetValue(OffTextProperty, value);
        }
    }

    protected override void OnAttached()
    {
        bool value;
        if (IsolatedStorageSettings.ApplicationSettings.TryGetValue(this.SettingName, out value) == false)
        {
            this.AssociatedObject.IsChecked = false;
        }
        else
        {
            this.AssociatedObject.IsChecked = value;
        }

        UpdateContent();

        this.AssociatedObject.Checked += AssociatedObject_Checked;
        this.AssociatedObject.Unchecked += AssociatedObject_Unchecked;
    }

    protected override void OnDetaching()
    {
        this.AssociatedObject.Checked -= AssociatedObject_Checked;
        this.AssociatedObject.Unchecked -= AssociatedObject_Unchecked;
    }

    private void UpdateContent()
    {
        this.AssociatedObject.Content =
            (this.AssociatedObject.IsChecked ?? false) ? this.OnText : this.OffText;
    }

    private void AssociatedObject_Checked(object sender, RoutedEventArgs e)
    {
        IsolatedStorageSettings.ApplicationSettings[this.SettingName] = true;
        UpdateContent();
    }

    private void AssociatedObject_Unchecked(object sender, RoutedEventArgs e)
    {
        IsolatedStorageSettings.ApplicationSettings[this.SettingName] = false;
        UpdateContent();
    }
}

というようなビヘイビアを作っておいて、

<ListBox Grid.Row="1" Margin="0,0,0,0" Padding="0,0,0,0"
    <ListBox.ItemContainerStyle>
        <Style TargetType="ListBoxItem"
            <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
        </Style>
    </ListBox.ItemContainerStyle>

    <toolkit:ToggleSwitch Header="正規表現を使用する">
        <i:Interaction.Behaviors>
            <Behaviors:AppSettingsBehavior SettingName="IsRegexMode" OnText="オン" OffText="オフ" />
        </i:Interaction.Behaviors>
    </toolkit:ToggleSwitch>

    <toolkit:ToggleSwitch Header="大小文字を区別する">
        <i:Interaction.Behaviors>
            <Behaviors:AppSettingsBehavior SettingName="IsCaseSensitiveMode" OnText="オン" OffText="オフ" />
        </i:Interaction.Behaviors>
    </toolkit:ToggleSwitch>
</ListBox>

と書くだけで、次々とToggleSwitch項目を簡単に増やすことができる。SettingNameには、分離ストレージに格納するキーの名前を、OnTextとOffTextはスイッチ左側に表示される状態を示すテキストを指定する。これらは依存関係プロパティにしてあるので、バインディングで多国語化することもできる。

Metro UIのボタンエフェクトをWindows Phoneに適用する

Windows Phoneで遊び始めている。
Windows Phone自体もそうだが、WPFも殆ど触ったことがないので、とても苦しいw
取り合えず、簡単なアプリを実際に公開してみて感触をつかみつつ、WPF理解への足掛かりにしたいなと思っている。

で、早速製作中なのだが、Metro UI(名称がぽしゃったので、何と呼べばいいのか困るなぁ)に使用するユーザーインターフェイスのボタン、

WP8MetroUI

のマウスカーソルのあたりをタップした時に、ボタンの隅が押されて変形したような挙動でフィードバックがある、アレをやりたいと思ったのだが、どうも簡単に出来ないようだ。
で、WPFの変形の基礎とか、コントロールのカスタマイズの方法など、色々調べて以下のコードを書いた。

(ここまで紆余曲折の末、約2日 orz 直前にGeometry/Path/RenderTargetBitmapだけいじっていたのが幸いした。でないと、座標がdoubleというだけでも悶絶していたかもしれない…)

public sealed class TiltBehavior : Behavior&lt;UIElement&gt;
{
    private PlaneProjection projection_;

<pre><code>public TiltBehavior()
{
    this.Depth = 30.0;
    this.Tracking = true;
}

public double Depth
{
    get;
    set;
}

public bool Tracking
{
    get;
    set;
}

protected override void OnAttached()
{
    this.AssociatedObject.MouseLeftButtonDown += AssociatedObject_MouseLeftButtonDown;
    this.AssociatedObject.MouseLeftButtonUp += AssociatedObject_MouseLeftButtonUp;
    this.AssociatedObject.LostMouseCapture += AssociatedObject_LostMouseCapture;

    if (this.Tracking == true)
    {
        this.AssociatedObject.MouseMove += AssociatedObject_MouseMove;
    }
}

protected override void OnDetaching()
{
    this.AssociatedObject.MouseLeftButtonDown -= AssociatedObject_MouseLeftButtonDown;
    this.AssociatedObject.MouseLeftButtonUp -= AssociatedObject_MouseLeftButtonUp;
    this.AssociatedObject.LostMouseCapture -= AssociatedObject_LostMouseCapture;

    if (this.Tracking == true)
    {
        this.AssociatedObject.MouseMove -= AssociatedObject_MouseMove;
    }
}

private static void Apply(Size size, Point point, PlaneProjection projection, double depth)
{
    // コントロールのサイズからノーマライズした割合を得る
    var normalizePoint = new Point(
        point.X / size.Width,
        point.Y / size.Height);

    // 0~1の範囲外を切り捨てる
    var satulatePoint = new Point(
        (normalizePoint.X &amp;gt; 1.0) ? 1.0 : ((normalizePoint.X &amp;lt; 0.0) ? 0.0 : normalizePoint.X),
        (normalizePoint.Y &amp;gt; 1.0) ? 1.0 : ((normalizePoint.Y &amp;lt; 0.0) ? 0.0 : normalizePoint.Y));

    // 中心位置からの割合を得る
    var originPoint = new Point(
        satulatePoint.X * 2.0 - 1.0,
        satulatePoint.Y * 2.0 - 1.0);

    // 絶対位置
    var absolutePoint = new Point(
        Math.Abs(originPoint.X),
        Math.Abs(originPoint.Y));

    // 中心からの位置関係
    var directionX = originPoint.X &amp;gt;= 0.0;
    var directionY = originPoint.Y &amp;gt;= 0.0;

    // タップされた位置に応じて、回転軸位置を固定する(0又は1)
    projection.CenterOfRotationX = directionX ? 0.0 : 1.0;
    projection.CenterOfRotationY = directionY ? 0.0 : 1.0;

    // 辺ではなく、中心をタップした場合にも、フィードバックを得る
    // (辺をタップした場合は0に近づく事で影響を避ける)
    var distance = (absolutePoint.X &amp;gt; absolutePoint.Y) ? absolutePoint.X : absolutePoint.Y;
        projection.GlobalOffsetZ =
        (1.0 - distance) *
        0.5 *       // 中心位置でのZ座標
        (-depth);

    // Rotationは角度なので、計算して算出
    projection.RotationY =
        Math.Atan2(depth * (0.0 - originPoint.X) * 0.5, size.Width) /       // 0.5はGlobalOffsetZに含まれているので
        (Math.PI / 180.0);
    projection.RotationX =
        Math.Atan2(depth * originPoint.Y * 0.5, size.Height) /      // 0.5はGlobalOffsetZに含まれているので
        (Math.PI / 180.0);
}

private void AssociatedObject_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    if (this.AssociatedObject.Projection == null)
    {
        this.AssociatedObject.CaptureMouse();

        projection_ = new PlaneProjection();
        this.AssociatedObject.Projection = projection_;

        var size = this.AssociatedObject.RenderSize;
        if ((size.Width * size.Height) &amp;gt; 0)
        {
            var point = e.GetPosition(this.AssociatedObject);
            Apply(size, point, projection_, this.Depth);
        }
    }
}

private void AssociatedObject_MouseMove(object sender, MouseEventArgs e)
{
    if (projection_ != null)
    {
        var size = this.AssociatedObject.RenderSize;
        if ((size.Width * size.Height) &amp;gt; 0)
        {
            var point = e.GetPosition(this.AssociatedObject);
            Apply(size, point, projection_, this.Depth);
        }
    }
}

private void Uncapture()
{
    if (object.ReferenceEquals(this.AssociatedObject.Projection, projection_) == true)
    {
        this.AssociatedObject.Projection = null;
        projection_ = null;

        this.AssociatedObject.ReleaseMouseCapture();
    }
}

private void AssociatedObject_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
    Uncapture();
}

private void AssociatedObject_LostMouseCapture(object sender, MouseEventArgs e)
{
    Uncapture();
}
</code></pre>

}

何しろWPFは完全に初心者なので、変なことをやっていたり、思想から外れる設計なのかもしれないのであしからず。これはUIElementクラスに適用できるビヘイビアクラスで、プロジェクトに入れておいて、ページのXAMLで以下のような感じで使う。

&lt;ListBox x:Name=&quot;MainListBox&quot; Margin=&quot;0,0,0,0&quot; Padding=&quot;0,0,0,0&quot;&gt;
    &lt;ListBox.ItemTemplate&gt;
        &lt;DataTemplate&gt;
            &lt;StackPanel Margin=&quot;8,0,8,8&quot;&gt;
                &lt;TextBlock Margin=&quot;0,0,0,0&quot; Padding=&quot;0,0,0,0&quot; Text=&quot;{Binding Name}&quot; TextWrapping=&quot;Wrap&quot; Style=&quot;{StaticResource PhoneTextSubtleStyle}&quot;/&gt;
                &lt;TextBlock Margin=&quot;8,8,0,8&quot; Padding=&quot;0,0,0,0&quot; Text=&quot;{Binding Description}&quot; TextWrapping=&quot;Wrap&quot; Style=&quot;{StaticResource PhoneTextSubtleStyle}&quot;/&gt;
                &lt;i:Interaction.Behaviors&gt;   &lt;!-- ココ --&gt;
                    &lt;Behaviors:TiltBehavior /&gt;
                &lt;/i:Interaction.Behaviors&gt;
            &lt;/StackPanel&gt;
        &lt;/DataTemplate&gt;
    &lt;/ListBox.ItemTemplate&gt;
&lt;/ListBox&gt;

ListBoxにコレクションをバインディングし、その要素毎にStackPanelで表示する。そのStackPanelにビヘイビアの指定を行うと、対応するクラスのビヘイビアが呼び出される。名前空間「i」は、System.Windows.Interactivityで、これはWindows Phone以外ではアセンブリが違うかもしれないが存在すると思う。最初にxmlns:iで宣言しておくこと。

なお、Buttonに適用するとうまく動かない。何故かは、これから悩むところ (^^;; StackPanelのクリックを検出する方向で逃げたほうがいいのか、Buttonで真面目にやるほうがいいのか、それすら分からないのが困ったものだ…