Windowsストア・アプリとWindows Phone 8アプリの両方に共通するコードを一括管理して再利用しやすくするには、どうすればよいのか? 2通りの方法を解説。
powered by Insider.NET
「Metroスタイル・アプリ」と以前は呼ばれていたアプリ(=Windows 8/Windows RT(以降、Win 8)向けのWindowsストア・アプリとWindows Phone 8(以降、WP 8)向けのアプリの両方)の開発をしていると、Win 8とWP 8で同じようなコードを書いていることに気付くだろう。両方に共通するコードをコピー&ペースト以外で再利用する方法はないだろうか? そこで本稿では、コードを共有する2通りの方法(ソース・コード・レベルとバイナリ・レベル)を解説する。本稿のサンプルは「Windows Store app samples:MetroTips #19」からダウンロードできる。
●事前準備
Win 8向けのWindowsストア・アプリを開発するには、Windows 8とVisual Studio 2012(以降、VS 2012)が必要である。これらを準備するには、第1回のTIPSを参考にしてほしい。
また、WP 8向けのアプリを開発するには、SLAT対応PC上の64bit版Windows 8 Pro以上とWindows Phone SDK 8.0が必要となる。
そして、本稿の後半で説明するPCL(Portable Class Library)を作成するには、有償のVS 2012が必要である(PCLのバイナリはExpressでも利用可)。お持ちでない読者も、試用版が提供されているので、ぜひ試してみてほしい。
●ソース・コード・レベルで共有するには?
ソース・コードのリンク機能を使う。つまり、プロジェクト外のソース・コードを、リンクとしてプロジェクトに取り込むのだ。この場合、ソース・コードの実体は1つのままである。
まず、共有したいソース・コードを置いておくためのプロジェクトを作る。これはWP 8のクラス・ライブラリにしておくとよいだろう。Win 8よりWP 8の方が使えるAPIが少ないので、こうしておけば他方で使えないコードを書いてしまうリスクが減る。例として、Launcherクラス(Windows.System名前空間)のLaunchUriAsyncメソッドを使ったコードを次に挙げておく。
public class CommonLib
{
public static async Task<bool> LaunchUrl(string url)
{
var uri = new Uri(url);
return await Launcher.LaunchUriAsync(uri);
}
}
Public Class CommonLib
Public Shared Async Function LaunchUrl(url As String) As Task(Of Boolean)
Dim uri = New Uri(url)
Return Await Launcher.LaunchUriAsync(uri)
End Function
End Class
Win 8用にもWP 8用にもコンパイルできるコードの例(上:C#、下:VB)
上のソース・コードを、Win 8やWP 8のプロジェクトにリンクとして追加する方法を説明する。
[ソリューション エクスプローラー]でプロジェクト項目の右クリック・メニューから[既存項目の追加]を選ぶと表示される[既存項目の追加]ダイアログで、上のソース・コード・ファイルを指定する。そこで[追加]ボタンではなく、その右端に付いている[▼]マークをクリックするとメニューが現れるので、[リンクとして追加]を選ぶ(次の画像)。
これでソース・コードがプロジェクトに取り込まれた。リンクしたソース・コードがプロジェクト内で呼び出せるようになっていることを確かめてみてほしい。
なお、Win 8とWP 8の微妙な差異を吸収するには、「#ifディレクティブ」を利用するとよい。例えばWP 8のプロジェクトに「WINDOWS_PHONE」という名前のコンパイル定数を定義しておく(C#ではデフォルトで設定されている)。次のコードは、Win 8とWP 8で異なる名前空間を参照する例である。
#if WINDOWS_PHONE
using System.ServiceModel.Syndication;
#else
using Windows.Web.Syndication;
#endif
#If WINDOWS_PHONE Then
Imports System.ServiceModel.Syndication
#Else
Imports Windows.Web.Syndication
#End If
Win 8とWP 8の微妙な差は、#Ifディレクティブで吸収する(上:C#、下:VB)
●バイナリ・レベルで共有するには?
ポータブル・クラス・ライブラリ(Portable Class Library、PCL、汎用性のあるクラス・ライブラリ*1)を作成する。PCLは、Win 8アプリからでもWP 8アプリからでも利用できるDLLだ。実行時にはそれぞれのランタイムで動作するので、注意してほしい。次の図のように、Win 8アプリからPCLを呼び出すとWin 8のランタイムで動作し、WP 8アプリのときはWP 8のランタイムで動作する。
*1 マイクロソフトのドキュメントで翻訳が揺らいでいる。最近のものでは「汎用性のあるクラス ライブラリ」が多いが、多少古い文書やVS 2012のダイアログなどでは「ポータブル クラス ライブラリ」の訳が使われている。
このことがなぜ重要かというと、ランタイムの違いによって動作が変わることがあるからだ。例えばEncodingクラス(System.Text名前空間)のGetEncodingメソッドは、PCL中に書いてコンパイルできる。例えば「GetEncoding("Shift-JIS")」というコードが書ける。しかしこれは、Win 8で実行するとうまくいくが、WP 8で実行すると実行時例外が発生してしまう。PCLの開発は、ユニット・テスト(=単体テスト)の楽なWin 8ランタイム側で行うことが多いだろうが、WP 8側でのテストも抜かりなく実施してほしい。
さて、前置きが長くなったが、PCLを作るのは簡単だ。VS 2012(有償版に限る)でプロジェクトを作るときに、Windowsの「ポータブル クラス ライブラリ」を選ぶだけだ。ただし、[OK]ボタンを押してもすぐにプロジェクトは作成されず、次の画像のダイアログが出てくる。
このダイアログで、共通に使えるプロジェクトの種類を設定する。ターゲット・フレームワークを広く指定すると、PCL内で利用できるAPIが減る。より多くのAPIを使うには、範囲を絞るとよい。この画像の例では、Win 8とWP 8だけで使えるPCLが出来上がる*2 *3。
*2 VB(Visual Basic)でPCLのプロジェクトを作った場合は、そのままではビルドできない(2013年1月現在)。次のブログ記事に従って、プロジェクト・ファイルを修正する必要がある。
*3 このように適用範囲を絞ったPCLのプロジェクトを、WP 8のプロジェクトから参照するとXAMLデザイナで例外が発生することがある(2013年1月現在)。詳細は「リリース ノート - Windows Phone SDK 8.0」の「Visual Studio Designer」の項を参照してほしい。対策としては、UIデザインにBlend for Visual Studio 2012(=Expressも含むVS 2012に同梱されている)を使うのがよさそうだ。
プロジェクトができてしまえば、あとは普通のクラス・ライブラリの作り方と同じだ。ただし、利用できるAPIはかなり制限される。中には、共通で使えるはずなのにPCLでは使えないものさえある。前述したLauncherクラスがそれに当たる。
例として、RSSフィードを読み込むコードを挙げておこう(次のコード)。
public class CommonDownloader
{
private const string RssUrl
= "http://rss.rssad.jp/rss/itmatmarkit/fdotnet/rss.xml";
public static async Task<string> GetRssAsync()
{
var req = HttpWebRequest.Create(new Uri(RssUrl));
using (var res = await req.GetResponseAsync()) //←注
using (var stream = res.GetResponseStream())
using (var rdr = new StreamReader(stream, UTF8Encoding.UTF8))
{
return await rdr.ReadToEndAsync();
}
}
}
Public Class CommonDownloader
Private Const RssUrl As String _
= "http://rss.rssad.jp/rss/itmatmarkit/fdotnet/rss.xml"
Public Shared Async Function GetRssAsync() As Task(Of String)
Dim req = HttpWebRequest.Create(New Uri(RssUrl))
Using res = Await req.GetResponseAsync() '←注
Using stream = res.GetResponseStream()
Using rdr = New StreamReader(stream, UTF8Encoding.UTF8)
Return Await rdr.ReadToEndAsync()
End Using
End Using
End Using
End Function
End Class
PCLのコード例(上:C#、下:VB)
注:NuGetでBcl.Asyncを導入しておくこと。
このコードは、RSSフィードがUTF-8なので文字列として読み取るところまでPCLに書くことができた。前述したようにGetEncodingメソッドはWP 8で動作しないので、UTF-8以外のエンコーディングの場合はバイト配列として読み取り、呼び出した側で文字列にエンコードすることになる(あるいは、独自の文字コード変換ロジックをPCL内に実装する)。
●実行結果
以上のコードを利用する画面を実装して動かしてみたのが次の画像だ。
実行している様子(上:Win 8、下:WP 8)
それぞれの画面で、上の[ブラウザを開く]ボタンは共有したソース・コードによりブラウザを起動、下の[RSS を取得する]ボタンはPCLを利用してRSSフィードをダウンロードする。
なお、画面のXAMLコードやボタンのクリック・イベントのコードなどにも共通する部分は多いのだが、残念ながらそれらを共有するのは困難である*4。
*4 MVVMのMやVMをPCL化する話が、海外ではよく出てくる(末尾の参考ドキュメントに掲載)。しかし、PCLからプラットフォーム依存のコードを呼び出せないので、例えば前述の「GetEncoding("Shift-JIS")」を含む処理がMやVMで必要になった時点で、PCL化のもくろみは挫折してしまう。この点は将来に期待しよう。
●まとめ
Win 8とWP 8でコードを共有するには、ソース・コード・レベルでプロジェクトにリンクする方法と、バイナリ・レベルでPCLを作成する方法がある。現状は、例えばWebからダウンロードする部分は共通にできても、文字列に変換する部分は別々にしなければならないなど、かなり細かく切り分けることになる。
PCLについて詳しくは、次のドキュメントを参考にしてほしい。
Copyright© Digital Advantage Corp. All Rights Reserved.