特集:XAMLファミリ共通開発のすゝめ(後編)

MVVMパターンを使ったクロス・ターゲット開発

岩永 信之
2012/01/17
Page1 Page2

Portableプロジェクト

 Portable Class Libraryを使えば、ビルド結果のDLL自体をクロス・ターゲットにできるという利点がある半面、参照できるライブラリに制限がかかる。WPF/Silverlight/WP7の間で歩調に差があるのも問題だろう。「次のバージョンで入る」という機能があったとき、一番リリースが遅いものに合わせざるを得ない。

 例えば、今回のサンプルとして作ったTwitterクライアントの場合、以下のような点で困った。

  • Webアクセス機能がWebRequest/WebResponseクラス(System.Net名前空間)しかなく、さらに、非同期メソッドしか使えない
    • Taskクラス(System.Threading.Tasks名前空間)がない
  • 暗号化処理(System.Security.Cryptography名前空間以下のクラス)がない
  • LINQ to XML(System.Xml.Linq名前空間以下のクラス)がない
  • ObservableCollectionクラス(System.Collections.ObjectModel名前空間)がない

 それぞれの問題点について詳しく説明しよう。

非同期処理

 マイクロソフトとしては今後、時間のかかる処理に対して、同期版のAPIを提供するつもりはないようだ。SilverlightやWP7ではネットワーク関係のAPIが全て非同期版のみになっているし、Metroにいたってはファイルの読み書きすらも非同期版のみである。

 当然、Portable Class Libraryでも、WebRequestクラスなどのメソッドが非同期版のみになっている。

Taskクラス

 問題は、Portable Class LibraryにTaskクラスが含まれていないことである。

 Metroで、ファイルの読み書きなども含めて、時間がかかる、ありとあらゆる処理に対して非同期APIしか提供しないという決断をできたのは、TaskクラスとC# 5.0の非同期メソッドの存在が大きいだろう。Taskクラスなしで非同期処理を書くのは骨が折れる。

 結局、今回のPortableプロジェクト側では、List 1に示すようなAPM(Asynchronous Programming Model)型のメソッド(=Begin/Endメソッドのペア)として非同期APIを作った。

public IAsyncResult BeginGetTweets(AsyncCallback callback, string timeLineUrl)
{
  // ……中略……
}
 
public IEnumerable<Status> EndGetTweeets(IAsyncResult asyncResult)
{
  // ……中略……
}
List 1 : APM型の非同期APIの例

 そして、リンク・プロジェクト側で、Taskクラスを返すTAP(Task-Based Asynchronous Pattern)型の拡張メソッドを用意した。

public static Task<IEnumerable<Status>> GetTweets(this TwitterClient client, string timeLineUrl)
{
  return Task.Factory.FromAsync(
    (c, o) => client.BeginGetTweets(c, timeLineUrl),
    ar => client.EndGetTweeets(ar),
    null);
}
List 2: APM型の非同期APIをTAP型にラッピングする例

暗号化処理

 Twitterでは、OAuthプロトコルによる認証/認可を行うが、その処理の中で、HMACSHA1クラス(System.Security.Cryptography名前空間)などを使ったハッシュ値生成が必要になる。

 このクラスはWPF/Silverlight/WP7のいずれにも含まれているが、Portable Class Libraryには含まれていない。そのため今回は、ハッシュ値生成関数をデリゲートで外部から渡すようにした。

 今回は、参考にしたReactiveOAuthのコードからあまり大きく書き換えたくなかったので、多少強引ではあるが、List 3からList 4のような変更を行った。

private string GenerateSignature(…)
{
        using (var hmacsha1 = new HMACSHA1(Encoding.UTF8.GetBytes(hmacKeyBase)))
  {
    // ……中略……
    var hash = hmacsha1.ComputeHash(Encoding.UTF8.GetBytes(signatureBase));
    return Convert.ToBase64String(hash).UrlEncode();
  }
}
List 3 : 書き換え前(ReactiveOAuthを参考)のハッシュ値生成部分

public HashFunction ComputeHash;

private string GenerateSignature(…)
{
  // ……中略……
  var hash = ComputeHash(Encoding.UTF8.GetBytes(hmacKeyBase), Encoding.UTF8.GetBytes(signatureBase));
  return Convert.ToBase64String(hash).UrlEncode();
  }
}
List 4 : 書き換え後のハッシュ値生成部分

LINQ to XML

 Twitter APIの結果はJSON形式もしくはXML形式で返ってくる。

 LINQ to XMLもPortable Class Libraryに入っていないため、今回はXmlSerializerクラス(System.Xml.Serialization名前空間)を使ってXML形式のデータを読み込んだ。

 ただ、LINQ to XMLは、次のアップデートでPortable Class Libraryに入るようだ。

 また、現在発表されている情報によれば、Metro版の.NET Frameworkでは、むしろXmlSerializerクラスがなく、LINQ to XMLへの移行を推奨する流れになっている。

ObservableCollectionクラス

 MVVMパターンでコレクションを使う際に必要になるのがObservableCollectionクラス(=要素の追加・削除など、内容の変化を外部に伝える機能付きのコレクション)だ。

 ObservableCollectionクラス自体は、WPF/Silverlight/WP7/Metroのいずれでも利用できるが、WPFでだけ、クラスが定義されているアセンブリが異なっている(WPFではSystem.dll、そのほかの場合はSystem.Windows.dll)。そのため、ObservableCollectionクラスは、WPF以外をターゲットにした場合にしか使えない(Portable Class Libraryでは、この場合に限ってSystem.Windws.dllを参照できる)。

 今回、ViewModelをPortable Class Libraryに入れず、リンク・プロジェクト側に入れることにした理由の1つとなっている。

リンク・プロジェクト

 Modelの大部分はPortableプロジェクトに入っているものの、不足分(非同期処理がらみの拡張メソッドなど)はリンク・プロジェクトに入っている。

 また、以下の理由から、ViewModelはリンク・プロジェクト内に作ることにした。

  • 非同期処理に関して、TaskクラスやRxがPortableではない
  • MVVM開発をサポートするライブラリがPortableではない

 ちなみに今回、このリンク・プロジェクト内のソース・コードは、Project Linkerを使って自動リンクすることにしている。

 場合によっては、部分クラス条件付きコンパイルを使って、一部分だけ個別コードにすることも可能だが、今回はそれもせず、100%同一のソース・コードを使っている*2(個別に処理が必要な部分は、個別プロジェクト側で定義している)。

*2 一般論としては、個別に処理が必要な部分は、そもそも債務が分かれていて別クラスに分離すべきである場合が多い。

非同期処理

 前述のとおり、非同期処理は、Portableプロジェクト側でAPM型API(=Begin/Endメソッドのペア)を用意して、リンク・プロジェクト側でTAP型API(=Taskクラスを返すメソッド)にラッピングしている。

 実は、各ターゲットでTaskクラスが使えるかどうかは、以下のような状況になっている。

  • WPF: 利用可能
  • Silverlight: 先日リリースされたばかりのSilverlight 5で追加された。機能的には.NET Framework 4のTaskクラス相当
  • WP7: 現状、正式リリース版の範囲ではTaskクラスを利用できない
  • Metro: .NET Framework 4.5と同時リリースの予定なので、そのTaskクラスを利用可能

 非同期処理APIをどう書くかは悩んだが、今回は結局、Async CTPを利用した。

 Async CTPは、C# 5.0で追加される予定の非同期メソッドを使えるようにするもので、.NET Framework 4.5相当のTaskクラスの入ったDLLが同梱されている。このDLLは、WPF/Silverlight/WP7用でそれぞれ別々に3つ用意されている。ライセンスは“as-is”(=無保証・自己責任ではあるが、自由に利用可能)となっている。

 ちなみに、今回のようにPortable Class Libraryを利用するのではなく、全てリンク・ベースの開発でよいのであれば、ReactiveOAuthをそのまま使うのが現状の最良の選択肢だと思われる。

MVVMライブラリ

 今回利用したMVVM Light Toolkitなど、多くのMVVMライブラリはWPF/Silverlight /WP7いずれにも対応している。ただし、Portable Class Libraryとして提供されているわけではなく、WPF/Silverlight /WP7版がそれぞれ別のアセンブリとして提供されている。

 実際、今回、WPF/Silverlight /WP7、それぞれがリンク・プロジェクトと個別プロジェクトあり、合計6つのプロジェクトがあるが、それぞれ別々に、NuGetパッケージ・マネージャを使ってMVVM Light Toolkitをインストールしている。

個別プロジェクト

 Viewに関しては、もちろん個別に書く必要がある。書き換えているのは以下の2点である。コピー&ペーストして、部分的に書き換えた箇所も多い。

  • レイアウトの変更(画面サイズなどの差に対応)
  • ナビゲーション(=子ウィンドウを表示するか、ページ遷移するかなど)の変更

 View以外に関して、設定の保存場所(ファイルの読み書きが必要)も変更が必要なため、個別プロジェクトに含めた。

 WPFではApplicationSettingsBaseクラス(System.Configuration名前空間)を、Silverlight/WP7ではIsolatedStorageSettingsクラス(System.IO.IsolatedStorage名前空間)を使っている。

 今回のサンプル・アプリでは、最初にWPFで作成したものを、SilverlightとWP7に移植した。最後に、この移植の際のポイントについて少し説明しておこう。

Silverlight

 ブラウザ内のHTTP通信には制限が多く、SilverlightからRESTサービスを参照するためには、Out-of-Browser実行にしたうえで、List 5に示すような設定コードを書いて、クライアント版HTTPに切り替える必要がある。

var creator = System.Net.Browser.WebRequestCreator.ClientHttp;
WebRequest.RegisterPrefix("http://", creator);
WebRequest.RegisterPrefix("https://", creator);
List 5 : クライアントHTTPの利用

 もう1点、書き換えが必要だったのは子ウィンドウの扱いで、List 6のようなコードからList 7のようなコードになった。簡単にいうと、Silverlightでは、子ウィンドウの表示でも非同期処理が必要になる。

var vm = new ViewModel.AuthorizeViewModel(ConsumerKey, ConsumerSecret);
var dlg = new Views.AuthorizeWindow { DataContext = vm };
if ((dlg.ShowDialog() ?? false) && vm.AccessToken != null)
{
  // [OK]ボタンを押した後の処理
}
List 6 : WPFでの子ウィンドウ(ダイアログ)表示

var vm = new ViewModel.AuthorizeViewModel(ConsumerKey, ConsumerSecret);
var dlg = new Views.AuthorizeWindow { DataContext = vm };
 
var tcs = new TaskCompletionSource<AccountInfo>();
 
dlg.Closed += (sender, arg) =>
{
  if ((dlg.DialogResult ?? false) && vm.AccessToken != null)
  {
    // [OK]ボタンを押した後の処理
  }
};
dlg.Show();
 
return tcs.Task;
List 7 : Silverlightでの子ウィンドウ表示
ウィンドウを閉じた後の継続処理を書くために、Taskクラスを利用している。

WP7

 今回作ったTwitterクライアントでは、[ホーム][返信][自分の発言]用の3つのリストボックスを作っている。WPF版やSilverlight版では単純に横に3つ並べたが、WP7版では画面サイズが小さく、並べることができないため、パノラマ・コントロールを使った。

 そのほか、WP7向けのSilverlightはバージョンが古く(ブラウザ版でいうと3と4の中間相当の機能)、データ・バインディングがらみの機能にも不足がある。ただ、多くの部分は、MVVMライブラリを使うことで補えるので、ぜひともライブラリを活用してほしい。end of article

 

 INDEX
  特集:XAMLファミリ共通開発のすゝめ(前編)
  WINDOWS 8時代のGUI開発を考える
    1.“XAMLファミリ”/GUIフレームワークの分岐
    2.一貫性のある開発スタイル/XAMLファミリ間の差
    3.WinRT
 
  特集:XAMLファミリ共通開発のすゝめ(後編)
  MVVMパターンを使ったクロス・ターゲット開発
    1.ソース・コード共有の方法/サンプル・アプリ
  2.Portableプロジェクト/リンク・プロジェクト/個別プロジェクト


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)
- PR -

注目のテーマ

業務アプリInsider 記事ランキング

本日 月間
ソリューションFLASH