ユニバーサルアプリの中断時にその状態を復元するためのセッションデータを保存し、アクティブ化時にこれを復元する方法を解説する。
powered by Insider.NET
Windowsランタイムアプリ*1では、中断時にデータを保存すべしという。では、実際にはどのようにすればよいのだろうか? 以前にも説明したことはあるが、本稿ではあらためてユニバーサルプロジェクトの場合について解説する。なお、本稿のサンプルは「Windows Store app samples:MetroTips #97」からダウンロードできる。
*1 このMSDNのWebページは、2014年12月下旬に改訂された。以前は「ストアアプリ」と記述されていた部分が「Windowsランタイムアプリ」に変更されている。
ユニバーサルプロジェクトを使ってユニバーサルWindowsアプリを開発するには、以下の開発環境が必要である。本稿では、無償のVisual Studio Community 2013 with Update 4を使っている。
*2 SLAT対応ハードウェアは、Windows Phone 8.1エミュレーターの実行に必要だ。ただし未対応でも、ソースコードのビルドと実機でのデバッグは可能だ。SLAT対応のチェック方法はMSDNブログの「Windows Phone SDK 8.0 ダウンロードポイント と Second Level Address Translation (SLAT) 対応PCかどうかを判定する方法」を参照。なお、SLAT対応ハードウェアであっても、VM上ではエミュレーターが動作しないことがあるのでご注意願いたい。
*3 事前には「Windows 8.1 Update 1」と呼ばれていたアップデート。スタート画面の右上に検索ボタンが(環境によっては電源ボタンも)表示されるようになるので、適用済みかどうかは簡単に見分けられる。ちなみに公式呼称は「the Windows RT 8.1, Windows 8.1, and Windows Server 2012 R2 update that is dated April, 2014」というようである。
*4 Windows Phone 8.1エミュレーターを使用しないのであれば、32bit版のWindows 8.1でもよい。
*5 マイクロソフトのダウンロードページから誰でも入手できる(このURLはUpdate 4のもの)。
*6 本稿に掲載したコードを試すだけなら、無償のExpressエディションやCommunityエディションで構わない。Visual Studio Express 2013 with Update 4 for Windows(製品版)はマイクロソフトのページから無償で入手できる。Expressエディションはターゲットプラットフォームごとに製品が分かれていて紛らわしいが、Windowsストアアプリの開発には「for Windows」を使う(「for Windows Desktop」はデスクトップで動作するアプリ用)。また、2014年11月12日(米国時間)に新しくリリースされたVisual Studio Community 2013 with Update 4(製品版)もマイクロソフトのページから無償で入手できる。Communityエディションは本稿執筆時点では英語版だけなので、同じ場所にあるVisual Studio 2013 Language Packの日本語版を追加インストールし、オプションダイアログで言語を切り替える必要がある。
本稿では、紛らわしくない限り次の略称を用いる。
Visual Studio 2013 Update 2(Update 3/4も)では、残念なことにVB用のユニバーサルプロジェクトのテンプレートは含まれていない*7。そのため、本稿で紹介するコードはC#のユニバーサルプロジェクトだけとさせていただく。
*7 VB用のユニバーサルプロジェクトは、2015年にリリースされるといわれているVisual Studio 2015(開発コード「Visual Studio 14」)からの提供となるようだ。「Visual Studio UserVoice」(英語)のリクエストに対する、2014年6月18日付けの「Visual Studio team (Product Team, Microsoft)」からの回答による。
Windowsランタイムアプリのライフサイクルは、次の図のように説明される。
上の図で、アプリが正常に終了した後で次に起動(=アクティブ化)されるときには、中断したときの状態を復元するとよい(過去の認定要件の3.6項では義務付けられていた。2014年1月23日付の改訂で廃止)。中断状態からアプリが終了させられるときには、アプリには何もできない。従って、中断したときの状態を復元するためのデータは、それ以前に保存しておかねばならない。一般的には、中断されるときに保存する。本稿では、中断されるときにデータを保存する方法と、次に起動されるときにデータを復元する方法を説明する。
ところで、アプリが保存すべきデータには2種類ある。強制終了時に失ってもよいデータと、強制終了時であっても失っては困るデータだ。上の図で、「終了させる」操作をしたときや、例外を処理できずに異常終了したとき、あるいは長時間無応答などでシステムから強制的に終了させられたときなどに、失っても構わないデータかどうかで判断する。強制終了されたアプリが次に起動されるときには状態を初期化して起動すればよい(すなわち、中断時の状態を復元するためのデータを使わずに起動すればよい)のだから、中断時の状態を復元するためのデータは失っても構わない。しかし、ユーザーが保存したと考えているデータは失ってはいけないだろう。本稿では、前者を「セッションデータ」、後者を「ユーザーデータ」と呼ぶことにする(次の表)。以下ではこのうち「セッションデータ」の保存と復元を行う方法を見ていく。
データ区分 | 意味 | 喪失 | 保存タイミング |
---|---|---|---|
セッションデータ | アプリが中断された時の状態を復元するためのデータ | 強制終了時には失われてもよい | 中断時 |
ユーザーデータ | それ以外(ユーザーが保存したと考えているデータ) | どんなときでも失われるのは望ましくない | データが生成/変更される都度 |
中断時にセッションデータを保存するのに適した場所は二つある。ローカルフォルダーとローミングフォルダーだ*8。そして、ローカルフォルダーに保存する場合には、VS 2013のプロジェクトテンプレートで提供されているSuspensionManagerクラスを利用できる。SuspensionManagerクラスを使えば実装が簡単になるのだ。本稿では、その方法を説明する。その他の方法も併せて、次の表にまとめておく。
保存先 | セッションの範囲 | 実装手段 |
---|---|---|
ローカルフォルダー | デバイスごと(別のデバイスでは中断時の状態を復元しない) | SuspensionManagerクラスを利用 または 独自に実装 |
ローミングフォルダー | アプリごと(別のデバイスでも中断時の状態を復元する) | 独自に実装 |
*8 アプリケーションデータ記憶域の種類は「WinRT/Metro TIPS:アプリケーション・データ記憶域のテキスト・ファイルを読み書きするには?[Win 8/WP 8]」を参照。その中で一時フォルダーは、次に起動されるまでの間にファイルが消されてしまう可能性があるので、セッションデータの保存先としてはふさわしくない。なお、ネットワークに保存するのは、中断時に許された処理時間(=5秒)では保存に失敗する可能性が大きいためお勧めできない。
本稿では、VS 2013の[ハブ アプリケーション (ユニバーサル アプリ)]プロジェクトテンプレートから始めよう。このプロジェクトテンプレートには、SuspensionManagerクラスが含まれている。新しくプロジェクトを作るときにこのテンプレートを選ぶと、生成されたプロジェクトのCommonフォルダーにSuspensionManagerクラスがある(次の画像)。
なお、プロジェクトを作るときに[空のアプリケーション (ユニバーサル アプリ)]を選んだ場合にはCommonフォルダーが作成されない。その後、Windows用のプロジェクト(プロジェクト名の末尾が「.Windows」)かPhone用のプロジェクト(プロジェクト名の末尾が「.WindowsPhone」)に画面を追加するときに、[空白のページ]を除く[基本ページ]などを選ぶと、Commonフォルダーが作成される(画面を追加したプロジェクトに作成されるが、そのCommonフォルダーを丸ごと共有プロジェクトにカット&ペーストする)。ただしその場合には、AppクラスにSuspensionManagerクラスを使うためのコードが入っていないので、[ハブ アプリケーション (ユニバーサル アプリ)]プロジェクトテンプレートで作られた「App.xaml.cs」ファイルを参考にしてコードを修正する必要がある。
プロジェクトが作成できたら、次の画像とコードで示すようなUIを作っておいてほしい。後のコードで、上のテキストボックスに入力した文字列をセッションデータとして保存/復元する。下のテキストボックスには、セッションデータとして保存しておいた中断時刻を表示する。
<StackPanel DataContext="{Binding DataContext, ElementName=pageRoot}">
<TextBox Header="TextBox1" Width="200" HorizontalAlignment="Left"
Text="{Binding TextBox1Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<StackPanel Orientation="Horizontal" Margin="0,40,0,0">
<TextBox Header="前回中断時刻" IsReadOnly="True" Width="150" Background="Transparent"
BorderBrush="LightGray" Text="{Binding LastSuspnededTime}"
HorizontalAlignment="Left" />
<AppBarButton Icon="Refresh" Click="RefreshButton_Click" IsCompact="True" />
</StackPanel>
</StackPanel>
最後に、テキストボックスの初期値をセットするコードを追加しておく(次のコード)。
private async void NavigationHelper_LoadState(object sender, LoadStateEventArgs e)
{
……省略(自動生成されたコード)……
// DefaultViewModelの初期値をセット
this.DefaultViewModel["TextBox1Text"] = string.Empty; this.DefaultViewModel["LastSuspnededTime"] = string.Empty;
}
画面のコードビハインドに保存/復元するコードを記述すればよい。NavigationHelper_SaveStateメソッドで保存し、NavigationHelper_LoadStateメソッドで復元する。以下、処理の流れを追いながら、追加すべきコードを説明していく。
中断イベントを受信
プロジェクトテンプレートで自動生成されたAppクラスのコンストラクターでは、そのSuspendingイベントハンドラーとしてOnSuspendingメソッドが登録されている(次のコード)。これにより、システムから「中断状態に入れ」というイベント(=Suspendingイベント)が送られてくると、OnSuspendingメソッドが実行される。
public App()
{
this.InitializeComponent();
this.Suspending += this.OnSuspending;
}
OnSuspendingメソッド
自動生成されたOnSuspendingメソッドは、次のコードのようになっている。その中で呼び出しているSuspensionManagerクラスのSaveAsyncメソッドが、各画面のNavigationHelper_SaveStateメソッドを呼び出した後にセッションデータをファイルに保存してくれるのだ。
private async void OnSuspending(object sender, SuspendingEventArgs e)
{
var deferral = e.SuspendingOperation.GetDeferral();
// テンプレートで生成されたコードのSaveAsyncメソッドは、各画面のNavigationHelper_SaveStateメソッドを呼び出し、
// それからローカルフォルダーの「_sessionState.xml」ファイルにセッションデータを書き出してくれる
await SuspensionManager.SaveAsync();
// テンプレートに記述されたGetDeferral〜deferral.Completeは、
// 非同期処理の完了を待つようにシステムへ指示する
deferral.Complete();
}
なお、OnSuspendingメソッドに許された時間は現在のところ5秒間である。処理に使える残り時間は、次のコードのようにして取得できる。この時間を超えると、処理中であっても強制的に中断されるので気を付けてほしい。
private async void OnSuspending(object sender, SuspendingEventArgs e)
{
var deferral = e.SuspendingOperation.GetDeferral();
// 許された残り時間は次のコードで確認できる
var remainTime = e.SuspendingOperation.Deadline.Subtract(DateTimeOffset.Now);
// → remainTime.TotalSeconds=4.9970003秒(Windows)/4.9800024秒(Phone)
await SuspensionManager.SaveAsync();
deferral.Complete();
}
NavigationHelper_SaveStateメソッド
各画面のNavigationHelper_SaveStateメソッドで、画面ごとのセッションデータを保存する(正確には、ここではSuspensionManagerクラスにデータを渡すだけであり、実際にファイルへ保存されるのはNavigationHelper_SaveStateメソッドを抜けてからである)。
ここでは、テキストボックスにエンドユーザーが入力した文字列を、「TextBox1Text」というキー名で保存してみよう(次のコード)。
private void NavigationHelper_SaveState(object sender, SaveStateEventArgs e)
{
e.PageState["TextBox1Text"] = this.DefaultViewModel["TextBox1Text"] as string;
}
なお、自動生成されたWindows用のコードビハインドにはNavigationHelper_SaveStateメソッドが記述されていない。上のコードを追加するとともに、次のコードのようにコンストラクターでイベントハンドラーを登録する必要がある。
public HubPage()
{
this.InitializeComponent();
this.navigationHelper = new NavigationHelper(this);
this.navigationHelper.LoadState += this.NavigationHelper_LoadState;
this.navigationHelper.SaveState += this.NavigationHelper_SaveState;
}
セッションデータが保存される
以上で、テキストボックスに入力した文字列がセッションデータとして中断時に保存されるようになったはずである(復元はまだ)。デバッグ実行して、文字列をキーインし、中断からシャットダウンしてみてほしい(次の画像)。
中断からシャットダウンした後でアプリを起動
中断からシャットダウンした後でアプリを起動すると、AppクラスのOnLaunchedメソッドの中でSuspensionManagerクラスのRestoreAsyncメソッドが呼び出される(次のコード)。この部分は、プロジェクトテンプレートで自動生成されたコードだ。
SuspensionManagerクラスのRestoreAsyncメソッドは、画面ごとにセッションデータを復元し、画面のNavigationHelper_LoadStateメソッドを呼び出してくれる(その他に、画面の遷移履歴も復元する)。
protected async override void OnLaunched(LaunchActivatedEventArgs e)
{
……省略……
if (rootFrame == null)
{
rootFrame = new Frame();
……省略……
if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
{
// 必要な場合のみ、保存されたセッション状態を復元します
try
{
await SuspensionManager.RestoreAsync();
}
catch (SuspensionManagerException)
{
}
}
Window.Current.Content = rootFrame;
}
if (rootFrame.Content == null)
{
// ナビゲーションの履歴スタックが復元されていない場合、最初のページに移動します。
……省略……
}
Window.Current.Activate();
}
NavigationHelper_LoadStateメソッド
画面のNavigationHelper_LoadStateメソッドの第2引数には、復元したセッションデータが入っている(復元したセッションデータがある場合)。従って、次のコードのようにして引数からセッションデータを取り出して画面にセットすればよい。
private async void NavigationHelper_LoadState(object sender, LoadStateEventArgs e)
{
……省略(自動生成されたコード)……
// DefaultViewModelの初期値をセット
this.DefaultViewModel["TextBox1Text"] = string.Empty;
this.DefaultViewModel["LastSuspnededTime"] = string.Empty;
// 画面ごとのセッションデータは、次のようにして取り出す
if (e.PageState != null)
{
if (e.PageState.Keys.Contains("TextBox1Text"))
{
this.DefaultViewModel["TextBox1Text"] = e.PageState["TextBox1Text"] as string;
}
}
}
以上で、セッションデータの復元も完了だ。実行して、テキストボックスに何か文字列を入力した状態で中断からシャットダウンをし、再び起動したときに以前の文字列が表示されることを確認してほしい。
処理の流れは複雑だったが、実際に記述したのはNavigationHelper_SaveStateメソッドでデータをセットするコードと、NavigationHelper_LoadStateメソッドでデータを取り出すコードの2カ所だけであった(WindowsではSaveStateイベントハンドラーを登録するコードも記述した)。[ハブ アプリケーション (ユニバーサル アプリ)]プロジェクトテンプレートを使うと、セッションデータの保存/復元が簡単に記述できるようになっているのである。
なお、NavigationHelper_SaveStateメソッドは、画面遷移するときにも呼び出される。そのときはPageStateオブジェクトにセッションデータが保持されるだけで、ファイルには保存されない(画面遷移した先で中断されたときには、ファイルに保存される)。
画面によらず、アプリとして保存/復元したいセッションデータは、どこで処理すればよいだろうか? 保存は、自動生成されたAppクラスのOnSuspendingメソッドで行えばよい。復元は、AppクラスのOnLaunchedメソッド内でSuspensionManagerクラスのRestoreAsyncメソッドを呼び出した後なら、どこで行ってもよい。
しかし、OnSuspendingメソッドでセッションデータを保存しようにも、画面でやったときのようにPageStateオブジェクトにアクセスできない。どこに保存すればよいだろうか? 画面のセッションデータが保存される「_sessionState.xml」ファイルへ一緒に保存するには、SuspensionManagerクラスのSessionStateプロパティを利用する(独自のファイルに保存してもよいが、その場合はSuspensionManagerクラスのRestoreAsyncメソッドで復元してもらえない)。
それでは実際のコード例を紹介しよう。ここでは、中断されたときの時刻をセッションデータとして保存してみる(次のコード)。注意点は、保存するキー名として「AppFrame」は使えないことと、SuspensionManagerクラスのSaveAsyncメソッドを呼び出す前に行わなければならないことだ。
private async void OnSuspending(object sender, SuspendingEventArgs e)
{
var deferral = e.SuspendingOperation.GetDeferral();
// SuspensionManager.SessionStateのキー"AppFrame"にはページごとのセッションデータが入っている
// アプリ全体のセッションデータは、それとは別のキーで保存する
SuspensionManager.SessionState["SuspendTime"] = DateTimeOffset.Now.ToString("HH:mm:ss");
await SuspensionManager.SaveAsync();
deferral.Complete();
}
このセッションデータは、SuspensionManagerクラスのRestoreAsyncメソッドを呼び出すと復元されるので、その後ならどこからでも取り出せる。例えば、画面のNavigationHelper_LoadStateメソッドで次のコードのように記述できる。
private async void NavigationHelper_LoadState(object sender, LoadStateEventArgs e)
{
……省略(自動生成されたコード)……
// DefaultViewModelの初期値をセット
this.DefaultViewModel["TextBox1Text"] = string.Empty;
this.DefaultViewModel["LastSuspnededTime"] = string.Empty;
// 画面ごとのセッションデータは、次のようにして取り出す
if (e.PageState != null)
{
if (e.PageState.Keys.Contains("TextBox1Text"))
{
this.DefaultViewModel["TextBox1Text"] = e.PageState["TextBox1Text"] as string;
}
}
// アプリ全体のデータは、SuspensionManager.SessionStateから直接取り出す
if (SuspensionManager.SessionState.Keys.Contains("SuspendTime"))
this.DefaultViewModel["LastSuspnededTime"]
= SuspensionManager.SessionState["SuspendTime"] as string;
}
あるいは、ボタンのクリックイベントで取り出してもよい(次のコード)。
private void RefreshButton_Click(object sender, RoutedEventArgs e)
{
if (SuspensionManager.SessionState.Keys.Contains("SuspendTime"))
this.DefaultViewModel["LastSuspnededTime"]
= SuspensionManager.SessionState["SuspendTime"] as string;
}
セッションデータを保存/復元する処理は、結構複雑だ。しかし、VS 2013の[ハブ アプリケーション (ユニバーサル アプリ)]プロジェクトテンプレートにはその複雑な部分が組み込まれているので、画面ごとのセッションデータの保存/復元は、NavigationHelper_SaveStateメソッドでデータをセットするコードと、NavigationHelper_LoadStateメソッドでデータを取り出すコードを記述するだけで済む。また、画面によらないセッションデータの保存は、自動生成されたAppクラスのOnSuspendingメソッドの中で行える。
Copyright© Digital Advantage Corp. All Rights Reserved.