連載
» 2014年06月05日 17時45分 UPDATE

WinRT/Metro TIPS:WindowsとPhoneで設定をローミングするには?[ユニバーサルWindowsアプリ開発]

WindowsアプリとPhoneアプリに共通の体験を提供するために必須の機能ともいえる、データや設定のローミングの実装方法を解説する。

[山本康彦(http://www.bluewatersoft.jp/),BluewaterSoft]
WinRT/Metro TIPS
業務アプリInsider/Insider.NET

powered by Insider.NET

「WinRT/Metro TIPS」のインデックス

連載目次

 ユニバーサルWindowsアプリの特徴の1つに、データや設定のローミングがある。例えば、Windows Phoneのアプリで設定を変えると、それがWindowsストアアプリにも自動的に反映するといったものだ。これはどのようにして実現しているのだろうか? ユニバーサルプロジェクトを使ってユニバーサルWindowsアプリを開発しているのなら、じつはとても簡単なのだ。本稿ではその方法を解説する。なお、本稿のサンプル(次の画像)は「Windows Store app samples:MetroTips #77」からダウンロードできる。

別途公開のサンプルコードを実行しているところ(Visual Studio 2013)
別途公開のサンプルコードを実行しているところ(Visual Studio 2013) 別途公開のサンプルコードを実行しているところ(Visual Studio 2013)
それぞれの画像の左は、Visual Studio 2013 Update 2に付属のWindows 8.1シミュレーター。右は、同じく付属のWindows Phone 8.1エミュレーターである。 上の画像は、左のWindows 8.1シミュレーターで背景色を緑色に変更したところ。しばらく経つとその設定変更がローミングされ、右のWindows Phone 8.1エミュレーターの背景色が自動的に変化する(下の画像)。逆に、右のWindows Phone 8.1エミュレーターで背景色を変えれば、それが左のWindows 8.1シミュレーターに反映される。

事前準備

 ユニバーサルプロジェクトを使ってユニバーサル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。そのため、本稿で紹介する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とPhoneを横断するローミングの仕組み

 仕組みとしては、複数端末にインストールされたWindowsストアアプリ間のローミングと変わらない。アプリがローミングフォルダーに保存したデータを、OSが自動的にOneDriveへアップロードする。アップロードされるOneDriveの領域(エンドユーザーからは見えない)は、アプリのAppID(および、エンドユーザーのMicrosoftアカウント)に結び付けて管理されている。

 ユニバーサルWindowsアプリとは、WindowsとPhoneのアプリのAppIDを結び付けて管理する仕掛けだ(「WinRT/Metro TIPS:ユニバーサルプロジェクトで開発するには?[ユニバーサルWindowsアプリ開発]」参照)。ローミングに使用するOneDriveの領域はAppIDに結び付いているのだから、ユニバーサルWindowsアプリであればWindowsとPhoneのアプリは同じ領域を使うことになる。すなわち、ユニバーサルWindowsアプリにおいては、WindowsとPhoneの区別なくローミングされる(次の図も参照)。

OneDriveを介したローミングの動作 OneDriveを介したローミングの動作
Build 2014セッション2-522「Dealing with Data: Storage, Roaming, and Backup on Windows and Windows Phone」の資料p.15より。
左下のPhoneでは、アプリがローミング用のフォルダーに書き込んだデータが、OSによって自動的にOneDriveへアップロードされる。アップロードする先のOneDriveの領域は、AppIDに結び付いている(この図では「PFN 12345」という架空のAppIDになっている)。
右下のWindowsでは、OneDriveからWNS(Windows Push Notification Services、Windowsプッシュ通知サービス)経由の通知をOSが受け取って、ローミングデータをダウンロードし、アプリに対してイベントをトリガーする。そのイベントを受けたアプリは、ローミングデータを読み込み直す。

WindowsとPhoneで設定をローミングするには?

 これまでWindowsストアアプリで行っていたのと同様な実装を、ユニバーサルプロジェクトの共有プロジェクトで行えばよい。

 ローミングの実装は、WindowsとPhoneで個別に行っても構わない。とはいうものの、ちょっとしたミス(例えば定数の書き間違えなど)からローミングに失敗してしまう事態を避けるために、共有プロジェクト(あるいは共通で利用するクラスライブラリ)に置くことをお勧めする。

 アプリの設定をローミングするには、ApplicationDataクラス(Windows.Storage名前空間)のRoamingSettingsプロパティを使って設定データを読み書きすればよい。また、他の端末からのローミングデータが届いたときには、ApplicationDataクラスのDataChangedイベントが発火されるので、そのイベントハンドラーで設定データを読み込み直す。

 以上を踏まえて、ローミングされる設定に画面の背景色を保存するクラスを書いてみると、次のコードのようになる。少々長いコードだが、UIから使うのはCurrentDispatcherBackgroundSettingColorNamesの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

ローミングされる設定に背景色を保存するクラス(上:C#、下:VB)
このコードは「RoamingOptionSettings.cs/.vb」というファイル名で共有プロジェクトに配置する。
設定データがローミングされてきたときのイベントハンドラーは、UIスレッドとは異なるスレッドで呼び出される。そのため、CoreDispatcherオブジェクトを使ってUIスレッドで処理を行わねばならない。「Windowsストア・アプリ開発入門:第8回」(p.6)では「App.xaml.cs」ファイル内に一緒に記述したが、今回は別のクラスとしたために、CoreDispatcherオブジェクトを保持しておくプロパティが必要になった。
また、コード後半のRoamingSettingsを読み書きするための汎用的なメソッドについては、「WinRT/Metro TIPS:ユーザー操作によるオプション設定を即座にデータに反映するには?[Win 8]」(ローミングしないLocalSettingsプロパティを使っている)を参考にしてほしい。

 次に、画面にリストボックスを配置し、上記のRoamingOptionSettingsクラスのオブジェクトとバインドする。

 RoamingOptionSettingsオブジェクトを「App.xaml」ファイルのリソースに置き、それを画面のデータコンテキストに設定する。画面の背景色はRoamingOptionSettingsオブジェクトのBackgroundSettingプロパティに、また、リストボックスの内容はColorNamesプロパティに、それぞれ一方向バインディングする。リストボックスの選択された項目は、BackgroundSettingプロパティに双方向バインディングする(以下の2つのコード)。

<Application.Resources>
  <local:RoamingOptionSettings x:Key="RoamingOptionSettings" />
</Application.Resources>

共有プロジェクトの「App.xaml」ファイルにRoamingOptionSettingsオブジェクトを定義する(XAML)

<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"
              />

    ……省略……

Windows/Phoneプロジェクトの「MainPage.xaml」ファイルにリストボックスなどを追加する(XAML)
太字の部分を追加/変更する。

 最後に、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()
    ……省略……

共有プロジェクトの「App」クラスでCoreDispatcherオブジェクトを取得する(上:C#、下:VB)
太字の部分を追加する。

 以上、コードの紹介が長くなったが、従来のWindowsストアアプリで実装していたコードと全く変わるところはない。

ストアと関連付けせずにローミングをテストするには?

 後は、ユニバーサルWindowsアプリとしてストアとの関連付けを行えば*7、WindowsとPhone間で設定がローミングされるようになる。しかし、ストアとの関連付けを行う前にテストしたいこともあるだろう。そういう場合は、マニフェストで同じパッケージ名を設定すればよい(次の画像)。

マニフェストで同じパッケージ名を設定する(VS 2013) マニフェストで同じパッケージ名を設定する(VS 2013)
WindowsとPhoneで同じパッケージ名を設定すれば、ローミングのテストができる。 ソリューションエクスプローラーで「Package.appxmanifest」ファイルを開き、[パッケージ化]タブを選ぶと、[パッケージ名]を設定するテキストボックスがあるので、そこにWindowsとPhoneで同じ名前を設定する。なお、このパッケージ名は、ストアとの関連付けを行うと自動的に書き換えらえる(それ以降は変更してはいけない)。

*7 ユニバーサルWindowsアプリとしてストアとの関連付けを行うには、WindowsとPhoneで同じアプリ名を付ければよい。「WinRT/Metro TIPS:ユニバーサルプロジェクトで開発するには?」を参照してほしい。


 なお、Phoneのエミュレーターは、初期状態ではローミングしないので気を付けよう。Windowsと同じMicrosoftアカウントを登録しておく必要がある(次の画像)。また、ローミングは、数分で反映されることもあるし、30分以上かかることもある。さらに、ストアで公開していないアプリでのローミングでは、その頻度の上限値が低く抑えられているらしく、頻繁にローミングされなくなる*8。テストには十分な時間を見込んでほしい(できれば別のテスト環境を用意できるとよい)。

Phoneでアプリのローミングを有効にする(Phoneエミュレーター) Phoneでアプリのローミングを有効にする(Phoneエミュレーター)
  (1)スタート画面を左にスライドしてアプリ一覧を出し、その中から[設定]を開き、さらに[設定を同期](赤枠内)を選ぶ。
  (2)Microsoftアカウントが未登録の場合はこの画面になるので、[サインイン](赤枠内)を選択し、後は画面の指示に従ってアカウントを登録する(すでに登録済みの場合は、(1)から直接(4)に行く)。
  (3)アカウントの登録が完了すると[バックアップしますか?]という画面になる。これはどちらを選んでもよい。
  (4)[設定を同期]画面が表示される。[アプリの設定]がオンになっていることを確かめる(赤枠内)。 設定が終わったらWindowsキーをタップしてスタート画面に戻す。なお、その後でエミュレーター右の[ツール]メニュー(「≫」アイコン)を開いて、[チェックポイント]タブで[新しいチェックポイント]を作り、それを[既定]にしておけば、エミュレーターを起動するたびにアカウントの登録作業をしなくて済む。また、[設定]の[キーボード]で英語キーボードを追加しておくと、メールアドレスやURLの入力が楽になる。

*8 MSDNの「アプリのデータのローミングのガイドライン」には、「リソースの不適切な使用を防止するために、システムにはさまざまな保護メカニズムが備わっています。アプリ データが想定どおりに移行(筆者注:原文は「transition」(場所を移すことも含む)=ローミング)されない場合は、デバイスが一時的に制限されていることが考えられます。通常、この状況はしばらくすると自動的に解決されるため、操作は必要ありません」とある。制限される条件は公表されていないようだ。筆者の経験からは、ストアで公開していないアプリでは制限値が低く設定されているように思う。


まとめ

 ストアとOneDriveのサポートのおかげで、ユニバーサルプロジェクトでの設定やデータのローミングはとても簡単に実装できる。従来のWindowsストアアプリでローミングするコードがそのまま使えるのだ。なお、ストアとの関連付けを行う前にローミング機能をテストをするには、マニフェストで同じパッケージ名を設定すればよい。

 ユニバーサルWindowsアプリでのローミングについては、次のドキュメントも参照してほしい。

「WinRT/Metro TIPS」のインデックス

WinRT/Metro TIPS

Copyright© 1999-2017 Digital Advantage Corp. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。