WindowsアプリとPhoneアプリに共通の体験を提供するために必須の機能ともいえる、データや設定のローミングの実装方法を解説する。
powered by Insider.NET
ユニバーサルWindowsアプリの特徴の1つに、データや設定のローミングがある。例えば、Windows Phoneのアプリで設定を変えると、それがWindowsストアアプリにも自動的に反映するといったものだ。これはどのようにして実現しているのだろうか? ユニバーサルプロジェクトを使ってユニバーサルWindowsアプリを開発しているのなら、じつはとても簡単なのだ。本稿ではその方法を解説する。なお、本稿のサンプル(次の画像)は「Windows Store app samples:MetroTips #77」からダウンロードできる。
ユニバーサルプロジェクトを使ってユニバーサルWindowsアプリを開発するには、以下の開発環境が必要である。本稿では、無償のVisual Studio Express 2013 for Windowsを使っている。
*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」はデスクトップで動作するアプリ用)。
本稿では、紛らわしくない限り次の略称を用いる。
Visual Studio 2013 Update 2のRTMがリリースされたが、残念なことに本稿執筆時点ではVB用のユニバーサルプロジェクトのテンプレートがまだ含まれていない*5。そのため、本稿で紹介するVBのサンプルコードはユニバーサルプロジェクトではない*6。WindowsとPhoneを横断するローミングのためには、ユニバーサルWindowsアプリであることは必須であるが、必ずしもユニバーサルプロジェクトで作らなくてもよい(ただし手間は掛かる)。説明の中で「共有プロジェクトに作成する」などと書いてあるが、VBにおいては「ユニバーサルプロジェクトがサポートされたときにはそのようにできる」と読み替えてほしい。
*5 VB用のユニバーサルプロジェクトも近い将来に提供されるものと思われる。例えば、Windowsストアアプリ用のVBプロジェクトのCommonフォルダーに自動生成される「NavigationHelper.vb」ファイルには、Phoneの[戻る]ボタン(ハードウェアボタン)からの割り込みを処理するためのコードがUpdate 2ですでに追加されている。これはユニバーサルプロジェクトのために必要になるコードであり、ユニバーサルプロジェクトを提供する予定がないのなら不要なものだ。
*6 プロジェクト間のファイルリンクを使えば、VBでもユニバーサルプロジェクトに似たソリューション構成にできる。別途公開のサンプルコードでは、VBでもWindows用/Phone用/共通コードの3プロジェクトに分けて書いてみたので、ご興味のある方はご覧いただきたい。ただし、このような形にするにはかなりの手間が掛かった(説明するには本連載の1回分では足りないほどだ)。ユニバーサルプロジェクトテンプレートの形にこだわらず、素直に作った方がよさそうである。なお、ユニバーサルプロジェクトで作らなくてもユニバーサルWindowsアプリはリリースできるので、お間違えなきよう(「WinRT/Metro TIPS:ユニバーサルプロジェクトで開発するには?」参照)。
仕組みとしては、複数端末にインストールされたWindowsストアアプリ間のローミングと変わらない。アプリがローミングフォルダーに保存したデータを、OSが自動的にOneDriveへアップロードする。アップロードされるOneDriveの領域(エンドユーザーからは見えない)は、アプリのAppID(および、エンドユーザーのMicrosoftアカウント)に結び付けて管理されている。
ユニバーサルWindowsアプリとは、WindowsとPhoneのアプリのAppIDを結び付けて管理する仕掛けだ(「WinRT/Metro TIPS:ユニバーサルプロジェクトで開発するには?[ユニバーサルWindowsアプリ開発]」参照)。ローミングに使用するOneDriveの領域はAppIDに結び付いているのだから、ユニバーサルWindowsアプリであればWindowsとPhoneのアプリは同じ領域を使うことになる。すなわち、ユニバーサルWindowsアプリにおいては、WindowsとPhoneの区別なくローミングされる(次の図も参照)。
これまでWindowsストアアプリで行っていたのと同様な実装を、ユニバーサルプロジェクトの共有プロジェクトで行えばよい。
ローミングの実装は、WindowsとPhoneで個別に行っても構わない。とはいうものの、ちょっとしたミス(例えば定数の書き間違えなど)からローミングに失敗してしまう事態を避けるために、共有プロジェクト(あるいは共通で利用するクラスライブラリ)に置くことをお勧めする。
アプリの設定をローミングするには、ApplicationDataクラス(Windows.Storage名前空間)のRoamingSettingsプロパティを使って設定データを読み書きすればよい。また、他の端末からのローミングデータが届いたときには、ApplicationDataクラスのDataChangedイベントが発火されるので、そのイベントハンドラーで設定データを読み込み直す。
以上を踏まえて、ローミングされる設定に画面の背景色を保存するクラスを書いてみると、次のコードのようになる。少々長いコードだが、UIから使うのはCurrentDispatcher/BackgroundSetting/ColorNamesの3つのプロパティだけだ。
public class RoamingOptionSettings : System.ComponentModel.INotifyPropertyChanged
// INotifyPropertyChangedインターフェースの実装は、データバインド先にプロパティの変化を伝えるため
{
// ローミングされる設定を読み書きするためのオブジェクト
private Windows.Storage.ApplicationDataContainer _settings
= Windows.Storage.ApplicationData.Current.RoamingSettings;
// ローミングされてきたときの処理をUIスレッドで行うためのCoreDispatcher
public Windows.UI.Core.CoreDispatcher CurrentDispatcher { get; set; }
public RoamingOptionSettings()
{
#if DEBUG
// VS 2013のXAMLエディター内でインスタンス化されたときは何もしない
if (Windows.ApplicationModel.DesignMode.DesignModeEnabled) return;
#endif
// 設定データがローミングされてきたときのイベントハンドラーをセットする
Windows.Storage.ApplicationData.Current.DataChanged += RoamingSettings_DataChanged;
}
// 設定データがローミングされてきたときのイベントハンドラー
async void RoamingSettings_DataChanged(Windows.Storage.ApplicationData sender, object args)
{
if (CurrentDispatcher == null) // コーディングミスにより未設定の場合は、例外を出す
throw new InvalidOperationException(
"RoamingOptionSettingsのCurrentDispatcherプロパティを事前に設定してください。");
// プロパティに変更があったことをイベントでバインド先に通知する
// 注意:このメソッドは別スレッドで呼び出されるため、
// UIに影響を及ぼす操作はCoreDispatcherを使ってこのように書く必要がある
await CurrentDispatcher.RunAsync(
Windows.UI.Core.CoreDispatcherPriority.Normal,
() =>
{
OnPropertyChanged("BackgroundSetting");
});
}
// UIにバインドするプロパティ(2つ)
// ローミング設定に保存されるプロパティ(背景色)
private const string BackgroundDefault = "Blue"; // 既定の色
public string BackgroundSetting
{
get { return GetValue<string>(BackgroundDefault); }
set { SetValue<string>(value); }
}
// ユーザーに提示する色名のリスト
private IReadOnlyList<string> _colorNames = new string[] { "DarkRed", "Green", BackgroundDefault, };
public IReadOnlyList<string> ColorNames { get { return _colorNames; } }
// 以下は、RoamingSettingsを読み書きし、また、バインド先へのイベントを発生させるための汎用メソッド
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
private T GetValue<T>(T defaultValue,
[System.Runtime.CompilerServices.CallerMemberName] string key = "")
{
if (!_settings.Values.ContainsKey(key))
return defaultValue;
object value = _settings.Values[key];
return (value == null) ? default(T) : (T)value;
}
private void SetValue<T>(T newValue,
[System.Runtime.CompilerServices.CallerMemberName] string key = "")
{
_settings.Values[key] = newValue;
OnPropertyChanged(key);
}
private void OnPropertyChanged(
[System.Runtime.CompilerServices.CallerMemberName] string propertyName = null)
{
var eventHandler = this.PropertyChanged;
if (eventHandler != null)
{
eventHandler(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
}
}
Public Class RoamingOptionSettings
Implements System.ComponentModel.INotifyPropertyChanged
' INotifyPropertyChangedインターフェースの実装は、データバインド先にプロパティの変化を伝えるため
' ローミングされる設定を読み書きするためのオブジェクト
Private _settings As Windows.Storage.ApplicationDataContainer _
= Windows.Storage.ApplicationData.Current.RoamingSettings
' ローミングされてきたときの処理をUIスレッドで行うためのCoreDispatcher
Public Property CurrentDispatcher As Windows.UI.Core.CoreDispatcher
Public Sub New()
#If DEBUG Then
' VS 2013のXAMLエディター内でインスタンス化されたときは何もしない
If (Windows.ApplicationModel.DesignMode.DesignModeEnabled) Then Return
#End If
' 設定データがローミングされてきたときのイベントハンドラーをセットする
AddHandler Windows.Storage.ApplicationData.Current.DataChanged, _
AddressOf RoamingSettings_DataChanged
End Sub
' 設定データがローミングされてきたときのイベントハンドラー
Private Async Sub RoamingSettings_DataChanged(sender As ApplicationData, args As Object)
If (CurrentDispatcher Is Nothing) Then ' コーディングミスにより未設定の場合は、例外を出す
Throw New InvalidOperationException(
"RoamingOptionSettingsのCurrentDispatcherプロパティを事前に設定してください。")
End If
' プロパティに変更があったことをイベントでバインド先に通知する
' 注意:このメソッドは別スレッドで呼び出されるため、
' UIに影響を及ぼす操作はCoreDispatcherを使ってこのように書く必要がある
Await CurrentDispatcher.RunAsync(
Windows.UI.Core.CoreDispatcherPriority.Normal,
Sub()
OnPropertyChanged("BackgroundSetting")
End Sub
)
End Sub
' UIにバインドするプロパティ(2つ)
' ローミング設定に保存されるプロパティ(背景色)
Private Const BackgroundDefault As String = "Blue" ' 既定の色
Public Property BackgroundSetting As String
Get
Return GetValue(Of String)(BackgroundDefault)
End Get
Set(value As String)
SetValue(Of String)(value)
End Set
End Property
' ユーザーに提示する色名のリスト
Private _colorNames As IReadOnlyList(Of String) _
= New String() {"DarkRed", "Green", BackgroundDefault}
Public ReadOnly Property ColorNames As IReadOnlyList(Of String)
Get
Return _colorNames
End Get
End Property
' 以下は、RoamingSettingsを読み書きし、また、バインド先へのイベントを発生させるための汎用メソッド
Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) _
Implements INotifyPropertyChanged.PropertyChanged
Private Function GetValue(Of T)(defaultValue As T, _
<System.Runtime.CompilerServices.CallerMemberName> Optional key As String = "") As T
If (Not _settings.Values.ContainsKey(key)) Then
Return defaultValue
End If
Dim value As Object = _settings.Values(key)
If (value Is Nothing) Then
Return Nothing
Else
Return CType(value, T)
End If
End Function
Private Sub SetValue(Of T)(newValue As T, _
<System.Runtime.CompilerServices.CallerMemberName> Optional key As String = "")
_settings.Values(key) = newValue
OnPropertyChanged(key)
End Sub
Private Sub OnPropertyChanged(
<System.Runtime.CompilerServices.CallerMemberName> Optional propertyName As String = Nothing)
RaiseEvent PropertyChanged(Me, New System.ComponentModel.PropertyChangedEventArgs(propertyName))
End Sub
End Class
次に、画面にリストボックスを配置し、上記のRoamingOptionSettingsクラスのオブジェクトとバインドする。
RoamingOptionSettingsオブジェクトを「App.xaml」ファイルのリソースに置き、それを画面のデータコンテキストに設定する。画面の背景色はRoamingOptionSettingsオブジェクトのBackgroundSettingプロパティに、また、リストボックスの内容はColorNamesプロパティに、それぞれ一方向バインディングする。リストボックスの選択された項目は、BackgroundSettingプロパティに双方向バインディングする(以下の2つのコード)。
<Application.Resources>
<local:RoamingOptionSettings x:Key="RoamingOptionSettings" />
</Application.Resources>
<Page
……省略……
DataContext="{Binding Source={StaticResource RoamingOptionSettings}}"
>
<!-- 最初のグリッドの背景色にバインド -->
<Grid ……省略……
Background="{Binding BackgroundSetting}"
>
……省略……
<!-- リストボックスを追加し、選択肢と選択項目にバインド -->
<ListBox ItemsSource="{Binding ColorNames}"
SelectedItem="{Binding BackgroundSetting, Mode=TwoWay}"
FontSize="30" Width="400" HorizontalAlignment="Left"
/>
……省略……
最後に、UIスレッドのCoreDispatcherオブジェクトを取得してRoamingOptionSettingsオブジェクトにセットするコードを、「App.xaml.cs/.vb」ファイルに追加する(次のコード)。
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
……省略……
if (rootFrame == null)
{
// UIスレッドのDispatcherをRoamingOptionSettingsに渡しておく必要がある
(this.Resources["RoamingOptionSettings"] as RoamingOptionSettings).CurrentDispatcher
= Windows.UI.Core.CoreWindow.GetForCurrentThread().Dispatcher;
rootFrame = new Frame();
……省略……
Protected Overrides Sub OnLaunched(e As LaunchActivatedEventArgs)
……省略……
If rootFrame Is Nothing Then
' UIスレッドのDispatcherをRoamingOptionSettingsに渡しておく必要がある
DirectCast(Me.Resources("RoamingOptionSettings"), RoamingOptionSettings).CurrentDispatcher _
= Windows.UI.Core.CoreWindow.GetForCurrentThread().Dispatcher
rootFrame = New Frame()
……省略……
以上、コードの紹介が長くなったが、従来のWindowsストアアプリで実装していたコードと全く変わるところはない。
後は、ユニバーサルWindowsアプリとしてストアとの関連付けを行えば*7、WindowsとPhone間で設定がローミングされるようになる。しかし、ストアとの関連付けを行う前にテストしたいこともあるだろう。そういう場合は、マニフェストで同じパッケージ名を設定すればよい(次の画像)。
*7 ユニバーサルWindowsアプリとしてストアとの関連付けを行うには、WindowsとPhoneで同じアプリ名を付ければよい。「WinRT/Metro TIPS:ユニバーサルプロジェクトで開発するには?」を参照してほしい。
なお、Phoneのエミュレーターは、初期状態ではローミングしないので気を付けよう。Windowsと同じMicrosoftアカウントを登録しておく必要がある(次の画像)。また、ローミングは、数分で反映されることもあるし、30分以上かかることもある。さらに、ストアで公開していないアプリでのローミングでは、その頻度の上限値が低く抑えられているらしく、頻繁にローミングされなくなる*8。テストには十分な時間を見込んでほしい(できれば別のテスト環境を用意できるとよい)。
*8 MSDNの「アプリのデータのローミングのガイドライン」には、「リソースの不適切な使用を防止するために、システムにはさまざまな保護メカニズムが備わっています。アプリ データが想定どおりに移行(筆者注:原文は「transition」(場所を移すことも含む)=ローミング)されない場合は、デバイスが一時的に制限されていることが考えられます。通常、この状況はしばらくすると自動的に解決されるため、操作は必要ありません」とある。制限される条件は公表されていないようだ。筆者の経験からは、ストアで公開していないアプリでは制限値が低く設定されているように思う。
ストアとOneDriveのサポートのおかげで、ユニバーサルプロジェクトでの設定やデータのローミングはとても簡単に実装できる。従来のWindowsストアアプリでローミングするコードがそのまま使えるのだ。なお、ストアとの関連付けを行う前にローミング機能をテストをするには、マニフェストで同じパッケージ名を設定すればよい。
ユニバーサルWindowsアプリでのローミングについては、次のドキュメントも参照してほしい。
Copyright© Digital Advantage Corp. All Rights Reserved.