WindowsとPhoneでロジックを切り分けるには?[ユニバーサルWindowsアプリ開発]:WinRT/Metro TIPS
WindowsアプリとWindows Phoneアプリで一部の処理のみが異なるコードを共有するには、どのような方法があるか。その方法と使い分けの方針を取り上げる。
powered by Insider.NET
ユニバーサルプロジェクトを使ってユニバーサルWindowsアプリを開発するとき、WindowsストアアプリとWindows Phoneアプリに共通するロジックは共有プロジェクトや共通のクラスライブラリに置ける。では、完全に同じではなく、一部だけ異なっているコードはどうしたらよいだろうか? 本稿ではその方法を解説する。なお、本稿のサンプル(次の画像)は「Windows Store app samples:MetroTips #75」からダウンロードできる。
別途公開のサンプルコードを実行しているところ(VS2013)
左は、Visual Studio 2013 Update 2に付属のWindows 8.1シミュレーター。右は、USBケーブルで接続したWindows Phone 8.1の実機の画面を映している「Windows Phoneの画面出力アプリ」(英語名は「Project My Screen App for Windows Phone」)。 「Windowsで実行」/「Phoneで実行」と表示されているテキストブロックは「App」クラスに定義した同じ文字列リソースにデータバインドされており、それを共有プロジェクトに置いたコードで書き換えている。 「共通のイベントハンドラー」と表示されているボタンは、XAMLコードによるボタンの定義はそれぞれのプロジェクトに書かれているが、イベントハンドラーのメソッドは共有プロジェクトに置かれている。このボタンをタップするとメッセージダイアログが出てきて、WindowsとPhoneのどちらで実行しているかを表示する。その文字列を生成するコードは、共有プロジェクトとPCL(=ポータブルクラスライブラリ)に置かれている。 いずれも、共通部分に置いたコードでWindowsとPhoneに応じて処理を切り分けているのである。
事前準備
ユニバーサルプロジェクトを使ってユニバーサルWindowsアプリを開発するには、以下の開発環境が必要である。本稿では、無償のVisual Studio Express 2013 for Windowsを使っている。
- SLAT対応のPC*1
- 2014年4月のアップデート*2適用済みの64bit版Windows 8.1 Pro版以上
- Visual Studio 2013 Update 2*3適用済みのVisual Studio 2013(以降、VS 2013)*4
*1 SLAT対応ハードウェアは、Windows Phone 8.1エミュレーターの実行に必要だ。ただし未対応でも、ソースコードのビルドは可能だ。SLAT対応のチェック方法はMSDNブログの「Windows Phone SDK 8.0 ダウンロードポイント と Second Level Address Translation (SLAT) 対応PCかどうかを判定する方法」を参照。なお、SLAT対応ハードウェアであっても、VM上ではエミュレーターが動作しないことがあるのでご注意願いたい。
*2 事前には「Windows 8.1 Update 1」と呼ばれていたアップデート。スタート画面の右上に検索ボタンが(環境によっては電源ボタンも)表示されるようになるので、適用済みかどうかは簡単に見分けられる。ちなみに公式呼称は「the Windows RT 8.1, Windows 8.1, and Windows Server 2012 R2 update that is dated April, 2014」というようである。
*3 マイクロソフトのダウンロードページから誰でも入手できる。
*4 本稿に掲載した手順を試すだけなら、無償のExpressエディションで構わない。Visual Studio Express 2013 Update 2 for Windows(製品版)はマイクロソフトのページから無償で入手できる。Expressエディションはターゲットプラットフォームごとに製品が分かれていて紛らわしいが、Windowsストアアプリの開発には「for Windows」を使う(「for Windows Desktop」はデスクトップで動作するアプリ用)。
用語
本稿では、紛らわしくない限り次の略称を用いる。
- Windows:Windows 8.1とWindows RT 8.1(2014年4月のアップデートを適用済みのもの)
- Phone:Windows Phone 8.1
サンプルコードについて
Visual Studio 2013 Update 2のRTMがリリースされたが、残念なことに本稿執筆時点ではVB用のユニバーサルプロジェクトのテンプレートがまだ含まれていない*5。そのため、本稿で紹介するサンプルコードはC#だけとさせていただきたい*6。
*5 VB用のユニバーサルプロジェクトも近い将来に提供されるものと思われる。例えば、Windowsストアアプリ用のVBプロジェクトのCommonフォルダーに自動生成される「NavigationHelper.vb」ファイルには、Phoneの[戻る]ボタン(ハードウェアボタン)からの割り込みを処理するためのコードがUpdte 2ですでに追加されている。これはユニバーサルプロジェクトのために必要になるコードであり、ユニバーサルプロジェクトを提供する予定がないのなら不要なものだ。
*6 プロジェクト間のファイルリンクを使えば、VBでもユニバーサルプロジェクトに似たソリューション構成にできる。別途公開のサンプルコードでは、VBでもWindows用/Phone用/共通コードの3プロジェクトに分けて書いてみたので、ご興味のある方はご覧いただきたい。ただし、このような形にするにはかなりの手間が掛かった(説明するには本連載の1回分では足りないほどだ)。ユニバーサルプロジェクトテンプレートの形にこだわらず、素直に作った方がよさそうである。なお、ユニバーサルプロジェクトで作らなくてもユニバーサルWindowsアプリはリリースできるので、お間違えなきよう(「WinRT/Metro TIPS:ユニバーサルプロジェクトで開発するには?」参照)。
数行程度のコードを切り分けるには?
「#if」ディレクティブを使えばよい。Windows用とPhone用のプロジェクトに別々の条件付きコンパイルシンボルが定義されているので、それを利用する。
共有プロジェクトのソースコードは、ビルド時にWindows用プロジェクトまたはPhone用プロジェクトとマージされる。従って、Windows用/Phone用プロジェクトに定義されている条件付きコンパイルシンボルが、それぞれのビルド時に適用されるのだ。C#のユニバーサルプロジェクトでは、次のシンボルが定義されている。
- Windows用プロジェクト:WINDOWS_APP
- Phone用プロジェクト:WINDOWS_PHONE_APP
例えば、共有プロジェクトの「App.xaml」ファイルに定義した文字列リソース「Message」があって、これにWindowsとPhoneで異なる文字列を設定したいとする。それを、共有プロジェクトに置いたクラス「SharedClass」で行うには、「#if」ディレクティブを利用して次のコードのように書ける。
<Application
x:Class="MetroTips075CS.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MetroTips075CS">
<Application.Resources>
<x:String x:Key="Message">(message)</x:String>
</Application.Resources>
</Application>
class SharedClass
{
public static void SetMessage()
{
#if WINDOWS_APP
App.Current.Resources["Message"] = "Windows で実行";
#endif
#if WINDOWS_PHONE_APP
App.Current.Resources["Message"] = "Phone で実行";
#endif
}
}
ここで、途中にある「#endif」〜「#if WINDOWS_PHONE_APP」の2行は、今のところは「#else」の1行にしてもよい(将来、ユニバーサルプロジェクトの範囲がXboxなどまで広がったときには注意)。
この「SetMessage」メソッドは、「App」クラスで画面を表示する前に呼び出すようにする。
そして、「Message」リソースを画面のテキストブロックにバインドしておけば、WindowsとPhoneで異なる文字列が表示される(冒頭の画像を参照)。
なお、「App.xaml」ファイルに定義するリソースの一部をWindowsとPhoneで切り分けるだけなら、ResourceDictionaryクラス(Windows.UI.Xaml名前空間)のMergedDictionariesプロパティを使って、WindowsとPhoneのそれぞれのプロジェクトに置いたリソースファイルをマージするという方法もある(MergedDictionariesの記述例は「WinRT/Metro TIPS:游ゴシック/游明朝を正しく使うには?[Windows 8.1ストア・アプリ開発]」の「generic.xamlの指定を上書きするには?」の項を参照)。
数行以上のコードを切り分けるには?
数行以上のコードはメソッドに切り出してから、パーシャルクラスにするとよい。
「#if」ディレクティブではコードの見通しが悪いと感じたときは、パーシャルクラスに分けるべきである。Windows/Phone共に似た処理があるのならば、双方のプロジェクトにパーシャルクラスを作る。片側だけにある処理は、そちらの方のプロジェクトだけにパーシャルクラスを作り、呼び出す部分を「#if」ディレクティブで切り分ける。
例として、Windowsだけに存在する処理を実装してみよう。プライバシーポリシーのWebページへのリンクを設定チャームに追加する処理だ*7。この処理は、「App」クラスのパーシャルクラスとして「App.xaml.SettingsCharm.cs」というファイルをWindows側のプロジェクトに作って記述する(次のコード)。
……省略(usingと名前空間宣言)……
public sealed partial class App
{
// 設定コントラクトに[プライバシーポリシー]のリンクを表示する
// 次のURLは、アプリに応じて必ず変更すること
private const string URI_PrivacyPolicy = "http://www.example.com/privacypolicy";
private const string SID_PrivacyPolicy = "privacyPolicy";
private const string SLB_PrivacyPolicy = "プライバシー ポリシー";
private void SettingsLoad()
{
SettingsPane.GetForCurrentView().CommandsRequested += onCommandsRequested;
}
// 設定チャームに、カスタムメニューを追加
void onCommandsRequested(SettingsPane settingsPane, SettingsPaneCommandsRequestedEventArgs eventArgs)
{
UICommandInvokedHandler handler = new UICommandInvokedHandler(onSettingsCommand);
SettingsCommand generalCommand = new SettingsCommand(SID_PrivacyPolicy, SLB_PrivacyPolicy, handler);
eventArgs.Request.ApplicationCommands.Add(generalCommand);
}
// 設定チャームからメニューが呼ばれた後の処理
async void onSettingsCommand(IUICommand command)
{
SettingsCommand settingsCommand = (SettingsCommand)command;
switch (settingsCommand.Id.ToString())
{
case SID_PrivacyPolicy:
// プライバシーポリシーのページを表示
await Launcher.LaunchUriAsync(new Uri(URI_PrivacyPolicy));
break;
default:
throw new NotImplementedException();
}
}
}
……省略……
これは「App」クラスのパーシャルクラスである。設定チャームにリンクを追加する処理だ。
この処理はWindowsに固有のものであり、また、このコードはPhone用としてビルドするとコンパイルエラーになる。そこで、このようにパーシャルクラスとして分離してWindows用のプロジェクトに置く。
共有プロジェクトに置いた「App.xaml.cs」ファイルでは「#if」ディレクティブを使って、Windows用としてビルドされるときだけ上の「SettingsLoad」メソッドを呼び出すようにする(次のコード)。
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
……省略……
Frame rootFrame = Window.Current.Content as Frame;
if (rootFrame == null)
{
……省略……
// フレームを現在のウィンドウに配置します
Window.Current.Content = rootFrame;
// Windowsのときだけ設定チャームにリンクを追加する
#if WINDOWS_APP
SettingsLoad();
#endif
}
……省略……
}
これを実行してみると、次の画像のようにWindowsの設定チャームに[プライバシー ポリシー]というリンクが表示される。
設定チャームに表示されたプライバシーポリシーへのリンク
上記のコードにより、設定チャームに[プライバシー ポリシー]というリンクが表示される(赤枠内)。 このアプリは「連載:Windowsストア・アプリ開発入門」で制作し、Windowsストアで公開しているもの。ユニバーサルプロジェクトは使っていないが、リンクを表示するコードはほぼ同じである。 なお、Windowsストアからインストールすると、この画像のように[プライバシー ポリシー]のリンクが現在は2つ表示される。上がコードによって表示されたもの。下は、2014年5月のWindows Updateによって自動的に表示されるようになったものだ*7。
*7 プライバシーポリシーへのリンクを設定チャームに追加する処理は、どうやら近い将来には不要になりそうである。今月(=2014年5月)のWindows Updateでストアがアップデートされたが(KB2956575)、その変更の1つとして、設定チャームの一番下にプライバシーポリシーへのリンクを自動的に表示する機能が追加されたのだ(新機能の項の下から2番目、「A supplemental privacy shortcut」で始まる項目)。そのため現在は、コードから追加したリンクと、システムが自動的に表示するリンクと、2つのプライバシーポリシーへのリンクが表示される状態になっている(上の画像)。しかし、審査基準がまだ変更されていないようで、リンクを追加するコードを書かずに申請すると現在は不合格になるのでご注意願いたい(筆者を含め数人以上が試して全て不合格になっている)。審査基準の変更時にはマイクロソフトからアナウンスがあると思われる。
クラスライブラリではどうする?
Windowsランタイムコンポーネント(以降、WinMD)とポータブルクラスライブラリ(以降、PCL)は、WindowsとPhoneのプロジェクトから共通に利用するように作成できる。ところが、WinMDとPCLはあらかじめビルドしておくため、ユニバーサルプロジェクトのように「#if」ディレクティブを使ってコードを切り分けることができない。どうしたらよいのだろうか?
真っ当な回答としては、「そのような手段は提供されていない」となる。実行中に、Windowsで動いているのか、Phoneで動いているのかを判別するAPIが提供されていれば条件分岐できるのだが、現時点ではそのようなAPIが見当たらないのだ。
しかし、一定の条件の下で利用するのであれば、できないことはない。例として、アセンブリ名を利用してみよう。
アセンブリ名(=実行ファイルの名前)は、既定ではプロジェクト名になっている。そして、ユニバーサルプロジェクトのWindowsプロジェクトの名前は末尾が「.Windows」となっており、Phoneプロジェクトでは「.WindowsPhone」となっている。この部分を変更しないならば、プラットフォームの判別に利用できる(次のコード)。
public static string GetPlatform()
{
// 注:ファイル冒頭に「using System.Reflection;」が必要
string exeFileName
= Windows.UI.Xaml.Application.Current.GetType().GetTypeInfo().Module.Name;
bool isPhone = exeFileName.ToUpperInvariant().Contains(".WINDOWSPHONE.EXE");
return isPhone ? "Phone" : "Windows";
}
あくまでも、判別のためのAPIが提供されていないことに代わる「間に合わせ」のコードである。ユニバーサルプロジェクトのWindowsとPhoneのアセンブリ名を既定から変更していない場合に、このコードは正しく動作する。
このコードはModuleプロパティを見ているので、このメソッドが置かれているPCLやWinMDのモジュール名を読み取っているように思えるかもしれない。しかしそうではなく、「Application.Current」(=Appクラス)が定義されているモジュールを読み取っているのである。
また、これ以外にもさまざまな方法が考えられるだろう。例えば、筆者の「クラウディアさんタイマーVer.2」では、Windows.ApplicationModel.Store.CurrentApp.LinkUriプロパティ(=アプリが掲載されているストアのWebページのURI)で判別している(ストアからインストールしたときのみ有効)。
まとめ
共有プロジェクトでロジックを切り分けるには、「#if」ディレクティブやパーシャルクラスを利用する。また、PCLやWinMDでは安心して使える手段はないものの、一定の条件の下でなら切り分けが可能だ。
Windows 8.1を扱う大規模カンファレンスのご紹介
5月29日(木)〜5月30日(金)、マイクロソフトの最新技術情報(例えば本稿で解説したような内容)を日本語で日本人向けに提供するカンファレンス「de:code」が日本マイクロソフト主催で開催される。このカンファレンスは、米国時間で4月2〜4日に開催された「Build 2014」の内容をベースに、さらに日本向けのプラスアルファを含めたものになる。詳しい内容は(セッション内容は開催日までに決定していくとのこと)、リンク先を参照されたい。
Copyright© Digital Advantage Corp. All Rights Reserved.