Windows PhoneアプリでF#を使う

この記事はWindows Phone Advent Calendar 2014の20日目の記事です。

さて「F#で2ch専用ブラウザをつくろう」では「2ちゃんねる」の掲示板データを読みこむためのライブラリをF#で記述しました。今回はこのライブラリをWindows Phoneアプリの開発から使えるようにしてみます。Windows PhoneアプリにはSilverlightアプリとユニバーサルアプリの2種類がありますが、ここではもちろんユニバーサルアプリを対象にしています。

F#でPortable Class Libraryを作る

F#のコードをユニバーサルアプリから使う基本的な流れは以下のようになります。

  1. F#のコードをPortable Class Library (PCL)の形でコンパイルしておく
  2. PCLのアセンブリをユニバーサルアプリのプロジェクトから参照する

PCLを作るにはプロジェクトの新規作成からPortable Libraryを選びます。このプロジェクトテンプレートで生成されるクラスライブラリは.NET 4.5ベースのPCLを生成します。

とりあえず、プロジェクトを生成して、前回作成したコードをインポートしてみます。これで完了!と書きたいところなのですが、実はこのライブラリ、このままでは使えません。

「2ちゃんねる」APIの解説で少し触れましたが、掲示板データのやりとりはShift JISで行います。ところがWindows Phone上のWindows Runtimeではこのエンコーディングはサポートされていないため、Encoding.GetEncoding(“SJIS”)を呼び出した時点で例外が発生してしまうのです。

Shift JISがWindows Phone上のランタイムでサポートされていないのはWindows Phone 7時代からの話なので、実は改めて驚くことはないのですが、PCLでコンパイルが通ったからといって実行できるとは限らない典型例といえます。自分でなんとか実装するしかないのですが、幸い @kazuakix さんによる SJIS Encoding の実装例があるので、そちらを参考にすることにしましょう。

まずはWin32 APIの関数であるMultiByteToWideCharを呼び出すためにWindows Runtimeコンポーネント(SjisEncoding)を定義します。

次にこれをEncodingクラスとして利用できるようにSjisEncodingクラスを定義します。


さて、記事にしたがってエンコーディングのクラスを書いてみたのですが、これをF#のコードから使うのにはもうひと工夫必要だったりします。というのも、F#のPCLから参照できるのはPCLのライブラリのみなのですが、このSjisEncodingをPCLのライブラリとしてコンパイルすることはできません。というのもWin32のAPIを呼び出す部分がWinRTコンポーネントになっており、これをPCLから参照することはできないからです。

そこでMonalith.Communicationモジュールから直接このクラスを参照するのではなく、外部からEncodingのインスタンスを受け取るようにして、Monalith.Communicationからユニバーサルアプリプロジェクトへの参照をなくします。

モジュールとして定義していたHttpClientをクラスとして定義しなおし、Encodingをコンストラクタの引数として受け取るように変更しています。とりあえず、これでWindows Phoneアプリの開発にも使えるPCLとして形なりました。

明日のWindows Phone Advent Calendar 2014 21日目の記事は @karno さんです.

謝辞:快く参照を許可してくれた@kazuakixさん、ありがとうございました。あと、はやくSurface Pro 3買ってください!

F#で2ch専用ブラウザをつくろう

この記事はF# Advent Calendar 2014 12日目の記事です。前日は @gab_km さんの『Visual F# Power Tools の紹介』でした。

F#でアプリケーションを書こう!

今回はいつもと少し趣向を変えて、F#でのアプリケーションプログラミングについて取り上げてみたいと思います。題材として取り上げるのは「2ちゃんねる」の専用ブラウザです。といってもその開発すべてを取り上げるとさすがに退屈になりそうなので、アプリケーションのキモとなる掲示板データのアクセス部分にフォーカスして解説してみます。

「2ちゃんねる」にはさまざまな内容をとりあつかう掲示板があり、これらの掲示板はその内容にあわせて便宜上カテゴリー分けされています。さらに、掲示板は特定の話題ごとにスレッドという単位で、ユーザーからの書きこみをまとめています。「2ちゃんねる」にアクセスするためのAPIについて特にオフィシャルなドキュメントは存在しないのですが、ネット上の有志の方による資料がいくつか存在します。ここでは参考資料として「専ブラ開発への道」を紹介しておきますが、このブログの記事を理解する上ではこれらのドキュメントを参照する必要は特にありません。

カテゴリーごとの掲示板情報の取得

さきほど掲示板はその内容にあわせて「便宜上」カテゴリー分けされていると書いたのですが「便宜上」と書いたのには訳があります。というのもカテゴリーと掲示板の論理的な関係を厳密に取得するAPIというのは特になく、掲示板の一覧を表示するHTML文書の形でしか用意されていないのです。よって、カテゴリーと関連付けて掲示板の一覧情報を入手するには、以下のような形式で記述されているHTML文書をパースすることになります。

スレッドリスト

掲示板の一覧から掲示板のURLを取得することができます。このURL配下のsubjects.txtというテキストファイルに掲示板のスレッド一覧の情報が収められています。形式は以下のような形式になっており、一行がひとつのスレッドに対応する形になっています。

nnnnnnnnnnは10桁の整数であり、スレッドが生成された日時を1970年1月1日00時00分00秒からの経過秒数で表現しています。またxは1-1000の整数であり、スレッドに対する書き込み数を表します。

書き込み一覧

スレッドに対する書き込みの一覧もテキストファイルとしてアクセス可能です。ボードのURLおよびスレッドのIDに基づいて以下のようなURLを合成します。

ダウンロードしたファイルは以下のような形式になっており、一行がひとつの書き込みに対応する形になっています。なお、特に断りがない場合、このAPIでやりとりされるテキストはShift JISでエンコードされているので注意してください。

ここでも項目の区切りが'<>’になっていることにさえ気を付ければ非常にパースしやすい形式ですが、落とし穴がいくつかあります。

  • 時刻の表現の基本形はyyyy/MM/dd(曜) HH:mm:ss.ffという形式ですが掲示板/スレッドによっては.ffがありません
  • 同様にIDの部分がない掲示板/スレッドもあります
  • スレッド名称は最初の一行にのみ存在します

掲示板データの読み込み

これらのカテゴリー、掲示板、スレッドおよび書き込みを以下のようなF#のデータ型として表現することにします。

掲示板にAPIを通じてアクセスするということは、HTTPでダウンロードしたテキストファイルを、これらのデータ構造に変換するということになります。テキストをダウンロードするコードは.NETのライブラリを用いると簡単に実装できるので、ここではテキストファイルを解析して上記のデータ型に変換するところの処理について詳しくみてみましょう。

スレッド(書き込みのリスト)の読み込み

まずは掲示板の情報そのものともいえるスレッド(書き込みのリスト)の読み込みからみていきましょう。

先ほどしめした書き込みの形式は正規表現であらわせので、それにマッチした結果に基づいてMoPostの値を返せばよいということなります。ただし、パースの処理とMoPostへの変換処理を区別するという観点で、ここでは正規表現のマッチングはActive Patternとして定義してあります。

スレッドリストの読み込み

スレッドの一覧もほぼ同様のパターンで実装できます。正規表現の複雑さという観点ではこちらの方が簡単なので特に問題ないでしょう。

カテゴリーと掲示板の読み込み

以上で見たように、スレッドおよび書き込みの情報そのものは正規表現であらわせる形のものでしたので、繰り返し文字列に対してマッチングさせていけばよいだけでした。カテゴリーおよび掲示板の情報そのものも正規表現を使えば問題なく解析できるのですが、厄介なのがカテゴリーと掲示板の階層関係の解析です。

おおざっぱにいうとカテゴリーおよび掲示板の一覧を表示するHTMLは以下のような構造になっています。

カテゴリー名および掲示板にマッチする行以外は無視すると、このHTMLファイルはカテゴリー名(CategoryTitle)および掲示板(Board)のリストとしてみなすことができます。このリストを(カテゴリー、掲示板のリスト)というTupleにうまく分割するととができればMoCategoryに変換できそうです。

まず、カテゴリー名および掲示板の正規表現に対応するActive Patternを記述しておきましょう。

Active Patternにマッチした結果を中間表現として一時的にTokenというUnionで表現することにしましょう。こうすれば件のHTML文書はTokenのリストとして表現できます。

これで、このリストに対してパターンマッチングを適用し、(カテゴリー名、掲示板のリスト)という組を取り出し、それをMoCategoryに変換することができるようになりました。

あと「掲示板のようにみえる行」が最初にあるので、それを読みとばすという処理を追加すると完成です。

以上、掲示板のカテゴリー、掲示板、スレッドおよび書き込みのデータをパースする関数が定義できました。以下のようにSystem.Net.WebRequestと組み合わせれば、一応簡単なクライアントモジュールの仕上がりとなります。

以上の関数ほかをモジュールとして定義しているソースはこちらから入手可能です。

13日目は @omanuke さんです。

設定チャームを使う

Widowsストアアプリにおいて、アプリケーションの設定は設定チャームを通じて行います。実はFeedroでも設定チャームを通じたアプリケーションの設定画面の実装は行われています。試しにFeedroを起動し、Windows+[I](アルファベットのアイ)を押して設定チャームを開いてみてください。【一般】というFeedroが提供するメニュー項目が追加されています。この【一般】を選択すると【フィードを保持する日数】というコンボボックスがひとつ表示されているだけの設定画面が表示されます。

settingsCharm generalSettings

この設定項目により、RSSフィードから読み取ったブログ記事をローカルに保存する期間を選択できます。

DaysToKeepOptions

この設定画面を実現するための、モデル、ビューモデルおよびビューについてそれぞれ見ていきましょう。

モデル

第6章で触れたようにアプリケーションの設定情報を保存するには、Windows.Storage.ApplicationDataクラスのオブジェクトのLocalSettingsプロパティを用います。このLocalSettingsプロパティは永続化機能をもつオブジェクト辞書で、名前 を付けてオブジェクトを保存、読み出しする機能を提供します。以下のような点が異なりますが、 System.Collections.Generics.Dictionaryクラスと似たようなものだと考えるとわかりやすいでしょう。

  • 自動的にファイルに保存される
  • キーは文字列に固定されている
  • 値となるオブジェクトの型はWindows Runtimeがサポートする標準型に限定される

【注】Windows Runtimeがサポートする標準型の一覧についてはこちらを参考にしてください。

アプリケーションのコードからApplicationDataのLocalSettingsプロパティを直接操作しても実用上は問題ありませんが、ここではSettingsHelperというユーティリティクラスを定義しておきましょう。

このようなユーティリティークラスを介して設定情報を取り扱うことで、のちのち、設定情報を端末間で同期させたい、といった要件がでてきた時に対応しやすくなります。さらにその上で、設定値そのものをモデルStoreクラスのプロパティDaysToKeepとしてアクセスできるようにしておきましょう。

ビューモデル

ビューモデルは以下のふたつのプロパティを実装しています。

DaysToKeepOptions コンボボックスで表示する選択可能な日数の一覧一週間(7)、二週間(14)、一か月(30)、二か月(60)、三か月(90)、半年(180)、一年(365)
SelectedDaysToKeep コンボボックスで選択された日数

SelectedDaysToKeepの実装は、Store.DaysToKeepの値を読み書きしているだけです。

ビュー

ビューに配置されているオブジェクトは、日数を選ぶためのComboBoxおよびそのラベルであるTextBlock(CaptionDaysToKeep)のみです。

ComboBoxのItemsSource(選択可能なオプションの一覧)をビューモデルのDaysToKeepOptionsに、SelectedValue(選択された値)をビューモデルのSelectedDaysToKeepにバインドしておきます。SelectedDaysToKeepへのバインドは双方向になっているため、コンボボックスで日数を選択すると、その値が自動的にビューモデルのSelectedDaysToKeepプロパティに反映され、最終的にSettingsHelperを通じてアプリケーションの設定情報として保存されます。

設定チャームとの接続

設定画面ができたので、最後にこれを実際に設定チャームと接続してみます。設定チャームとして表示されるビューはWindows.UI.ApplicationSettings.SettingsPaneクラスのインスタンスです。まず、このビューに表示されるメニュー項目【一般】を追加します。SettingsPaneがビューの初期化を行う際、CommandsReuqestedイベントを発生させるので、そのタイミングでメニュー項目を追加します。

“General”というメニューの識別子、メニューラベルの文字列およびメニューが選択された場合の処理(showSettingsPanel)を登録しています。メニューが選択された場合の処理そのものは非常に簡単で、先ほど定義したビューのインスタンスを生成し、Showで表示しているだけです。

異なるサイズのライブタイルの同時更新

第10章で述べたとおり、スタート画面に表示できるアプリのタイルには4種類のサイズがあり、小サイズのタイル以外はライブタイルとして用いることができました。

いろいろなサイズ

おさらいになりますが、以下はライブタイルを更新するコードの例です。

ここではTileWide310x150Text01というテンプレートを用いて、横長のライブタイルの情報を更新しています。

ワイド

ここで、このライブタイルのサイズを中および大のサイズに変更してみると確認できますが、中および大サイズのタイルには、アプリのアイコンが表示されるのみです。使用したテンプレートは、横長のタイル用のものなので、横長のタイルのみ表示情報が更新された形なっているのです。

もちろん、ここで違うテンプレート、例えばTileSquare150x150Text01(中サイズ)やTlieSquare310x310Text01(大サイズ)を用いると、それぞれのサイズのタイルに表示されている情報を更新できます。しかし、それ以外のサイズのタイルはやはり変更されません。すべてのサイズのタイルに表示されている情報を同時に変更するにはどうすればよいのでしょうか。

ここで、テンプレートの仕組みについてもう少しく詳しく見てみましょう。書籍でも触れましたが、テンプレートの実体はXmlDocument形式のオブジェクト、すなわちXMLです。例えば、TileWide310x150Text01のテンプレートを等価なXMLとして表現すると以下のようなものになります。

bindingタグおよぼその子要素に注目していください。bindingの子要素であるtextタグが実際の更新情報を表しており、bindingタグのtemplate属性がどのテンプレート(レイアウト)を用いるか指定しています。ここでピンと来た方もいるかもしれませんが、bindingは複数指定することができるのです。

この例では以下の3種類のテンプレートを用い、中、大、横長サイズのそれぞれのライブタイルに対する更新情報を指定しています。

  • TileSquare150x150Text01
  • TlieSquare310x310Text01
  • TileWide310x150Text01

このXMLを用いてTileUpdateManagerのUpdateを呼び出すと、ワイド以外の2種類のサイズのライブタイルにおいても表示される情報が更新されるようになります。

中 大

コーディング上、少し面倒なのがこのXMLを表すXmlDocumentのオブジェクトをどのように準備するのか、という点です。この場合、TileUpdateManagerのGetTemplateContentメソッドは使えないので、XmlDocumentに用意されているDOM(Document Object Model)操作のメソッドを利用して、XmlDocumentのオブジェクトをいちから生成するのが正攻法になります。

ただし、これは実際にやってみるとわかりますが、大変煩雑かつ面倒なコーディングになります。そこでここでは、System.Xml.Linqに用意されているXDocumentを用いる方法を紹介しておきます。

XDocumentもXmlDocumentと同じく、XMLをオブジェクトとして操作するためのものです。LINQ to XMLでの利用を想定して新たにデザインされており、XmlDocumentと比較して使い勝手がかなり改善されています。ここでは、この使いやすいXDocumentを使用してXMLのオブジェクトを構築し、それをXmlDocumentに変換するというアプローチを用いました。メモリ消費およびパフォーマンスという観点からは必ずしもベストなやり方ではないのですが、ここでは、コードの理解しやすさという観点からこの方法を紹介しています。

NOTE: この方法を用いたとしても以下のような条件下であれば特に問題にならないかもしれません。

  • ライブタイルの更新頻度がそれほど頻繁でない
  • 構築するXMLのサイズがそれほど極端に大きくはない

実際に使用される場合は十分にテストしてみてください。

サンプルコードはgithubから入手可能です。

非同期処理におけるセマフォを用いた排他制御

第5章で書いたように lock 節の中では await の使用が禁止されています。

「ロックを取得する」とよく言いますが、これは「実行スレッドがロックを取得する」という意味です。スレッドプールを通じてスレッドを共有する可能性のある Task 処理の間では、「ロックを取得する」ことでは排他制御を行えないのです。lock 節中での await の使用は、そもそも期待通りの排他制御が行われないため禁止されているともいえます。このような理由のため、lock 節を用いずに Mutex のオブジェクトを直接使用したとしても、排他処理はうまく意図したとおりに機能しません。なぜなら、Mutex もスレッドが「取得する」ものだからです。

これでは排他制御ができないじゃないか、という話になってしまいます。アプリのアーキテクチャを設計する上で、ロックを使わないように努力するにしても、どこかで最低限の排他制御は必ず必要になりえます。

スレッドと紐づかないで排他制御をする仕組みはないものでしょうか。

そこで登場するのが「セマフォ」というものです。セマフォという名前は、黎明期の鉄道における交通整理に用いられた手旗信号に由来しています。この手旗信号がどのように使われたか、鉄道マニアをのぞけばピンと来る方は少ないと思われるので、このあたりの由来はあまり気にしないほうがよいでしょう。

ここでは、加減算のできる単なる「カウンター」がセマフォだと思ってください。ただし、このカウンターには以下のような性質があります。

  • カウンターの最大値を指定できる
  • カウンターの加算および減算を実行できるのは、ある一時点でひとつのスレッドに限られる
  • カウンターの値が最大値の時に加算をしようとすると、カウンターの値が減算されるまで待機させられる

たとえば、このカウンターの最大値が3に設定されていると仮定してください。このカウンターを4つのスレッドから同時に加算しようとした場合、何がおこるでしょうか。カウンターの加算を同時に実行できるのは、ひとつのスレッドのみです。なので、4つのスレッドは、あるランダムな順番で順にカウンターの加算を実行していきます。ただし、最初の3つのスレッドが加算を実行した時点で、カウンターは最大値の3に到達するので、4番目のスレッドが加算をしようとすると、その時点で待機することになります。なにがしかの事象によりカウンターが減算されてはじめて、4番目のスレッドは加算を実行し、処理を継続することができるようになります。

ここで、カウンターの最大値を1に設定したと考えてください。ある処理Aの前にカウンターを加算し、処理後にカウンターを減算するようにしておけば、この処理Aが同時に複数実行されることはなくなります。つまり、排他制御を実現できるわけです。

実際のコードでどのように使えるのか確認しておきましょう。

System.Threading.SemaphoreSlimクラスのオブジェクトがセマフォであり、以下のように用います。

  • コンストラクターの引数でカウンターの初期値および最大値を指定
  • カウンターの加算はWaitもしくはWaitAsyncで行う
  • カウンターの減算はReleaseで行う

上記のコードではコレクションに要素を追加すると同時にファイルに書き出していますが、この前後でセマフォを用いて排他制御を行っています。ここで、ファイルの書き出しが非同期処理である点に注意してください。また、セマフォのカウンターの加算を非同期に行っている点も重要です。

サンプルコードはgithubから入手可能です。

NOTE: ところで SemaphoreSlim クラスという名前が示すように、このクラスはSemaphoreクラスの「軽量版」です。SlimがつかないSemaphoreでも同等の排他制御は可能ですが、少し処理が重くなるので、通常は SemaphoreSlim クラスを用いるとよいでしょう。

スレッド間でなくプロセス間でセマフォを利用したい場合は名前付きセマフォを利用する必要があります。このような場合は、SemaphoreSlimでなくSemaphoreクラスを用いる必要がでてきます。ただし、単にプロセス間でのロックが必要なだけであれば、セマフォでなく名前付きミューテックスを利用すればよいでしょう。

参考資料

より詳しく調べてみたい方は以下の資料などを参考にしてみてください。

日本語資料ということであれば、山本康彦(@biac)さんの資料付きデモアプリがわかりやすいです。

双方向データバインディングの利用

第4章で取り上げたように、データバインディングを用いることで、ビューモデルのプロパティの値を、ビュー上のコントロールのプロパティに簡単に反映させることができます。例として、書籍のタイトルと価格を保持するBookクラスを考えてみます。

これを以下のようなビューを用いて表示します。

ビューモデルの初期化は以下のようにMainPageのコンストラクタで行っておきましょう。

アプリをデバッグモードで起動すると、書籍情報が以下のように表示されます。

書籍情報を表示する

ここまでは第4章でみたとおりです。

さて、このビューをユーザーインターフェイスとして用い、ビューモデルの値を編集することを考えてみます。ビューモデルのそれぞれのプロパティがTextBoxのTexstプロパティにバインドされているのだから、テキストを編集すればTextBoxのプロパティが変更されて、それが自動的にビューモデルのプロパティに反映されるのでは?と思った方がいるかもしれません。もし、データバインディングがそのような挙動を示すのであれば大変うれしいのですが、そのような振る舞いをするのでしょうか?

実験で確かめてみましょう。ビューモデルのTitleプロパティのSetメソッドの先頭の箇所に、デバッガーでブレークポイントを設定します。

 2

デバッグ起動したのち、TextBoxに表示されている書籍の題名を編集し、TABキーを押して変更を確定させてください。ここでもし、TexstBoxのTextプロパティの変更がビューモデルに書き戻されているのであれば、ビューモデルのプロパティのSetメソッドが呼ばれるので、先ほど設定したブレークポイントでアプリケーションの実行が一時中断するはずです。

中断しましたか?しませんでしたね。特別に指定しなければ、データバインディングは一方通行で作用します。このため、期待したいような振る舞いにはならないのです。

ところで、「特別に指定しなければ」とあるように、実はデータバインディングを双方向で作用するように、指定することもできます。

上記のようにMode=TwoWayの指定を追加した上で、もう一度、書籍の題名を変更してみてください。今度はTitleプロパティのSetメソッドに設定したブレークポイントで実行が中断されるはずです。

さて、TwoWayモードを指定した双方向バインディングを利用する上で注意すべきことが一点あります。書籍の価格を表示しているTextBoxについてもTwoWayを指定し、編集した金額がビューモデルのプロパティPriceに反映されるか同様に実験してみてください。

どうもデバッガーで設定したブレークポイントがヒットしていない(=変更が反映されていない)模様です。何が起こっているのでしょうか。ここで、Visual StudioのOutputウィンドウに目を向けてください。 以下のようなエラーメッセージに気がつくでしょう。

このエラーは、ビューモデルとビューのプロパティの型が異なるために発生しています。ここまで明示的には書いていませんでしたが、バインド元(ソース)のプロパティとバインド先(ターゲット)のプロパティの型が異なる場合、基本的になんらかの形でその変換方法を指定する必要があります。唯一の例外はターゲットの型が文字列(string)型の場合です。この場合、ソースのオブジェクトのToStringメソッドが呼び出され、文字列に変換されます。

双方向バインディングを用いる場合、ターゲットからソースへの書き戻しが発生しますが、この時にも同様の原則があてはまります。上記の例ではPriceプロパティがdecimal型のため、文字列のTextプロパティの値をどのように書き戻せばよいのかを指定しない限り、エラーとなってしまいます。

このような場合、双方向の変換を明示的に行うコンバーターを実装し、それをバインディング時に指定する必要があります。ここでは以下のようなコンバーターを実装してみましょう。

このコンバーターを静的リソースとして定義し、バインディング時に使用します。

この状態で再度実験してみてください。今度はPriceプロパティへの書き戻しが行われていることを確認できることかと思います。

サンプルコードはgithubから入手可能です。

Windows 8プログラミング本格入門

拙著「Windows 8プログラミング本格入門」の記述に関する訂正、注釈、記載できなかった技術情報などはこちらの一覧に記載しています。

WebViewにURLをバインドする

Windows.UI.Xaml.Controls.WebView を使うと、簡単に任意の URL のページを表示させることができます。この WebView、主に使用するメソッドは Navigate と NavigateToString のふたつだけとあって、非常に簡単に使えるのですが、使い勝手が微妙な点もあります。

たとえば、あるビューモデルにプロパティ Uri があると仮定します。対応するビュー には WebView が定義されており、ビューモデルの Uri に対応する Web ページを表示させたいというような場合を考えてください。

WebView にこの Uri を直接バインドできれば話は簡単なんですが、残念ながらそのような便利なプロパティはないため、Navigate メソッドをどこかで呼び出す必要がでてきます。ビューモデルのプロパティ Uri がどのタイミングで変更されるかにもよりますが、このビューモデルのプロパティ Uri とビューで表示されているページを完全に同期させようとするといろいろと面倒なことをする必要がでてきます。

もし WebView が表示するページの Uri をセットするためのプロパティをもっていれば話は簡単です。ビューモデルのプロパティをバインドするだけでよいのです。そう、WebView にそんなプロパティさえあれさえすれば。

というわけで、WebView をラップしてそのようなプロパティを提供するユーザーコントロールを書いてみました。