イベントハンドラーを正しいタイミングで外すには?[ユニバーサルWindowsアプリ開発]WinRT/Metro TIPS

ストアアプリのライフサイクルイベントの扱いは難しい。本稿ではページ遷移の際にイベントハンドラーを外す適切なタイミングについて解説する。

» 2014年12月04日 16時49分 公開
[山本康彦BluewaterSoft/Microsoft MVP for Windows Platform Development]
WinRT/Metro TIPS
業務アプリInsider/Insider.NET

powered by Insider.NET

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

連載目次

 イベントハンドラーは不要になったら外せとは、よくいわれることだ。そうしないと実際に不具合が出ることもある。ところが、イベントハンドラーを外すようにすると、今度は予想外のタイミングで外れてしまうことがあるのだ。どのようにして外すとよいのだろうか? 本稿では、イベントハンドラーを外す際の問題点と、その解決法を説明する。なお、本稿のサンプルは「Windows Store app samples:MetroTips #95」からダウンロードできる。

事前準備

 ユニバーサルプロジェクトを使ってユニバーサルWindowsアプリを開発するには、以下の開発環境が必要である。本稿では、無償のVisual Studio Express 2013 for Windowsを使っている。

  • SLAT対応のPC*1
  • 2014年4月のアップデート*2適用済みの64bit版Windows 8.1 Pro版以上*3
  • Visual Studio 2013 Update 2(またはそれ以降)*4を適用済みのVisual Studio 2013(以降、VS 2013)*5

*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 Windows Phone 8.1エミュレーターを使用しないのであれば、32bit版のWindows 8.1でもよい。

*4 マイクロソフトのダウンロードページから誰でも入手できる(このURLはUpdate 4のもの)。

*5 本稿に掲載したコードを試すだけなら、無償のExpressエディションやCommunityエディションで構わない。Visual Studio Express 2013 with Update 4 for Windows(製品版)はマイクロソフトのページから無償で入手できる。Expressエディションはターゲットプラットフォームごとに製品が分かれていて紛らわしいが、Windowsストアアプリの開発には「for Windows」を使う(「for Windows Desktop」はデスクトップで動作するアプリ用)。また、この11月12日(米国時間)に新しくリリースされたVisual Studio Community 2013 with Update 4(製品版)もマイクロソフトのページから無償で入手できる。Communityエディションは本稿執筆時点では英語版だけなので、同じ場所にあるVisual Studio 2013 Language Packの日本語版を追加インストールする必要がある。


用語

 本稿では、紛らわしくない限り次の略称を用いる。

  • Windows:Windows 8.1とWindows RT 8.1(2014年4月のアップデートを適用済みのもの)
  • Phone:Windows Phone 8.1

サンプルコードについて

 Visual Studio 2013 Update 2(Update 3/4も)では、残念なことにVB用のユニバーサルプロジェクトのテンプレートは含まれていない*6。そのため、本稿で紹介するVBのコードはユニバーサルプロジェクトではなく、PCL(ポータブルクラスライブラリ)を使ったプロジェクトのものである*7

*6 VB用のユニバーサルプロジェクトは、来年にリリースされるといわれているVisual Studio 2015(開発コード「Visual Studio 14」)からの提供となるようだ。「Visual Studio UserVoice」(英語)のリクエストに対する、6月18日付けの「Visual Studio team (Product Team, Microsoft)」からの回答による。

*7 Visual Studio 2013 Update 2以降のVBでユニバーサルWindowsアプリを作る場合のお勧めは、「The Visual Basic Team」のブログ記事(英語)によれば、PCLを使う方法のようである。PCLに置いたものは、コードだけでなくXAML(画面)やリソースディクショナリなども共通に利用できる。そこで別途公開のサンプルコードでも、VBはWindows用/Phone用/共通コード(PCL)の3プロジェクト構成とした。ユニバーサルプロジェクトで作らなくてもユニバーサルWindowsアプリはリリースできるのである(「WinRT/Metro TIPS:ユニバーサルプロジェクトで開発するには?」参照)。


共有ソースのイベントハンドラーの例

 次のようなアプリを考えてみよう。起動時に表示されるメイン画面と、メイン画面から遷移する[Page-A」画面という、2つの画面を持っているアプリ。そして、[Page-A]画面では、エンドユーザーが選択した画像データを共有に送れるようにしたい(=[Page-A]画面に共有ソースのイベントハンドラーを実装する)。

 そのような場合には、[Page-A」画面が表示されるときにイベントハンドラーをセットするだろう(次のコード)。

// 共有にデータを渡すためのイベントハンドラー
// 冒頭に「using Windows.ApplicationModel.DataTransfer;」が必要
private TypedEventHandler<DataTransferManager, DataRequestedEventArgs> _shareToEventHandler;

public PageA()
{
  ……省略……

  // 共有にデータを渡すためのイベントハンドラーをメンバー変数に保持しておく
  _shareToEventHandler
    = new TypedEventHandler<DataTransferManager,
             DataRequestedEventArgs>(this.MyUserControl1.ShareToHandler);
}

private void navigationHelper_LoadState(object sender, LoadStateEventArgs e)
{
  // このページが最初に要求されたときに、イベントハンドラーを結び付ける
  DataTransferManager.GetForCurrentView().DataRequested += _shareToEventHandler;
}

' 共有にデータを渡すためのイベントハンドラー
' 冒頭に「Imports Windows.ApplicationModel.DataTransfer」が必要
Private _shareToEventHandler As TypedEventHandler(Of DataTransferManager, DataRequestedEventArgs)

Public Sub New()
  ……省略……

  ' 共有にデータを渡すためのイベントハンドラーをメンバー変数に保持しておく
  _shareToEventHandler _
    = New TypedEventHandler(Of DataTransferManager, DataRequestedEventArgs)(
            AddressOf Me.MyUserControl1.ShareToHandler)
End Sub

Private Sub NavigationHelper_LoadState(sender As Object, e As Common.LoadStateEventArgs)
  ' このページが最初に要求されたときに、イベントハンドラーを結び付ける
  AddHandler DataTransferManager.GetForCurrentView().DataRequested, _shareToEventHandler
End Sub

画面が表示されるときにイベントハンドラーをセットするコード例(上:C#、下:VB)
プロジェクトを作成するときに[ハブ アプリケーション]テンプレートを選び、[Page-A]画面として[基本ページ]テンプレートを追加して「PageA.xaml」と名前を付け、そこに太字の部分を追加した。
イベントハンドラーのメソッドの内容は本稿では説明しない。別途公開のサンプルコードのMyUserControlクラスを参照していただきたい。
コンストラクターでイベントハンドラーをメンバー変数「_shareToEventHandler」に保持しているのは、後からこのイベントハンドラーを外したいためである。
なお、画面のテンプレートとして[空白のページ]を選んだ場合はnavigationHelper_LoadStateメソッドが存在しない。そのときは、Pageクラス(Windows.UI.Xaml.Controls名前空間)のOnNavigatedToメソッドをオーバーライドして同じ内容を記述する。

 これで実行してみると、[Page-A]画面で共有ソースのイベントハンドラーが動作するのはよいのだが、その後でメイン画面に戻っても共有ソースが機能してしまう(次の画像)。

[Page-A]画面の共有に送る機能が、メイン画面に戻っても働いてしまう(Windows)
[Page-A]画面の共有に送る機能が、メイン画面に戻っても働いてしまう(Phone) [Page-A]画面の共有に送る機能が、メイン画面に戻っても働いてしまう(上:Windows、下:Phone)
画面が表示されるときにイベントハンドラーを結び付けただけだと、メイン画面に戻ってもそのイベントハンドラーは有効のままになる。そのため、メイン画面で[Page-A]画面の共有機能が働くというおかしな現象が起きる。
ここでWindowsでの共有の送り先に使っているのは、「WinRT/Metro TIPS:共有でファイルを受け取るには?[Windows 8/Windows 8.1ストアアプリ開発]」で作成したアプリだ。
なお、別途公開のサンプルコードでは、後述する対策が実装されているため、こうはならない。この画像のような動きになるのを確かめるには、OnNavigatingFromメソッドの内容を削除してほしい。

 これは一般的には望ましくない挙動であろう。画面表示時に結び付けたイベントハンドラーは、他の画面に遷移するときに外さねばならない。

OnNavigatedFrom(=SaveState)で外すのは問題がある

 他の画面へ遷移するときに行いたい処理は、通常はOnNavigatedFromメソッド(画面作成時に[基本ページ]テンプレートなどを使った場合はnavigationHelper_SaveStateメソッド)に記述するだろう。イベントハンドラーもここで外してみよう(次のコード)。

private void navigationHelper_SaveState(object sender, SaveStateEventArgs e)
{
  // 他の画面へ遷移するときに、イベントハンドラーを外すようにしてみる(よくない例)
  DataTransferManager.GetForCurrentView().DataRequested -= _shareToEventHandler; 
}

Private Sub NavigationHelper_SaveState(sender As Object, e As Common.SaveStateEventArgs)
  ' 他の画面へ遷移するときに、イベントハンドラーを外すようにしてみる(よくない例)
  RemoveHandler DataTransferManager.GetForCurrentView().DataRequested, _shareToEventHandler
End Sub

イベントハンドラーを外す、よくない例(上:C#、下:VB)
これはよくない例である。ここで外すと、中断時にデータ保存をしたときに問題が出る(後述)。
なお、画面のテンプレートとして[空白のページ]を選んだ場合はnavigationHelper_SaveStateメソッドが存在しない。そのときは、PageクラスのOnNavigatedFromメソッドをオーバーライドして同じ内容を記述する。

 これで実行してみると、[Page-A]画面からメイン画面に戻ったときには、共有の機能がきちんと停止する。

 ところが、中断時に画面遷移履歴などのデータを保存するようにしていると、問題が発生するのだ。[ハブ アプリケーション]テンプレートなどを使ってプロジェクトを作成すると、自動的にそのような実装がなされている*8

*8 ご興味のある方は、[ハブ アプリケーション]テンプレートを使ってプロジェクトを作成し、確認してみてほしい。Appクラスの中でSuspensionManagerクラスを使用している箇所がそれである。AppクラスのOnLaunchedメソッドの中に「SuspensionManager.RegisterFrame」(作成したフレームをSuspensionManagerクラスに登録)/「SuspensionManager.RestoreAsync」(画面遷移履歴を復元)の2箇所、OnSuspendingメソッドの中に「SuspensionManager.SaveAsync」(画面遷移履歴や画面ごとの中断時のデータを保存)の1カ所である。別途公開のサンプルコードにも実装してある。


 そのような中断時のデータ保存機能が実装されている場合は、[Page-A]画面を表示しているときに中断してすぐに再開するとイベントハンドラーが外れてしまい、共有機能が働かなくなってしまうのだ(次の画像)。

[Page-A]画面の共有に送る機能が、中断から再開したときに働かなくなってしまった(Windows)
[Page-A]画面の共有に送る機能が、中断から再開したときに働かなくなってしまった(Phone) [Page-A]画面の共有に送る機能が、中断から再開したときに働かなくなってしまった(上:Windows、下:Phone)
メイン画面に戻ったときに共有機能がきちんと停止するように、OnNavigatedFrom(=SaveState)メソッドでイベントハンドラーを外すようにした。さらに、中断時に画面の遷移履歴を保存するようにした。
すると、中断から再開したときに共有機能が働かなくなってしまう。中断時に「SuspensionManager」クラスからOnNavigatedFrom(=SaveState)メソッドが呼び出されたために、イベントハンドラーが外れてしまったのだ。
なお、別途公開のサンプルコードでは、後述する対策が実装されているため、こうはならない。この画像のような動きになるのを確かめるには、OnNavigatingFromメソッドの内容を削除してほしい。

 これはよろしくない。他のアプリに切り替えてまた戻ってきたときに機能が効かなくなってしまっては、まずいだろう。

正しいタイミングで外すには?

 中断からの再開時には、イベントハンドラーが外れてほしくない。それには、OnNavigatingFromメソッドで外せばよい(次のコード)。

protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
  base.OnNavigatingFrom(e);

  // ここで外すようにすれば、中断時には外れない!
  DataTransferManager.GetForCurrentView().DataRequested -= _shareToEventHandler;
}

Protected Overrides Sub OnNavigatingFrom(e As NavigatingCancelEventArgs)
  MyBase.OnNavigatingFrom(e)

  ' ここで外すようにすれば、中断時には外れない!
  RemoveHandler DataTransferManager.GetForCurrentView().DataRequested, _shareToEventHandler
End Sub

OnNavigatingFromでイベントハンドラーを外せば、問題は起きない(上:C#、下:VB)
[Page-A]画面のコードビハインドに、このメソッドを追加する。
「OnNavigatedFrom」ではなく、「OnNavigatingFrom」である。

 OnNavigatingFromメソッドは、「SuspensionManager」クラスからは呼び出されない。実際に画面遷移が起きる直前だけに呼び出されるのだ(「SuspensionManager」クラス以外のフレームワークを利用している場合は違うかもしれない)。

 これで、他の画面に遷移したときにはイベントハンドラーが外れるとともに、この画面で中断から再開したときにはイベントハンドラーは外れなくなる。

まとめ

 画面を表示するときにセットしたイベントハンドラーは、その画面のOnNavigatingFromメソッドで外す(OnNavigatedFromメソッドではない)。画面遷移や中断時の処理によってはそれ以外の場所で外しても構わない場合もあるのだが、「イベントハンドラーはOnNavigatingFromで外す」と覚えておけば問題ないだろう。

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

WinRT/Metro TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

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

メールマガジン登録

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