プログラム関連 >  メモ >  Windows Phone 7 (開発者サイド)
最終更新日:2014/5/10

Windows Phone 7 (開発者サイド)

WinRT、ユニバーサルアプリの情報はこっちを参照してください。

WP7 ユーザーサイドの情報はこっちを参照してください。 バージョンについては、説明内に明記していない場合、基本、NoDo をターゲットにしていたはずですが、最近は Mango がターゲットになってるかもしれません。 それと、そろそろ NoDo の情報が必要な時期でもなくなってきたので、NoDo をターゲットにした情報についてはそのうち削除するかもしれません。

本ページも内容が増えてきたため、探しものがある場合は Ctrl + F 推奨です。

あえて Index からは外して先頭に載せますが、WP7 の機能改善の要望を行えるサイトがあります。以下のサイトです。

一応、開発者向けとはなっていますが、実際にサイトを見てもらえば分かりますが、開発以外の、OS の機能などについてもたくさんの意見が寄せられていて、それぞれ投票できるようになっています。 大量に意見が寄せられているので、「それ、絶対に実現してほしい」と言った内容も、いくつかは必ず見つかると思います。 残念ながら英語のサイトなので、英語が苦手な人にとってはとっつきにくいかもしれませんが、投稿済みの意見に投票するだけであれば [vote] ボタンを押すだけなので、 まず上記サイトを見てもらって、気になる要望があったら、片っ端から vote しましょう。持分は合計 25 票あるので、とりあえず、全部使い切るつもりで、気になる意見に片っ端から投票するのが良いと思います。 一度使用した票も、再度 [vote] ボタンを押して回収し、他の意見の投票に回すことができるので、とりあえず、全部使いきりましょう。 英語が苦手だと少し厳しいですが、ユニークな意見も含め、様々な意見が寄せられていますので、どんな意見が寄せられているのかざっと確認するだけでも面白いと思います。 いずれにしても、今後の改善のためにも、まず、投票です。

・・・開発者向けフィードバックサイトは、日本語版もできたようです。日本語サイトは下記。

開発者向けではない、一般ユーザー向けに用意されているフィードバックサイトは下記。

Index

 

■ アプリケーション ライフサイクル

参考情報

アプリケーション ライフサイクルにおける実際のイベント発生順序


クラス構成
===========================
新規 Windows Phone Application プロジェクトにページ遷移動作確認用の SecondPage を加えただけなので、クラスは以下の 3 クラスのみ。

App
MainPage
SecondPage


トレースログ (Debug.WriteLine) の設置場所
===========================
各クラスのコンストラクタ

下記アプリケーションイベント
Application_Launching
Application_Activated
Application_Deactivated
Application_Closing

各ページの下記イベント
Loaded
Unloaded
OnNavigatedFrom
OnNavigatedTo


アプリケーションの基本的な状態
===========================
Running (実行中)
Dormant (休眠中)
Tombstoned (停止中)
Closing (終了)


Dormant は Mango から追加された状態。
アプリ中断時、状態は Running から Dormant にまず遷移するが、Dormant 状態からの復帰であれば、OS が全て復元の処理を行ってくれる。
Dormant から Tombstoned に遷移していた場合は、NoDo までと同様、アプリ側で状態の復元処理を行う必要がある。


トレースログの出方
============================
.cctor はクラス コンストラクタ
.ctor はインスタンス コンストラクタを示す。


(1) 起動時
(2) MainPage で [Back] ボタン押下して終了
(3) MainPage で [Start] ボタン押下してツームストーン (tombstone)
(4) [Back] ボタン押下してツームストーン or ドーマント復帰
(5) MainPage で NavigationService.Navigate() を使用して SecondPage にページ遷移
(6) SecondPage で [Back] ボタン押下して MainPage にページ遷移
(7) SecondPage で [Start] ボタン押下してツームストーン
(8) SecondPage で [Start] ボタン押下してツームストーン後、[Back] ボタンでツームストーン復帰


(1) 起動時
--------------------
App..cctor
App..ctor
App.Application_Launching
MainPage..cctor
MainPage..ctor
MainPage.OnNavigatedTo (e.IsNavigationInitiator = False, e.NavigationMode = New, e.Uri = /MainPage.xaml)
MainPage.MainPage_Loaded


(2) MainPage で [Back] ボタン押下して終了
--------------------
MainPage.OnNavigatedFrom (e.IsNavigationInitiator = False, e.NavigationMode = Back, e.Uri = app://external/)
App.Application_Closing


(3) MainPage で [Start] ボタン押下してツームストーン (tombstone)
--------------------
MainPage.OnNavigatedFrom (e.IsNavigationInitiator = False, e.NavigationMode = New, e.Uri = app://external/)
App.Application_Deactivated


(4) [Back] ボタン押下してツームストーン or ドーマント復帰
--------------------
- OS 7.0 (NoDo)
App..cctor
App..ctor
App.Application_Activated
MainPage..cctor
MainPage..ctor
MainPage.OnNavigatedTo (e.Uri = /MainPage.xaml)
MainPage.MainPage_Loaded

- OS 7.1 (Mango RTM, app in a dormant state)
App.Application_Activated (e.IsApplicationInstancePreserved = True)
MainPage.OnNavigatedTo (e.IsNavigationInitiator = False, e.NavigationMode = Back, e.Uri = /MainPage.xaml)

- OS 7.1 (Mango RTM, app in a tombstone state)
App..cctor
App..ctor
App.Application_Activated (e.IsApplicationInstancePreserved = False)
MainPage..cctor
MainPage..ctor
MainPage.OnNavigatedTo (e.IsNavigationInitiator = False, e.NavigationMode = Back, e.Uri = /MainPage.xaml)
MainPage.MainPage_Loaded


(5) MainPage で NavigationService.Navigate() を使用して SecondPage にページ遷移 (クエリーも付加)
--------------------
SecondPage..cctor (但し、SecondPage の初回表示時のみ)
SecondPage..ctor
MainPage.OnNavigatedFrom (e.IsNavigationInitiator = True, e.NavigationMode = New, e.Uri = /SecondPage.xaml?query=test)
SecondPage.OnNavigatedTo (e.IsNavigationInitiator = True, e.NavigationMode = New, e.Uri = /SecondPage.xaml?query=test)
MainPage.MainPage_Unloaded
SecondPage.SecondPage_Loaded


(6) SecondPage で [Back] ボタン押下して MainPage にページ遷移
--------------------
SecondPage.OnNavigatedFrom (e.IsNavigationInitiator = True, e.NavigationMode = Back, e.Uri = /MainPage.xaml)
MainPage.OnNavigatedTo (e.IsNavigationInitiator = True, e.NavigationMode = Back, e.Uri = /MainPage.xaml)
SecondPage.SecondPage_Unloaded
MainPage.MainPage_Loaded


(7) SecondPage で [Start] ボタン押下してツームストーン
--------------------
SecondPage.OnNavigatedFrom (e.IsNavigationInitiator = False, e.NavigationMode = New, e.Uri = app://external/)
App.Application_Deactivated


(8) SecondPage で [Start] ボタン押下してツームストーン後、[Back] ボタンでツームストーン復帰
--------------------
- OS 7.0 (NoDo)
App..cctor
App..ctor
App.Application_Activated
SecondPage..cctor
SecondPage..ctor
SecondPage.OnNavigatedTo (e.Uri = /SecondPage.xaml?query=test)
SecondPage.SecondPage_Loaded

- OS 7.1 (Mango RTM, app in a dormant state)
App.Application_Activated (e.IsApplicationInstancePreserved = True)
SecondPage.OnNavigatedTo (e.IsNavigationInitiator = False, e.NavigationMode = Back, e.Uri = /SecondPage.xaml?query=test)

- OS 7.1 (Mango RTM, app in a tombstone state)
App..cctor
App..ctor
App.Application_Activated (e.IsNavigationInitiator = False, e.NavigationMode = Back, e.IsApplicationInstancePreserved = False)
SecondPage..cctor
SecondPage..ctor
SecondPage.OnNavigatedTo (e.Uri = /SecondPage.xaml?query=test)
SecondPage.SecondPage_Loaded


その他補足
============================
ページが切り替わるコントロール (例えば Toolkit の DatePicker クラスとか) を使用している場合、上記の動作は基本的に同じ場合があるので、注意すること。
つまり、上記条件のうち、(5) と (6) のフローが同様に発生する可能性がある。
具体的には、以下のようになる。


(9) MainPage から DatePicker 編集画面呼び出し
--------------------
MainPage.OnNavigatedFrom (e.IsNavigationInitiator = True, e.NavigationMode = New, e.Uri = /Microsoft.Phone.Controls.Toolkit;component/DateTimePickers/DatePickerPage.xaml)
MainPage.MainPage_Unloaded


(10) DatePicker 編集画面から MainPage へ復帰
--------------------
MainPage.OnNavigatedTo (e.IsNavigationInitiator = True, e.NavigationMode = Back, e.Uri = /MainPage.xaml)
MainPage.MainPage_Loaded


上記のようなコントロールを使用している場合、自分が作成したページのみを考慮して OnNavigatedTo() や Loaded() イベントなどでページ遷移処理を行っていると、予期せぬ動きとなる場合がある。


上記アプリケーション ライフサイクルの動作から見る共通イベント
=============================================================
画面が新たに表示される際は、以下のイベントが必ず発生する。(インスタンス名は適当)

SecondPage.OnNavigatedTo (e.Uri = /SecondPage.xaml)

また、dormant から直接復帰するページ以外の表示では、以下のイベントも OnNavigatedTo に続いて発生する。

SecondPage.SecondPage_Loaded 

逆に、画面が表示されなくなる際は、以下のイベントが必ず発生する。

MainPage.OnNavigatedFrom (e.Uri = /Microsoft.Phone.Controls.Toolkit;component/DateTimePickers/DatePickerPage.xaml)

その他のイベントは、状況によって発生するタイミングが異なるが、必ず上記の共通イベントと組み合わせた形で発生する。

 

■ 各イベント毎の注意事項

Page_Loaded イベントが発生した時点で、当該ページは既に表示されている。そのため、表示に関する初期化処理を Page_Loaded イベントで処理するのは、コントロールの初期値が一瞬表示されるためあまり適切なタイミングではない。OnNavigatedTo イベント発生時はまだ当該ページは表示されていないため、このイベント内で表示に関する初期化処理を行えば、コントロールの初期値が一瞬表示されることはなくなる。

Focus メソッドなど画面描画が関連する一部の処理は、Page クラスのコンストラクタや OnNavigatedTo では正常に機能しない事がある。その場合、Page_Loaded で行う必要がある。

 

■ ツームストーン復帰時に OS が行ってくれること

ツームストーン状態からの復帰時、OS がやってくれる事は以下の処理です。

  1. App インスタンスを新規生成
  2. App.Application_Launching イベントではなく、App.Application_Activated イベントの呼び出し
  3. 前回 Deactivate 時の、ページ呼び出し順 (Back キーで戻るページの線形リスト) の復元。クエリーストリング付きの URL として復元。 (※ 線形リストの復元とは表現していますが、ドキュメントなどでこの動作に関する情報は見つけていません。ここでは、実際の動作から勝手に私がそう表現しています。 ・・・よく考えたら、もっと単純にスタックと言っても問題ないかもしれません。)
  4. 復元した線形リストの最後のページのインスタンスを新規生成し、クエリーストリング付きで URL 引渡し。
  5. (以降、Back キーでページを戻る場合は、ページインスタンスの新規作成とクエリーストリング付きの URL 引渡しが行われる)

要は、ほぼ何もしてくれません。そのため、ツームストーンからの復元について何も対応していないアプリの場合、結構悲しいユーザー体験が発生します。 本来ユーザーが望んでいるであろうユーザー体験 (要は前回中断時のイメージの復元) を実現するには、Activated イベントで、前回 Deactivated 時に保存しておいた状態データをロードし、 ページの OnNavigatedTo イベントや Loaded イベントにてそれらの状態データを使用した画面の復元を行う必要があります。

 

■ ドーマント or ツームストーン状態のアプリ終了時に OS が行ってくれること

ドーマント or ツームストーン状態を保持しているアプリ数が 5 個を超えるなどして、その結果、一時保存状態から復帰することなく、アプリがバックグラウンドで終了させられた状況を言っています。 こんなタイトルですが、結論から書くと、OS は何もしてくれません。単純に一時保存データを破棄して終了させるだけです。 そのため、App.Application_Closing イベントはアプリが終了するにあたって呼ばれない可能性があります。 そのため、App.Application_Closing イベントでの処理は、その事を考慮して行う必要があります。例えば、Isolated Storage への永続データの保存をこのイベントのみで行ってしまうと、保存が行われない可能性があります。 「ユーザーがアプリを明示的に終了させた」際のイベントとして、使用可能かと思います。

 

■ OnNavigatedTo() での考慮事項

どのような状態でのページ表示時にも必ず呼ばれる唯一のイベントが OnNavigatedTo イベントなので、基本的にページの初期化や状態復元に関する処理は全てこのイベントで行う必要がある。 とりあえず考えつく、対処が必要と思われる条件は、下記。

  1. 初回表示 (別ページから呼ばれての新規表示)
  2. dormant からの復帰
  3. tombstone からの復帰
  4. 別ページから戻ってきての表示 (別ページにて tombstone 未発生)
  5. 別ページから戻ってきての表示 (別ページにて tombstone 発生)
  6. 別コントロール (自アプリ外) から戻ってきての表示 (別コントロールにて tombstone 未発生)
  7. 別コントロール (自アプリ外) から戻ってきての表示 (別コントロールにて tombstone 発生)

初めの 3 つへの対処は、どのようなアプリでも恐らく必須。残りは必要に応じて対処。別ページや別コントロール表示後に dormant 状態に遷移したとしても、dormant であれば全ての状態が復元されるので、 アプリが終了していないと同義と考えて問題ない。逆に、別ページや別コントロール表示後に tombstone に遷移した場合は、ページ表示時の URL 以外の情報は全てなくなるので、考慮が必要。

 

■ アプリケーション ライフサイクルにおける、データおよび状態保存の基本アプローチ

参考情報

かなり適当なアプローチは下記。あんまり適切でない可能性が高いです。上記の参考情報 (特に一番上のドキュメント) なども含め、 この辺りは詳しいドキュメントや参考になるサンプルが結構あるので、そちらを確認することをお勧めします。自分も本当はそっちを熟読して理解するべきなのですが、 結構理解するのが大変そうなので、動作ベースで確認しています。また、画面の構成によっても変わってくると思います。

 

■ メーラーなど外部タスク使用時の注意点

メーラーの起動などもアプリから行うことができるが (例えば Microsoft.Phone.Tasks.EmailComposeTask 等)、これら外部のタスクを起動する場合、自アプリの処理の流れとしては、[Start] ボタンを押下した時と変わらない。 つまり、自アプリが一度終了 (ドーマント or ツームストーン) された後に外部タスクが起動し、外部タスクの終了と共に自動的にドーマント or ツームストーンからの復帰が行われる。 そのため、処理の流れとしては上述したライフサイクルの条件の (3)、(4) と全く同じ (移動先の URI も app://external/)。

参考情報

 

■ Mango における、状態保存の変更点

上述したイベント発生順の通り、基本的には OS 7.1 (Mango) でも OS 7.0 (NoDo) と同様の流れとなるが、アプリ状態一時保存状態からの復帰時のみ、動作が異なる場合がある。 NoDo までは、[Start] ボタン押下時は、アプリは必ずツームストーン状態に遷移したが、Mango ではこの際の動作が変わり、まずはドーマント状態に遷移する。 これはどういう状態かというと、アプリから抜ける際のメモリ状態を、OS が全て保存している状態。 メモリ状態が完全に保存されているため、ドーマント状態からの復帰時は、アプリ側では復元処理は何もする必要がない (何かやるとしてもドーマント状態かどうかの判断は必要と思われる)。 またその仕組み上、復元速度も非常に速く、この機能は Fast Application Switching (アプリケーションの高速切り替え) と呼ばれている。 FAS が実装されたことにより、一見、NoDo の時と比べ、アプリの状態を個別に保存・復元する処理はもう不要のように見えるが、そんなことはない。 ドーマント状態はアプリの動作は停止しているが、メモリ状態は丸ごと保持している。 ドーマント処理によりメモリ確保している状態がフォアグランドで動作しているアプリの動作に影響すると OS が判断した場合、ドーマント状態で確保しているメモリ状態は破棄し、ツームストーン状態に移行する。 大雑把に言うと、画面遷移時の URI (QueryString も含む) のスタックと、Page および PhoneApplicationService の State データのみ保持される。 ここで初めて、NoDo と同じ状態になる。そのため、Mango でも NoDo と同様、ツームストーン状態から復帰する場合に備えて、状態を保存し、必要に応じて自己復元する処理は引き続き必要。 また、ツームストーン状態の保持可能数も、上限がある。Mango では、アプリの状態を保持しておける数は、5 個までとなっている。 これは、[Back] ボタン長押し時に表示されるタスク切り替え画面でも確認ができる (5 個以上は表示されない)。 そのため、5 個を超えて更にアプリの状態を保存しようとすると、一番古いイメージ (タスク切り替え画面で言うところの一番左のアプリイメージ) は、ツームストーン状態からクローズ状態に移行する。 クローズ状態に移行した場合、一時保存していたデータも削除されるので、当たり前だが次回起動時は初回起動となる。

そんなわけで、アプリ開発側としては、OS へのドーマント機能の追加は開発が楽になったのではなく、 これまでの対応に追加してドーマント用の処理も追加しなくちゃならず、むしろ面倒くさくなったぐらいの感覚でいたほうが良いと思います。

逆にアプリを使う側からすると、通信処理中にわざとドーマントに移行させ、ドーマント復帰した後の挙動を見ることで、 どれぐらい丁寧に作られているかを窺うことができます。通常、復帰後に通信エラーが発生するので、何も考慮されてなければ最悪落ちます。 落ちなくても、通信エラーが発生してその後の処理ができなくなるってのはありがちな挙動です。 エラーにならなかったとしても、データの重複や抜け、あるいは不整合が発生する状況は、比較的良く見かける現象です。 ドーマント(or ツームストーン)復帰後に何事もなかったかのようにアプリサスペンド前の処理が行われるのが一番の理想ですが、 これを不整合なく適切に行うのは、処理が複雑であればあるほど難しいです。 全ての処理について適切なドーマント復帰処理を行うのは、通常、現実的ではないことが多いと思うので、 少なくとも落ちることはない状態を最低限のベースにして、後は使用頻度や重要度などを考慮しつつ、部分的に対応していくことになるかと思います。

ちなみに、ここで言っている OS 7.1 (Mango) とは、OS 7.1 上で動かすことを言っているのではなく、プロジェクトのターゲットのバージョンが 7.0 か 7.1 かを言っています。 ターゲットが 7.0 のアプリであれば、Mango OS 上で動作させる場合も OS 7.0 (NoDo) と同様の流れとなるようです。 そのため現状、ドーマントの動作は、マーケットプレイスからインストールしたアプリでは確認できません (ターゲットが NoDo のアプリのみ登録可能なため)。

また、SDK 7.1 Beta2 Refresh では、デバッガをアタッチした状態でアプリの保存状態 5 個を超えてアプリが終了した場合、「The program '[532545974] UI Task: Managed' has exited with code 0 (0x0).」と言った感じのメッセージがデバッガの出力ウィンドウに出力され、デバッガがデタッチされます。またこの時、アプリの種類によってはドーマントからツームストーンに遷移した段階でアプリ自体も終了することがあるようです (デバッガをアタッチしてなければ大丈夫)。 デバッガをアタッチした状態で当該アプリがドーマントからツームストーン状態に移行した場合、「The thread '<No Name>' (0x19970002) has exited with code 0 (0x0).」と言った感じのメッセージがデバッガの出力ウィンドウに出力されますが、デタッチはされないので、そのままデバッグ可能となっています。

なお、具体的にどんなタイミングでドーマントからツームストーンに移行するかはちょっと判断つきづらいですが、自分で少し確認した限り、重そうな動画を再生させると、再生の途中でドーマントからの移行が発生しました。 上述したように、デバッガがアタッチされていれば、出力ウィンドウに出力されるスレッド終了のメッセージからドーマント状態が破棄されたか知ることができると思います。 それと、自分が動作確認した限りでは、デバッガをアタッチしている時の方が、ドーマント状態の破棄が発生しやすい気がしました。 Trophy でデバッガアタッチした状態だと、例えば、「鳥の詩 1080p」の再生は 2 回持たない感じです(360p で)。

最後に、ドーマントおよびツームストーン時のデバッグ作業をしやすくする為、プロジェクトのプロパティで、アプリの Deactivated 時にドーマントに移行せず、 必ずツームストーン状態に移行させるオプションが用意されています。 プロジェクトのプロパティを開き、[Debug] タブの中にある、[Tombstone upon deactivation while debugging] チェックボックスがこれに該当します。 既定ではチェック付いていないので、まずドーマント状態に移行しますが、ここにチェックをつけると、ドーマント状態には移行せず、強制的にツームストーン状態に移行します。 これにより、ツームストーン状態からの復帰時の動作確認が簡単に行えます。なお、項目名の通り、デバッグ時のみ有効な設定です。 デバッガがアタッチされていない場合は、通常通り、ドーマントにまず移行します。

参考情報

 

■ Mango における、タスク切り替え画面

[Back] 長押しによるタスク切り替え画面使用時の動作は、基本的に [Back] ボタン押下時と同じ。 当該アプリケーション画面で [Back] 長押しして、タスク切り替え画面が表示された時点でドーマント or ツームストーン状態になる (上述したイベント発生順序だと 3 番)。 その後、タスク切り替え画面で当該アプリを選択すると、ドーマント or ツームストーンから復帰する (同様に 4 番)。

 

■ デバッガ (VS2010) をアタッチ中に、Resume 動作が発生した時の注意点

アプリケーションにデバッガをアタッチしている状態でツームストーン状態からの復帰処理 (Resuming) が発生すると、たまに、デバッガが終了してアプリケーションは Resuming 状態のまま止まってしまうことがある。 アプリケーション側ではデバッガが再アタッチされるのを待っているようなので、VS で F5 押して再度デバッグ実行を開始することで、Resuming 状態から処理が再開する。

参考情報

Keep the debugger alive when "Tombstoned"?

 

■ アプリの新規起動時にデバッガをデタッチしないようにする

当たり前ですが、Visual Studio からデバッグ実行すると、デバッガをアタッチした状態でアプリを起動してデバッグすることができます。 また、アプリ起動後にスタートボタンをクリックするなどしてドーマント (or ツームストーン) に移行した場合もアタッチ状態は継続するので、 ドーマントからの復帰後も継続してデバッグすることが出来るのですが、強制的にデタッチされてデバッグが継続できない状況があります。 それは、アプリを新規に起動した場合です。特定の機能を使用していないアプリであれば、全て VS からの起動で問題ないのですが、 特定の機能を使用している場合、VS からは起動できないパターンがあります。WP7 特有の起動が発生するパターンはいくつかありますが、 デバッグが困ってしまう代表的な起動としては、ライブタイルからの起動があるかと思います。 自分が確認した限り、VS からアタッチし直すことはできそうもないので、このパターンだとデバッグができません(まあこの場合、アタッチするタイミングも微妙ですが)。

そんな状況ですが、絶対にデバッグできないかと言うと、そんなこともありません。 どうするかと言うと、バックグラウンドタスク使用時の副作用を利用します。 バックグラウンドタスクを使用したアプリをデバッグしたことがあると分かると思いますが、バックグラウンドタスクを使用するアプリの場合、 デバッガ側で明示的にデバッグを停止しない限り、デタッチされません。そのため、ライブタイルなどから新規起動された場合も、デバッグを継続して行うことができます。 正直あんまりエレガントな方法じゃないんですが、これ以外の方法を見つけられてません。 この方法を使用する場合、マニフェスト ファイル (WMAppManifest.xml) を開いて、以下のようにバックグラウンドタスクの登録を Task セクションに行います。デバッグ終わったら削除しておきましょう。


<Tasks>
  <DefaultTask Name="_default" NavigationPage="MainPage.xaml" />
  <ExtendedTask Name="DummyTask">
    <BackgroundServiceAgent Specifier="ScheduledTaskAgent"
                            Name="dummy"
                            Source="dummy"
                            Type="dummy" />
  </ExtendedTask>
</Tasks>

参考情報

 

■ 10 秒ルールについて

WP7 では、アプリ起動や終了、画面表示に 10 秒以上かかると、OS により強制的に kill される仕組みがあります。 具体的には、以下のようなイベントの実行です。

補足事項としては、まず、上記でも括弧書きしているように、Application のコンストラクタや Launching イベントも、10 秒制限があると考えた方が無難です。 Mango で動作確認した限りでは kill されなかったのですが、ドキュメントにも記載されているので、今後のバージョンアップなどで制限がかかる可能性があると思います。 それと、Page 表示時の動作については、正確にはコンストラクタと OnNavigatedTo でそれぞれ 10 秒の制限があるわけではなく、 それぞれ合わせて、つまり、インスタンス生成から OnNavigatedTo が終了するまでの期間 (正確には、Loaded イベントが開始するまでの期間?) で 10 秒の制限となります。 上記のイベントの中で唯一、リミットがないので確定と思えるのは、Page の Loaded イベントです。 Mango での動作検証上も kill されていませんが、Loaded イベント開始時には既に画面の表示は行われているので、そういう意味でも 10 秒ルール対象外のイベントだと思います。 また、ドキュメント上も Loaded イベントは制限対象のイベントして記載されていません。

とは言うものの、上記はあくまで OS 上の制限であり、マーケットプレイスへの登録時の審査では、これとは別のルールが用意されています。 3 秒、5 秒、20 秒のルールがあります。

OS の 10 秒ルールに直接影響する審査項目は、5 秒ルールになると思います。 アプリをマーケットプレイスに登録するのであれば、実質、起動から 5 秒以内に初期画面を表示する必要があります。

とは言ったものの、実際問題、起動から 5 秒近く初期画面が表示されないとか、起動から 20 秒近くユーザーが操作できないなどの状態は UX 的にそもそもどうかという話になるので、 個人的に一番該当しやすいと思われる状況が、画面表示時に MessageBox (Popup) を表示して、ユーザーに何かしらの情報を伝える処理になるかと思います。 この場合、ユーザーがボタンをクリックして MessageBox を閉じるまでの時間はユーザー次第なので、OnNavigatedTo イベント内で表示してしまうと、10 秒ルールにより kill される可能性があります。 そのため、そのような場合は Page の Loaded イベントなどで行う必要があると思います。

また、画面表示時ではなく Deactivated 時の制限については、細かくは確認していないですが、タスクの起動や [Start] ボタン押下の操作が行われた瞬間から Deactivated イベントが終了するまでに 10 秒以上かかると、 kill されるようです。kill が発生した場合は当たり前ですが tombstone 状態のアプリ一覧からも削除されるため、[Back] ボタン長押しによるタスク切り替え画面でも kill されたか確認することができます。

なお、OS の 10 秒ルールはデバッガがアタッチされている場合は(当たり前かもしれませんが)適用されないので、ご注意ください。

参考情報

 

■ イベントと画面表示状態について

量が多かったので別ページで説明しています。こっち参照。

 

■ Navigate(), GoBack(), [Start] ボタン押下時や外部タスク起動時の処理の流れについて

要は、アプリ内ページ遷移系のメソッドを実行した場合や、アプリが一旦 dormant に移行する系のアクションが発生した場合、 それらのアクションが発生した後のコードの流れはどうなるのかという疑問。 ここでは、メソッド実行中にこれらのアクションが発生し、そのアクションの直後に時間がかかる処理が続いているような状況を検証。 検証したコードは下記。


Private Sub btnTest_Click(sender As System.Object, e As System.Windows.RoutedEventArgs)
    Debug.WriteLine("Test() --- Start ---")

    ' (1) NavigationService.GoBack() 実行
    ' (2) [Back] ボタン押下
    ' (3) NavigationService.Navigate() 実行
    ' (4) [Start] ボタン押下
    ' (5) 外部タスク実行 (Tasks.EmailComposeTask.Show() など)

    For intCount As Integer = 1 To 15
        Debug.WriteLine("Test() sleeping... " & intCount)
        System.Threading.Thread.Sleep(1000)    ' 1 秒待機
    Next

    Debug.WriteLine("Test() --- End ---")

End Sub

Protected Overrides Sub OnNavigatedFrom(e As System.Windows.Navigation.NavigationEventArgs)
    Debug.WriteLine("OnNavigatedFrom() --- Start ---")

    MyBase.OnNavigatedFrom(e)
End Sub

まず、いずれの場合も、当該アクションが発生した後の処理は通常の流れと同様になる。 アクションが発生した後のコードが実行されないということはない。 ただ、全て同じ結果になるかというと、ちょっと違う。 まず、上記アクションの内、(1) と (2) については 15 秒のループを終えた後、NavigatedFrom などのページ遷移処理が発生する。 つまり、以下のような出力となる。


Test() --- Start ---
Test() sleeping... 1
Test() sleeping... 2
Test() sleeping... 3
Test() sleeping... 4
Test() sleeping... 5
Test() sleeping... 6
Test() sleeping... 7
Test() sleeping... 8
Test() sleeping... 9
Test() sleeping... 10
Test() sleeping... 11
Test() sleeping... 12
Test() sleeping... 13
Test() sleeping... 14
Test() sleeping... 15
Test() --- End ---
OnNavigatedFrom() --- Start ---

(3) から (5) のアクションについては、処理中に 10 秒ルールが発動するので、プロセスは OS により kill される。 あまり細かくは調べていないが、もし IS にデバッグ文を書き込んでいたら、以下の様な出力になっているものと思われる。


Test() --- Start ---
Test() sleeping... 1
Test() sleeping... 2
Test() sleeping... 3
Test() sleeping... 4
Test() sleeping... 5
Test() sleeping... 6
Test() sleeping... 7
Test() sleeping... 8
Test() sleeping... 9

また、途中で kill されると言っても、(4) と (5) については更に状況が異なる。[Start] ボタン押下によるホーム画面の表示や、外部タスクの起動は、それ自体はアプリとは非同期で実行されるようなので、 上記のサンプルで言うと「Test() sleeping... 1」が出力される時には、ホームやタスクの起動によって、自アプリは見えなくなっている。 但し、裏で実行は進んでいるので、処理の流れは上記出力と同じ。また、Test() メソッド終了後、OnNavigatedFrom メソッドも実行される。 その様子がホームやタスクの画面に隠れて見えないだけ。 また更に、(5) の外部タスクについては呼び出し元と同じプロセスで実行されるようなので(アプリの dormant は発生する)、アプリが kill された時点で、表示されていたタスクも終了する。

上記のため、上述したアクションを行った際は、同一メソッド内の残りの処理は可能な限り速やかに終了した方が良い。 問題なさそうであれば、Exit Sub などで終了するのが手っ取り早いと思う。

また、上記の検証では、GoBack(), [Back] ボタン共に、OS による kill は発生していないが、基本的にはこれらも 10 秒ルールに該当すると考えた方が良いと思う。

なお、少なくともデバッグ実行で確認する限り、外部タスクやホーム画面からアプリに戻る際はアプリの Acitivate イベントから再開され、 アプリの Activate は Deactivate が終了していないと実行されないようなので、Deactivate が完了する前にアプリを再度表示しようとすると、Deactivate イベントが完了するまで待機が発生する。 そのような動きになっているので、処理の流れについては、全て一緒と考えて問題ないと思う。 流れが強制的に変わるのは、Exception がスローされた場合のみ。

 

■ [Start], [Back] ボタン押下時のバックグラウンド スレッドの処理の流れについて

上の話題の続き。上では UI スレッドしかテストしなかったので、ここではアプリがドーマントに移行する際のバックグラウンド スレッドの流れについて検証。 検証したパターンとコード抜粋は下記。 コードは基本的にボタンクリック時に UI スレッドとバックグラウンドスレッドでそれぞれメソッドをコールするだけ。 残りのコードはほぼ、ウェイトとログ用です。 なお、UI スレッドのスリープ時間についてはパターンに合わせて多少変えている場合があります。

以下 2 パターンは、一番発生しやすく、且つ対応が必要なパターン。基本的には下記パターンのみの考慮で十分。

以下 3 パターンは、通常起こり得ない状況、もしくは気にしなくていい状況だが、「もしそう言う状況になったらどういう動きになるのか」と言う実験を行った結果です。

以下 2 パターンは、ツームストーンから復帰するパターン。

検証コード抜粋


Partial Public Class App
    Inherits Application

...

    Private Sub Application_Launching(ByVal sender As Object, ByVal e As LaunchingEventArgs)

        App.WriteLog(String.Format("★ {0} {1} {2}", _
                       DateTime.Now.ToLongTimeString(), _
                       Thread.CurrentThread.ManagedThreadId, _
                      System.Reflection.MethodInfo.GetCurrentMethod.Name))

    End Sub

    Private Sub Application_Activated(ByVal sender As Object, ByVal e As ActivatedEventArgs)
        App.WriteLog(String.Format("★ {0} {1} {2} - START", _
                               DateTime.Now.ToLongTimeString(), _
                               Thread.CurrentThread.ManagedThreadId, _
                              System.Reflection.MethodInfo.GetCurrentMethod.Name))

        For intIndex As Integer = 1 To 2
            Thread.Sleep(1000)

            App.WriteLog(String.Format("■ {0} {1} {2} - {3}", _
                                          DateTime.Now.ToLongTimeString(), _
                                          Thread.CurrentThread.ManagedThreadId, _
                                            System.Reflection.MethodInfo.GetCurrentMethod.Name, _
                                            intIndex))
        Next

        App.WriteLog(String.Format("★ {0} {1} {2} - END", _
                               DateTime.Now.ToLongTimeString(), _
                               Thread.CurrentThread.ManagedThreadId, _
                              System.Reflection.MethodInfo.GetCurrentMethod.Name))

    End Sub

    Private Sub Application_Deactivated(ByVal sender As Object, ByVal e As DeactivatedEventArgs)
        App.WriteLog(String.Format("★ {0} {1} {2} - START", _
                               DateTime.Now.ToLongTimeString(), _
                               Thread.CurrentThread.ManagedThreadId, _
                              System.Reflection.MethodInfo.GetCurrentMethod.Name))

        For intIndex As Integer = 1 To 2
            Thread.Sleep(1000)

            App.WriteLog(String.Format("■ {0} {1} {2} - {3}", _
                                          DateTime.Now.ToLongTimeString(), _
                                          Thread.CurrentThread.ManagedThreadId, _
                                            System.Reflection.MethodInfo.GetCurrentMethod.Name, _
                                            intIndex))
        Next

        App.WriteLog(String.Format("★ {0} {1} {2} - END", _
                               DateTime.Now.ToLongTimeString(), _
                               Thread.CurrentThread.ManagedThreadId, _
                              System.Reflection.MethodInfo.GetCurrentMethod.Name))

    End Sub

    Private Sub Application_Closing(ByVal sender As Object, ByVal e As ClosingEventArgs)
        App.WriteLog(String.Format("★ {0} {1} {2} - START", _
                               DateTime.Now.ToLongTimeString(), _
                               Thread.CurrentThread.ManagedThreadId, _
                              System.Reflection.MethodInfo.GetCurrentMethod.Name))

        For intIndex As Integer = 1 To 2
            Thread.Sleep(1000)

            App.WriteLog(String.Format("■ {0} {1} {2} - {3}", _
                                          DateTime.Now.ToLongTimeString(), _
                                          Thread.CurrentThread.ManagedThreadId, _
                                            System.Reflection.MethodInfo.GetCurrentMethod.Name, _
                                            intIndex))
        Next

        App.WriteLog(String.Format("★ {0} {1} {2} - END", _
                               DateTime.Now.ToLongTimeString(), _
                               Thread.CurrentThread.ManagedThreadId, _
                              System.Reflection.MethodInfo.GetCurrentMethod.Name))

    End Sub

    Public Sub Application_UnhandledException(ByVal sender As Object, ByVal e As ApplicationUnhandledExceptionEventArgs) Handles Me.UnhandledException

        App.WriteLog(String.Format("★ {0} {1} {2}", _
                               DateTime.Now.ToLongTimeString(), _
                               Thread.CurrentThread.ManagedThreadId, _
                              System.Reflection.MethodInfo.GetCurrentMethod.Name))

    End Sub

...

End Class


Partial Public Class MainPage
    Inherits PhoneApplicationPage

    Const SLEEP_WAIT As Integer = 1000
    Const TRY_MAX As Integer = 10

    Public Sub New()
        InitializeComponent()
    End Sub

    Protected Overrides Sub OnNavigatedTo(e As System.Windows.Navigation.NavigationEventArgs)
        App.WriteLog(String.Format("★ {0} {1} {2} - START", _
                               DateTime.Now.ToLongTimeString(), _
                               Thread.CurrentThread.ManagedThreadId, _
                              System.Reflection.MethodInfo.GetCurrentMethod.Name))

        For intIndex As Integer = 1 To 2
            Thread.Sleep(SLEEP_WAIT)

            App.WriteLog(String.Format("■ {0} {1} {2} - {3}", _
                                          DateTime.Now.ToLongTimeString(), _
                                          Thread.CurrentThread.ManagedThreadId, _
                                            System.Reflection.MethodInfo.GetCurrentMethod.Name, _
                                            intIndex))
        Next

        App.WriteLog(String.Format("★ {0} {1} {2} - END", _
                               DateTime.Now.ToLongTimeString(), _
                               Thread.CurrentThread.ManagedThreadId, _
                              System.Reflection.MethodInfo.GetCurrentMethod.Name))

        MyBase.OnNavigatedTo(e)
    End Sub

    Protected Overrides Sub OnNavigatedFrom(e As System.Windows.Navigation.NavigationEventArgs)
        App.WriteLog(String.Format("★ {0} {1} {2} - START", _
                               DateTime.Now.ToLongTimeString(), _
                               Thread.CurrentThread.ManagedThreadId, _
                              System.Reflection.MethodInfo.GetCurrentMethod.Name))

        For intIndex As Integer = 1 To 2
            Thread.Sleep(SLEEP_WAIT)

            App.WriteLog(String.Format("■ {0} {1} {2} - {3}", _
                                          DateTime.Now.ToLongTimeString(), _
                                          Thread.CurrentThread.ManagedThreadId, _
                                            System.Reflection.MethodInfo.GetCurrentMethod.Name, _
                                            intIndex))
        Next

        App.WriteLog(String.Format("★ {0} {1} {2} - END", _
                               DateTime.Now.ToLongTimeString(), _
                               Thread.CurrentThread.ManagedThreadId, _
                              System.Reflection.MethodInfo.GetCurrentMethod.Name))

        MyBase.OnNavigatedFrom(e)
    End Sub

    Private Sub btnTest_Click(sender As System.Object, e As System.Windows.RoutedEventArgs) Handles btnTest.Click

        Try
            App.WriteLog("■ btnTest clicked")

            Dim myThread As Thread = New Thread(AddressOf BGTest1)
            myThread.Start()

            App.WriteLog("■ background thread invoked.")

            'For intIndex As Integer = 1 To TRY_MAX
            '    Thread.Sleep(SLEEP_WAIT)

            '    App.WriteLog(String.Format("■ {0} {1} {2} - {3}", _
            '                                  DateTime.Now.ToLongTimeString(), _
            '                                  Thread.CurrentThread.ManagedThreadId, _
            '                                    System.Reflection.MethodInfo.GetCurrentMethod.Name, _
            '                                    intIndex))
            'Next

            UITest1()

        Catch ex As Exception
            App.WriteLog(String.Format("■ {0} {1} {2} - {3}", _
                                              DateTime.Now.ToLongTimeString(), _
                                              Thread.CurrentThread.ManagedThreadId, _
                                                System.Reflection.MethodInfo.GetCurrentMethod.Name, _
                                                ex.ToString()))

        End Try

    End Sub

    Private Sub UITest1()

        Try
            App.WriteLog("■ UITest1 started")

            For intIndex As Integer = 1 To 3
                Thread.Sleep(SLEEP_WAIT)

                App.WriteLog(String.Format("■ {0} {1} {2} - {3}", _
                                              DateTime.Now.ToLongTimeString(), _
                                              Thread.CurrentThread.ManagedThreadId, _
                                                System.Reflection.MethodInfo.GetCurrentMethod.Name, _
                                                intIndex))
            Next

            App.WriteLog("■ UI thread done.")

        Catch ex As Exception
            App.WriteLog(String.Format("■ {0} {1} {2} - {3}", _
                                              DateTime.Now.ToLongTimeString(), _
                                              Thread.CurrentThread.ManagedThreadId, _
                                                System.Reflection.MethodInfo.GetCurrentMethod.Name, _
                                                ex.ToString()))

        End Try

    End Sub

    Private Sub BGTest1()
        Try
            App.WriteLog("□ BGTest1 started")

            For intCount As Integer = 1 To TRY_MAX
                Thread.Sleep(SLEEP_WAIT)

                App.WriteLog(String.Format("□ {0} {1} {2} - {3}", _
                                              DateTime.Now.ToLongTimeString(), _
                                              Thread.CurrentThread.ManagedThreadId, _
                                                System.Reflection.MethodInfo.GetCurrentMethod.Name, _
                                                intCount))
            Next

            BGTest2()

        Catch ex As Exception
            App.WriteLog(String.Format("□ {0} {1} {2} - {3}", _
                                              DateTime.Now.ToLongTimeString(), _
                                              Thread.CurrentThread.ManagedThreadId, _
                                                System.Reflection.MethodInfo.GetCurrentMethod.Name, _
                                                ex.ToString()))

        End Try

    End Sub

    Private Sub BGTest2()
        Try
            App.WriteLog("□ BGTest2 started")

            For intCount As Integer = 1 To TRY_MAX
                Thread.Sleep(SLEEP_WAIT)

                App.WriteLog(String.Format("□ {0} {1} {2} - {3}", _
                                              DateTime.Now.ToLongTimeString(), _
                                              Thread.CurrentThread.ManagedThreadId, _
                                                System.Reflection.MethodInfo.GetCurrentMethod.Name, _
                                                intCount))
            Next

            App.WriteLog("□ background thread done.")

        Catch ex As Exception
            App.WriteLog(String.Format("□ {0} {1} {2} - {3}", _
                                              DateTime.Now.ToLongTimeString(), _
                                              Thread.CurrentThread.ManagedThreadId, _
                                                System.Reflection.MethodInfo.GetCurrentMethod.Name, _
                                                ex.ToString()))

        End Try

    End Sub

End Class

以下が上記コードでの検証結果。Tango 端末上で実施。


パターン 1
バックグラウンド スレッドの処理中に、[Start] ボタン押下してアプリサスペンドした時の状況

(UI スレッドは制限時間内に終了するが、バックグラウンド スレッドはドーマント移行までに終了しないパターン。
状況としては一番多いと思われる)

その他条件
=============
UI スレッドは 10 秒以内に処理終了する。但し、動作をわかりやすくするため、ある程度 wait を置いてから終了するように調整。
デバッガ (VS) はアタッチしない状態で実行 (-> 通常のアプリ実行状態なので、10 秒ルールが適用される)

トレースログ
=============
★ 18:58:01 552733542 Application_Launching
★ 18:58:01 552733542 OnNavigatedTo - START
■ 18:58:02 552733542 OnNavigatedTo - 1
■ 18:58:03 552733542 OnNavigatedTo - 2
★ 18:58:03 552733542 OnNavigatedTo - END
■ btnTest clicked
■ background thread invoked.
□ BGTest1 started
■ UITest1 started
□ 18:58:10 569511070 BGTest1 - 1
■ 18:58:10 552733542 UITest1 - 1
□ 18:58:11 569511070 BGTest1 - 2
■ 18:58:11 552733542 UITest1 - 2
(大体この辺りで [Start] ボタンクリック)
□ 18:58:12 569511070 BGTest1 - 3
■ 18:58:12 552733542 UITest1 - 3
■ UI thread done.
★ 18:58:13 552733542 OnNavigatedFrom - START
□ 18:58:14 569511070 BGTest1 - 4
■ 18:58:14 552733542 OnNavigatedFrom - 1
□ 18:58:15 569511070 BGTest1 - 5
■ 18:58:15 552733542 OnNavigatedFrom - 2
★ 18:58:15 552733542 OnNavigatedFrom - END
★ 18:58:15 552733542 Application_Deactivated - START
□ 18:58:16 569511070 BGTest1 - 6
■ 18:58:16 552733542 Application_Deactivated - 1
□ 18:58:17 569511070 BGTest1 - 7
■ 18:58:17 552733542 Application_Deactivated - 2
★ 18:58:17 552733542 Application_Deactivated - END
([Back] ボタンクリック)
★ 18:58:38 552733542 Application_Activated - START
□ 18:58:38 569511070 BGTest1 - 8
■ 18:58:39 552733542 Application_Activated - 1
□ 18:58:40 569511070 BGTest1 - 9
■ 18:58:40 552733542 Application_Activated - 2
★ 18:58:41 552733542 Application_Activated - END
□ 18:58:41 569511070 BGTest1 - 10
★ 18:58:41 552733542 OnNavigatedTo - START
□ BGTest2 started
■ 18:58:42 552733542 OnNavigatedTo - 1
□ 18:58:42 569511070 BGTest2 - 1
■ 18:58:43 552733542 OnNavigatedTo - 2
□ 18:58:43 569511070 BGTest2 - 2
★ 18:58:43 552733542 OnNavigatedTo - END
□ 18:58:44 569511070 BGTest2 - 3
□ 18:58:45 569511070 BGTest2 - 4
□ 18:58:46 569511070 BGTest2 - 5
□ 18:58:47 569511070 BGTest2 - 6
□ 18:58:48 569511070 BGTest2 - 7
□ 18:58:49 569511070 BGTest2 - 8
□ 18:58:51 569511070 BGTest2 - 9
□ 18:58:52 569511070 BGTest2 - 10
□ background thread done.
([Back] ボタンクリック)
★ 18:59:03 552733542 OnNavigatedFrom - START
■ 18:59:04 552733542 OnNavigatedFrom - 1
■ 18:59:05 552733542 OnNavigatedFrom - 2
★ 18:59:05 552733542 OnNavigatedFrom - END
★ 18:59:05 552733542 Application_Closing - START
■ 18:59:06 552733542 Application_Closing - 1
■ 18:59:08 552733542 Application_Closing - 2
★ 18:59:08 552733542 Application_Closing - END

結果
=============
10 秒ルールは有効だが、UI スレッドが 10 秒以内に全処理終了するため、UI スレッドが終了後(Application_Deactivated イベント終了後)、
処理中のバックグラウンド スレッドは処理途中でサスペンドされ、ドーマント状態に移行する。

ドーマントからの復帰(Application_Activated イベント開始)と同時に、
バックグラウンド スレッドはサスペンドした位置からレジュームされて、そのまま処理続行する。

バックグラウンド スレッドのサスペンド・レジュームのタイミングは、ページのイベントと連動しているわけではないことに注意。


パターン 2
バックグラウンド スレッドの処理中に、[Back] ボタン押下してアプリ終了した時の状況

(UI スレッドは制限時間内に終了するが、バックグラウンド スレッドは UI スレッド終了までに終了しないパターン。
これも状況としてはパターン 1 に次いで多いと思われる)

その他条件
=============
UI スレッドは 10 秒以内に処理終了する。但し、動作をわかりやすくするため、ある程度 wait を置いてから終了するように調整。
デバッガ (VS) はアタッチした状態でもアタッチしていない状態でも、下記トレース結果については変わらない。

トレースログ
=============
★ 18:52:19 566168514 Application_Launching
★ 18:52:19 566168514 OnNavigatedTo - START
■ 18:52:20 566168514 OnNavigatedTo - 1
■ 18:52:21 566168514 OnNavigatedTo - 2
★ 18:52:21 566168514 OnNavigatedTo - END
■ btnTest clicked
■ background thread invoked.
□ BGTest1 started
■ UITest1 started
□ 18:52:25 541919862 BGTest1 - 1
■ 18:52:25 566168514 UITest1 - 1
□ 18:52:26 541919862 BGTest1 - 2
■ 18:52:26 566168514 UITest1 - 2
([Back] ボタンクリック)
□ 18:52:27 541919862 BGTest1 - 3
■ 18:52:27 566168514 UITest1 - 3
■ UI thread done.
★ 18:52:27 566168514 OnNavigatedFrom - START
□ 18:52:28 541919862 BGTest1 - 4
■ 18:52:29 566168514 OnNavigatedFrom - 1
□ 18:52:29 541919862 BGTest1 - 5
■ 18:52:30 566168514 OnNavigatedFrom - 2
★ 18:52:30 566168514 OnNavigatedFrom - END
★ 18:52:30 566168514 Application_Closing - START
□ 18:52:30 541919862 BGTest1 - 6
■ 18:52:31 566168514 Application_Closing - 1
□ 18:52:31 541919862 BGTest1 - 7
■ 18:52:32 566168514 Application_Closing - 2
★ 18:52:32 566168514 Application_Closing - END
□ 18:52:33 541919862 BGTest1 - System.Threading.ThreadAbortException: ThreadAbortException
   at System.Threading.WaitHandle.WaitMultiple(WaitHandle[] waitHandles, Int32 millisecondsTimeout, Boolean WaitAll)
   at System.Threading.WaitHandle.WaitOne(Int32 millisecondsTimeout, Boolean exitContext)
   at System.Threading.EventWaitHandle.WaitOne(Int32 millisecondsTimeout, Boolean exitContext)
   at System.Threading.WaitHandle.WaitOne(Int32 millisecondsTimeout)
   at System.Threading.Thread.Sleep(Int32 millisecondsTimeout)
   at DormantTest.MainPage.BGTest1()
   at System.Threading.ThreadHelper.ThreadStartHelper(ThreadHelper t)
   at System.Threading.ThreadHelper.ThreadStart_Context(Object state)

結果
=============
UI スレッドが終了後(Application_Closing イベント終了後)、処理中のバックグラウンド スレッドには ThreadAbortException が発行されて強制停止させられる。
UX 的要因として、OnNavigatedFrom イベントに入るまでは、(見た目上)アプリは終了されずに表示されたまま(画面が固まったまま)になるので、注意が必要。

以下 3 パターンは、通常起こり得ない状況、もしくは気にしなくていい状況だが、「もしそう言う状況になったらどういう動きになるのか」と言う実験を行った結果です。


パターン 3
バックグラウンド スレッドの処理中に、[Start] ボタン押下してアプリサスペンドするが、UI スレッドが 10 秒以内に終了しない状況。

(通常、そのような作りはしないはずなので、発生し得ないパターン。
ただ、実際にそのような状況になった場合にどのような挙動となるかの検証として実施)

その他条件
=============
UI スレッドは 10 秒以内に処理終了しない。
デバッガ (VS) はアタッチしない状態で実行 (-> 通常のアプリ実行状態なので、10 秒ルールが適用される)

トレースログ
=============
★ 19:09:40 552602170 Application_Launching
★ 19:09:41 552602170 OnNavigatedTo - START
■ 19:09:42 552602170 OnNavigatedTo - 1
■ 19:09:43 552602170 OnNavigatedTo - 2
★ 19:09:43 552602170 OnNavigatedTo - END
■ btnTest clicked
■ background thread invoked.
□ BGTest1 started
■ 19:09:51 552602170 btnTest_Click - 1
□ 19:09:51 557452954 BGTest1 - 1
(大体この辺りで [Start] ボタンクリック)
■ 19:09:52 552602170 btnTest_Click - 2
□ 19:09:52 557452954 BGTest1 - 2
■ 19:09:53 552602170 btnTest_Click - 3
□ 19:09:54 557452954 BGTest1 - 3
■ 19:09:55 552602170 btnTest_Click - 4
□ 19:09:55 557452954 BGTest1 - 4
■ 19:09:56 552602170 btnTest_Click - 5
□ 19:09:56 557452954 BGTest1 - 5
■ 19:09:57 552602170 btnTest_Click - 6
□ 19:09:57 557452954 BGTest1 - 6
■ 19:09:58 552602170 btnTest_Click - 7
□ 19:09:58 557452954 BGTest1 - 7
■ 19:09:59 552602170 btnTest_Click - 8
□ 19:09:59 557452954 BGTest1 - 8
■ 19:10:00 552602170 btnTest_Click - 9
□ 19:10:00 557452954 BGTest1 - 9
■ 19:10:01 552602170 btnTest_Click - 10
■ UITest1 started
□ 19:10:01 557452954 BGTest1 - 10
□ BGTest2 started

(killed)

結果
=============
[Start] ボタン押下によりアプリがドーマントに移行しても UI スレッドが処理を続けるため、10 秒ルールの適用により、10 秒後にプロセス(アプリ)が OS により強制終了させられる。


パターン 4
バックグラウンド スレッドの処理中に、[Start] ボタン押下してアプリサスペンドした時の状況

(デバッグ実行時の動作なので、この動作を気にする必要はない。
実験的な検証として実施)

その他条件
=============
UI スレッドは 10 秒以内に処理終了する。但し、動作をわかりやすくするため、ある程度 wait を置いてから終了するように調整。
デバッガ (VS) はアタッチした状態で実行 (-> デバッグ実行状態なので、10 秒ルールは適用されない)

トレースログ
=============
★ 18:40:58 567675326 Application_Launching
★ 18:40:59 567675326 OnNavigatedTo - START
■ 18:41:00 567675326 OnNavigatedTo - 1
■ 18:41:01 567675326 OnNavigatedTo - 2
★ 18:41:01 567675326 OnNavigatedTo - END
■ btnTest clicked
■ background thread invoked.
□ BGTest1 started
■ UITest1 started
□ 18:41:16 541657254 BGTest1 - 1
■ 18:41:16 567675326 UITest1 - 1
([Start] ボタンクリック)
□ 18:41:17 541657254 BGTest1 - 2
■ 18:41:17 567675326 UITest1 - 2
□ 18:41:18 541657254 BGTest1 - 3
■ 18:41:18 567675326 UITest1 - 3
■ UI thread done.
★ 18:41:18 567675326 OnNavigatedFrom - START
□ 18:41:19 541657254 BGTest1 - 4
■ 18:41:19 567675326 OnNavigatedFrom - 1
□ 18:41:20 541657254 BGTest1 - 5
■ 18:41:20 567675326 OnNavigatedFrom - 2
★ 18:41:21 567675326 OnNavigatedFrom - END
□ 18:41:21 541657254 BGTest1 - 6
★ 18:41:21 567675326 Application_Deactivated - START
□ 18:41:22 541657254 BGTest1 - 7
■ 18:41:23 567675326 Application_Deactivated - 1
□ 18:41:23 541657254 BGTest1 - 8
■ 18:41:24 567675326 Application_Deactivated - 2
★ 18:41:24 567675326 Application_Deactivated - END
([Back] ボタンクリック)
★ 18:41:39 567675326 Application_Activated - START
□ 18:41:40 541657254 BGTest1 - 9
■ 18:41:40 567675326 Application_Activated - 1
□ 18:41:41 541657254 BGTest1 - 10
□ BGTest2 started
■ 18:41:42 567675326 Application_Activated - 2
★ 18:41:42 567675326 Application_Activated - END
★ 18:41:42 567675326 OnNavigatedTo - START
□ 18:41:42 541657254 BGTest2 - 1
■ 18:41:43 567675326 OnNavigatedTo - 1
□ 18:41:43 541657254 BGTest2 - 2
■ 18:41:44 567675326 OnNavigatedTo - 2
★ 18:41:44 567675326 OnNavigatedTo - END
□ 18:41:44 541657254 BGTest2 - 3
□ 18:41:45 541657254 BGTest2 - 4
□ 18:41:47 541657254 BGTest2 - 5
□ 18:41:48 541657254 BGTest2 - 6
□ 18:41:49 541657254 BGTest2 - 7
□ 18:41:50 541657254 BGTest2 - 8
□ 18:41:51 541657254 BGTest2 - 9
□ 18:41:52 541657254 BGTest2 - 10
□ background thread done.
([Back] ボタンクリック)
★ 18:41:55 567675326 OnNavigatedFrom - START
■ 18:41:56 567675326 OnNavigatedFrom - 1
■ 18:41:57 567675326 OnNavigatedFrom - 2
★ 18:41:57 567675326 OnNavigatedFrom - END
★ 18:41:57 567675326 Application_Closing - START
■ 18:41:58 567675326 Application_Closing - 1
■ 18:42:00 567675326 Application_Closing - 2
★ 18:42:00 567675326 Application_Closing - END

結果
=============
UI スレッドが終了後(Application_Deactivated イベント終了後)、処理中のバックグラウンド スレッドは処理途中でサスペンドされ、ドーマント状態に移行する。
ドーマントからの復帰(Application_Activated イベント開始)と同時に、バックグラウンド スレッドはサスペンドした位置からレジュームされて、そのまま処理続行する。

10 秒ルールが発動しないため、結果的にはパターン 1 と同じ動きになっている。


パターン 5
バックグラウンド スレッドの処理中に、[Start] ボタン押下してアプリサスペンドするが、UI スレッドが 10 秒以内に終了しない状況。

(パターン 4 と同様、これもデバッグ実行時の動きなので、気にする必要なし。
実験的な検証として実施)

その他条件
=============
UI スレッドは 10 秒以内に処理終了しない。
デバッガ (VS) はアタッチした状態で実行 (-> デバッグ実行状態なので、10 秒ルールは適用されない)

トレースログ
=============
★ 19:04:23 552274614 Application_Launching
★ 19:04:24 552274614 OnNavigatedTo - START
■ 19:04:25 552274614 OnNavigatedTo - 1
■ 19:04:26 552274614 OnNavigatedTo - 2
★ 19:04:26 552274614 OnNavigatedTo - END
■ btnTest clicked
■ background thread invoked.
□ BGTest1 started
□ 19:04:31 538250638 BGTest1 - 1
■ 19:04:31 552274614 btnTest_Click - 1
□ 19:04:32 538250638 BGTest1 - 2
■ 19:04:32 552274614 btnTest_Click - 2
([Start] ボタンクリック)
■ 19:04:33 552274614 btnTest_Click - 3
□ 19:04:33 538250638 BGTest1 - 3
■ 19:04:34 552274614 btnTest_Click - 4
□ 19:04:35 538250638 BGTest1 - 4
■ 19:04:36 552274614 btnTest_Click - 5
□ 19:04:36 538250638 BGTest1 - 5
■ 19:04:37 552274614 btnTest_Click - 6
□ 19:04:37 538250638 BGTest1 - 6
■ 19:04:38 552274614 btnTest_Click - 7
□ 19:04:38 538250638 BGTest1 - 7
■ 19:04:39 552274614 btnTest_Click - 8
□ 19:04:39 538250638 BGTest1 - 8
■ 19:04:40 552274614 btnTest_Click - 9
□ 19:04:40 538250638 BGTest1 - 9
■ 19:04:41 552274614 btnTest_Click - 10
■ UITest1 started
□ 19:04:41 538250638 BGTest1 - 10
□ BGTest2 started
■ 19:04:42 552274614 UITest1 - 1
□ 19:04:42 538250638 BGTest2 - 1
■ 19:04:43 552274614 UITest1 - 2
□ 19:04:44 538250638 BGTest2 - 2
■ 19:04:44 552274614 UITest1 - 3
■ UI thread done.
□ 19:04:45 538250638 BGTest2 - 3
★ 19:04:45 552274614 OnNavigatedFrom - START
□ 19:04:46 538250638 BGTest2 - 4
■ 19:04:46 552274614 OnNavigatedFrom - 1
□ 19:04:47 538250638 BGTest2 - 5
■ 19:04:47 552274614 OnNavigatedFrom - 2
★ 19:04:47 552274614 OnNavigatedFrom - END
★ 19:04:47 552274614 Application_Deactivated - START
□ 19:04:48 538250638 BGTest2 - 6
■ 19:04:49 552274614 Application_Deactivated - 1
□ 19:04:49 538250638 BGTest2 - 7
■ 19:04:50 552274614 Application_Deactivated - 2
★ 19:04:50 552274614 Application_Deactivated - END
□ 19:04:50 538250638 BGTest2 - 8
([Back] ボタンクリック)
★ 19:05:06 552274614 Application_Activated - START
□ 19:05:07 538250638 BGTest2 - 9
■ 19:05:07 552274614 Application_Activated - 1
□ 19:05:08 538250638 BGTest2 - 10
□ background thread done.
■ 19:05:08 552274614 Application_Activated - 2
★ 19:05:08 552274614 Application_Activated - END
★ 19:05:09 552274614 OnNavigatedTo - START
■ 19:05:10 552274614 OnNavigatedTo - 1
■ 19:05:11 552274614 OnNavigatedTo - 2
★ 19:05:11 552274614 OnNavigatedTo - END
([Back] ボタンクリック)
★ 19:05:24 552274614 OnNavigatedFrom - START
■ 19:05:25 552274614 OnNavigatedFrom - 1
■ 19:05:26 552274614 OnNavigatedFrom - 2
★ 19:05:26 552274614 OnNavigatedFrom - END
★ 19:05:26 552274614 Application_Closing - START
■ 19:05:27 552274614 Application_Closing - 1
■ 19:05:28 552274614 Application_Closing - 2
★ 19:05:28 552274614 Application_Closing - END


結果
=============
10 秒ルールは適用されないため、[Start] ボタンが押下された後、UI スレッドの処理が終了するまで、動作し続ける。
UI スレッドが終了後(Application_Deactivated イベント終了後)、処理中のバックグラウンド スレッドは処理途中でサスペンドされ、ドーマント状態に移行する。
ドーマントからの復帰(Application_Activated イベント開始)と同時に、バックグラウンド スレッドはサスペンドした位置からレジュームされて、そのまま処理続行する。

以下 2 パターンは、ツームストーンから復帰するパターン。


パターン 6
バックグラウンド スレッドの処理中に、[Start] ボタン押下してアプリサスペンドした時の状況

(UI スレッドは制限時間内に終了するが、バックグラウンド スレッドはドーマント移行までに終了しないパターン。
且つ、ドーマント復帰ではなく、ツームストーンにより復帰するパターン)

その他条件
=============
UI スレッドは 10 秒以内に処理終了する。但し、動作をわかりやすくするため、ある程度 wait を置いてから終了するように調整。
デバッガ (VS) はアタッチしない状態で実行 (-> 通常のアプリ実行状態なので、10 秒ルールが適用される)
ドーマント移行後、ツームストーンに移行が発生。

トレースログ
=============
★ 4:19:19 398002146 Application_Launching
★ 4:19:20 398002146 OnNavigatedTo - START
■ 4:19:21 398002146 OnNavigatedTo - 1
■ 4:19:22 398002146 OnNavigatedTo - 2
★ 4:19:22 398002146 OnNavigatedTo - END
■ btnTest clicked
■ background thread invoked.
□ BGTest1 started
■ UITest1 started
□ 4:20:40 394855886 BGTest1 - 1
■ 4:20:40 398002146 UITest1 - 1
□ 4:20:41 394855886 BGTest1 - 2
■ 4:20:41 398002146 UITest1 - 2
([Start] ボタンクリック)
□ 4:20:42 394855886 BGTest1 - 3
■ 4:20:42 398002146 UITest1 - 3
■ UI thread done.
★ 4:20:42 398002146 OnNavigatedFrom - START
□ 4:20:43 394855886 BGTest1 - 4
■ 4:20:43 398002146 OnNavigatedFrom - 1
□ 4:20:44 394855886 BGTest1 - 5
■ 4:20:44 398002146 OnNavigatedFrom - 2
★ 4:20:44 398002146 OnNavigatedFrom - END
★ 4:20:45 398002146 Application_Deactivated - START
□ 4:20:45 394855886 BGTest1 - 6
■ 4:20:46 398002146 Application_Deactivated - 1
□ 4:20:46 394855886 BGTest1 - 7
■ 4:20:47 398002146 Application_Deactivated - 2
★ 4:20:47 398002146 Application_Deactivated - END
(ドーマントからツームストーンに移行)
([Back] ボタンクリック)
★ 4:22:08 385812470 Application_Activated - START
■ 4:22:09 385812470 Application_Activated - 1
■ 4:22:10 385812470 Application_Activated - 2
★ 4:22:10 385812470 Application_Activated - END
★ 4:22:11 385812470 OnNavigatedTo - START
■ 4:22:12 385812470 OnNavigatedTo - 1
■ 4:22:13 385812470 OnNavigatedTo - 2
★ 4:22:13 385812470 OnNavigatedTo - END
([Back] ボタンクリック)
★ 4:22:16 385812470 OnNavigatedFrom - START
■ 4:22:17 385812470 OnNavigatedFrom - 1
■ 4:22:18 385812470 OnNavigatedFrom - 2
★ 4:22:18 385812470 OnNavigatedFrom - END
★ 4:22:18 385812470 Application_Closing - START
■ 4:22:19 385812470 Application_Closing - 1
■ 4:22:20 385812470 Application_Closing - 2
★ 4:22:20 385812470 Application_Closing - END


結果
=============
ツームストーン状態からの復帰のため、ドーマント移行前にサスペンドされたバックグラウンド スレッドは既に消滅しており、
当たり前だがバックグラウンド スレッドの処理は再開しない。


パターン 7
バックグラウンド スレッドの処理中に、[Start] ボタン押下してアプリサスペンドした時の状況

(UI スレッドは制限時間内に終了するが、バックグラウンド スレッドはドーマント移行までに終了しないパターン。
且つ、VS のデバッグオプションにより、強制的にツームストーンから復帰するパターン)

その他条件
=============
UI スレッドは 10 秒以内に処理終了する。但し、動作をわかりやすくするため、ある程度 wait を置いてから終了するように調整。
デバッガ (VS) はアタッチしない状態で実行 (-> 通常のアプリ実行状態なので、10 秒ルールが適用される)
VS のデバッグ設定で、「Tombstone upon deactivation while debugging」を有効にする

トレースログ
=============
★ 4:31:33 387516590 Application_Launching
★ 4:31:33 387516590 OnNavigatedTo - START
■ 4:31:34 387516590 OnNavigatedTo - 1
■ 4:31:35 387516590 OnNavigatedTo - 2
★ 4:31:36 387516590 OnNavigatedTo - END
■ btnTest clicked
■ background thread invoked.
□ BGTest1 started
■ UITest1 started
□ 4:31:40 380766274 BGTest1 - 1
■ 4:31:40 387516590 UITest1 - 1
□ 4:31:41 380766274 BGTest1 - 2
■ 4:31:41 387516590 UITest1 - 2
([Start] ボタンクリック)
□ 4:31:42 380766274 BGTest1 - 3
■ 4:31:42 387516590 UITest1 - 3
■ UI thread done.
★ 4:31:42 387516590 OnNavigatedFrom - START
□ 4:31:43 380766274 BGTest1 - 4
■ 4:31:43 387516590 OnNavigatedFrom - 1
□ 4:31:44 380766274 BGTest1 - 5
■ 4:31:44 387516590 OnNavigatedFrom - 2
★ 4:31:44 387516590 OnNavigatedFrom - END
★ 4:31:44 387516590 Application_Deactivated - START
□ 4:31:45 380766274 BGTest1 - 6
■ 4:31:46 387516590 Application_Deactivated - 1
□ 4:31:46 380766274 BGTest1 - 7
■ 4:31:47 387516590 Application_Deactivated - 2
★ 4:31:47 387516590 Application_Deactivated - END
□ 4:31:47 380766274 BGTest1 - System.Threading.ThreadAbortException: ThreadAbortException
   at System.Threading.EventWaitHandle.WaitOneInternal(Int32 millisecondsTimeout)
   at System.Threading.WaitHandle.WaitMultiple(WaitHandle[] waitHandles, Int32 millisecondsTimeout, Boolean WaitAll)
   at System.Threading.WaitHandle.WaitOne(Int32 millisecondsTimeout, Boolean exitContext)
   at System.Threading.EventWaitHandle.WaitOne(Int32 millisecondsTimeout, Boolean exitContext)
   at System.Threading.WaitHandle.WaitOne(Int32 millisecondsTimeout)
   at System.Threading.Thread.Sleep(Int32 millisecondsTimeout)
   at DormantTest.MainPage.BGTest1()
   at System.Threading.ThreadHelper.ThreadStartHelper(ThreadHelA first chance exception of type 'System.Threading.ThreadAbortException' occurred in DormantTest.dll
([Back] ボタンクリック)
★ 4:31:59 393153062 Application_Activated - START
■ 4:32:00 393153062 Application_Activated - 1
■ 4:32:01 393153062 Application_Activated - 2
★ 4:32:01 393153062 Application_Activated - END
★ 4:32:02 393153062 OnNavigatedTo - START
■ 4:32:03 393153062 OnNavigatedTo - 1
■ 4:32:04 393153062 OnNavigatedTo - 2
★ 4:32:04 393153062 OnNavigatedTo - END
([Back] ボタンクリック)
★ 4:32:11 393153062 OnNavigatedFrom - START
■ 4:32:13 393153062 OnNavigatedFrom - 1
■ 4:32:14 393153062 OnNavigatedFrom - 2
★ 4:32:14 393153062 OnNavigatedFrom - END
★ 4:32:14 393153062 Application_Closing - START
■ 4:32:15 393153062 Application_Closing - 1
■ 4:32:16 393153062 Application_Closing - 2
★ 4:32:16 393153062 Application_Closing - END


結果
=============
自然にツームストーンに移行した時と異なり、VS のオプションによる強制ツームストーンは、
ドーマント移行時にバックグラウンド スレッドに ThreadAbortException を発行して強制的に終了させている様子。
当たり前だが復帰後にバックグラウンド スレッドの処理は再開しない。
ぱっと見、結構強引な方法なので、SDK のバージョンなどによってもこの動きは少し変わってくるかもしれない。

上記実験では単純に Sleep でウェイトをかけて、擬似的に時間がかかる処理を再現しているだけなので、現実的には、上記のように単純な結果にはならない。 例えば、通信などを行なっている場合、ドーマントからの復帰後はオブジェクトへのアクセス時に例外が発生した気がするが・・・今回の実験ではその辺りの動きは確認していません。 その後、確認しました。結果はこの後に載せてます。

簡単なまとめ

 

■ [Start], [Back] ボタン押下時のバックグラウンド スレッドの処理の流れについて (HttpWebRequest 編)

上の話題の更に続き。上では「通信時は問題発生するかも」みたいな事を書いちゃったので、HttpWebRequest を使用して実際に動作検証。どっちかと言うと、ここでの実験結果が本命です。 検証したパターンとコード抜粋は下記。 コードは基本的に、上で使用したコードの使い回し。ボタンクリック時に HttpWebRequest 発行するように修正。 なお、スリープ時間についてはパターンに合わせて多少変えている場合があります。

検証パターン

検証コード抜粋 (変更・追加箇所のみ)


    Private Sub btnTest_Click(sender As System.Object, e As System.Windows.RoutedEventArgs) Handles btnTest.Click

        Try
            App.WriteLog("■ btnTest clicked")

            ProcessWebRequest()
            UITest1()

        Catch ex As Exception
            App.WriteLog(String.Format("■ {0} {1} {2} - {3}", _
                                              DateTime.Now.ToLongTimeString(), _
                                              Thread.CurrentThread.ManagedThreadId, _
                                                System.Reflection.MethodInfo.GetCurrentMethod.Name, _
                                                ex.ToString()))

        End Try

    End Sub

    Public Sub ProcessWebRequest()
        App.WriteLog("■ ProcessWebRequest started")

        ' UI Thread

        Try
            Dim wr As HttpWebRequest = WebRequest.CreateHttp("http://m.bing.com/")
            wr.BeginGetResponse(New AsyncCallback(AddressOf RespCallback), wr)

        Catch ex As Exception
            App.WriteLog(String.Format("■ {0} {1} {2} - {3}", _
                                              DateTime.Now.ToLongTimeString(), _
                                              Thread.CurrentThread.ManagedThreadId, _
                                                System.Reflection.MethodInfo.GetCurrentMethod.Name, _
                                                ex.ToString()))

        End Try

    End Sub

    Private Sub RespCallback(ByVal ar As IAsyncResult)
        App.WriteLog("□ RespCallback started")

        ' Background Thread
        Try
            For intCount As Integer = 1 To TRY_MAX
                Thread.Sleep(SLEEP_WAIT)

                App.WriteLog(String.Format("□ {0} {1} {2} - {3}", _
                                              DateTime.Now.ToLongTimeString(), _
                                              Thread.CurrentThread.ManagedThreadId, _
                                                System.Reflection.MethodInfo.GetCurrentMethod.Name, _
                                                intCount))
            Next

            Dim myRequestState As HttpWebRequest = CType(ar.AsyncState, HttpWebRequest)
            Dim response As HttpWebResponse = CType(myRequestState.EndGetResponse(ar), HttpWebResponse)

            Dim responseStream As StreamReader = New StreamReader(response.GetResponseStream())
            Dim strResult As String = responseStream.ReadToEnd()

            App.WriteLog(String.Format("□ {0} {1} {2} - result: {3}", _
                              DateTime.Now.ToLongTimeString(), _
                              Thread.CurrentThread.ManagedThreadId, _
                                System.Reflection.MethodInfo.GetCurrentMethod.Name, _
                                strResult))

        Catch ex As Exception
            App.WriteLog(String.Format("□ {0} {1} {2} - {3}", _
                                              DateTime.Now.ToLongTimeString(), _
                                              Thread.CurrentThread.ManagedThreadId, _
                                                System.Reflection.MethodInfo.GetCurrentMethod.Name, _
                                                ex.ToString()))

        End Try

    End Sub


パターン 1
ドーマントには移行しない。単純に HttpWebRequest 呼び出し時のフローを確認するためのパターン。バックグラウンド スレッドでのウェイトはコメントアウト。

その他条件
=============
デバッガ (VS) はアタッチした状態でもアタッチしていない状態でも、下記トレース結果については変わらない。

トレースログ
=============
★ 3:34:21 470942446 Application_Launching
★ 3:34:22 470942446 OnNavigatedTo - START
■ 3:34:23 470942446 OnNavigatedTo - 1
■ 3:34:24 470942446 OnNavigatedTo - 2
★ 3:34:24 470942446 OnNavigatedTo - END
■ btnTest clicked
■ ProcessWebRequest started
■ UITest1 started
■ 3:34:31 470942446 UITest1 - 1
■ 3:34:32 470942446 UITest1 - 2
■ 3:34:33 470942446 UITest1 - 3
■ UI thread done.
□ RespCallback started
□ 3:34:34 486278850 RespCallback - result: <?xml version="1.0" encoding="utf-8"?> ... (omitted)
([Back] ボタンクリック)
★ 3:34:40 470942446 OnNavigatedFrom - START
■ 3:34:42 470942446 OnNavigatedFrom - 1
■ 3:34:43 470942446 OnNavigatedFrom - 2
★ 3:34:43 470942446 OnNavigatedFrom - END
★ 3:34:43 470942446 Application_Closing - START
■ 3:34:44 470942446 Application_Closing - 1
■ 3:34:45 470942446 Application_Closing - 2
★ 3:34:45 470942446 Application_Closing - END


結果
=============
単純に実行しているだけなので、特記事項無し。
HttpWebRequest なので、結果の戻りについてはバックグラウンドで別途起動される。


パターン 2
HttpWebRequest からのレスポンスが返る前に、ドーマントへの移行が完了するパターン。

その他条件
=============
デバッガ (VS) はアタッチした状態でもアタッチしていない状態でも、下記トレース結果については変わらない。

トレースログ
=============
★ 3:42:20 392901402 Application_Launching
★ 3:42:20 392901402 OnNavigatedTo - START
■ 3:42:21 392901402 OnNavigatedTo - 1
■ 3:42:22 392901402 OnNavigatedTo - 2
★ 3:42:22 392901402 OnNavigatedTo - END
■ btnTest clicked
■ ProcessWebRequest started
■ UITest1 started
■ 3:42:27 392901402 UITest1 - 1
([Start] ボタンクリック)
■ 3:42:28 392901402 UITest1 - 2
■ 3:42:30 392901402 UITest1 - 3
■ UI thread done.
★ 3:42:30 392901402 OnNavigatedFrom - START
■ 3:42:31 392901402 OnNavigatedFrom - 1
■ 3:42:32 392901402 OnNavigatedFrom - 2
★ 3:42:32 392901402 OnNavigatedFrom - END
★ 3:42:32 392901402 Application_Deactivated - START
■ 3:42:33 392901402 Application_Deactivated - 1
■ 3:42:34 392901402 Application_Deactivated - 2
★ 3:42:34 392901402 Application_Deactivated - END
([Back] ボタンクリック)
★ 3:42:45 392901402 Application_Activated - START
■ 3:42:46 392901402 Application_Activated - 1
■ 3:42:47 392901402 Application_Activated - 2
★ 3:42:47 392901402 Application_Activated - END
★ 3:42:47 392901402 OnNavigatedTo - START
■ 3:42:48 392901402 OnNavigatedTo - 1
■ 3:42:49 392901402 OnNavigatedTo - 2
★ 3:42:50 392901402 OnNavigatedTo - END
□ RespCallback started
□ 3:42:51 387326722 RespCallback - 1
□ 3:42:52 387326722 RespCallback - 2
□ 3:42:53 387326722 RespCallback - 3
□ 3:42:54 387326722 RespCallback - 4
□ 3:42:55 387326722 RespCallback - 5
□ 3:42:56 387326722 RespCallback - 6
□ 3:42:57 387326722 RespCallback - 7
□ 3:42:58 387326722 RespCallback - 8
□ 3:43:00 387326722 RespCallback - 9
□ 3:43:01 387326722 RespCallback - 10
□ 3:43:01 387326722 RespCallback - System.Net.WebException: WebException ---> System.Net.WebException: WebException
   at System.Net.Browser.ClientHttpWebRequest.InternalEndGetResponse(IAsyncResult asyncResult)
   at System.Net.Browser.ClientHttpWebRequest.<>c__DisplayClass2.<EndGetResponse>b__1(Object sendState)
   at System.Net.Browser.AsyncHelper.<>c__DisplayClass4.<BeginOnUI>b__0(Object sendState)
   at System.Reflection.RuntimeMethodInfo.InternalInvoke(RuntimeMethodInfo rtmi, Object obj, BindingFlags invokeAttr, 
        Binder binder, Object parameters, CultureInfo culture, Boolean isBinderDefault, Assembly caller, Boolean verifyAccess, StackCrawlMark& stackMark)
   at System.Reflection.RuntimeMethodInfo.InternalInvoke(Object obj, BindingFlags invokeAttr, Binder binder, 
        Object[] parameters, CultureInfo culture, StackCrawlMark& stackMark)
   at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)
   at System.Delegate.DynamicInvokeOne(Object[] args)
   at System.MulticastDelegate.DynamicInvokeImpl(Object[] args)
   at System.Windows.Threading.Dispatcher.<>c__DisplayClass4.<FastInvoke>b__3()
   at System.Reflection.RuntimeMethodInfo.InternalInvoke(RuntimeMethodInfo rtmi, Object obj, BindingFlags invokeAttr, 
        Binder binder, Object parameters, CultureInfo culture, Boolean isBinderDefault, Assembly caller, Boolean verifyAccess, StackCrawlMark& stackMark)
   at System.Reflection.RuntimeMethodInfo.InternalInvoke(Object obj, BindingFlags invokeAttr, Binder binder, 
        Object[] parameters, CultureInfo culture, StackCrawlMark& stackMark)
   at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)
   at System.Delegate.DynamicInvokeOne(Object[] args)
   at System.MulticastDelegate.DynamicInvokeImpl(Object[] args)
   at System.Windows.Threading.DispatcherOperation.Invoke()
   at System.Windows.Threading.Dispatcher.Dispatch(DispatcherPriority priority)
   at System.Windows.Threading.Dispatcher.OnInvoke(Object context)
   at System.Windows.Hosting.CallbackCookie.Invoke(Object[] args)
   at System.Windows.RuntimeHost.ManagedHost.InvokeDelegate(IntPtr pHandle, Int32 nParamCount, ScriptParam[] pParams, ScriptParam& pResult)

   at System.Net.Browser.AsyncHelper.BeginOnUI(SendOrPostCallback beginMethod, Object state)
   at System.Net.Browser.ClientHttpWebRequest.EndGetResponse(IAsyncResult asyncResult)
   at DormantTest.MainPage.RespCallback(IAsyncResult ar)
   at System.Net.Browser.ClientHttpWebRequest.<>c__DisplayClassa.<InvokeGetResponseCallback>b__8(Object state2)
   at System.Threading.ThreadPool.WorkItem.WaitCallback_Context(Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ThreadPool.WorkItem.doWork(Object o)
   at System.Threading.Timer.ring()

([Back] ボタンクリック)
★ 3:43:08 392901402 OnNavigatedFrom - START
■ 3:43:09 392901402 OnNavigatedFrom - 1
■ 3:43:10 392901402 OnNavigatedFrom - 2
★ 3:43:10 392901402 OnNavigatedFrom - END
★ 3:43:10 392901402 Application_Closing - START
■ 3:43:11 392901402 Application_Closing - 1
■ 3:43:12 392901402 Application_Closing - 2
★ 3:43:12 392901402 Application_Closing - END


結果
=============
ドーマント復帰後に、レスポンス用のメソッドはバックグラウンドでコールしてくれる。
ただ、EndGetResponse メソッド呼び出し時に WebException が発生する。


パターン 3
HttpWebRequest からのレスポンスが返った後に、ドーマントへの移行が完了するパターン。

その他条件
=============
デバッガ (VS) はアタッチした状態でもアタッチしていない状態でも、下記トレース結果については変わらない。

トレースログ
=============
★ 3:46:34 457579730 Application_Launching
★ 3:46:35 457579730 OnNavigatedTo - START
■ 3:46:36 457579730 OnNavigatedTo - 1
■ 3:46:37 457579730 OnNavigatedTo - 2
★ 3:46:37 457579730 OnNavigatedTo - END
■ btnTest clicked
■ ProcessWebRequest started
■ UITest1 started
■ 3:46:40 457579730 UITest1 - 1
■ 3:46:41 457579730 UITest1 - 2
■ 3:46:42 457579730 UITest1 - 3
■ UI thread done.
□ RespCallback started
([Start] ボタンクリック)
★ 3:46:43 457579730 OnNavigatedFrom - START
□ 3:46:43 480970122 RespCallback - 1
■ 3:46:44 457579730 OnNavigatedFrom - 1
□ 3:46:44 480970122 RespCallback - 2
■ 3:46:45 457579730 OnNavigatedFrom - 2
★ 3:46:45 457579730 OnNavigatedFrom - END
□ 3:46:45 480970122 RespCallback - 3
★ 3:46:46 457579730 Application_Deactivated - START
□ 3:46:47 480970122 RespCallback - 4
■ 3:46:47 457579730 Application_Deactivated - 1
□ 3:46:48 480970122 RespCallback - 5
■ 3:46:48 457579730 Application_Deactivated - 2
★ 3:46:48 457579730 Application_Deactivated - END
([Back] ボタンクリック)
★ 3:47:02 457579730 Application_Activated - START
□ 3:47:02 480970122 RespCallback - 6
■ 3:47:03 457579730 Application_Activated - 1
□ 3:47:04 480970122 RespCallback - 7
■ 3:47:04 457579730 Application_Activated - 2
★ 3:47:04 457579730 Application_Activated - END
★ 3:47:04 457579730 OnNavigatedTo - START
□ 3:47:05 480970122 RespCallback - 8
■ 3:47:05 457579730 OnNavigatedTo - 1
□ 3:47:06 480970122 RespCallback - 9
■ 3:47:06 457579730 OnNavigatedTo - 2
★ 3:47:07 457579730 OnNavigatedTo - END
□ 3:47:07 480970122 RespCallback - 10
□ 3:47:07 480970122 RespCallback - result: <?xml version="1.0" encoding="utf-8"?> ... (omitted)
([Back] ボタンクリック)
★ 3:47:18 457579730 OnNavigatedFrom - START
■ 3:47:20 457579730 OnNavigatedFrom - 1
■ 3:47:21 457579730 OnNavigatedFrom - 2
★ 3:47:21 457579730 OnNavigatedFrom - END
★ 3:47:21 457579730 Application_Closing - START
■ 3:47:22 457579730 Application_Closing - 1
■ 3:47:23 457579730 Application_Closing - 2
★ 3:47:23 457579730 Application_Closing - END


結果
=============
ドーマント移行前にレスポンス用のメソッドがコールバックされていれば、ドーマント復帰後も問題なく結果にアクセス可能。


パターン 4
HttpWebRequest からのレスポンスが返る前に、[Back] ボタン押下してアプリ終了するパターン

その他条件
=============
デバッガ (VS) はアタッチした状態でもアタッチしていない状態でも、下記トレース結果については変わらない。

トレースログ
=============
★ 5:25:31 483853474 Application_Launching
★ 5:25:32 483853474 OnNavigatedTo - START
■ 5:25:33 483853474 OnNavigatedTo - 1
■ 5:25:34 483853474 OnNavigatedTo - 2
★ 5:25:34 483853474 OnNavigatedTo - END
■ btnTest clicked
■ ProcessWebRequest started
([Back] ボタンクリック)
■ UITest1 started
■ 5:25:40 483853474 UITest1 - 1
■ 5:25:41 483853474 UITest1 - 2
■ 5:25:42 483853474 UITest1 - 3
■ UI thread done.
★ 5:25:42 483853474 OnNavigatedFrom - START
■ 5:25:43 483853474 OnNavigatedFrom - 1
■ 5:25:44 483853474 OnNavigatedFrom - 2
★ 5:25:44 483853474 OnNavigatedFrom - END
★ 5:25:44 483853474 Application_Closing - START
■ 5:25:45 483853474 Application_Closing - 1
■ 5:25:46 483853474 Application_Closing - 2
★ 5:25:46 483853474 Application_Closing - END


結果
=============
OnNavigatedFrom イベントが開始する前にコールバック メソッドが呼ばれるかと思ったが、このパターンではコールバック メソッドは呼ばれずにアプリ終了した。

まとめ

 

■ PhoneApplicationPage および PhoneApplicationService の State 使用に関する注意点

前述したように、Page に紐付くデータやアプリ全体で使用するデータのうち、一時的に保存する必要があるデータは、基本的にそれぞれの Page の State と PhoneApplicationService の State に保存します。 但し、State の利用にあたっては注意しなければいけない点があります。何かというと、保存できるデータのサイズに上限があるということです。 MSDN ドキュメントでは、PhoneApplicationPage の State の説明に、この点が記載されています。

PhoneApplicationPage の Remarks に、以下の説明があります。


You should not use this property for excessive storage because there is a limit of 2 MB for each page and 4 MB for the entire application.

上記の通り、PhoneApplicationPage の State は、各 Page 毎に 2MB、アプリ全体で 4MB が格納できるデータサイズの上限になっていると記載されています。 逆に、PhoneApplicationService のドキュメントにはこの点については何も記載されていません。 そんな状況なのですが、実際のところどうなのかと言うことで、検証した結果が下記になります。

検証コードは下記。


Dim bytTest() As Byte
ReDim bytTest((1024 * 1024 * 1.4))  ' OK
'ReDim bytTest((1024 * 1024 * 1.5)) ' NG - COM exception occurs

For intIndex As Integer = 0 To bytTest.Length - 1
    bytTest(intIndex) = 255
Next

Me.State("test") = bytTest

上限を超えて System.Runtime.InteropServices.COMException {"0x80040205"} がスローされた場合のコールスタックはそれぞれ下記。


- PhoneApplicationPage.OnNavigatedFrom

at Microsoft.Phone.Shell.Interop.ShellPage.SetProperty(String Name, Byte[] blob, Int32 blobSize)
at Microsoft.Phone.Shell.StreamPersister.Save(ShellPage shellPage, String key, IDictionary`2 dictionary, IEnumerable`1 knownTypes)
at Microsoft.Phone.Controls.PhoneApplicationPage.InternalOnNavigatedFrom(NavigationEventArgs e)
at System.Windows.Navigation.NavigationService.RaiseNavigated(Object content, Uri uri, NavigationMode mode, Boolean isNavigationInitiator, 
    PhoneApplicationPage existingContentPage, PhoneApplicationPage newContentPage)
at System.Windows.Navigation.NavigationService.Journal_NavigatedExternally(Object sender, JournalEventArgs args)
at System.Windows.Navigation.Journal.OnNavigatedExternally(String name, Uri uri, NavigationMode mode)
at System.Windows.Navigation.Journal.ShellPage_NavigatedAway(Object sender, NavigateAwayEventArgs args)
at Microsoft.Phone.Shell.Interop.ShellPageCallback.FireOnNavigateAway(IntPtr thisPage, Direction direction, IntPtr pageNext)

- Application_Deactivated

at Microsoft.Phone.Shell.Interop.ShellPageManager.SetProperty(String name, Byte[] blob)
at Microsoft.Phone.Shell.StreamPersister.Save(ShellPageManager shellPageManager, String key, IDictionary`2 dictionary, IEnumerable`1 knownTypes)
at Microsoft.Phone.Shell.PhoneApplicationService.FireDeactivated()
at Microsoft.Phone.Execution.NativeEmInterop.FireOnPause()

検証からは、上記の通りとなった。 ドキュメントの内容と比較すると、データサイズの上限が実際には少し低めな点と、PhoneApplicationService の State も、 データサイズの上限については PhoneApplicationPage と同じ扱いとなっている点が注意(正直なところホントかよって感じですが)。 また、ちょっと盲点だったのが、[Back] ボタンで破棄されたページに格納されていた State もアプリ全体の上限に含まれる点。 アプリで保存する一時データが小さい場合は、上記いずれの注意点も無視出来ると思うが、イメージなどを扱う場合は無視できなくなる可能性が高い。 特に、WP7 のアプリでは ListBox の項目としてイメージも表示する場合が多いと思うが、この場合、ユーザーが大量の項目を表示するような操作を行うと、マズイことになるかも。 例えば、一つ 10KB の画像があったとして、150 個ロードすると単純計算で 1.5MB 換算となる。つまり、これだけで既に上限を超えることになる。 ロードされるイメージの数に上限がない場合、必要に応じて一時データの保存についても Isolated Storage の利用を検討する必要があると思われる。

参考情報

 

■ 永続データの保存について

ツームストーン復帰用のページ一時データ等の保存は Page or PhoneApplicationService の State を利用するとして、ここでは永続的に保存が必要なデータの保存方法について簡単に説明します。 まず、保存方法としては、大きく分けて下記になるかと思います。

どの保存方法を選択するかは、データサイズや使用頻度、使用用途などによると思いますが、小規模のアプリにフォーカスして、且つ大雑把に決めてしまうと、 基本的には IsolatedStorageSettings か IsolatedStorageFile の 2 択になるかと思います。 で、もっと言ってしまうと、保存するデータのサイズが大きくないのであれば、IsolatedStorageSettings に全部保存してしまえば良いと思います。 IsolatedStorageSettings であれば、個別にシリアライズ・デシリアライズなどする必要もないため、一番お手軽簡単に使用出来ます。

ただ、単純な文字列や数値などのデータであれば、IsolatedStorageSettings で一括管理したとしてもファイルサイズは数十 KB ぐらいで収まると思いますが、 複雑なクラスやイメージなどが含まれると、MB 単位になる可能性があると思うので、そのようなデータを保存するのであれば、パフォーマンスへの影響を考慮し、 IsolatedStorageFile を使用して適切なサイズに収まるように各ファイルに保存した方が良いと思います。

また、NoDo の頃は一つのフォルダに含まれるファイル数が 128 以上になるとファイル操作のパフォーマンス低下が始まったというような情報もあるので、OS のバージョンなどによっても異なるとは思いますが、 大量のファイルの保存が予想される場合、フォルダ管理などもよく検討した方が良いかもしれません。

なお、Isolated Storage に格納されているファイルの状況を確認する方法としては、SDK 付属の ISETool.exe や CodePlex で公開されている GUI ツールなどいくつか選択肢がありますが、 個人的には、単純に確認するだけであれば IsoStoreSpy というツールがお手軽簡単に確認できて、便利かと思います(眼球が怖いのと動作が少し不安定なことを除けば)。 特にファイルサイズについてはアプリを使用していてもパッと見分かりづらいと思いますので、問題ないサイズに収まっているか、 これらのツールを使用して適宜チェックした方が良いと思います。

参考情報

 

■ SIP (Software Input Panel) の使用に際して

参考情報 (ユーザーサイド)

参考情報 (開発者サイド)

日本語はともかくとして、英語を入力するときは SIP が非常に重要になる。 で、アプリの動作と SIP を組み合わせて挙動を考えるときは、当たり前だけど SIP の機能や動作について知っていないと話にならないので、まずは上記のユーザーサイドのドキュメントで、SIP でできることについて確認。 その上で、その下の開発者サイドのドキュメントで、アプリにどう適用すればよいか確認。 アプリに適用するときに誰しも考えるだろう事項は、キーボードのレイアウト。で、かなり重要だけどいまいち分かりづらい事項が、Suggestion 機能。 Suggestion が有効になる InputScope の設定は決まっていて、Chat, Maps, Text の 3 つ。後、ApplicationEnd も有効になるけど、ドキュメントの説明によるとこの設定は Internal 用でサポートされていないようなので、とりあえず除外。 また、Suggestion 機能が有効になっている場合も更に有効になる機能が分かれる。Text の場合のみ、Suggestion 機能に加え、Auto Correction 機能が有効になる。 これは、タイピング中に、文字を打ち間違ったと WP7 が判定した際に、候補の先頭に Bold で訂正項目が表示される機能。この状態でスペースを入力すると、Bold の単語に入力中文字が置き換えられる。

後、数字のみ入力できそうなキーボード レイアウトとして、TelephoneNumber があるんだけど、個人的には、実用的ではないレイアウトだと思う。 なぜかと言うと、このレイアウトのみ、Enter キーがないから。そのため、このレイアウトのみ Enter キーを入力確定キーとして使うことができない。 そのため、数字入力については Number 系のレイアウトを使ったほうが実用的と思われる (Enter キーを入力確定キーに使うのであれば)。 (2012/01 追記) Mango で一部のキーボードレイアウトについて変更が入ったため、Mango では Number のレイアウトも TelephoneNumber と同じレイアウトになってしまいました。 そのため、入力確定キーなども考慮する場合、Time などのキーボード レイアウトを使用する必要があります。

ちなみに、InputScope の値とキーボード レイアウトの比較は上記開発者サイドの参考情報の Windows Phone 7 InputScope dictionary を参考にしてほしいんだけど、 "Text" のキーボード レイアウトが実際には違っていて、上記ドキュメントでは顔文字入力キーなしのキーボードに分類されているんだけど、 実際には "Chat" と同じ顔文字入力キー付きのキーボード レイアウトになるので、そこだけ注意。 まあ、実際に値を変えて試してみれば、すぐ分かります。 MSDN ドキュメントの InputScope の説明では、Chat に関してはあらかじめ定義されている略語も認識するみたいな記載があるんだけど、簡単に確認した限り、 Chat, Maps, Text ともに同様の Dictionary データが使用されているようでした。 例えば、BTW や LOL, ETA などは Dictionary に存在しているようです。ただ、AFAIK や AKA, ASAP などヒットしない略語も多数あるので、どの程度の略語が含まれるのかは不明。 それと、自分はほとんど英語の略語は分からないので、かなり適当な確認です。MSDN 上には具体例が記載されていないので、使用される Dictionary の内容に本当に違いがあるのかも不明。 Suggestion 機能が有効になるキーボードに焦点を当てて違いを比較すると、以下のようになります (自分で動作確認した範囲での違い)。

それぞれの Suggestion 動作の具体例は下記。略語である btw を打ち込もうとして、btq と打ち間違えてしまった場面を想定しています。 Text のみ、Auto-Correction 機能 (Bold での Suggestion) が働いていることが分かります。 但し Chat と Maps についても Bold での Auto-Correction 機能は働いていませんが、Suggestion 候補としては辞書から BTW が提示されているため、space キー押下時に自動で訂正されるかどうかの違いぐらいとは言えます。

wp7_chat.png

wp7_maps.png

wp7_text.png

 

■ プログラムで SIP を非表示にする

SIP の表示・非表示をプログラムから操作するメソッドなどは用意されていませんが、テキスボトックスなど入力可能コントロールにフォーカスが当たっているかどうかで自動的に表示・非表示が行われるので、 フォーカスをコントロールすることで表示・非表示もコントロールできます。 まあ、表示についてはフォーカスが当たっていない状態で SIP だしてもしょうがないと思うので、そのようなことを行うことはないと思いますが、非表示に関しては、 入力可能でないコントロールにフォーカスを移動することで実現できます。 コントロールは何でも良いのですが、表示している Page (this or Me) の Focus メソッドを呼べば良いと思います。

参考情報

 

■ グローバリゼーション

Thread.CurrentThread.CurrentCulture を明示的に設定することで、国際化対応することが可能。この設定については、OS の Settings 画面のリストでは選択できない Culture でも使用可能な場合がある。 例えば、ja-JP も設定可能。数値や日付などは Culture に影響受けることが多いし、使用するコントロールが国際化対応していれば、ほぼ何もせずに一部国際化対応できてしまう。 例えば、Toolkit の DatePicker は OS の Settings で現在選択できない言語も含めて国際化対応されているようで、カレントスレッドの Culture 設定に従って適切な表示が行われる。ja-JP が設定されている場合は、以下のように日本語で表示される。

wp7_choose_date.png

現時点で OS で使用可能な culture 一覧は後述します。

参考情報

文字列のローカライズは言語ごとにリソースファイルを用意して対応。詳細は下記ページ参照。

方法: Windows Phone のローカライズしたアプリケーションを構築する

実行時にどの言語リソースが使用されるかは、コードで何もしなくても OS の言語設定を基に自動で判断される。 つまり、CurrentThread.CurrentUICulture の情報を基に判断している。 OS の言語設定に依存させず、使用するリソースをプログラムでコントロールしたい場合は、CurrentThread.CurrentUICulture を適宜変更する必要がある。 リソースが使用される度に CurrentThread.CurrentUICulture の値が判定されているので、使用時までに適切な値になっていれば問題ないが、 xaml の定義で使用しているリソースについては、Page コンストラクタで呼ばれている InitializeComponent() よりも前に設定されている必要がある。 ただ、各ページ毎個別に設定してもあまり意味ないと思うので、App のコンストラクタで一度設定しておけば良いと思う。

アプリタイトルのローカライズは言語ごとにリソース用の DLL を用意して対応。詳細は下記ページ参照。

方法: Windows Phone のアプリケーション タイトルをローカライズする

ちなみに上記方法は VC++ を使用する必要があるが、下記ツールを使用することで VC++ が使えない人もタイトルのローカライズに対応できそう。けど、自分は試してません。

WP7 Localize | Patricks Blog

注意事項としては、ニュートラルリソースである AppResLib.dll の内容も審査提出前にちゃんと確認すること。 それぞれの言語の値についてはテストで確認できるので問題ないはずだが、ニュートラルリソースはテストでは確認できないと思うので、正しい値が設定されているか良く確認する。 通常、英語リソースと同じ名称をニュートラルリソースにも設定すると思うが、マーケットプレイス上の英語のアプリタイトル名は、ニュートラルリソースの値が使用される様子。 審査提出時、application title の項目は手動で入力する項目ではなく、上記のリソースの値が xap から自動的に取り出されて設定される。 ので、まあ、提出時に application title の英語名がちゃんとしていることを確認すれば、基本は問題ない。 逆にニュートラルリソースの名称が間違ったまま審査を通過した場合、間違った名称がマーケットプレイスに表示される可能性があるが、これを訂正するには、 ニュートラルリソースの内容を修正した後、再度アプリを申請し直す必要がある。

Toolkit の画面の一部文字(CHOOSE DATE など)のローカライズは、ソースをリビルドして対応。詳細は下記。

hfkhkuの備忘録: Silverlight for Windows Phone Toolkitの文字列のローカライズ

その他参考情報

 

■ OS 設定の CultureInfo への影響

設定 (settings) -> 地域&言語 (region+language) の設定項目はアプリの CultureInfo にも一部影響する。

 

■ OS 設定の DateTime 現地時刻への影響

そのまんまではあるが、OS の設定 (settings) -> 日付&時刻 (date+time) の設定が DateTime のローカルタイムに影響する。 自動設定がオンの場合、詳細情報が表示されないので、注意。

 

■ Windows Phone で使用可能な Culture とそれぞれのフォーマット情報

Windows Phone で使用可能な Culture とそれぞれのフォーマット情報は、フォーマット情報の追加により出力量がかなり多くなってしまったため、別ページに用意しました。

 

■ どんなコントロールを使うか。どのように使うか

用意されているコントロールや使い方を簡単に把握するためには、例えば下記。

 

■ デザインに関する情報

WP7 ではデザインが重要になるからか、関連する情報は詳細なドキュメントが大量に用意されている。

 

■ 長いテキストが途中で切り捨てられて表示される現象について

結論から先に書くと、OS の仕様によるものです。 対象要素が 2048 x 2048 pixels 以上の表示領域を必要とする場合、パフォーマンスの関係で 2048 x 2048 pixels を超えた領域については OS によりトリミングされ、 スペースになります。長文などを設定した TextBlock や TextBox がこの影響を受けやすいかと思います。 OS が強制的に行う以上、根本的な対策としては、要素が必要とする領域を計算し、トリムされる場合はプログラム側で分割して表示対処するなどが必要になるかと思います。 TextBlock については以下の方法が使えます。

Creating Scrollable TextBlock for WP7. - Alex Yakhnin's Blog - Site Home - MSDN Blogs

 

■ TextBlock の内容をコピー可能にする

タイトルはこんなんですが、もちろん TextBlock の内容はコピーできません。 NoDo アップデートにより文字のコピー&ペーストが可能になりましたが、コピー可能となるのは TextBox の表示内容のみです。 TextBox 以外のコントロールで表示内容をコピーしたいコントロールとしては、TextBlock がほとんどだと思います。 なので、対処方法としては、単純に TextBlock を TextBox に置き換えて読み取り専用にすれば OK と考えてしまうのですが、 やってみれば分かるのですが、たぶん酷い結果になると思います。 そんな場合の対処方法が載っているのが下記。

Copyable TextBlock for Windows Phone - Peter Foot - APPA Mundi

ただ、この置換えを行ってしまうと、上述した TextBlock に対するトリミング問題の対処方法が使用できなくなってしまいます・・・。 TextBox でも同様に対応できるかなとは思ったのですが、ちょっと確認した限りではうまくいきませんでした。

 

■ MessageBox 使用時のサウンドとバイブレーションを無効にする

(2011/11/22 以下の内容は、mango では状況違うかも。何か、mango だと MessageBox.Show した際にサウンドもバイブレーションも発生しなくなってる気がします。 ただ、音とは別にメッセージボックスのボタンに表示する文字がカスタマイズできない問題はあるので、基本 XNA の MessageBox を使用する方向で良い気がします。後は Coding4Fun も使える。)

やればすぐ分かりますが、Silverlight の MessageBox を使用すると、どうしてもサウンドとバイブレーションが発生します。 MessageBox の引数、つまりプログラムからはコントロールできないため、サウンドとバイブレーションが発生するかは完全にユーザーの OS 設定に依存します。 独自のメッセージボックスを作るなどの方法もありますが、サウンドとバイブレーションを発生させずにメッセージボックスを表示する方法として、 Silverlight ではなく XNA のメッセージボックスを使用する方法があります。具体的なやり方は下記フォーラムで Nick が回答しています。

How to disable the sound and vibration in MessageBox.Show - App Hub Forums

ちなみに、同じスレッドの中で、Silverlight アプリから XNA の API を使用した場合に審査が通るかを懸念する書き込みもありますが、 これもこのスレッドで回答されていますが、MessageBox については問題ないようです。 メッセージボックス表示時にサウンドとバイブレーションを使用する必要がある場合をのぞき、 基本的には上記方法を使用してサウンドとバイブレーションを無効にしてメッセージボックスを表示したほうがユーザー的にも望ましいかと思います。 OS 標準のアプリ (カレンダーなど) では普通に無効になっているので、簡単にできそうな気がするのですが・・・意外と罠なネタです。

それと一点注意が必要な点として、メッセージボックス表示中に [Back] や [Start] が押された場合、戻り値に Nothing が設定されてその後の処理が継続するため、 この点を考慮して戻り値取得後の実装を行う必要があります。

参考情報

ちなみに、バイブレーションについては VibrateController クラスが用意されているので、音は上記方法で消しつつ別途バイブすることも可。


Dim vib As Microsoft.Devices.VibrateController = Microsoft.Devices.VibrateController.Default
vib.Start(TimeSpan.FromMilliseconds(60))

参考情報

 

■ HTTP リクエストのレスポンスがキャッシュされる現象について

HTTP を使用していると、正常に動作しているようで、期待する結果が返らないことがあります。 そんな時は、前に全く同じ URL でリクエストを行っていないか、確認して下さい。 WP7 は既定の動作として、全く同じ URL へのリクエストを以前行っていた場合は、サーバーへの問い合わせは一切行わず、以前サーバーより取得したレスポンスを返します。 本当にレスポンスが変わっていないのであればそれでも良いのですが、レスポンスが変わっている場合は、期待していない結果が返されることになります。 プログラム側ではキャッシュされたレスポンスが返ってきているのか全く分からないのが、結構面倒くさい。 プログラム上は、普通にサーバーからレスポンスが返ってきているように見えるのですが、パケットをキャプチャしてみると、 キャッシュされたレスポンスが返る場合はサーバーへの通信が全く発生していないことが分かります。 既定で有効となっている動作ではあるのですが、無効にするオプションなどは用意されていません。 キャッシュを無効にして常にサーバーからレスポンスを得る一番手軽で確実な方法は、リクエストの URL に GUID や時刻などを追加で付与して、必ず異なる URL でリクエストする方法になります。

参考情報

Windows Phone 7 HttpWebRequest returns same response from Cache - Nick Harris .NET

 

■ 文字エンコードに Shift_JIS を使用する

恐ろしいことに、文字エンコードは Unicode しか対応していません。 日本の Web サイトはほとんどが Shift_JIS を使用していると思われますが、そのため、それら Shift_JIS のページの内容を読み込んで処理することは、デフォルトの機能だけでは実現できません。 対処方法としては、デコード用のサーバーを用意して毎回サーバーにリクエストするか、カスタム エンコーディング クラスを用意する必要があるそうです。 デコード用のサーバーを用意して毎回リクエストするとかありえないので、実質、カスタム エンコーディング クラスを用意するしかありません。 カスタム エンコーディング クラスは、以下の情報を基に用意できます。

参考情報

 

■ MD5 を使用する

WP7 と言うよりは、そもそも Silverlight に MD5 を扱えるクラスが用意されていません。 代わりに下記モジュールが使用可能。

Silverlight MD5 implementation - Home

 

■ GPS の精度について

明示された資料は見つけていませんが、動作結果からは、精度誤差 (HorizontalAccuracy プロパティ) については使用状況が影響する閾値がいくつか存在するようです。 実測値から判断した結果は、下記。正直、キャリアや端末、OS バージョンにも依存する気はしますが、今のところ、複数端末、複数キャリア、複数バージョンで、(恐らく)同様の挙動となっていることを確認しています。

なお、取得できた GPS 情報はキャッシュされるようで、衛星が捕捉できていない場合の結果には、それも影響します。 基本、取得時刻から時間が経過すればするほど精度誤差を拡大するロジックになっているようですが、詳細は不明(速度などが考慮されているかも不明)。 そのため、取得直後であれば、衛星未捕捉、WiFi オフの状態でも、350m 未満の値が返ることがあります(まあ、取得直後であれば実際問題、大体あっているとは思いますが)。

個人的な感覚としては、500m 未満であれば、使用に値する状況かなと言った感じです。 確実に言えるところとしては、衛星を捕捉できているか否かが全てで、衛星を捕捉している状況である 350m 未満の値は、信頼に値すると言うことです。 問題なのは、衛星が捕捉できておらず、WiFi オンの状況で 350m が返された場合。 自分もこの辺りの仕組みには疎いのですが、この場合はどうやら WiFi アクセスポイントのデータベースに登録された位置情報を利用しているだけのようで、つまり、登録されている位置情報に依存するということになります。 誤差数メートル程度の高い精度が返る場合もあるのですが、数十キロ離れたようなとんでもない誤差(たぶん、単純に登録間違えだとは思う)が 350m で返される場合もあるので(特に駅構内とか)、 通常は使用に値する結果が得られていると思われる、と考えて使うしかないような気がします。 ちなみに、WiFi アクセスポイントの位置情報を利用する場合、アクセスポイントが検出できれば良いだけなので、ログインとかは不要です。

取得した GPS データの取り扱いは結構面倒くさいですが、時刻が古い場合は以前計測した時のキャッシュ情報なので、破棄する必要があると思います。 また、時刻が最新の場合も、最初の取得データは破棄した方が確実かも。 衛星が捕捉できている場合は、捕捉し続けることである程度まで精度が上がります。 後、エミュレータ付属の GPS については、実際の動きと結構違うこともあるので、あくまで簡単な確認として使用した方が良いと思います。 それとドキュメントとかでは MovementThreshold の範囲内では PositionChanged イベントは発生しないとかの記載もありますが、 実際に動作させてみるとそんな動きになっているようにもみえないので、GPS 周りの動作確認は実機で入念に行った方が良いと思います。

A-GPS の仕組みを知らない方は(自分もそうでしたが)、一度基本的な仕組みを確認してから使用した方が無難です。 WP7 が行なっている位置情報サービスのデータベース構築や利用方法については、下記ページにも色々と情報があります。例えば、「Microsoftによる位置測位サービスの提供方法 」とかを参照。

参考情報

 

■ 非同期で動作し続ける機能を使用する際の注意点について

代表的なところとしては、GeoCoordinateWatcher や Compass、DispatcherTimer などの使用が挙げられます。 これらの機能を使用する場合、ユーザー入力とは関係なしにバックグラウンドで動き続ける(と言うかコールバック関数が呼ばれ続ける)ことになりますが、この関係で、使用にあたっては注意が必要となる点があります。 一番押さえなくてはいけない点は、「現在表示している画面と、これら機能の動作継続には関連がない」と言う点です(より正確には、特にこれらの機能に限った話でもないんですが・・・)。 何か当たり前のようにも思えるんですが、デバッグ出力とかしていないと、意外と気づいていない場合が多いです。 具体的な例を挙げると、初期画面で地図を表示して GPS (GeoCoordinateWatcher) を開始するアプリがあるとします。で、このアプリには設定画面が別画面としてあるとします。 メインの地図画面から設定画面に移動でき、設定画面からメインの地図画面に戻れる動きになっているとします。まあ、よくあるパターンです。 この時、GPS の起動についてはメイン画面の表示時に一度だけ行い、後は何も行なっていないとします。 この場合、メイン画面では GPS からのレスポンスに合わせて地図上の位置情報も更新され、設定画面を表示して設定画面から戻った場合も、地図上の位置情報は自動的に更新され続けます。

ここで見た目からは分かりづらい点が、「設定画面を表示している際も、メイン画面では GPS からのレスポンスを受信し続けている」と言う点です。 この動作は、コールバック関数でデバッグ出力させればすぐに分かります。 通常、表示していない画面の機能が動作し続けるというのは、CPU リソースの消費や動作の安定に悪影響を与えると思いますので、 OnNavigatedTo で動作を開始させ、OnNavigatedFrom で停止させるのが無難かと思います。

ちなみに、[Back] ボタンで前の画面に戻る場合も、状況はほとんど同じなので同様に注意する必要があります。 「[Back] ボタン押下で現在の画面が終了する=現在の画面のインスタンスが即破棄される」と言うわけではないので、明示的に停止させないと、前の画面に戻った後も動作し続けます。 と言うか、リークして動き続けます。

なお、上記に関連して、アプリで予期せぬ例外がスローされた場合も注意する必要があります。 通常、予期せぬ例外については App の UnhandledException でキャッチして、エラーレポートなどを行う事が多いかと思います。 この場合、例外が発生したからと言って自動的に GPS 処理などが停止されるというわけではないため、例外発生時に明示的に停止しないと、バックグラウンドで動き続けることになります。 一番マズイのが、予期せぬ例外がこれらのコールバック関数内で発生した場合です。この場合、明示的に動作を停止させるまで例外が発生し続ける事になるため、最悪、OS が落ちます (UI スレッドでコールバック関数が実行される場合、メッセージボックスの表示などを行なって UI スレッドが止まっている間はコールバック関数も処理されませんが・・・状況はあまり変わらないと思います)。 また、UnhandledException 発生はページナビゲーションには影響を与えず、表示しているページが終了するというわけでもないため、 上述した OnNavigatedFrom などのイベントも発生しません。UnhandledException 発生→即 Application_UnhandledException イベント発生という流れになります。 このため、Application_UnhandledException イベントでは、まず最初に自動更新を停止させた方が良いと思います。例えば、以下のように処理します。


App.xaml.vb
===========================
...

Public Sub Application_UnhandledException(ByVal sender As Object, ByVal e As ApplicationUnhandledExceptionEventArgs) Handles Me.UnhandledException

    ' メイン画面の自動更新を停止する
    If CType(CType(RootVisual, PhoneApplicationFrame).Content, MainPage) IsNot Nothing Then
        CType(CType(RootVisual, PhoneApplicationFrame).Content, MainPage).OnUnexpectedErrorOccured()
    End If

    ' Show graphics profiling information while debugging.
    If Diagnostics.Debugger.IsAttached Then
        Diagnostics.Debugger.Break()
    Else
        ' エラーレポート
...


MainPage.xaml.vb
===========================

Private _blnUnexpectedErrorOccurred As Boolean = False  ' NavigatedTo などでの判定に使用

...

Public Sub OnUnexpectedErrorOccured()

    Me._blnUnexpectedErrorOccurred = True

    StopLocationService()
    StopCompass()

End Sub

...


 

■ UI スレッドで実行しているかの確認方法

UI 以外のスレッドからコントロールにアクセスしようとした場合、"Invalid cross-thread access" みたいなメッセージで UnauthorizedAccessException 例外がスローされたりします。 この場合、UI スレッドで関数を呼び直す必要があります。 が、UI スレッドで実行されているかの確認が行える、CheckAccess() や InvokeRequired() がインテリセンスで出て来ないんですけど? という問題(?)です。 結論から先に書いてしまうと、単純にインテリセンスの一覧に出てこない問題なだけで、CheckAccess() 自体は使えるようです。 なので、Dispatcher の CheckAccess を使って、対処できます。

参考情報

 

■ UI スレッドでの実行が必要となるパターン

実際にその状況になれば例外が発生するので、否が応にも駄目だっていうのは分かるのですが、ここでは代表的と思われる状況について説明します。 ちなみに、コントロールの操作は UI スレッドから行う必要がある、レベルは理解していることを前提としています(正直なところ、私もそのレベルの理解ですが・・・)。 それと、ここではバックグラウンドスレッドで処理を実行していることとします(そうじゃなければそもそも問題発生しないので)。

まず一つ目。WP7 のアプリだと、結局のところ、MVVM 的な作りにすると思います。そうすると、xaml 側はモデルのプロパティに binding し、 モデルはプロパティ更新時に INotifyPropertyChanged.PropertyChanged を呼んで、更新を通知することになると思います。 ここで問題になるのが、このフローは、結局のところ PropertyChanged を実行したスレッドでバインドされた側も更新されると言う点です。 つまり、バックグラウンドスレッド上で UI 要素を更新することになるので、Invalid cross-thread access の例外が発生します。 このため、PropertyChanged で通知する箇所は、全て UI スレッドで実行するようにする必要があります。

二つ目。一つ目とほとんど同じようなものですが、RaiseEvent (VB の場合です) でイベントを発生させる場合、結局、最終的に UI の要素を操作する可能性が高いと思います。 このパターンの場合、イベントを受け取った側でマーシャリングする必要があるか判断した方がパフォーマンス的には良いと思うのですが、 あまり面倒くさい状態になるのを避けたい場合は、一旦イベントを呼ぶ側で全て強制的にマーシャリングしてしまって、必要であれば後から個別対応という方針でも良いかと思います。 実際の対応方法ですが、例えば以下の様な感じ。


    Public Event PropertyChanged(sender As Object, e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
    Private Sub NotifyPropertyChanged(ByVal strPropertyName As String)
        Me.ExecuteOrBeginInvoke(Sub() RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(strPropertyName)))
    End Sub

    Private Sub ExecuteOrBeginInvoke(action As Action)
        Dim d = Deployment.Current.Dispatcher
        If d Is Nothing OrElse d.CheckAccess() Then
            action()
        Else
            d.BeginInvoke(action)
        End If
    End Sub

三つ目。UI 要素のクラス、と言うか、DependencyProperty を操作する処理と言うことになるらしいんですが、そのような操作をする場合も、UI スレッドで実行する必要があるそうです。 一例を挙げると、BitmapImage クラスのインスタンス化があります。この場合、本当にインスタンス化するだけで Invalid cross-thread access の例外が発生します。 対処方法はマーシャリングするしかないのですが、上述した二つの状況と比べ、このパターンの状況は単一呼び出しで済むことはほとんどないと思うので、対処はより困難です。 可能な限り UI スレッドでの呼び出しは局所化するべきだとは思いますが、このパターンの場合、当該メソッド全体を丸ごとマーシャリングした方が面倒くさくならないかもしれません。

四つ目。List の通知版は ObservableCollection になりますが、ObservableCollection のアイテム操作時に CollectionChanged イベントが発生し、 ここから通知が行われるので、この際にバックグラウンドで実行していると、一つ目の例と同じく Invalid cross-thread access の例外が発生します。 またこの場合、一つ目の例のように PropertyChanged メソッドで対処することもできないため、もし対応するとしたら、項目を操作するメソッド呼び出し自体を UI スレッドで行う必要があります。

特殊な注意事項として、バインド対象の要素が表示されている場合にのみ、バックグラウンドからの操作が行えない場合があります。 具体的には、Pivot の別ページに存在する要素へのバインドになります。この場合、要素が表示される前のバインドであれば、バックグラウンドからのバインドでも例外発生しません。 ここからは完全な推測になりますが、恐らく、Pivot 特有の動作として、実際に表示対象となるまで UI としてのロードが遅延される動作が影響しているのではないかと思います。 ちなみにここでの言及については、バックグラウンドから実行しても良い場合があるとか言っているわけではなく、UI スレッドからの実行を忘れてしまっていても偶然問題が発生しない可能性もあるが、 それはたまたまなので、ちゃんと全部 UI スレッドにマーシャリングして実行しましょう、と言うことを言いたいだけです。 上記 Pivot の動作はドキュメントに記載されている仕様ではない(はず)ので、バージョンアップとか動作が変わる可能性を考えると、そんな動作に依存するプログラムはお勧めしません。

参考情報

 

■ Serialize, Deserialize に備えた Property の使い方

WP7 アプリでは、データの保存にクラスのプロパティとシリアライズの機能を使用することが多いと思いますが、Serialize 実行時は Set Property が、Deserialize 実行時は Get Property が呼び出されます。 そのため、Property の中であまりトリッキーな処理を行うと、意図しないタイミングで処理が行われる可能性があるため、シンプルな処理に留めるのが無難です。

 

■ Serialize 関連の問題点その 1 (Data Contract Serialization)

特定のクラスが Serialize できない場合、一番可能性として高いのが、Data Contract Serialization を使ってないパターン。 その場合はたぶん、XML Serialization が使われてる。この辺り自分もあまり詳しくないのですが、一口に Serialize と言ってもいくつかパターンがあるらしく、 代表的なのが XML Serialization と Data Contract Serialization の二つ。で、Data Contract の方は明示的にそれを使うと宣言する必要があるので、もしそれを使わずにシリアライズしてるんであれば、 たぶん XML Serialize を使ってることになります。

で、ここからがややこしいんですが、通常は、XML Serialization も行けるんですよ(いや、たぶんそのはず)。 けど、対象となるクラスの構成が影響しているんだとは思うんですが、シリアライズできない時があるんですよね。 そんな場合は、まず、Data Contract Serialization で対応できないか、試すことをお勧めします。 まあ、最初から Data Contract Serialization 使ってればその辺りの問題は出ないのかもしれませんが、特に何も作業する必要がない XML Serialization と比べるとやや面倒くさいんですよね。 Data Contract Serialization の使い方はすいませんが自分で調べて下さい。ネットを調べれば情報はたくさん出てくると思います。 基本はクラスとプロパティに属性を追加するだけなので、そんなに難しくないと思います。

(2012/4 追記) 基本は、Data Contract を使う方が良いようです。名前空間の使用やメンバの順番、引数なしコンストラクタの使用などは注意する必要がありそう。 特に、引数なしコンストラクタが逆シリアル化時に呼ばれない動作には注意が必要。事実上、引数なしコンストラクタ内に処理を実装することは不可。 また、プロパティ、変数に依らず、DataMember 対象は Public となっている必要があるみたい。Private だとシリアライズ時に SecurityException が発生するので注意。 ちなみにランタイムのバージョンによっては(WP7 で確認)、クラス自身も Public じゃないと同エラーが発生するみたい。なので、何も考えずに全部 Public にするのが無難。 それと、デシリアライズ時に引数なしコンストラクタが呼ばれないのに起因しているかは確認していないけど、メンバ変数の初期化を変数の宣言と同時に行っている場合も、 デシリアライズ後のオブジェクトではその初期化が行われていない状態で復元されているようなので注意。

参考情報

 

■ Serialize 関連の問題点その 2 (TimeSpan 型のシリアライズ)

(少なくとも XMLSerializer では) TimeSpan 型はシリアライズできないらしいので、シリアライズだけ、String 型とかシリアライズできる型で代用する。もちろん、TimeSpan 型のプロパティ自体はシリアライズの対象から外す。(まだ試してないけど、Data Contract Serializer では問題なさそう) 例えば、以下の様な感じ。


Private _tsTotalTimeForToday As TimeSpan

<XmlIgnore()> _
Public Property TotalTimeForToday() As TimeSpan
    Get
        Return _tsTotalTimeForToday
    End Get
    Set(value As TimeSpan)
        If (value <> _tsTotalTimeForToday) Then
            _tsTotalTimeForToday = value
            NotifyPropertyChanged("TotalTimeForToday")
        End If
    End Set
End Property

<Browsable(False)> _
<XmlElement(DataType:="duration", ElementName:="TotalTimeForToday")> _
Public Property TotalTimeForTodayString As String
    Get
        Return XmlConvert.ToString(TotalTimeForToday)
    End Get
    Set(value As String)
        If String.IsNullOrEmpty(value) Then
            TotalTimeForToday = TimeSpan.Zero
        Else
            TotalTimeForToday = XmlConvert.ToTimeSpan(value)
        End If
    End Set
End Property

参考情報

 

■ Serialize 関連の問題点その 3 (Bitmap 型のシリアライズ)

(XML Serializer 及び DataContract Serializer 共に) Bitmap 型もそのままではシリアライズできないようなので、byte などでシリアライズ処理を代替する。 TimeSpan の例と比べるとゴチャゴチャしてる気がするけど、例えば、以下の様な感じ。 ちなみに以下の例だと、TimeSpan の例のような自動化は図られていないので、デシリアライズした後、個別に LoadThumbnailImageFromMemoryData() を呼ぶとかの処理は必要になります。 それとちょっと本題からはズレますが、画像系の場合、そのサイズや数によっては、シリアライズからは外して個別にファイルとして保存した方が良いかもしれません。まあこの辺りはケースバイケースで。


Private _biThumbnailImage As BitmapImage

<XmlIgnoreAttribute()> _
Public Property ThumbnailImage() As BitmapImage
    Get
        Return _biThumbnailImage
    End Get
    Set(value As BitmapImage)
        If (value IsNot _biThumbnailImage) Then
            _biThumbnailImage = value
            NotifyPropertyChanged("ThumbnailImage")
        End If
    End Set
End Property

Public Property ThumbnailMemoryImage As Byte()


Public Sub SetThumbnailImageFromResource(ByVal myUri As Uri)

    Dim myResInfo As Windows.Resources.StreamResourceInfo = App.GetResourceStream(myUri)
    Dim imageStream As Stream = myResInfo.Stream

    Dim b() As Byte
    ReDim b(imageStream.Length)
    imageStream.Read(b, 0, imageStream.Length)
    imageStream.Close()

    Me.ThumbnailMemoryImage = b
    Me.LoadThumbnailImageFromMemoryData()

End Sub

Public Sub LoadThumbnailImageFromMemoryData()

    If ThumbnailMemoryImage IsNot Nothing Then
        Dim ms As MemoryStream = New MemoryStream(ThumbnailMemoryImage)
        Dim bmp As BitmapImage = New BitmapImage
        bmp.SetSource(ms)
        Me.ThumbnailImage = bmp
    End If

End Sub

参考情報

 

■ Serialize 関連の問題点その 4 (Collection 系クラスから継承したクラスのシリアライズ)

どうやら、Collection 系のクラスから継承したクラスの場合、追加したプロパティ情報がシリアライズされないらしいんですよね。 じゃあどうすればいいのと言うことになるんですが、自分が探した限り、Collection 系のクラスからの派生はやめて、通常のクラスのメンバーとして Collection プロパティを追加する方法しか見つかりませんでした。

参考情報

 

■ タップ、クリック系イベントの発生順

タップ、クリック系のイベントを処理する場合、代表的なイベントとしては以下がある。

まず、基本形として、Button コントロールに対して操作を加えた場合のイベント発生状況は以下のようになる。


ボタンを軽くタップ
=============================================
18:57:35 Button_ManipulationStarted
18:57:35 Button_ManipulationCompleted
18:57:35 Button_Click
18:57:35 Button_Tap


ボタンをタップした後、3 秒間ホールドしてから離す
=============================================
19:01:16 Button_ManipulationStarted
19:01:17 Button_Hold
19:01:19 Button_ManipulationCompleted
19:01:19 Button_Click


ボタンをタップ&ホールドしたまま、コントロール外に移動してから離す
=============================================
19:02:22 Button_ManipulationStarted
19:02:23 Button_Hold
19:02:24 Button_ManipulationCompleted


次に、重なりあったコントロール操作時のイベント ルーティング等も考慮した、もう少し複雑な状況を考える。 以下のように、Button コントロールの上に StackPanel コントロールを配置し、StackPanel に対して操作を開始する場合を考えてみる。


<Button Width="300" Height="300">
    <StackPanel Background="Red" Width="100" Height="100">
    </StackPanel>
</Button>

結果は下記。StackPanel_ManipulationStarted、StackPanel_MouseLeftButtonDown イベントについては、それぞれのイベントで e.Handled プロパティに True を設定した場合の動作も確認。 イベント名の後ろに (e.Handled = True) と記載されていない項目については、その時の e.Handled プロパティの状態が False であることを示す。


軽くタップ (操作対象はスタックパネル。以降、全て同様)
=============================================
19:43:52 StackPanel_ManipulationStarted
19:43:52 StackPanel_MouseLeftButtonDown
19:43:52 Button_ManipulationStarted
19:43:52 StackPanel_ManipulationCompleted
19:43:52 Button_ManipulationCompleted
19:43:52 Button_Click
19:43:52 StackPanel_Tap
19:43:52 Button_Tap


軽くタップ (StackPanel_ManipulationStarted で e.Handled に True を設定)
=============================================
19:45:28 StackPanel_ManipulationStarted (e.Handled = True)
19:45:28 StackPanel_MouseLeftButtonDown (e.Handled = True)
19:45:28 Button_MouseLeftButtonDown (e.Handled = True)
19:45:28 StackPanel_ManipulationCompleted
19:45:28 StackPanel_MouseLeftButtonUp
19:45:28 Button_ManipulationCompleted
19:45:28 StackPanel_Tap
19:45:28 Button_Tap


軽くタップ (StackPanel_MouseLeftButtonDown で e.Handled に True を設定)
=============================================
19:47:14 StackPanel_ManipulationStarted
19:47:14 StackPanel_MouseLeftButtonDown (e.Handled = True)
19:47:14 Button_ManipulationStarted (e.Handled = True)
19:47:14 StackPanel_ManipulationCompleted
19:47:14 StackPanel_MouseLeftButtonUp
19:47:14 Button_ManipulationCompleted
19:47:14 StackPanel_Tap
19:47:14 Button_Tap


軽くタップ (StackPanel_Tap で e.Handled に True を設定)
=============================================
8:05:34 StackPanel_ManipulationStarted
8:05:34 StackPanel_MouseLeftButtonDown
8:05:34 Button_ManipulationStarted
8:05:34 StackPanel_ManipulationCompleted
8:05:34 Button_ManipulationCompleted
8:05:34 Button_Click
8:05:34 StackPanel_Tap (e.Handled = True)


タップした後、3 秒間ホールドしてから離す
=============================================
19:44:26 StackPanel_ManipulationStarted
19:44:26 StackPanel_MouseLeftButtonDown
19:44:26 Button_ManipulationStarted
19:44:27 StackPanel_Hold
19:44:27 Button_Hold
19:44:28 StackPanel_ManipulationCompleted
19:44:28 Button_ManipulationCompleted
19:44:28 Button_Click


タップした後、3 秒間ホールドしてから離す (StackPanel_ManipulationStarted で e.Handled に True を設定)
=============================================
19:46:00 StackPanel_ManipulationStarted (e.Handled = True)
19:46:00 StackPanel_MouseLeftButtonDown (e.Handled = True)
19:46:00 Button_MouseLeftButtonDown (e.Handled = True)
19:46:01 StackPanel_Hold
19:46:01 Button_Hold
19:46:02 StackPanel_ManipulationCompleted
19:46:02 StackPanel_MouseLeftButtonUp
19:46:02 Button_ManipulationCompleted


タップした後、3 秒間ホールドしてから離す (StackPanel_MouseLeftButtonDown で e.Handled に True を設定)
=============================================
19:47:31 StackPanel_ManipulationStarted
19:47:31 StackPanel_MouseLeftButtonDown (e.Handled = True)
19:47:31 Button_ManipulationStarted (e.Handled = True)
19:47:32 StackPanel_Hold
19:47:32 Button_Hold
19:47:33 StackPanel_ManipulationCompleted
19:47:33 StackPanel_MouseLeftButtonUp
19:47:33 Button_ManipulationCompleted


タップした後、3 秒間ホールドしてから離す (StackPanel_Hold で e.Handled に True を設定)
=============================================
8:06:54 StackPanel_ManipulationStarted
8:06:54 StackPanel_MouseLeftButtonDown
8:06:54 Button_ManipulationStarted
8:06:55 StackPanel_Hold (e.Handled = True)
8:06:56 StackPanel_ManipulationCompleted
8:06:56 Button_ManipulationCompleted
8:06:56 Button_Click


タップ&ホールドしたまま、コントロール外に移動してから離す
=============================================
19:44:45 StackPanel_ManipulationStarted
19:44:45 StackPanel_MouseLeftButtonDown
19:44:45 Button_ManipulationStarted
19:44:46 StackPanel_Hold
19:44:46 Button_Hold
19:44:47 StackPanel_ManipulationCompleted
19:44:47 Button_ManipulationCompleted


タップ&ホールドしたまま、コントロール外に移動してから離す (StackPanel_ManipulationStarted で e.Handled に True を設定)
=============================================
19:50:25 StackPanel_ManipulationStarted (e.Handled = True)
19:50:25 StackPanel_MouseLeftButtonDown (e.Handled = True)
19:50:25 Button_MouseLeftButtonDown (e.Handled = True)
19:50:26 StackPanel_Hold
19:50:26 Button_Hold
19:50:27 StackPanel_ManipulationCompleted
19:50:27 Button_ManipulationCompleted


タップ&ホールドしたまま、コントロール外に移動してから離す (StackPanel_MouseLeftButtonDown で e.Handled に True を設定)
=============================================
19:47:50 StackPanel_ManipulationStarted
19:47:50 StackPanel_MouseLeftButtonDown (e.Handled = True)
19:47:50 Button_ManipulationStarted (e.Handled = True)
19:47:51 StackPanel_Hold
19:47:51 Button_Hold
19:47:53 StackPanel_ManipulationCompleted
19:47:53 Button_ManipulationCompleted


タップ&ホールドしたまま、コントロール外に移動してから離す (StackPanel_Hold で e.Handled に True を設定)
=============================================
8:08:30 StackPanel_ManipulationStarted
8:08:30 StackPanel_MouseLeftButtonDown
8:08:30 Button_ManipulationStarted
8:08:31 StackPanel_Hold (e.Handled = True)
8:08:32 StackPanel_ManipulationCompleted
8:08:32 Button_ManipulationCompleted


スタックパネル上でタップ開始し、コントロール外で指を離す操作を一瞬で行う (フリックのような動作)
=============================================
20:08:17 StackPanel_ManipulationStarted
20:08:17 StackPanel_MouseLeftButtonDown
20:08:17 Button_ManipulationStarted
20:08:17 StackPanel_ManipulationCompleted
20:08:17 Button_ManipulationCompleted


ダブルタップ
=============================================
20:34:22 StackPanel_ManipulationStarted
20:34:22 StackPanel_MouseLeftButtonDown
20:34:22 Button_ManipulationStarted
20:34:22 StackPanel_ManipulationCompleted
20:34:22 Button_ManipulationCompleted
20:34:22 Button_Click
20:34:22 StackPanel_Tap
20:34:22 Button_Tap
20:34:22 StackPanel_ManipulationStarted
20:34:22 StackPanel_MouseLeftButtonDown
20:34:22 Button_ManipulationStarted
20:34:22 StackPanel_DoubleTap
20:34:22 Button_DoubleTap
20:34:22 StackPanel_ManipulationCompleted
20:34:22 Button_ManipulationCompleted
20:34:22 Button_Click

注 : Click イベントは StackPanel には存在しないので。Button コントロールのみ使用しています。

上記結果から分かる特徴的な動作としては、例えば以下。

なお、上記は Mango SDK での動作結果です。

参考情報

 

■ 一覧から項目を選択するシンプルなコントロール

すぐ思い浮かぶコントロールとして ComboBox があると思いますが、これは一応用意されているだけで、WP7 用に最適化されておらずサポートもされていないので、使っては駄目です。 WP7 に適したコントロールとしては、Silverlight for Windows Phone Toolkit の ListPicker があります。 デザインや動作としては、このコントロールで目的を達成できると思いますが、一つ、非常に困る問題を抱えています。 何かというと、項目数が多い場合、コントロールの初期化に非常に時間がかかると言う問題です。 パフォーマンスの問題なので、動作させる実機や OS バージョンにも依存するかもしれませんが、例えば、自分の Trophy + Mango Beta の環境では、項目数が 100 個ぐらいあると、こいつの初期化だけで 3 秒近くかかります。 ページの表示に時間がかかること自体、UX 的に問題ありますが、それ以上に、そもそもマーケットプレイス登録時の審査要件として、 「処理中のインジケーターなどを表示しない状態で、3 秒以上応答を返さない状態にしてはいけない」という 3 秒ルールがあります (5.1.3 Application Responsiveness)。 そのため画面初期化時にそのような状態になってしまうと、UX 云々じゃなくて、そもそも審査が通りません。 それなりのスペックの PC 上のエミュレータだと、実機に比べてかなり早く処理されてこの辺りのパフォーマンスの判断は困難なので、注意が必要です。ちなみに審査は実機で行われます。

この問題については、もしかしたら次期バージョンで修正されるかもしれませんが、少なくとも Feb 2011 バージョンでは駄目です。 ネットの情報を見ると、パッチも出ているようなのですが、少なくとも自分が試した限りでは、目に見える効果がありませんでした。 そんな状況なので、ある程度の数、恐らく 10 から 20 程度ぐらいまでであれば使用可能だと思いますが、それを超える可能性がある場合、toolkit Feb11 の ListPicker の使用は初めから諦めた方が良いかもしれません。 一応、Picker Box なるものも以前あったようで、こちらも使えそうな感じがしますが、自分はまだ試せてません。

(2012/01 追記) Nov 2011 の toolkit と Mango で簡単に再度確認してみました。項目数 100 個ぐらいで試しましたが、まだ初期化に少し時間がかかるようです。 以前よりは改善した感じがしますが、感覚で大体 1.5 秒とかそのぐらい。少なくとも画面表示時に少しひっかかる感じはしてしまう。 単純に ListPicker 置いただけの画面でこれなので、他の要素の処理とかが通常はあることを考えると、項目数が多い場合はまだ使えない感じです。

 

■ TiltEffect の対象ではないコントロールも Tilt するようにする

toolkit に含まれる TiltEffect を使用することで WP7 らしい UX を実現することができますが、この効果が適用されるコントロールは TiltableItems に登録されているクラスに限定されます。 Nov 2011 バージョンでは、ButtonBase、ListBoxItem、MenuItem の 3 つのようです。 なので、これ以外のコントロールで tilt したい時は、TiltEffect.TiltableItems.Add(GetType(Grid)) みたいな感じで任意に追加してあげれば良いのですが、 例えば Grid みたいにどこでも使われて、構成も複雑になりがちなコントロールだと思ったような結果が簡単に得られないこともあります。 そんな場合、ダミーの Button コントロールでラップするなどして、比較的シンプルに適用できるっぽいです。 もちろん、単純にそんな構成にすると UI がおかしなことになるので、ラップするコントロールはスタイルを無効にします。 例えば、Grid 全体を Tilt の対象にしたいのであれば、以下のように対処。スタイルを削除する方法もあんまり詳しくないのでもっと良い方法があるのかもしれませんが、知りません。


<phone:PhoneApplicationPage 
...
    toolkit:TiltEffect.IsTiltEnabled="True">

...

<Button>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>

        <Image Grid.Row="0" Grid.RowSpan="2" Grid.Column="0" />

        <TextBlock Grid.Row="0" Grid.Column="1" />
        <TextBlock Grid.Row="1" Grid.Column="1" />
    </Grid>

    <Button.Template>
        <ControlTemplate TargetType="Button">
        </ControlTemplate>
    </Button.Template>
</Button>

参考情報

 

■ Build Action プロパティの Resource と Content について

アプリにファイルをリソースとして含める場合、当該ファイルの Build Action プロパティで、Resource か Content かを選べます。 両者の大きな違いとしては、パスの指定方法が異なることと、ファイルがどのように配置されるかの違いがあります。 特に後者のファイルの配置のされ方については、Resource だと対象 dll にファイルが直接埋め込まれて dll 自体のファイルサイズがその分増加するため、 dll ロード時のパフォーマンスを考えると、基本的にはファイルが外出しとなる Content を選んだ方が良いと思います。 既定値は Resource なので注意。

参考情報

 

■ GetResourceStream に渡す URI について

アプリにファイルをリソースとして含める場合の基本的なポイントとして、当該ファイルの Build Action プロパティを Content にする必要があるというのは大丈夫かと思いますが、 ここで問題にしたいのは、実際にリソースをロードする時のパスの指定の仕方です。 とりあえず自分で確認はしているのであっているとは思うのですが、使用するメソッドによって、パスの先頭にスラッシュを付けても問題ない場合と付けてはダメな場合の 2 パターンあることを確認しています。 例として、Images フォルダ配下に配置した png ファイルを指定する場合、メソッドによって渡す必要がある URI に以下のような違いがあります。


' URI の先頭にスラッシュを付けても付けなくても OK
Dim myAppBarAddToFavoriteButton As ApplicationBarIconButton = New ApplicationBarIconButton(New Uri("/Images/appbar.favs.addto.rest.png", UriKind.Relative))

' URI の先頭にスラッシュを付けたらダメ
Dim myResInfo As Windows.Resources.StreamResourceInfo = App.GetResourceStream(New Uri("Images/appbar.favs.addto.rest.png", UriKind.Relative))

一応 GetResourceStream メソッドの説明ではそのような記載もあるのですが、そんな違いがあるとは普通思わないと思うので、注意。

 

■ Silverlight toolkit November 2011 の参照について

2011 年 12 月時点での最新の Silverlight toolkit は November 2011 となります。toolkit についてはプロジェクトで使う事が多く、且つ定期的に新しいバージョンがリリースされるので、 基本は新しいバージョンが出たらそちらを使用することになると思います。ただ、November 2011 と同じバージョンとして、少し前にリリースされていた August 2011 がありますが、 こちらもインストールして共存している状況だと、November 2011 バージョンを参照しているつもりで、August 2011 バージョンを参照してしまうことがあるようです。 実際にどちらを参照しているかは、プロジェクトの参照項目で Path プロパティを見れば分かります。oct11 フォルダであれば November 2011 (月名が一致してないので注意)、 aug11 であれば、August 2011 バージョンを参照していることになります。この問題が出ている場合、何度 oct11 に参照しなおしても結局 aug11 が使われてしまうようなので、 その場合、コントロールパネルから August 2011 の toolkit をアンインストールしましょう。oct11 しか存在していなければ、確実にそちらを参照します。

 

■ ApplicationBar を使用した画面から次の画面へ遷移した際の画面のガクツキ対策

詳細な条件は不明だが、ApplicationBar を使用した画面から次の画面へ遷移した際、次の画面の表示時に一瞬、画面が上に引っ張られるような挙動が発生し、ガクツキが発生する場合がある。 発生する可能性がある状況でも、必ず発生するわけではないのが結構辛い。ただ、発生する可能性がある場合、対応策をとっていないと数回に一回程度の割合で発生する。 ほぼ確実と思えるのは、遷移先の画面の状況は関係ないこと。遷移元の画面構成および遷移時の状況が影響する様子。 今確認できている対応策は、Opacity を変更するか、非同期処理で画面遷移するかの、二つ。それぞれ、使用する状況が異なる。

ApplicationBar.Opacity については、どうやら、0.4 以下であれば、発生しづらくなる様子。 ApplicationBar が設置されている画面から次の画面に遷移する際に、ApplicationBar.Opacity を変更してから、Navigate する。 戻ってきた先に設定を元に戻す必要があるので、注意。

非同期処理については、ApplicationBar 項目 (メニュー項目も含む) のクリックイベント内で Navigate する場合に使用する。 このタイミングで発生するガクツキは、Opacity では回避できないようなので、非同期処理 (Deployment.Current.Dispatcher.BeginInvoke) で Navigate することにより回避する。 ApplicationBar 項目以外での画面遷移では、上述の Opacity を使用。

上記 2 点の対策で、大体 8 割方、ガクツキの発生を抑えられる様子。ただ、これやっても発生する場合は発生する。 他のアプリでは同様の現象を見たことがあまりないので、もしかしたら完全な回避策があるのかもしれないが、今のところ見つけられてない。 場合によっては、OS のアップデートなどで解消される可能性もあるかもしれない(上記現象は Mango で確認している現象)。

参考情報

(2012/1/25 追記) 上記方法でもガクツキが発生してしまう時はしてしまうのだが、別の方法を使用することにより、副作用的に(?)なんとかなるかも。 どうするかと言うと、Transition を使用する。Transition 使うとそもそもガクツキ的なものが判断しづらい気もするが、Opacity の方の影響は今のところ確認できていない。 ただ、非同期処理の方は、よく見ると Transition 中に発生している事が引き続きあるようなので(Transition するので分かりづらいけど)、 基本、ページ遷移には Transition を使用し、ApplicationBar 項目のクリックイベント内で Navigate する箇所は、非同期処理も追加する形で今のところ良いかと思う。 (2012/2/22 追記) Transition でなんとかなるかと思ったけど、そうでもなかったみたい。困った・・・。

参考情報

 

■ Pivot コントロールのヘッダー フォント サイズの調整

WP7 アプリでは、WP7 特有のコントロールである Pivot コントロールを使うことが結構あると思いますが、デフォルトだとヘッダーのフォント サイズが正直でかいです。 特に、日本語を表示する場合は更にでかく感じます。なので、大きいなと感じたら、適宜調整しましょう。コードは例えば下記(マージンなどは未調整)。


    <!--<controls:Pivot Title="pivot default header" 
                    Name="pvtBase">
        <controls:PivotItem>
            <controls:PivotItem.Header>
                ピボット1
            </controls:PivotItem.Header>
        </controls:PivotItem>

        <controls:PivotItem>
            <controls:PivotItem.Header>
                pivot2
            </controls:PivotItem.Header>
        </controls:PivotItem>
    </controls:Pivot>-->

    <controls:Pivot Title="pivot custom header">
        <controls:PivotItem>
            <controls:PivotItem.Header>
                <ContentControl>
                    <TextBlock Text="ピボット1" FontSize="48" />
                </ContentControl>
            </controls:PivotItem.Header>
        </controls:PivotItem>

        <controls:PivotItem>
            <controls:PivotItem.Header>
                <ContentControl>
                    <TextBlock Text="pivot2" FontSize="48" />
                </ContentControl>
            </controls:PivotItem.Header>
        </controls:PivotItem>
    </controls:Pivot>

上記コードを実行した結果は下記。左がデフォルト、右がカスタマイズ。

 

■ Periodic Task の終了の仕方と、その結果

Background Agent の一つである Periodic Task (たぶん Resource Intensive Task も状況は似ていると思うが、ここでは Periodic Task に限定する) はバックグラウンドで動作するのと、 いくつかの制限が存在するため、デバッグなどが結構困難。ここではまず、Periodic Task の終了の仕方により、バックグラウンドタスクの結果にはどのような値がセットされるかを確認する。 検証コードは、OnInvoke から test() メソッドを呼び、test メソッド内で終了パターンを分けて検証。コードは下記。


Public Sub New()
    DebugOutputMemoryUsage(Me.GetType().ToString & "." & System.Reflection.MethodInfo.GetCurrentMethod.Name)

    If Not _classInitialized Then
        _classInitialized = True
        ' Subscribe to the managed exception handler
        Deployment.Current.Dispatcher.BeginInvoke(Sub() AddHandler Application.Current.UnhandledException, AddressOf ScheduledAgent_UnhandledException)
    End If
End Sub

Private Sub ScheduledAgent_UnhandledException(ByVal sender As Object, ByVal e As ApplicationUnhandledExceptionEventArgs)
    DebugOutputMemoryUsage(Me.GetType().ToString & "." & System.Reflection.MethodInfo.GetCurrentMethod.Name, e.ExceptionObject.ToString())
End Sub

Protected Overrides Sub OnInvoke(ByVal task As ScheduledTask)
    DebugOutputMemoryUsage(Me.GetType().ToString & "." & System.Reflection.MethodInfo.GetCurrentMethod.Name)
    test()
End Sub

Private Sub test()
    DebugOutputMemoryUsage("Start - " & Me.GetType().ToString & "." & System.Reflection.MethodInfo.GetCurrentMethod.Name)

    ' Pattern1 - NotifyComplete
    ' Pattern2 - Abort
    ' Pattern3 - not call both NotifyComplete and Abort
    ' Pattern4 - throw exception and unhandle it
    ' Pattern5 - limitation time exceeded
    ' Pattern6 - limitation memory usage exceeded


    '' Pattern1 - NotifyComplete
    'NotifyComplete()

    '' Pattern2 - Abort
    'Abort()

    '' Pattern3 - not call both NotifyComplete and Abort

    '' Pattern4 - throw exception and unhandle it
    'Throw New Exception("test exception")

    '' Pattern5 - limitation time exceeded
    'For i As Integer = 0 To 30
    '    Thread.Sleep(1000)
    'Next

    ' Pattern6 - limitation memory usage exceeded
    'Dim bytTest() As Byte
    'ReDim bytTest((1024 * 1024 * 10))   ' 10 MB

    'For intIndex As Integer = 0 To bytTest.Length - 1
    '    bytTest(intIndex) = 255
    'Next

    DebugOutputMemoryUsage("End - " & Me.GetType().ToString & "." & System.Reflection.MethodInfo.GetCurrentMethod.Name)
End Sub

見ての通り、パターンは 6 通り。パターン 3 と 5 は、結局やっていることはほとんど同じだが、念のため分けて検証。 結果は下記。実行パターン、トレースログ、実行結果の順に記載。


' Pattern1 - NotifyComplete
NotifyComplete()

*** 4:03:29 - ScheduledTaskAgent1.ScheduledAgent..ctor ***
*** 4:03:29 - ScheduledTaskAgent1.ScheduledAgent.OnInvoke ***
*** 4:03:29 - Start - ScheduledTaskAgent1.ScheduledAgent.test ***

NotifyComplete() が呼び出された時点でバックグラウンドタスクは終了。(ただ、デバッガアタッチ時は少し遅れて kill される気がするので、念のため NotifyComplete() の後に Exit とか入れておいた方が安全かも)
バックグラウンドタスクの実行結果には以下の値がセットされる。

IsScheduled = True
LastExitReason = Complete


' Pattern2 - Abort
Abort()

*** 4:08:31 - ScheduledTaskAgent1.ScheduledAgent..ctor ***
*** 4:08:31 - ScheduledTaskAgent1.ScheduledAgent.OnInvoke ***
*** 4:08:31 - Start - ScheduledTaskAgent1.ScheduledAgent.test ***

Abort() が呼び出された時点でバックグラウンドタスクは終了。(ただ、デバッガアタッチ時は少し遅れて kill される気がするので、念のため Abort() の後に Exit とか入れておいた方が安全かも)
バックグラウンドタスクの実行結果には以下の値がセットされる。

IsScheduled = False
LastExitReason = Aborted


' Pattern3 - not call both NotifyComplete and Abort

*** 4:12:20 - ScheduledTaskAgent1.ScheduledAgent..ctor ***
*** 4:12:21 - ScheduledTaskAgent1.ScheduledAgent.OnInvoke ***
*** 4:12:21 - Start - ScheduledTaskAgent1.ScheduledAgent.test ***
*** 4:12:21 - End - ScheduledTaskAgent1.ScheduledAgent.test ***

25 秒経過時点で killed。AddHandler した UnhandledException は作動しない。
バックグラウンドタスクの実行結果には以下の値がセットされる。とりあえず、何度 ExecutionTimeExceeded が発生しても IsScheduled が False に設定されることはないようだ。

IsScheduled = True
LastExitReason = ExecutionTimeExceeded


' Pattern4 - throw exception and unhandle it
Throw New Exception("test exception")

*** 4:50:10 - ScheduledTaskAgent1.ScheduledAgent..ctor ***
*** 4:50:10 - ScheduledTaskAgent1.ScheduledAgent.OnInvoke ***
*** 4:50:10 - Start - ScheduledTaskAgent1.ScheduledAgent.test ***
*** 4:50:10 - ScheduledTaskAgent1.ScheduledAgent.ScheduledAgent_UnhandledException ***

System.Exception: test exception
   at ScheduledTaskAgent1.ScheduledAgent.test()
   at ScheduledTaskAgent1.ScheduledAgent.OnInvoke(ScheduledTask task)
   at Microsoft.Phone.Scheduler.ScheduledTaskAgent.Invoke(Uri uri, ParameterPropertyBag parameters)
   at Microsoft.Phone.BackgroundAgentDispatcher.AgentRequest.Invoke()
   at Microsoft.Phone.BackgroundAgentDispatcher.InvocationThread()
   at System.Threading.ThreadHelper.ThreadStartHelper(ThreadHelper t)
   at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ThreadHelper.ThreadStartHelper()

AddHandler した UnhandledException が作動する。
バックグラウンドタスクの実行結果として、1 回目は IsScheduled は False セットされないが、2 回目で False がセットされ、バックグラウンドタスクは停止させられる。

1 回目
IsScheduled = True
LastExitReason = UnhandledException

2 回目
IsScheduled = False
LastExitReason = UnhandledException


' Pattern5 - limitation time exceeded
For i As Integer = 0 To 30
    Thread.Sleep(1000)
Next

*** 5:16:13 - ScheduledTaskAgent1.ScheduledAgent..ctor ***
*** 5:16:13 - ScheduledTaskAgent1.ScheduledAgent.OnInvoke ***
*** 5:16:13 - Start - ScheduledTaskAgent1.ScheduledAgent.test ***

25 秒経過時点で killed。AddHandler した UnhandledException は作動しない。
バックグラウンドタスクの実行結果には以下の値がセットされる。とりあえず、何度 ExecutionTimeExceeded が発生しても IsScheduled が False に設定されることはないようだ。

IsScheduled = True
LastExitReason = ExecutionTimeExceeded


' Pattern6 - limitation memory usage exceeded
Dim bytTest() As Byte
ReDim bytTest((1024 * 1024 * 10))   ' 10 MB

For intIndex As Integer = 0 To bytTest.Length - 1
    bytTest(intIndex) = 255
Next

*** 6:35:12 - ScheduledTaskAgent1.ScheduledAgent..ctor ***
*** 6:35:12 - ScheduledTaskAgent1.ScheduledAgent.OnInvoke ***
*** 6:35:12 - Start - ScheduledTaskAgent1.ScheduledAgent.test ***
*** 6:35:12 - ScheduledTaskAgent1.ScheduledAgent.ScheduledAgent_UnhandledException ***

System.OutOfMemoryException: OutOfMemoryException
   at ScheduledTaskAgent1.ScheduledAgent.test()
   at ScheduledTaskAgent1.ScheduledAgent.OnInvoke(ScheduledTask task)
   at Microsoft.Phone.Scheduler.ScheduledTaskAgent.Invoke(Uri uri, ParameterPropertyBag parameters)
   at Microsoft.Phone.BackgroundAgentDispatcher.AgentRequest.Invoke()
   at Microsoft.Phone.BackgroundAgentDispatcher.InvocationThread()
   at System.Threading.ThreadHelper.ThreadStartHelper(ThreadHelper t)
   at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ThreadHelper.ThreadStartHelper()

AddHandler した UnhandledException が作動する。
Unhandled Exception 同様、バックグラウンドタスクの実行結果として、1 回目は IsScheduled は False セットされないが、2 回目で False がセットされ、バックグラウンドタスクは停止させられる。

1 回目
IsScheduled = True
LastExitReason = MemoryQuotaExceeded

2 回目
IsScheduled = False
LastExitReason = MemoryQuotaExceeded


追加の確認事項は下記。

※ バックグラウンドタスク実行中に電源断
 -> 電源断の時点で killed。但し、上述した kill とは異なり、Scheduled Task の Status は何も更新されない。つまり、Status 上は実行された形跡が確認できない。


※※ バックグラウンドタスク実行中に settings -> applications -> background tasks から当該タスクを強制停止

IsScheduled = False
LastExitReason = Other


※※※ バックグラウンドタスク非実行中に、 background tasks から当該タスクを停止

IsScheduled = False
他の項目は更新されない

参考情報

 

■ Periodic Task と Mutex を組み合わせて使う場合の注意事項

Mutex に関する情報(Mutex 使うのであればかなり重要)。 Mutex は、確保したスレッドがミューテックスを解放しないまま終了すると、自動的に破棄される。 そのため、バックグラウンドタスク内で確保した Mutex は、少なくともバックグラウンドタスクが終了した時点で全て解放(破棄)される。 また、NotifyComplete() などを呼んで正しく終了した場合や、制限時間超過などで OS に kill された場合、 発生した例外がハンドルされなかった場合(UnhandledException が呼ばれた場合)なども全てその時点でバックグラウンドタスクが終了するので、 結果的にバックグラウンドタスクのスレッドが全て終了し、確保済みミューテックスも全て破棄される。

逆に、UI 側で確保した Mutex だが、まず、プロセス終了時は自動的に解放される。但し、ドーマント中はアプリがアクティブと判断され、 Mutex も確保されたままの状態となる。ドーマントからツームストーンに移行した場合は、ツームストーンに移行した時点で Mutex が自動的に解放される。 ドーマント状態で待機している間は Mutex の自動解放はないため、基本的には、どんなに遅くともドーマントへの移行時には Mutex を明示的に解放する必要があると思われる。 この辺りはたぶんアンドキュメンテッドな内容なので(見つけてたら試さないし)、OS のバージョンなどによっても実際の動きが変わってくるかもしれない。

通常、.NET の Mutex の扱いとしては、スレッド終了により自動的に破棄されたミューテックスは他のスレッドで確保しようとした際に AbandonedMutexException がスローされるらしいが、 WP7 については、この例外はスローされない。そのため、明示的に解放されたとしても、自動的に破棄されたとしても、次に確保しようとしたスレッドでは、どちらも同様に確保できる (ドキュメントでは、ミューテックスが自動的に破棄された場合、ミューテックスによって守られていたデータの整合性が保たれない可能性があるような記載もあるが、実際の影響度は不明)。

バックグラウンドタスクの OnInvoke メソッドは、バックグラウンドタスクのメインスレッド(コンストラクタが実行されるスレッド)上で実行される。 OS から実行された OnInvoke メソッドが終了しても、メインスレッドが終了するわけではない。ただ、UI スレッドと違って、メインスレッドのメソッドを別途起動するトリガーなどはないので、 メインスレッド上で処理が行えるのは、実質 OnInvoke メソッドのみとなる。ちなみに、コンストラクタでイベント接続した UnhandledException もメインスレッド上で実行される。 簡単にまとめると、バックグラウンドタスクのコンストラクタ、OnInvoke メソッド、UnhandledException イベントは全てバックグラウンドタスクのメインスレッド上で実行されることになる。 且つ、OnInvoke メソッドが終了したとしても、メインスレッドが終了するわけではないので、ここでミューテックスを確保して解放せずに OnInvoke メソッドが終了したとしても、 その時点でミューテックスが破棄されるわけではない(メインスレッドは終了していないので)。UI スレッドで言うところの、OnNavigatedTo メソッドのようなものと考えても問題ないかも。 いずれにしても、バックグラウンドタスクは最大有効期間 20 秒のルールがあるので、ミューテックスが確保された状態となるのは、最大で 25 秒と言う事になる。

ちなみにここでは「メインスレッド」と便宜上呼んでいるが、Dispatcher.CheckAccess では Dispatcher に紐付いていないスレッドと認識される様子(細かいことは良く分かりません)。 そのため、恐らく、別のスレッドから呼ばれた時に、UI スレッドのようにマーシャリングしてメインスレッドで実行などは、できないと思われる。

上記内容は、7.5 Tango で確認。

参考情報

 

■ Periodic Task の登録時に発生し得る例外とその扱いについて

いくつかのパターンがあるが、基本的には、 方法: Windows Phone のバックグラウンド エージェントを実装する に記載されている方法に従えば、問題ない。 ただ、これだけだと実際にどんな結果が得られるのかが分からないのと、実際に試した所、異なる結果が得られるパターンがあったので、その辺りを詳しく。 まず、発生し得る例外と状況は ScheduledActionService.Add Method (Microsoft.Phone.Scheduler) を確認してもらいたいんだけど、列挙すると、以下のようになる。

  1. ユーザーによって当該バックグラウンドタスクが無効に設定されている (InvalidOperationException)
  2. 当該端末で有効にできるバックグラウンドタスクの最大数に既に達している (InvalidOperationException)
  3. 当該端末のメモリが 256 MB (新興国向けロースペックモデル) (InvalidOperationException)
  4. Scheduled Action Service がまだ起動していない (SchedulerServiceException)

まず一つ目については、上述した How to のページに記載されている通りの対処方法で良い。つまり、自分でメッセージを出す必要がある。

次に二つ目だが、こちらについても、上述した How to のページに記載されている通りの対処方法で良い。 ただ、上述したページには、OS がメッセージを出すので、特にプログラム側でやることはないとだけ書いてあって、具体的にどんな結果が得られるのかが不明瞭。 実際には、以下のようなメッセージが例外発生直後、UI とは非同期で表示される。

見てもらえば分かる通り、端末の表示言語によってメッセージの言語も変わるので、プログラム側で何かすることはない。 ただ、このメッセージボックスから設定画面に直接飛ぶことが可能なので、その辺りの流れには対処しておく必要がある。 同時起動可能なバックグラウンドタスクの最大数は最低 6 で、端末によって異なるとの事だが、上記は IS12T での結果。表示内容通り、最大 9 個まで起動可能。

次、三つ目。この制限については、今 (2012/3 頭) の時点では該当端末がそもそもリリースされてないのだが、リリースされる予定のロースペック端末では、バックグラウンドタスクは動作しないとのこと。 256 MB のエミュレータは SDK 7.1.1 に付属しているのでそちらで動作確認できるっぽいけど、自分はまだ入れてないので、実際の動作は未確認。 ただ、対応方法は How to: Disable Features of an Application for a 256-MB Device にまとまっているので、 こちらを参考に対応しておけば良いと思う。要は、この問題については Add メソッド呼び出し時に発生する例外を拾って云々ではなく、アプリ起動時にデバイスをチェックして、 256 MB であればバックグラウンドタスクの機能をそもそも無効にしておけよってことなので、それなりの対処が必要となる。

最後 4 つ目。たぶん、発生し得る例外の中では一番レアな例外だと思う。 要は、OS が起動したからと言って、スケジュールのサービスも必ず起動していると思ったら大間違いですと言うことで、 スケジュールのサービスが起動していない時に操作しようとすると、例外が発生するよと言う事。 ただ、MSDN だとこの例外も Add() メソッド呼び出し時に発生し得る例外としてリストされてるんだけど、実際に試した所、Add() 以前に、 Find() メソッドを呼び出した時点で発生する (もっと言うと、たぶん、ScheduledActionService クラスを操作しようとしたタイミングだとは思うけど、そこまではちゃんと確認してない)。 基本的に、Find() メソッドで登録済みのタスクを確認してから Add() メソッドでの登録を行う流れになると思うので、実質、Find() メソッド呼び出し時に発生し得る例外だと思う。 なので、Find() を try で囲んで、SchedulerServiceException をハンドリングする必要がある。 それと、この例外についての対処方法として、上述した MSDN ドキュメントでは、最大数に達している時と同様、特に何かやる必要はないって書いてあるけど、 スケジュールサービスが利用できないわけなので、個人的には対処する必要あるかなと。 アプリの作りにもよると思うけど、アプリ起動時にチェックするのであれば、この例外が発生したらアプリの再起動を促すような対応が必要かと思う。 実際にスローされる例外は下記 (OnNavigatedTo で Find 呼び出し)。


Microsoft.Phone.Scheduler.SchedulerServiceException: System is not ready
   at Microsoft.Phone.Scheduler.SystemNotificationInterop.CheckHr(Int32 hr)
   at Microsoft.Phone.Scheduler.SystemNotificationInterop.GetNotificationCount()
   at Microsoft.Phone.Scheduler.ScheduledActionService.get_ActionDictionary()
   at Microsoft.Phone.Scheduler.ScheduledActionService.Find(String name)
   at testapp.MainPage.OnNavigatedTo(NavigationEventArgs e)

ちなみに、スケジュールサービスが利用可能でない状況として、端末起動後 1 分以内と MSDN には記載されているが、手持ちの IS12T で実際に確認した所、 OS 起動直後から、大体 5 秒以内ぐらいに Find メソッドが呼び出された場合、SchedulerServiceException が発生した。 つまり、OS 起動後、ほぼノータイムで当該アプリを起動して、アプリの起動時に Find メソッドを呼んでると、発生し得る。 なので、ほとんどないとは思うけど、全くないとも言えない例外。対処はしておいた方が良いと思う。

その他参考情報

 

■ Periodic Task 使用時の注意点とデバッグ方法について

メモリや実行時間の制限など、いくつか注意点はあるが、個人的に一番注意が必要と思うのが、メモリ使用量の制限。 UI プロセスも上限 (こっちはデバイスによって異なる様子) があるとは言え、たぶん、ほとんどの人は普段アプリが使用しているメモリ量は気にしていないと思うし、 実際に OutOfMemory 例外が発生することもほとんどないと思う。 けど、バックグラウンドタスクは、別。MSDN ドキュメントにも記載されている通り、バックグラウンドタスクのプロセスが使用できるメモリ上限は、6 MB (6291456 byte)。 更に注意が必要なのは、プロセス全体のリミットとなるので、ロードされた DLL なども全て含んだ上でのリミットとなり、実際に使用できるメモリの使用量はもっと少ないと言う事。 この辺りは実際に確認すれば分かることなんだけど、デバッグ実行時に OnInvoke() メソッドの先頭でその時点のメモリ使用量を出力した場合(つまり何も処理する前の状態)、 以下のような結果が出力される。


*** 8:32:59 - ScheduledTaskAgent1.ScheduledAgent.OnInvoke ***
MemoryLimit = 6291456, MemoryUsage = 4567040, MemoryRemaining (KB) = 1684, Peak = 4567040, SafetyMargin (KB) = 1684

一番見るべきところは MemoryRemaining の値なんだけど、1684 KB となっている。つまり、何もしていないにも関わらず、もう残っているメモリ領域が 1.6 MB 程しかないと言うことになる。 とは言ったものの、これにはちょっと訳があって、上記はデバッガをアタッチしている状態なので、通常よりもメモリ消費量が多い。 デバッガをアタッチしていない場合は、以下のような出力となる。


*** 4:03:29 - ScheduledTaskAgent1.ScheduledAgent.OnInvoke ***
MemoryLimit = 6291456, MemoryUsage = 2576384, MemoryRemaining (KB) = 3628, Peak = 2576384, SafetyMargin (KB) = 3628

今度は 3628 KB、つまり、大体 3.5 MB ぐらいは残っていることになる。そんなわけなので、大体 3 MB ぐらいの中で、バックグラウンドの処理をやりくりする必要がある。 実際、3 MB でどんな処理が出来るのかというのが問題になるけど、例えば、Bitmap 画像を扱うような処理を行ったら、速攻でアウトになる可能性がある。 この辺りは、実際に動作確認しないと、何とも言えない。

バックグラウンドタスクをデバッグしていて陥りやすい罠が、デバッガがアタッチされている場合の挙動が、デバッガがアタッチされていない場合の挙動と結構違うという点。 MSDN ドキュメントにも記載されているけど、まず、デバッガがアタッチされている場合、メモリ上限と実行時間上限の制限は、無効になる。 なので、デバッグ実行だけしていると、そもそも上記の問題が発生している事に気がつけない。 また、上述したように、Debug ログでメモリ使用量を出力したとしても、実際の実行時の値以上の使用量が出力されるため、正確な状況を把握することが困難。 これは特に、ギリギリまでメモリを使用する可能性がある場合に、デバッグログのメモリ使用量は当てにならない事を意味する。 また、デバッガがアタッチされていると、本来 6 MB 超えそうになった時点で作動するはずの GC も、作動しない。 そんな状況なので、使用メモリ量にフォーカスしてデバッグする必要がある場合、Isolated Storage に都度ログを出力する仕掛けを行なっておき、 デバッガをアタッチしない状態で動かして、出力されたログを確認する必要がある。

後、バックグラウンドタスクのデバッグに有効な方法としては、MSDN のサンプルにもあるけど、LaunchForTest() メソッド使用による起動時間の短縮と、 OnInvoke() の先頭で ShellToast を使ってトーストを起動し、バックグラウンドタスクが起動された事を確認する方法がある(ShellToast の使用には注意しておくべき点があるが、それは後述する)。 プラス、上述したようにメモリ使用量を ISF に出力する仕掛けを行えば、良いと思う。 メモリ使用量を出力する仕掛けとしては、例えば、下記。


<Conditional("DEBUG")> _
Public Shared Sub DebugOutputMemoryUsage(ByVal strLabel As String)
    Dim strLog As String = ""

    Dim lngMemoryLimit As Long = 0
    Dim lngMemoryUsage As Long = 0
    Dim lngRemaining As Long = 0
    Dim lngPeak As Long = 0
    Dim lngSafetyMargin As Long = 0

    lngMemoryLimit = Microsoft.Phone.Info.DeviceStatus.ApplicationMemoryUsageLimit
    lngMemoryUsage = Microsoft.Phone.Info.DeviceStatus.ApplicationCurrentMemoryUsage
    lngRemaining = lngMemoryLimit - lngMemoryUsage
    lngPeak = Microsoft.Phone.Info.DeviceStatus.ApplicationPeakMemoryUsage
    lngSafetyMargin = lngMemoryLimit - lngPeak

    strLog &= vbNewLine

    If strLabel <> "" Then
        strLog &= String.Format("*** {0} {1} {2} ***{3}", DateTime.Now.ToLongTimeString(), Thread.CurrentThread.ManagedThreadId, strLabel, vbNewLine)
    End If

    strLog &= String.Format("MemoryLimit = {0}, MemoryUsage = {1}, MemoryRemaining (KB) = {2}, Peak = {3}, SafetyMargin (KB) = {4}", _
                            lngMemoryLimit, lngMemoryUsage, lngRemaining / 1024, lngPeak, lngSafetyMargin / 1024) & vbNewLine

    'GC.Collect()
    'GC.WaitForPendingFinalizers()
    'GC.Collect()

    'lngMemoryLimit = Microsoft.Phone.Info.DeviceStatus.ApplicationMemoryUsageLimit
    'lngMemoryUsage = Microsoft.Phone.Info.DeviceStatus.ApplicationCurrentMemoryUsage
    'lngRemaining = lngMemoryLimit - lngMemoryUsage
    'lngPeak = Microsoft.Phone.Info.DeviceStatus.ApplicationPeakMemoryUsage
    'lngSafetyMargin = lngMemoryLimit - lngPeak

    'strLog &= "Invoked GC" & vbNewLine

    'strLog &= String.Format("MemoryLimit = {0}, MemoryUsage = {1}, MemoryRemaining (KB) = {2}, Peak = {3}, SafetyMargin (KB) = {4}", _
    '                        lngMemoryLimit, lngMemoryUsage, lngRemaining / 1024, lngPeak, lngSafetyMargin / 1024) & vbNewLine

    Debug.WriteLine(strLog)

    Using store As IsolatedStorageFile = IsolatedStorageFile.GetUserStoreForApplication
        Using sw As New StringWriter
            Using writer As IsolatedStorageFileStream = store.OpenFile("log.txt", FileMode.OpenOrCreate)
                writer.Seek(0, SeekOrigin.End)

                Using myStreamWriter As New StreamWriter(writer)
                    myStreamWriter.Write(strLog)
                End Using
            End Using
        End Using
    End Using

End Sub

GC は 6 MB 超えそうになった時点で自動的に起動されるので、特に明示的に行う必要はないと思う。もちろん、開放できる状態になっているように、プログラムする必要あるけど。

後、これも MSDN ドキュメントに記載されてるけど、バックグラウンドタスクは UI プロセスの起動とは無関係に(非同期に)裏で勝手に実行されるので、 バックグラウンドタスクとデータのやり取りを行う場合は、同時に触ることがないように、気をつける必要がある。 MSDN ドキュメントでは、ISF プラス Mutex による直列化でスレッドセーフを保つことが推奨されている (LINQ 2 SQL も推奨されてるけど、自分は使った事無いので良く分かりません)。

後、最後に、これは Periodic Task とは直接は関係ないんだけど、OS の仕様として、Wi-Fi 接続のみ行ってる端末がバッテリー駆動状態でスクリーンロックした場合、Wi-Fi 接続が切断される動作がある。なので、直接は関係ないけど、Periodic Task で通信を行う場合、ほぼ確実に影響が出る制限となる。 プログラムではどうしようもないけど、ユーザーへの説明は必要かと思われる。

あ、それとそれと、ここではメモリ使用量に焦点を当てて説明したけど、25 秒の時間制限も、場合によっては引っかかる可能性がある。 たぶん一番可能性が高いのが、ネットワークへのアクセス。一回ならともかく、複数回直列でネットワークアクセスしているようだと、レスポンス具合によっては制限時間内に処理が終わらない可能性が出てくる。 バックグラウンドタスクは止まってないけど、処理が完了してないみたいな状態だと、制限時間を超えてしまった可能性が高い。 そのような場合、少なくともリクエストを行う直前とレスポンスが返ってきた直後にログを出力するようにして、ネットワーク呼び出しに想定以上の時間がかかっていないか、確認する必要がある。

参考情報

 

■ ShellToast 使用時の注意点

MSDN ドキュメントにも記載されているが、ShellToast 使用時の注意点として、ShellToast.Show() メソッドの実行元が現在実行中のアプリだった場合、メソッドが呼ばれてもトースト表示されないという動作がある。 ここで注意しなくてはいけないのが、この動作は、UI 側のプロセスから呼ばれた時にのみ該当するわけではなく、バックグラウンドタスクから呼ばれた場合も同じだと言う事。 バックグラウンドタスクは UI プロセスとは非同期で動作するが、この時に、当該バックグラウンドタスクに紐付く UI プロセスが実行中だと(ドーマントは実行中の扱いにはならない)、 ShellToast.Show() メソッドが呼ばれてもトースト表示はされない。当該バックグラウンドタスクに紐付く UI プロセスが実行中でなければ表示されるので、 例えば他のアプリを実行中の場合は、当該トーストは表示される。 特にデバッグのためにバックグラウンドタスクにトースト通知を仕込んで確認していると、トーストが表示されないとバックグラウンドタスクが実行されていないように誤解してしまうことがあるが、 トーストの表示とバックグラウンドタスクの実行は直接は関係ないので、注意する必要がある。

参考情報

 

■ バックグラウンドタスクからタイルを更新する場合に、アプリケーション タイルとセカンダリ タイル、どちらを使うか

アプリケーション タイルは、スタートに pin されているかどうかに関わらず、必ず存在する。 また、プログラムからスタートに pin されているか確認する方法がない(今のところ見つけられていない)。同様にスタートから削除する方法もない。 セカンダリ タイルは、スタートへの pin はプログラムからしか行えないが、削除はプログラムでもユーザーの操作でも可能。

上記仕様の違いから、Scheduled Task でライブタイルの情報を定期的に更新するようなアプリの場合、仕様にもよるが、セカンダリ タイルを使用した方が無難。 セカンダリ タイルであれば、スタートに pin されているかどうかをチェックできるので、pin されている時だけバックグラウンドの処理を行うことが可能になる。 アプリケーション タイルを使うとこれができないので、コストがかかる処理を行う時は結構痛い。

対象セカンダリ タイルが pin されているかを設定情報などに持つ(キャッシュレベルの扱いにはなるが)のであれば、 Application_Launching と Application_Activated の時点でチェックして状態を格納しておけば良いと思う(アプリ起動中にユーザーが削除することはできないので)。

Windows Phone のタイルの概要

 

■ DataContext と ItemsSource の違い

DataContext は全てのコントロールに実装されてる。ItemsSource はリスト系のコントロールのみ。 データソースの設定は、DataContext しかないコントロールは DataContext 使用で良い。 ItemsSource が存在する場合、ItemsSource にソースを接続するか、 ItemsSource="{Binding}" として DataContext の内容を ItemsSource に流してやる必要がある(そうしないとデータが表示されない)。

wpfのDataContextとItemsSourceの違いを教えてください - Yahoo!知恵袋

 

■ コンパスのデバッグ方法について

まあ、そんな大した話ではないんですが、コンパス機能を実装した際、キャリブレーションの動作チェックや、コンパスが有効でない端末上での動作チェックとかが必要になると思います。 で、その 2 点をチェックする際、どうやれば簡単にチェックできるかというお話です。

まずキャリブレーションですが、アプリに実装すれば分かると思いますが、精度誤差が 10 を超えると、キャリブレーションが必要であることを示すイベントが発生します。 なので、精度誤差を広げれば、結果的にキャリブレーションのイベントが発生します。で、どうやって広げるかというと・・・ええ、普通のコンパスと同じで、 磁石を端末に近づけることで無理やり方位を変えて、結果、誤差を広げられます。ただ、そもそも精密機械に磁石の発想が自分でもやばい気がするので、実験する時は自己責任でお願いします。 それと、これは自分の端末でしか試してないので確証は持てないのですが、IS12T については精度誤差が 10 固定になっている気がします (2012/3 時点)。 そのため、磁石をいくら近づけた所でキャリブレーションのイベントは発生しませんし、キャリブレーションの動作を行なっても精度誤差は (プロパティ上は) 変化しません。 なので、この実験を行うとしたら、IS12T 以外の端末で試すことをお勧めします。

次、コンパスが有効でない端末上でのテストなんですが、これは、少なくとも Mango のエミュレータであればコンパス機能がないので、エミュレータでテストすることができます。

 

■ バインドした要素の値更新が画面初期表示時に間に合わない場合

画面の描画 (xaml の描画) はページの Loaded() に入った時点で行われるので、それまで (OnNavigatedTo() 以前) に Binding した値は画面表示時には反映された状態で表示されて欲しい。 けど、一部の更新が間に合わず、画面初期表示時に一瞬だけ古い値の内容で要素が描画されることがある。 特に、他の要素の値を参照している時や、Converter を使用している時など単純なバインドでない要素で発生しやすい気がするが、 取り敢えずの対処方法としては、ページのコンストラクタまたは OnNavigatedTo() でページ等 (できればバインディングしている要素を含む最小限の要素が理想) の Opacity を 0 に設定し、 Loaded() で 1 に戻す。簡単に確認した限りは、Loaded() に入った時点で取り敢えず最新の状態で描画されるっぽい。 なので、描画が間に合わない現象は、OnNavigatedTo() が終了して描画処理が始まり、Loaded() が呼ばれるまでの間に発生している現象のように見える。

 

■ バインドしたデータが表示されない場合のデバッグ方法について

xaml 要素にバインドしたデータが全く表示されない場合、原因を特定するのが結構困難なことが多いのですが、まずは VS の出力ウィンドウでデバッグの出力を確認します。 以下のような情報が出力されていることがあるので、その場合はその内容を基に調査を行います。


System.Windows.Data Error: BindingExpression path error: 'Name' 
property not found on 'AppRanking.Category' 'AppRanking.Category' (HashCode=130598672). 
BindingExpression: Path='Name' DataItem='AppRanking.Category' (HashCode=130598672); 
target element is 'System.Windows.Controls.TextBlock' (Name=''); 
target property is 'Text' (type 'System.String')..

独自のクラスをバインドさせているのであれば、INotifyPropertyChanged の実装や public property として対象が公開されているかどうかなども確認します。Collection であれば、ObservableCollection かなども確認。

 

■ Binding で FindAncestor が使えない

WP7 (Silverlight) だと xaml の binding で FindAncestor が使用できないため、特定の状況、例えば Listbox などの中に、親要素への binding が必要な要素が含まれている状況で binding が困難になります。 そのような場合の対応策としては、Page に Name を付与し、ElementName でそのページを対象とするように設定することで、現在の Source 範囲から抜け出しての binding が可能となります。 後は、ここに書かれている内容が使えそうですが、自分はまだ試していません。

 

■ チラシの裏 for ListBox コントロール

ListBox コントロール内の ScrollViewer は ListBox の Visibility が false だと取れないようなので、ListBox の表示・非表示は Visibility ではなく、Opacity で対処するようにする。

とは言ったものの、個人的に ListBox コントロール使用時に気になることがあって、何かと言うと、リストボックスの下の余白部分。 スクロールしている時は下の余白はゼロでいいんだけど、一番下のアイテムまで到達した場合、余白が全くないと、個人的には結構見づらいし、タップもしづらい。 けど、単純にリストボックスのマージンで調整すると、常に余白ができてしまい、表示領域も狭くなってよろしくない。 で、色々と試行錯誤してみたんですが、今のところ考えついた方法は、リストボックス内部のスクロールは無効にして、ListBox の外に ScrollViewer を別途用意する方法。 で、ListBox の下に余白用のコントロールを設置。具体的には以下の様な xaml になる。


<ScrollViewer>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <ListBox Grid.Row="0">

            <!-- 内部 ListBox の Scroll 防止のため、ItemsPresenter を使用 -->
            <ListBox.Template>
                <ControlTemplate TargetType="ListBox">
                    <ItemsPresenter/>
                </ControlTemplate>
            </ListBox.Template>
        </ListBox>

        <TextBlock Grid.Row="1"
               Margin="24" />
    </Grid>
</ScrollViewer>

マージンの量は適宜調整。上記では ListBox のスクロール防止に ItemsPresenter を使用しているけど、もっと他に良い方法があるかもしれません。 上記方法だとそもそもリストボックスの内部 ScrollViewer は使用しないので、前述の Visibility が false だと取れないとかの問題も関係なくなる。

 

■ チラシの裏 for バインディング

OS のバージョンにも依存すると思うが、バインディングの対象を Element にした場合、評価が遅れる可能性がある。 例えば、あるコントロールの Visibility プロパティを他の要素のプロパティにバインディングして、自動的に表示・非表示を処理しようとしている場合、 本来であれば画面の表示前に評価が終わっていて欲しいが、Page_Loaded イベント以後にずれ込む事がある。 結果、画面表示時に一瞬だけ対象コントールが表示されてしまったりして、あまり見た目が良くない。 この場合、Element にバインディングするのではなく、判断基準となる大本のデータに直接バインディングする事で遅延の発生を防げる様子(Tango 時点)。

 

■ Web service への参照を VS で追加した時に ServiceReferences.ClientConfig が更新されない問題について

例えば、Bing Maps SOAP Services を利用しようと思って VS で参照に追加しても、追加した Web Service がなぜか正常に認識されていないようで、プログラムから使えないという問題です。 結論としては、どうやら Windows Phone 7 プロジェクトを使用している場合の VS のバグのようで、Client Config が更新されないことが直接の原因のようです。 下記フォーラムにある通り、一度当該 Service と bin、obj フォルダを削除し、VS を再起動した後、再度 Service を追加することで、対応できるようです。

 

■ アプリケーションを強制的に終了する方法について

Exit 的なメソッドが WP7 では用意されていないため、単純にこれをやろうとすると、考えつくのはプログラムで GoBack を実行することだと思いますが、 やると分かりますが、GoBack では終了できず、例外が発生します。なぜかというと、CanGoBack が False だから。 代替案として技術的にはいくつか方法はあるようですが、技術的な視点、作法的な視点、審査的な視点から考えると、以下のリンクの通り、 プログラムで終了させるのではなく、プログラムではアプリ使用不可の状態にした上で Back ボタン押下で終了できる状態にし、 ユーザーに Back ボタン押して終了してもらうのが、個人的には一番良いかなと思いました。

 

■ アプリケーションの自己同一性について

WP7 にインストールするアプリは、例えタイトルやアセンブリ名が違っていても同じアプリとして認識されたり、もしくはタイトルやアセンブリ名などが全く同じでも別のアプリとして認識されたりしますが、 この自己同一性の確認を OS はいったいどうやって行っているのかという内容です。 公式なドキュメントはちょっと見つけられていないのですが、恐らく以下の内容であっていると思います。

まず、結論から先に言ってしまうと、たぶん、ProductID のみで判断しています。ProductID は WMAppManifest.xml で定義されています。 そのため、タイトルやアセンブリ名称をいくら変えようと ProductID さえ同じであれば同じアプリとして OS には認識され、他の情報が全て同じでも ProductID さえ異なれば、別のアプリとして OS に認識されます。 開発したアプリをマーケットプレイスに登録した場合、マーケットプレイスからダウンロードしたアプリと自分が手元でデバッグしていた同じアプリはタイトルなどが同じでも別のアプリとして OS で共存できますが、 これはマーケットプレイス登録時に、マーケットプレイス登録用の ProductID が新しく割り当てられるためかと思います。 マーケットプレイス登録時に新しい ProductID が割り当てられる件は下記ドキュメントにも記載があります。

Application Manifest File for Windows Phone

アプリケーションのインストールポイントやアプリ個別の Isolated Storage などは全てこの ProductID で判断されます。 新規にプロジェクトを作成している限りは問題ないと思いますが、例えば、既存の NoDo ターゲットのプロジェクトを Mango ターゲットにアップグレードして、NoDo 用と Mango 用で別々に開発しようと考えている場合、 アップグレード時に ProductID が変わることはないため、別々のアプリとして OS からは認識されず、少しめんどくさいことになります。 既存のプロジェクトをフォルダ丸ごとコピーして、名称だけ変えて新しいプロジェクトとして開発しようとしている場合も同様です。

上記のような場合に別のプロジェクトとして OS に認識させたい場合は、WMAppManifest.xml に定義されている ProductID を変更すれば OK です。 実際にアプリが ProductID を基にインストールポイントなどを判断しているであろう情報は、application.Current.Host.Source の情報などでも確認できます。

SilverlightHost メンバー (System.Windows.Interop)

 

■ パフォーマンス関連

ほとんど上記のドキュメントに書いてありますが、自分がこれまでアプリを作成してて、特に気を付けないといけないなぁと思ったのは、例えば下記。

 

■ パフォーマンスの解析

SDK 7.1 Beta2 からパフォーマンスの測定を行える機能が追加されています。 対象プロジェクトのターゲットが OS 7.1 となっている必要がありますが、IDE のメニューから、[Debug] - [Start Windows Phone Performance Analysis] で起動できます。ただ、これでアプリを起動した場合、出力ウィンドウへの出力など使えない機能が出てくるっぽいので、少し注意。 WP7 アプリを審査に通す場合、レスポンスが何秒以内に返ってくる必要があるとかの要件もあるので(まあ、そういう状態になるアプリは不要だろとか言われたらそれまでなのですが)、そういうのも含め、ボトルネックになっている箇所を見つけるのに重宝すると思います。

Windows Phone Profiler

 

■ JSON 文字列から雛形のクラスを自動生成

最近は Web サービスも増えて、WP から(別に WP じゃなくてもいいんですが)利用する機会もあるかと思います。 それらを利用する場合、受け渡すデータの形式は XML か JSON が多いのですが、JSON だとデータを格納する雛形クラスが必ず必要になります。 項目数が多いとこのクラスを一から作成するのがかなりしんどいのですが、VS2012 の機能で、元データから推測して対応するクラスを自動的に作成してくれる機能が追加されたようです。 但し、少なくとも Express Edition の VS の場合は for Web のみの機能のようなので、例えば for Windows Phone を使っている場合はこの機能を使うためだけに for Web を別途インストールする必要があります。 とは言え JSON 用クラスの自動生成は間違いなく便利なので、JSON 使うならこの機能のためだけに for Web を入れておいても損はないと思います。 以下、具体的な手順です。

  1. 今回は次の URL の JSON を例にしてみます。Album (JSON)
  2. まず、手持ちの VS2012 で今回の機能が使えるか確認します。今回の機能は、VS2012 を起動してソース編集状態にした際、EDIT メニューに「Paste Special」として出てきます。 位置は通常の Paste のすぐ下なので、あるかないかはすぐに分かると思います。
  3. なければ、まず機能を使えるようにする必要があります。パッケージの VS の場合、for Web の機能も含まれていると思いますので、VS の Update で入るかもしれません(ただ、自分は試してません)。 VS Express for Windows Phone を使ってるなどしている場合は、別の Express 製品である、Visual Studio Express 2012 for Web を入れます。
  4. EDIT メニューに Paste Special が出て機能が使えるようになったら、実際に変換を開始します。
  5. 変換に必要な前準備は非常に簡単で、対象の JSON データをクリップボードにコピーしてから、クラスを生成するソースの場所にカーソルを置いて、Paste JSON As Classes を実行するだけです。
  6. JSON データをコピーする方法は大きくあると二つあると思います。一つは、前述のように JSON のサンプルが Web ページなどにある場合。その場合、それをそのままコピーします。 ただ、一番確実なのが、実際に JSON データを受け取って、その文字列を使用することです。実際のデータを使用するので、サンプルが古いなどの心配をする必要もありません。
  7. 私は JSON には全く強くないのですが、JSON データをコピーする時は最初と最後の中括弧も必ず含めるように注意します。 こうなると最上位のクラスの名前がない事になるので心配になるかもしれませんが、これは適当な名前で良いようで、自動生成時に適当なクラス名が振られるので、 ここでは気にする必要はありません。最初が { で 最後が } の JSON データを丸ごとコピーして OK です。 実際のデータが複数回繰り返して出現しているとかも、気にする必要はないです。丸ごとコピーして、VS のコマンドを実行します。
  8. Paste JSON Classes を使用してクラスが生成できたら、最上位のクラスは「Rootobject」クラスとして生成され(他の名前に変えても OK)、後はデータに従って名前付けされたクラスが生成されるはずです。
  9. これで JSON を使えるようになります。・・・と言いたいところなのですが、もしかしたら DataContractJsonSerializer クラスの ReadObject メソッド実行時に、 InvalidCastException が発生するかもしれません。これについては明確な情報を見つけられなかったのですが、 どうやら JSON クラスで配列構造を取っているメンバーの、自動生成コードでの表現が通常の配列となっているのが問題のようで、 例えば Items() As ClassA となっている個所を、Items As List(Of ClassA) のように List を使うように変更することで、対処できるかもしれません。 ちなみにこれは VB でしか試してないのと、ネット上では同様の問題についての情報が見つけられなかったので、VB 限定の問題かもしれません。
  10. ここまでの作業で取りあえず受信した JSON をクラスに格納することは可能になったはずですが、配列の件以外にもいくつか注意点が。 まず一つ目が、生成されたクラスの構成は、冗長な可能性があるという事。自動生成なのでしょうがないのですが、同じ Structure を使用しているのに、 異なるクラスとして複数のクラスが生成される場合があります。単純に使用する分には特に問題ないですが、以降のメンテや可読性を考えると、重複しているクラスは省いて適切な参照に集約した方が良いと思います。 二つ目ですが、これもしょうがないのですが、自動生成時に存在しないオプショナルな要素については、メンバが生成されません。 最新のデータ構造を確認し、必要であれば手動で追加しておく必要があります。
  11. JSON データの ReadObject 時にエラーが発生すると原因の絞り込みがかなり困難なので、ここからは自分用の備忘録。 まず、読み込むだけであれば、雛形クラスへの DataContract 属性の付与は必須ではない様子。なくてもデータは格納される(逆の場合は必須のはず)。 次に VB の場合は、Public Property Albums As Albums みたいな形でコードが自動生成されるので、クラス名とプロパティ名が同じで大丈夫かと心配するが、 これも問題ない。クラス名を変更しても、それはそれで問題ない。 まとめると、配列の記載を List に変換する以外は、自動生成されたコードをそのままの状態で使用することができる、はず。
 

■ タイトルとかに表示するアルファベットを大文字にするか、小文字にするか

タイトルとかラベルとかにアルファベットを表示する時の話です。動作には関係ないのでどうでもいいように思えますが、結構迷ったりすることが多いと思います。 一応、以下のようなガイドラインがあるので、基本はこれに従えば良いと思います。

全て小文字にする

必要に応じて単語の先頭を大文字にする

全て大文字にする

上記内容は下記ドキュメントからの引用です

Windows Phone 7 向け UI デザイン/操作ガイド (XPS、3.80 MB | PDF、3.29 MB)

 

■ アイコン集

まず、SDK についてくるアイコンは下記フォルダ参照。

C:\Program Files (x86)\Microsoft SDKs\Windows Phone\v7.1\Icons

ネットで公開されているアイコン集は色々あると思いますが、とりあえず自分が見つけたのは、下記(多謝)。それっぽいのがない場合、何かをベースにして自作に走るしかないかもしれない。

 

■ エミュレータをキーボードで操作する場合のショートカットキー

基本的なところとしては、下記。

全ての一覧は下記参照

Keyboard Mapping for Windows Phone Emulator

 

■ TextBlock に適用可能なフォント スタイルの、それぞれの表示のされ方

WP7 アプリでは、可能な限り既定で用意されたスタイルを使用するのが推奨される。使用可能なスタイル (と、それぞれのフォントサイズ) は下記。

使い方は例えば下記。


<TextBlock Text="テキスト" Style="{StaticResource PhoneTextNormalStyle}"></TextBlock>

詳細については下記ドキュメント参照。

Theme Resources for Windows Phone

ここでは、TextBlock に適用可能なスタイルに絞って、それぞれの表示のされ方を示します。まず、上記 MSDN ドキュメントの記載通りに並べると、以下のようになる。

wp7_jp_font_msdn_order.png

次に、フォントサイズの順に並べ直すと、以下のようになる。

wp7_jp_font_size_order.png

上記どちらも、Mango アプリで Language="ja-JP" 設定を行った状態で表示しています。 また、PhoneTextContrastStyle スタイルのみ、そのままだと前景色と背景色が同じで文字が見えないので、TextBlock の背景を変えています。

 

■ ブラシ リソースの、それぞれの表示のされ方

今度はブラシリソースについて。まず、使用可能な一覧は下記。

Button コントロールの背景色として上記のブラシをそれぞれセットした時の表示のされかたは、下記。 左半分はテーマが dark、右半分は light の時の実行結果。左端の緑の帯は、透過のされ具合を確認するために表示しています。

wp7_brush_resources.png

スタイルの時と同様、そのままだと前景色と背景色が同じで文字が見えない場合は、Button の前景色を変えています。

 

■ Font Family 毎の表示のされ方

FontFamily プロパティの設定値による表示のされ方の違いは、表示サイズの問題により別ページに用意しました。

 

■ 日本語フォントを使用する(適切な東アジア言語のフォントを使用する)

ちゃんとした日本語フォント表示を行うには、基本的にはアプリ側での対処が必要。下記 2 点を実施。

各ページの Language プロパティに "ja-JP" を指定するよりもっと簡単で、且つ他の東アジア言語にも対応可能な方法は、下記ページを参照。

各国のUIフォントに対するためのより簡単な対応方法 - 高橋 忍のブログ - Site Home - MSDN Blogs

大多数のユーザーの環境に対応する方法としては、上記方法で問題ないし、対処も簡単。 ただ、上記方法は OS の設定 (CurrentUICulture) に依存する方法となるので、この設定が想定していない状態の場合、期待しない結果となってしまう。 通常、OS の表示言語の設定には、使用している言語が設定されているはずで、上記方法もこれが前提となる。 つまり、OS の表示言語に「日本語 (ja-JP)」が設定されていれば、Language プロパティにも自動的に "ja-JP" が設定されるので、日本語のフォントが適切に使用される。 韓国語 (ko-KR) が設定されていれば、Language プロパティにも "ko-KR" が設定され、韓国語のフォントが適切に使用される、はず。

今のところ、日本語 SIP やメッセージボックスなどのクラスも、OS の表示言語の設定をその内部で Language プロパティに独自に割り当てて使用しているようなので、 東アジア言語のフォントを正しく使用するには、ユーザーが表示言語の設定に当該言語を指定し、且つアプリ側では表示言語の設定を Language に指定することが、 作法ということになると思う(一般のユーザーがそんなことを意識するはずがないというのはまた別として)。 と言うか、OS の設定としてこの問題に対処するための設定項目が用意されていない以上、表示言語で判断する以外に方法がない。

ただ、例えば、英語を母国語とする外人が、日本語の勉強をしようと、日本語が表示されるアプリを使用する場合はどうだろうか。 また、(自分がそうなんだけど)日本人でも表示言語の設定は英語にして使用しているという人も、いるはず。 そういう場合、表示言語の設定は英語とかになっているはずなので、結果として、上記方法だと東アジアの言語フォントとしては、簡体字中国語が使用される(恐らく)。 割合的には多くないかもしれないが、もしそういう状況にもより適切に対処したい場合は、恐らく、アプリの設定としてそのような設定を独自に持つしかないと思われる。 例えば、以下のように対処する。設定箇所は、上述した参考ページの対処方法通り、App のコンストラクタ。


' GetFontSettingForEastAsianLanguage() では、アプリの設定項目として用意した東アジア言語用の設定が返されるとする。
' 選択可能な項目は、"os"、"ja-JP"、"zh-CN"、"zh-TW"、"ko-KR" の 5 項目。"os" は OS 設定に準拠する項目として使用する。
Dim strCultureForFont As String = GetFontSettingForEastAsianLanguage()

If strCultureForFont = "os" Then
    RootFrame.Language = System.Windows.Markup.XmlLanguage.GetLanguage(CultureInfo.CurrentUICulture.Name)
Else
    RootFrame.Language = System.Windows.Markup.XmlLanguage.GetLanguage(strCultureForFont)
End If

 

■ Language プロパティと FontFamily プロパティの使用について

表示文字に対する適切なフォントの選択は、対象文字列と Language プロパティ (Default: "en-US")、FontFamily プロパティ (Page default: "StaticResource PhoneFontFamilyNormal" = "Segoe WP") の設定を基に決定される。 基本的には、Language プロパティ及び FontFamily プロパティが未設定 (既定値使用) であっても、表示文字によって適切なフォントが選択されるので特に問題ない。 また、対象言語の表示に使用できるフォントが一つしかない場合、Language プロパティと FontFamily プロパティの設定に関わらず、そのフォントが選択される。 ただ、日本語と中国語のように似通っている文字体系の場合、日本語文字列でも中華フォントが選択されてしまうので、この場合は明示的に Language プロパティに "ja-JP" を設定する必要がある (もしくは FontFamily に "Yu Gothic")。 以上のように、基本的には Language プロパティを設定すればフォントに関する問題は発生しないように見えるが、例外がある。 それは、WP8 で追加されたフォントを使う必要がある WP7 アプリの場合。具体的には、インド系のフォントである、Nirmala UI を使いたい場合。 WP8 端末にはこのフォントがインストールされているはずなので、Nirmala UI を FontFamily プロパティに指定すれば、インド系の文字列が正しく表示できる。 で、WP8 をターゲットとしたアプリの場合は、表示文字を判定して自動的にこのフォントが使用されるので、問題は発生しない。 但し、対象アプリが WP7 をターゲットとしている場合、WP8 上でも WP7 端末として動作するので、結果として、Nirmala UI フォントが自動選択されることはなく、適切でないフォントが使用されて文字化けが発生する。 この場合でも、FontFamily に Nirmala UI を明示的に指定すれば、WP8 で正しく表示できる。

なお、WP8 のプロジェクトテンプレートでは、App.xaml のコードに InitializeLanguage() が追加されていて、RootFrame の Language と FlowDirection についてはリソース設定を基に初期化しているので、注意する。

動作検証から判断した、使用フォント決定フローは下記。

  1. 表示対象文字列を確認。表示可能なフォントが一つしかない場合、そのフォントを使用する。そうでなければ次の判定へ。
  2. FontFamily プロパティに指定されたフォントが、表示対象文字列の言語圏用のフォントであれば、そのフォントを使用する。そうでなければ次の判定へ。 (例:漢字が対象文字列に含まれていて、FontFamily が Yu Gothic や Microsoft MHei などの漢字表示可能フォントであれば、そのフォントを使用。Segoe WP などの漢字表示用フォントでなければ、次の判定へ進む。 Language プロパティで判定させたいのであれば、FontFamily は未設定のままにしておくか、Segoe WP を設定しておけば OK。)
  3. Language プロパティに指定された言語用のフォントが存在すれば、そのフォントを使用する。そうでなければ次の判定へ。 注意点として、適切なフォントを選択させるためには、言語情報だけではなくリージョン情報も含めた設定 (例:"ja" ではなく "ja-JP") を行う必要がある。
  4. ここまで判定処理が進んだ場合、表示に一番適切と判断されたフォントを OS が自動で選択。

まとめると、下記。

Font and language configuration support for Windows Phone

 

■ 東アジア言語用の Font Family について

アプリを国際化対応する場合、東アジア言語のフォントを考慮する必要があると思います。 ただ、自分はフォントには全く詳しくないので、とりあえず手当たり次第、関連プロパティの設定を行い、それぞれ表示されるフォントを確認してみました。 まず、確認していて思ったのが、Language プロパティは明示的に設定しない限り、OS の設定によらず常に "en-US" が設定されてるんじゃないかということ。 明示されているドキュメントは確認できなかったけど、東アジア言語は Language プロパティを明示的に設定する必要があるとの記載は MSDN にあったので、そういうことなのだろうか。 ちなみに、Language が未指定 (= "en-US") だと、最終的に Microsoft YaHei が使用されるようです。 これが日本でよく言われる中華フォント表示の流れと思われる。

それとこれはまあ何となく分かってたけど、端末によって用意されているフォントに差異がある。 Mango SDK エミュレータと、IS12T と、ドイツ ボーダフォン Trophy の 3 つの環境で確認したけど、Trophy だけ中国語のフォントが Microsoft YaHei しか入っていないようで、 中国語が表示される状況だと全て Microsoft YaHei が使用されていた。 エミュレータと IS12T は、見た目同じ感じ。ちなみに下のイメージはエミュレータ。

ホントに適当な推測なのですが、「化」の右側が突き抜けているフォントがどうやら簡体字のようなので、そうなると、Microsoft YaHei (繁体字中国語、簡体字中国語) は簡体字フォントということなのだろうか。

下記結果から Language に設定する値は、日本語であれば "ja-JP"、韓国語であれば "ko-KR"、簡体字中国語であれば "zh-CN"、繁体字中国語であれば "zh-TW" で良いと思われる。

wp7_font_family_east_asia.png

参考情報

 

■ Bing Maps 関連

Bing Maps コントロールの Culture にはニュートラルカルチャは設定不可。設定できたとしても、扱いは en-US になる(はず)。 デザイン画面の Culture プロパティからは変更できないようだが、実行時は設定可能。 サポートされているカルチャの情報は見つけられていない(2012/5 時点)。 WP7 がサポートしているカルチャを全て試し、明らかに変化があったカルチャは下記。 下記以外のカルチャを設定した場合、特にエラーなどは発生しないが、en-US として処理されているっぽい。

政治的な理由により、中国とインドだけは Bing Maps のサーバーが別れているらしく、それぞれ別のリージョンの地図を参照した時の情報量が非常に少なくなる。 WP7 の Bing Maps コントロールも状況は同じ様子。そのため、中国の地図を確認しようとすると途端に情報が少なくなる。 中国の Bing Maps サーバーを使用すればこの逆の状態になるようだが、WP7 の Bing Maps コントロールからそれができるかは不明(たぶん、今のところできない)。

(2012/7 追加) Bing Maps コントロールを使用しているアプリを審査提出したところ、下記理由により拒否されてしまいました。(Group 1 は中国の事です)

Comments: Your application uses the Bing Maps Silverlight Control for Windows Phone. Bing Maps is not supported for Group 1 countries at this time. You may resubmit your application and deselect the Group 1 countries.

そのため、現状、Bing Maps コントロールを使用した WP7 アプリの場合、公開先から中国は外す必要があるようです。

 

■ Bing Maps REST Service 関連

Bing Maps コントロールとは違い、Culture の指定は基本的にニュートラルカルチャで行う。 WP7 専用のサービスと言うわけでもないので、WP7 で使用できるカルチャとは多少差異がある。 また、中国語のニュートラルカルチャは、古い形式の zh-CHS, zh-CHT が使用されているので注意(WP7 だと同 zh-Hans, zh-Hant)。 ja-JP のようにリージョン情報まで含んだ形で渡しても ja と認識してくれるようだが、場合によっては不正なパラメータとして結果が返るので、 基本的にはサポートされている形式でリクエストするのが無難。

上述したように、現在(2012/5 時点)WP7 ではサポートされていない言語、例えばアラビア語(ar : Arabic)を設定することも可能。 但し、(自分はこの辺りあまり詳しくないですが)端末に入っているフォントに対象言語が含まれていない場合、WP7 では □ などの形で文字化けして結果が表示されることになる。 手元の IS12T, Trophy(どちらも 2012/5 時点で最新の状態)で実際に動作確認してみたところ、ヒンディー語(hi: Hindi)のみ文字化けが発生した。

少なくとも 2012/5 時点では、Bing Maps REST Service はリージョンによって利用できる情報の差が激しい。 日本の場合、ジオコーディングはサポートされているが、逆ジオコーディングで得られる情報は少なく、県名レベルぐらいまでしか情報を取得できない。 Imagery API については、ほぼカルチャは役に立たず、基本的に en-US で結果が得られる。

なお、Bing Maps REST Service と同様のサービスとして Bing Maps SOAP Service があるが、こちらは古くてもうメンテもされていないため、SOAP Service の使用は推奨されない。

2013/8 時点では、Basic キーを使用して WP アプリから Bing Maps サービスを利用している場合、24 時間で最大 5 万 Billable transaction が使用可能。それ以上は制限されるらしい。 ちなみに Billable となっていると課金されそうで怖いが、これは Enterprise キーを使用している場合に関係してくるので、Basic キーを使用している限り課金の心配をする必要はない。 (それでも心配だったら直接問い合わせて下さい)

参考情報

 

■ MarketplaceReviewTask 使用時に Error code 805a0194 が発生

評価とレビューのページをアプリから表示する方法として MarketplaceReviewTask クラスが使用出来ます。 ただ、当たり前ですがマーケットプレイスに公開されている場合のみ使用可能なため、公開されていないアプリ(の app id)で呼んだ場合は、805a0194 のエラーが発生します。 公開されれば問題なく使えますし、構造上たぶんテストもできないので、805a0194 のエラーは気にせず公開しましょう。

参考情報

MarketplaceReviewTask.Show Method (Microsoft.Phone.Tasks)

 

■ 開発中のバージョンで Marketplace 関連のクラスを使用できるようにする

一つ上で、マーケットプレイスに公開されたバージョンじゃないとエラーが出てしまうとか書いたばかりですが、開発中のバージョンでも条件さえ揃えば強引に使用できるようにする方法はあります。 考え方は単純で、マーケットプレイス系のクラスでアプリ情報を必要とするクラスは公開されているアプリの app id を必要とするだけなので、開発中のアプリの app id が公開されているアプリの app id なら良いわけです。 何を言っているのかよく分からない人もいるかもしれませんが、つまり、 一度公開されたアプリの app id (Product ID) は APP HUB 上のアプリ詳細ページで確認できるので(まあ、今はマーケットプレイスでも分かってしまいますが)、 これで開発中バージョンの app id (WMAppManifest.xml の ProductID) を書き換えるというわけです。 MarketplaceReviewTask クラスなどもこの方法で動作確認できます。 ただ、相当強引な方法なので、(仕組み上何か起きるとも思えませんが)やるとしても最小限の確認のみで済ませて、 すぐに元の Product ID に戻すことをお勧めします(もちろんオリジナル Product ID のバックアップはしておきましょう)。 ちなみに、検証に使用しようとしている Product ID のアプリ、つまり正規バージョンのアプリが配置端末にインストール済みだと上書き配置などはできないので、 手動でアンインストールした後、配置する必要があります。 逆もまた然りで、正規バージョンの Product ID を使用した開発中バージョンのアプリが配置された場合、マーケットプレイスで更新があると通知が出ることがありますが、 この状態ではインストール時にエラーが出てインストール出来ません。

 

■ 一般的なテスト項目

WP7 アプリとして特に注意が必要なテスト項目を列挙。

 

■ 作成したアプリの配布前に必要な作業

まず、パクられても全く気にしないようなサンプルアプリでない限り、基本的に難読化 (obfuscation) は必要と思われます。 これは xap 配布する場合もマーケットプレイスに登録する場合も同様。 なぜかと言うのは WP7 アプリの難読化周りの記事を探して確認してもらいたいとは思いますが、要は、xap (IL) を disassemble されてアプリでやっていることが丸分かりになってしまい、 そこからパクったアプリを例えば有償のアプリとして誰かがマーケットプレイスに上げる事もできちゃうからってことらしいです。 マーケットプレイスに登録されたアプリも、今のところ xap を直接ダウンロードしちゃうことが可能らしいので、状況的には xap 直接配布とそこまで変わらない。 WP7 アプリの難読化を行うソフトについては、 例えば PreEmptive が出している Dotfuscator Windows Phone Edition がある。 他のは調べていないのでちょっと分かりません。期限付きとして無償で使えるようですが、期限が切れる前に自動で更新(?)されるみたいなので、 今のところ期限は実質気にしないで使えています。 既定の設定では処理を行っても何も難読化されないので、注意が必要。 Settings で難読化の内容を変更した上で実行する必要があります。本当に難読されたかは、ILDASM などで確認すれば OK。 この辺りの詳細は以下のページに載っています。

Dotfuscator Deep Dive with WP7 - Fear and Loathing

下記ページにも色々と書かれてます。「Obfuscating Your App」の項に具体的な記載がありますが、基本的には Renaming と Control Flow と String Encryption の使用で十分なようです。

Dotfuscator » PreEmptive Solutions Blog

それと、難読化したアプリは必ず実機でテストする必要があります。難読化後、意図しない動作になることがあります。 自分の場合も、原因は分かりませんが、Renaming を行うとアプリが起動しない事や、特定の処理を行った際に unspecified error が発生する事などがありました。 更に、難読化した場合の注意事項として、例外発生時のコールスタック情報にも影響が出る点があります。 例えば、メソッド名を難読化した場合、例外発生時のコールスタックに現れるメソッド名も難読化後のメソッド名になります。 あちらを立てればこちらが立たずの状態ですが、上述したように難読化は通常必須と思いますので、難読化による弊害は別途対処する必要があると思います。

後は、Marketplace Test Kit を使用した、アプリケーションで必要とする機能の洗い出し。 この作業についてはマーケットプレイスへの登録プロセスの中でも行われるらしいので、どこまで意義があるかは微妙なところですが、 人任せよりは自分で明示的に設定した上でテストする方が良いとは思います。 また、アプリケーションが使用する機能については、審査プロセスの中で行われる上書きでは不要な機能も含まれることがあるようです。 場合によってはユーザーが不信に思ってしまうこともあると思いますので、使っていない機能は自分で全てコメントアウトしておくのが無難です。 それと、使用しているライブラリによっては、Marketplace Test Kit だと自分のアプリで実際に使っていなくても自動的に検出されてしまう機能があるようなので、 Marketplace Test Kit で検出された機能が自分のアプリで本当に必要かは慎重に確認する必要があるかもしれません。 例えば、Coding4Fun を参照すると、ID_CAP_IDENTITY_DEVICE が必ず検出されるようになるようです。 なお、実際に使用しているのにマニフェストでその機能が有効になっていない場合、COM Exception とか比較的致命的なエラーが発生するので、 Capability を変更した場合は必ずアプリ全体のテストを行いましょう。

参考情報

 

■ 作成したアプリを審査に提出する前に必要な作業

ここでは通常の動作検証では確認できない部分について記載します。 そういう箇所はあまりないのですが、まず、アプリタイトルをリソース DLL を使用してローカライズしている場合、ニュートラル リソースのタイトル情報に間違えがないか、確認します。 この情報が英語ページのマーケットプレイス上のアプリタイトルに使用されるようです。

それと、マーケットプレイスで公開されているバージョンじゃないと動作確認できない機能、具体的には、MarketplaceDetailTask と MarketplaceReviewTask を使用している箇所については、 本当に正しいクラスが使用されているかを、コード上で確認する必要があります。開発バージョンだと、マーケットプレイスにアクセス出来ないということで一律にエラーになるのですが、 ここで呼ぶクラスが間違っていると、いざ公開された時に、アプリ詳細画面が表示されるはずが間違ってレビュー画面が表示されるなどの悲しい体験が発生する可能性があります。 開発時の動作検証では確認できないので、注意。

 

■ アプリケーション実行時、画面右上に表示されるカウンター情報を表示しないようにする

アプリケーション実行時、実機であってもエミュレータであっても、デバッガ (VS2010) がアタッチされていると画面右上に以下のようなデバッグ用の情報 (フレーム レートなど) が表示されます。

wp7_frame_rate_counter.png

普段はどうでもいいのですが、マーケットプレイスに登録するにあたって画面のスクリーンショットを取る際は邪魔になるので、 その場合はデバッグなしで実行(もしくはアプリ実行後に一旦アプリを終了してデバッガを切断)すればこの情報は表示されません。 それぞれどんな情報を示しているかは、下記ドキュメントを参照。上記画像も下記ドキュメントから拝借しています。

Frame Rate Counters in Windows Phone Emulator

 

■ マーケットプレイス登録時の画面スクリーンショットについて

基本的にはエミュレータに表示された画面をキャプチャして使用することになると思いますが、その時の注意点としては、一つ目は上述した、カウンター情報を表示しないようにすること。 もう一つは、エミュレータのサイズを 100% にすることです。マーケットプレイス登録時の画面サイズは 480x800 ですが、これはエミュレータのサイズを 100% にした時のサイズになります。

 

■ マーケットプレイス登録時のアイコンについて

マーケットプレイスへの登録時、99, 173, 200 の各サイズのアイコンを作成して登録する必要がありますが、このアイコンについては、透過色を使用しては駄目です。 タイル用のアイコンは透過色を使用することが多いと思うので、それを流用して 99, 173, 200 のアイコンを作ると意図せず透過 PNG のアイコンとなってしまう可能性がありますが、 その場合、非常に残念な表示となる可能性があります。具体的には、通常、透過 PNG のエリアはアクセントカラーが表示されるのですが、これがアクセントカラーではなく背景色が適用される場合があるようです。 そのため、例えば、透過 PNG に白の前景色でアイコンを作っていた場合、背景が白だと、全て白で表示されてしまい、非常に残念と言うか、悲しい感じになります。 そのような状態になる場面は多くないようなので気づきにくいのですが、少なくとも、アプリ更新の検出時に表示されるアイコンは、そのような状態で表示されてました。 一応この要件は Certification Requirements の 4.5 で定義されてはいるのですが、審査としてはほぼスルーされているようなので、注意が必要。 ちなみに私は、これまで全て透過 PNG で登録していたはずなのですが、一度も指摘されたことがありません。 たまたま白バックで更新を受けて、上記事象に気づきました。アプリの動作には関係ありませんが、労力が必要なわけでもないので、透過 PNG 使っている場合は直しておきましょう。

(2011/09/27) 何か上記は自分の勘違いだったようで、自分のアプリの 99, 173, 200 のアイコンは透過 PNG ではなかったようです。 今のところ、プロジェクトのプロパティの、Deployment options で設定した透過 PNG が更新時に表示され、且つその際に、アクセントカラーが適用されていない様子。 この状況は他の同じようなアイコンのアプリでも同じだったので、OS の問題の気がする。

 

■ マーケットプレイス登録時の審査について

審査対象となる内容は Windows Phone 7 Application Certification Requirements を参照。 暴力ネタや法に触れるようなアプリは禁止など常識的な項目も多いのですが、位置などの情報を端末から取得する場合はユーザーに許可を得る必要があるとか、 登録に必要なアイコンやスクリーンショットの規定とか、確認が必要な情報も色々あるので、 英語のドキュメントではありますが一度はざっと目を通して、だいたいの感触を得ておいたほうが良いと思います。 位置情報取扱い時の注意事項など、特定の機能や分野についての項目も結構あるので、全てのアプリに共通して確認が必要な項目はそこまで多くないです。 ちなみに、審査通らなかった場合はどこがダメだったかが記載された PDF がもらえます。 正直、適切なアイコンとかを作るのは、そういう事やったことがないプログラマにとってはかなり苦痛かと思いますが、マーケットプレイスに登録する上では必要な作業です。

また、最初はあまり気にかからない審査内容として、「3.10 Country/Region Specific Requirements」があります。 通常の国では問題ないアプリ内容だった場合も、いくつかの国では宗教的な事項や法律に抵触する可能性があり、それらの国がアプリの公開先に含まれた場合、追加の審査が行われます。 審査期間もその分伸びるようですが(たぶん、これについてはそこまで気にするような差はない)、それよりも困るのが、3.10 に抵触しているとの理由で審査が通らない場合です。 2012/5 時点では、3.10 で審査が通らなかった場合、具体的にどの部分が抵触しているかの説明が PDF に記載されていません。 そのため、一度 3.10 に引っかかると、どこを修正すれば良いかの判断が極めて難しいです。 この場合はアプリ側で対処するより、抵触している国を公開先から外すのが一番簡単で確実かもしれません。 自分が今まで経験した中では、3.10 のグループ 1 の、中国が一番厄介です。 公開先については後から別途追加することは簡単にできるため、3.10 に抵触する可能性が少しでもありそうで、且つ確実に審査通したい場合は、 とりあえず中国だけ公開先から外して審査に提出した方が、確実かもしれません(PDF でちゃんと指摘がもらえるようになれば、また別なのですが・・・)。 中国の場合、Web サービス等の情報遮断、規制の話を良く耳にしますが、そういうのがこのポリシーにも反映されていると思えば良いと思います。

参考情報

 

■ マーケットプレイスで公開されたアプリの開発について

やれば分かることですが、マーケットプレイスに登録された後もそのアプリの開発を続ける場合、マーケットプレイスからダウンロードした正式版と、開発中のバージョンが同一端末に混在することが多いと思います。 この場合、何も対処しないとアプリ名が同じになり、どっちのアプリを起動すれば良いのか判断つきづらいので、その場合は一時的に、開発中バージョンのタイトルを変更してしまうのが良いかと思います。

具体的な方法としては、プロジェクトのプロパティを開き、[Application] タブからタイトルに関する項目を変更します。単純に名前を変更すれば良いのですが、例えば下記。

リソース DLL を使用してタイトルの多言語対応している場合は、各リソース内の文字列を変更。

開発中のバージョンかどうかが分かれば十分だと思いますので、[beta] とか付けとけば十分かと思います。 Deployment は後ろに付けて Tile は前に付けてるのは、アプリリストの中では正式版と並んでた方が探しやすいと思うこと、タイル表示時は後ろが切れやすいので、逆に前につけないと文字が見えない可能性が高いからです。 審査提出前に元に戻すのはお忘れなく(仮にそのまま提出しても、審査で弾いてくれそうな気もするけど)。

 

■ マーケットプレイスに登録したアプリケーションを更新する

端末のアプリが更新された際も Isolated Storage の保存内容は保持されるので、その辺りに心配は大丈夫。 逆に、旧バージョンの Isolated Storage の内容を新バージョンで読み込んだ場合に正常に処理できるように対処する必要がある。 よく聞く、アプリを更新したら起動時にエラーが発生するようになったがインストールしなおしたら大丈夫だったというのは、たぶんこの処理がうまくいっていないことが原因と思われる。 旧バージョンからバージョンアップした際のデータ更新状態の再現は、少し面倒くさいが、旧バージョンの時に ISETool.exe でデータをバックアップしておき、 新バージョンインストール後にそのデータを戻すことで再現可能。 ちなみに、(セキュリティ上当たり前かもしれないが)マーケットプレイスからダウンロードした正規バージョンのアプリに対しては ISETool.exe によるデータ操作はできないので、 これをやるとしたら、旧バージョンのデータ取得も開発バージョンで行う必要がある。

上記の ISETool.exe を使用する方法は色々と面倒くさいが、簡単な検証で良ければ、もっと簡単な方法がある。 どうするかと言うと、CodePlex の Windows Phone Power Tools を使う。 SDK 付属の Application Deployment ツールでは、アプリのインストールは新規でしか行えないので、旧バージョンが存在する端末に新バージョンをインストールすると旧バージョンがアンインストールされてしまい、 結果、IS に保存されていた情報もなくなってしまう。なのだが、Windows Phone Power Tools を使えば、新規インストールではなく、更新インストールが可能になる。 そのため、旧バージョンで簡単にサンプルデータを作成できるのであれば、まず旧バージョンをインストールしてサンプルデータを作成し、 次に Windows Phone Power Tools を使って新バージョンを更新インストールして動作チェックするのが一番簡単だと思う。 なおこの場合、当たり前だけど、旧バージョンの xap も必要となる。

なお、Isolated Storage 以外で懸念される項目として、更新対象のアプリがドーマント (or ツームストーン) 状態だった時に、バージョンアップ後はどこから開始されるかという点があるかもしれないが、 これは、バージョンアップ前にドーマント状態だったアプリは一度メモリから削除された後にバージョンアップが行われるので、 バージョンアップ後は必ず最初から起動されるという認識で、今のところ (Mango では) 問題ない。 アプリ内から MarketplaceDetailTask を使用してアプリ詳細画面を表示し、アップデートを行った場合も同様です。 アップデート後にアプリに戻ろうとしても、既にイメージが消えているので、初めからの操作になります。 なお、見た目同じような流れとして、試用版から購入版へのアップグレードがありますが、こちらの内部動作としてはイメージが削除されることはないようなので、アプリ詳細画面からアプリに戻ることになるようです。

更新時に懸念される技術的な内容の詳細は下記ドキュメント参照。

Updating Applications in Windows Phone Marketplace

 

■ マーケットプレイスで公開されたアプリ (xap) のバックアップについて

前述の、前バージョンからアップデートした時の挙動検証などもあるため、ソース管理などとはまた別に、 マーケットプレイスに公開したアプリについては各バージョンの xap だけでも全てバックアップしておいた方が良いかと思われる。

 

■ 審査落ちしたバージョンの申請を削除する

説明したいことはタイトルそのままで、審査に落ちた後、「認定済み (Certified)」のフェーズで止まってしまっている申請バージョンを削除する方法についてです。 何を問題にしているのかよく分からない人もいると思いますが、審査に落ちた後も、ライフサイクルのフェーズとしては「認定済み (Certified)」に留まっていて、このフェーズの状態のままだと、 アプリを非表示の状態にするとか、そういう他の作業が何もできないんですよね。 で、普通に考えれば、だったら申請を取り消せばいいじゃんと言う考えになると思うのですが、その方法がよく分からなかったという話です。 ちなみに、単純に修正バージョンを申請し直すのであれば、この状態から作業を継続して、特に悩むことなく再提出することが可能です。 ここでは、「いや、再提出は一旦諦めるので、他の作業を行わせてください」と言う状態に対処します。

で、何でこんなくだらないことで悩んでいるかというと、「申請を削除する」という項目が、ライフサイクルページ上に存在しないからなのです。 サポートへの問い合わせと、製品の詳細を編集する項目しか、ないんですよ。つまり、それ以外に行える作業が、ない。 で、前からこんなんだったかなぁと思いながら、試行錯誤しつつ確認できた方法が、あまりにも間抜けな方法ではあるのですが、ライフサイクルのページから、 「製品の詳細を編集 (Edit product details)」をクリックして、詳細ページで、何か検証に失敗するような作業を行うという方法です。 例えば、サポート言語が別言語になった xap をアップロードするとか。そうすることで、ライフサイクルのフェーズが「認定済み」以外のフェーズ、例えば、 パッケージの検証に失敗とか、申請内容に不備があるとかのフェーズに移行し、そうなることで、ライフサイクルのページに「申請の削除」の項目が初めて現れます。 この項目をクリックすることで申請前の状態にライフサイクルが戻るので、アプリの非表示とか、そういう作業が行えるようになります。 何かあまりにもアホくさいので、他に単純な方法があるのかもしれませんが、とりあえず、上記手順でも申請の取り消しは行えるということで。 ちなみに、これは 2012/1 時点で確認した内容です。

 

■ パブリック マーケットプレイスに公開したアプリの新規配布先を限定する

ここで説明するのは、プライベート ベータ テストによる配布の事ではありません。マーケットプレイスに登録したアプリが対象です。 通常、マーケットプレイスへの登録が完了すると、WP 本体、Zune、Web Marketplace などから検索し、インストールすることが可能となります。 ただ、場合によっては、一時的に公開したくない場合があるかもしれません。 例えば、審査は通過したけど、その後、アプリが依存している外部要因の変化により、アプリを起動するだけで落ちるようになってしまった、とかです。 最悪、マーケットプレイスから一度削除してしまうという選択肢もありますが、本当に削除したいわけではないのであれば、現実的な選択肢ではありません。 そのような場合、マーケットプレイスには公開したままの状態で、非表示にするという選択肢があります。 これはどういう状態かと言うと、アプリに関するページ自体(インストール ポイント)はそのままですが、 マーケットプレイスでそのページにリンクする機能(ランキング、アプリ一覧や検索など)には一切表示されなくなる状態です。 直接開く以外にインストールページを開く手段がなくなるので、実質的に、新規のインストールは不可能となります。 (2012/01/20 追記:とは言うものの、プライベート状態でもなぜかおすすめに掲載されたことがありました・・・。なので、プライベートであればインストールされる確率が減るぐらいの感覚で構えていた方が良いかも)

通常公開時との差異としてはこれだけです。 他は全て同じなので、非表示の状態のままアプリを更新することも可能ですし、その際の審査プロセスも通常公開時と全く変わりません。 更新が審査通れば、アプリをインストールしているユーザーには、いつも通り更新が通知され、配布されます。 また、新規のユーザーに対しても、インストールページの URL を通知することで、URL から直接インストールページを開いてもらって、インストールしてもらうことが可能です。 この URL は、APP HUB で当該アプリの詳細ページを開き、ディープ リンクに表示されている URL となります。「http://windowsphone.com/s?appid={appid}」 みたいな感じで表示されている URL です。 この URL が WP 本体でインストール ポイントを開く時の URL になります。 逆に言うと、この URL さえ知っていれば、誰でもインストールは可能となります。 今は Web Marketplace でもアプリの情報を確認できるようになり、その URL にはアプリケーションの ID も含まれているので、 一回公開してしまうとインストール ポイントはバレバレになっていると考える必要はありますが、まあ、それを踏まえた上で利用するのであれば、結構有用な機能だと思います。 ちなみに、PC 用のリンク (Web Marketplace で見れる情報) は「http://www.windowsphone.com/ja-JP/apps/{appid}」となりますが、この画面からも、現在の表示状態を確認することができます。 非表示状態の場合は、バージョン情報の下に、「プライベート」と表示されます。表示状態の場合は何も表示されません。 それと、Web Marketplace はサイトの言語を変更することで簡単に海外のマーケットプレイスの情報も見ることができますが、 上記 URL の ja-JP のところを en-US にするだけでも切り替えることができます(Zune だと OS の設定やログインアカウントなどが影響するので、こう簡単には確認できません)。

前置きが長くなりましたが、実際に非表示の状態にする方法としては、APP HUB の当該アプリのライフサイクルのページから、「アプリケーションを非表示にする (Hide application)」のリンクをクリックするだけです。 操作自体は一瞬で終わります。アプリ非表示の状態から表示の状態に戻す場合は、「アプリケーションをライブにする (Make application live)」のリンクをクリックするだけです。 ただ、切り替え操作自体はすぐ終わりますが、実際にマーケットプレイスに反映されるまでは、最大で一日ぐらいかかるようです。また、反映処理は国毎に行われるようです(どうでもいい話ですが)。 非表示の状態でアップデートを審査に出す場合、審査通った後も非表示として公開したい場合は、審査依頼時の公開方法として「認定されたらすぐ、ただし非表示」を選択しておく必要があるかと思います。

それと、上記の「アプリケーションを非表示にする」などのリンクも、一度アプリを審査に出してしまうと審査が終わるまで選択できなくなってしまうので、この点は注意が必要。 例えば、アプリに致命的な問題があったので、新規インストールは制限しつつ、更新を審査に出したいような状況だと、まず、アプリを非表示にした後に、審査に出す必要があります。

参考情報

 

■ 税金対策用の W-8 Form の申請

有料アプリとして登録するのであれば、基本的に税金対策は必要と思います。 以前は ITIN やら EIN やらの取得と、エアメールでの W-8 書類の送付などハードルがかなり高かったのですが(特に ITIN、EIN)、現在はどちらも非常に簡略化されているので、 有料アプリを登録する際はあわせて銀行情報の登録と W-8 Form の申請もしてしまいましょう。どちらも英語で記入する必要がありますが、 記入する項目は多くないので、記入内容や手順がわかっていれば 1 時間もかからずに作業は終わると思います。

支払い用の税金対策として作業する必要がある項目は大きく二つ。銀行情報の登録と、W-8 Form の提出です。 銀行情報の登録は下記参照。住所なども含め、全て振込先の銀行の情報を記入する必要があります。 また、全て英語表記で行う必要があります(最初意味がよくわかっておらず、自分の家の住所を日本語で入力してました・・・)。 銀行の英語表記などについては、大きい銀行であれば Web サイトに情報があるかもしれません。

参考情報

次に W-8 Form の記入と提出です。基本的に下記サイトの情報を基に提出まで行えます(多謝)。提出についてもエアメールでの書類提出は不要で、メールのみで作業完了できます。 当初はメールでの送付は validation のみで、正式には W-8 Form のハードコピーの郵送が必要みたいだったのですが、 これもその後プロセスが変更され、現在はスキャンした W-8 Form の提出のみで OK とのことでした(メールで返事もらった際にその旨記載されてた)。 下記ページにも記載がありますが、APP HUB 上の対象アカウントの氏名表記が日本語の場合、英語に変更する必要があるので変更して構わないかの確認メールが来ます(たぶん)。その場合は構わない旨を返信すれば、後は向こうで勝手に変更してくれて、それでたぶん作業は終了です。 自分の場合は向こうの営業時間に送ったというのもあると思いますが、1 時間もしないうちに氏名変更の確認メールが来ました(混み具合にもよると思いますが)。 その後、2 営業日後ぐらいに、変更完了のメールが来て作業は完了しています。

APPHUBのW-8申請の手順記録 - KarinoTaroの日記

私はまだ有料アプリの登録をして支払いを受けていないので正確な事は何とも言えないのですが、実際に validation されるのは支払額が 200 ドルに達して実際に支払いが行われる時のようです。 以下はメールでやりとりした時にもらった内容。

We have changed your name on your App Hub account and at this time it appears that your W-8 has been completed successfully. Please note that due to new policies, we are now accepting a scanned version of your W-8 form; a hard-copy is no longer required. Once a payout is attempted, your form will be officially validated and your App Hub account will be updated. There is nothing more that you are required to do. Once you meet the minimum $200 threshold, you will automatically be included in the next payout cycle.

その他参考情報

 

■ サポートが必要な時は

2012/1 時点では、残念ながら、WP7 関連の MS サポートは英語でのみ利用が可能な状況となっています。 技術的なサポートに関しては有償インシデントサポートを利用する必要があるようですが、審査やマーケットプレイス、APP HUB 等の質問については、問い合わせフォームから無償で質問できます。 問い合わせフォームからの質問については、審査に落ちた時は当該ページへのリンクが現れるので比較的わかりやすいのですが、 それ以外だとどこから問い合わせれば良いのかちょっと分かりづらいかもしれません。 APP HUB にログインした後、下記ページから問い合せが行えます。

Support

ちなみに、上記ページヘのリンクが貼られているサポートページは、APP HUB ログイン後、画面上部のメニューから resources -> support で辿りつけるのですが、 2012/1 時点では、ページの表示言語が日本語以外が選択されている必要があるようです。何でかは知りませんが、日本語だと他の言語とは全く別のサポートページが表示され、 上記ページへのリンクを見つけることができませんでした。

また、上記サポートを何度か利用した限りでは、対応が結構いい加減な印象があります。 そもそも何の連絡も来ないことがあるのですが、何とかメールで返事が来たとしても、向こうからのレスは 5 営業日前後の時間がかかることが多いようです。 問い合わせ直後に届く自動応答メールでは 1, 2 営業日以内に連絡するとか記載があるのですが、あまり真に受けない方が良いと思います。 また、自動応答メールにはサポートチケットの番号的な情報もなく、メールでの返信先すら記載がないので、最初の返事が来ないとこちらからは何もできなかったりします (何度か問い合わせて全く返事がなく、その事を再度問い合わせたら、検索したけど前の問い合わせ情報が見つからなかったので、もう一度質問内容を教えてくれみたいなメールが届いたこともありました)。 支払い関連で Windows Phone Marketplace Commerce Team に問い合わせた時はめちゃくちゃレスポンスが良かったんですが、チームによって対応に結構差がある気がします。

そんな状況なので、確実にサポートを受けたい場合は、有償インシデントを使用した方が良いかもしれません(上述したように日本語設定だと表示されないので、日本から利用できるかとかは正直知りませんが)。 上記ページから問い合わせても何も返事がない場合、メールでその旨問い合わせたいかとも思いますが、自分の場合は apphub@microsoft.com から連絡を受けていたので、 こちらにメールすることでもしかしたら反応があるかもしれません(ない可能性のほうが高い気もしますが)。 apphub@microsoft.com のアドレス自体は、App Hub - support の説明を見る限りアカウントのレジストとかログインの問題について質問する先のようです。

とは言ったものの、恐らく大多数の日本人開発者にとっての問題は、英語でないと受け付けてもらえない点だと思うのですが、今のところ、これはもうどうにもならないかと思います。 日本語で対応してもらえる可能性がある問い合わせ先としては、一応、、フォーラムとか Windows Phone 開発事務局があるようですが、フォーラムでは限界があると思いますし、 Windows Phone 開発事務局のページも、「開発事務局はサポート窓口ではない」と明記されているので(まあ、そうでしょうけど)、苦手だとしても、何とか英語でやるしかないかと思います。

 

■ その他 APP HUB 関連

参考情報

Windows Phone Marketplace の FAQ

 

■ その他マーケットプレイス関連

参考情報

Windows Phone Marketplace の FAQ

 

■ 検索キーワード SEO

2012/4 時点での確認結果。変更される可能性があると思うので、注意。後述するが、現状、日本のマケプレに限ればあまり SEO を気にする必要はないと思われる。 ただ、アメリカのマケプレもターゲットにするのであれば、必須。

今のところ、タイトル、キーワード、作者名で特に検索時の重み付けがあるという訳ではないようなので、タイトルや作者名が人気タイトルやキーワードと被ると、結構悲惨なことになる。 検索結果は直近のダウンロード数順で、且つ、後ろの方の結果は表示されないことがある(特に端末での検索時は上位の限られたアプリしか表示されない)ので、 出たばかりのアプリだと、アプリタイトルで検索しても検索結果に表示されないことが起こり得る。つまり、アプリタイトルだけ知っていて、タイトルで検索してダウンロードしたいと思っても、 マケプレから対象アプリを探し出すことができずダウンロード出来ないという状況が発生し得る。なお、そういう場合は作者名と組み合わせて検索すればさすがに大丈夫なはず。 ちなみに今のところ、作者名に 2 バイト文字が含まれる場合に作者名クリック時の検索結果が 0 件とかになってしまうので、これも注意が必要(クリックではなくて通常の検索であれば問題ない)。 現時点では、作者名は英語且つキーワードなどに出現しないワードが理想。

そんな状況なので、特に英語のアプリタイトルを付ける場合は事前にその単語でマケプレを検索し、ヤバイことにならなさそうか確認した方が無難。 日本語など、英語以外のタイトルであれば対象となるアプリ数が全く違ってくるので、今のところどんなタイトルを付けても基本的に大丈夫。この問題が発生するのは英語だけ。

 

■ ダウンロード数に関する考察

ここでは、アプリのダウンロード数について考察してみたいと思います。 マーケットプレイスにアプリを公開していてダウンロード数を気にしないという人はあまりいないと思いますが、もしダウンロード数は全く気にしていないという方であれば、 この考察は読んでも意味ないと思います。 そうでない場合、このトピックについては、技術的な面と同じか、もしくはそれ以上に重要になる可能性があります。 ちなみに私は、WP7 アプリ以外のスマホアプリを開発したことはないので、それを前提で読んでもらえればと思います。

まず、公開アプリの状況を構成する要素は、ダウンロード数、評価数、平均評価、売上など色々とあると思いますが、総合的に何が一番大事かと言う事になると、 迷わず「ダウンロード数」と言い切ることが出来ると思います。評価数、平均評価、売上なども重要な項目ですが、アプリ構成要素ピラミッドのトップに位置するのは、確実に「ダウンロード数」です。 そのため、アプリ公開と同時に一番気にしなければいけない要素も、ダウンロード数と言う事になります。 もちろんダウンロードされたから使われるというわけではないですが、ダウンロードされなければアプリが使われないのは自明の理です。

そういう訳で、ユーザーにいかにダウンロードしてもらうか、と言う点を考える必要があります。 ただその前に、WP7 のマーケットに関しても状況を把握する必要があります。 周りのユーザーの声やニュースなどを見てれば何となく把握できると思いますし、アプリを公開した人であればもっと正確に状況を把握できてると思いますが、 2012 年 6 月時点では、スマホ OS の WP7 のシェアは、ほとんどないに等しいです。最初のバージョンが出てから既に一年半以上経過していますが、世界レベルのシェアは未だに 2, 3% 程度です。 また、日本については更に酷い状況で、未だに日本で発売されている WP7 端末は AU から発売されている IS12T のみという状況で、シェアは 1% もないです。 そんな状況なので、iPhone や Android に比べ、アプリのダウンロード数は相対的に非常に低いです。 自分は他の OS でアプリを出したことがないので直接は分かりませんが、ネットで見かける情報やシェアの差を考えると、同じアプリを公開したとしても数十倍程度のダウンロード数の差が存在すると思われます。 具体的な数字を出すと、月間ダウンロード数が 1000 を超えるようなアプリであれば、無料アプリの 10 位から 20 位ぐらいには入れるぐらいの立ち位置になり、 月間ダウンロード数が 500 ぐらいであれば、無料アプリの 50 位から 100 位ぐらいに入るぐらいの立ち位置になります(2012/6 時点)。 その他のアプリ(ランキング 100 位に入れないようなアプリ)だと、一日平均ダウンロード数が一桁と言う事になりますが、これが現状です。 ただ正直なところ、これは現時点の日本のマーケットが一年近く前に発売された IS12T 一機種のみという、やや特殊な環境に置かれていることも影響していると思うので、 定期的に新端末が出て、新規ユーザーが定常的に増加するような状況であれば、もうちょっと違う結果になるとは思います。

ちなみに、日本のマケプレだと上述のような状況ですが、海外のマーケットに目を移すと、結構違う状況になってます。 一言で言ってしまうと、海外の方が断然マーケットがでかいです。 特にアメリカは全世界の中心的なマーケットという特殊性もあり、他の国と比べ、段違いにサイズが大きいです。 評価数ベースの比較になりますが、2012/5 時点での各マーケットの状況は以下のようになっており、アメリカは日本の 100 倍近いサイズになってます。 また、アメリカ以外についても、イギリス、ドイツ、ロシアなど主要国のサイズは日本の 10 倍以上あります。 このため、グローバライズを行なって海外の人にも使ってもらえるようにする事で、日本人だけを対象とする場合よりも大量にダウンロードしてもらえる可能性が生まれます。 ただ、もちろん事はそんな簡単ではなく、競合するアプリの数も半端ないので、よっぽどのアプリでないとあっという間に他のアプリと共に埋もれることになりますが・・・。


無料アプリトップ 20 の評価数集計値 (2012/5)
==============================================
1. Total = 183850, Ave. = 9192.5 [en-US]
2. Total = 33818, Ave. = 1690.9 [en-GB]
3. Total = 30459, Ave. = 1522.95 [ru-RU]
4. Total = 30272, Ave. = 1513.6 [de-DE]
5. Total = 22384, Ave. = 1119.2 [it-IT]
6. Total = 18758, Ave. = 937.9 [zh-HK]
7. Total = 15593, Ave. = 779.65 [fr-FR]
8. Total = 15036, Ave. = 751.8 [en-IN]
9. Total = 13914, Ave. = 695.7 [es-ES]
10. Total = 9333, Ave. = 466.65 [en-CA]
11. Total = 8256, Ave. = 412.8 [en-AU]
12. Total = 7310, Ave. = 365.5 [zh-TW]
13. Total = 5976, Ave. = 298.8 [nl-NL]
14. Total = 5630, Ave. = 281.5 [en-SG]
15. Total = 5291, Ave. = 264.55 [es-MX]
16. Total = 4654, Ave. = 232.7 [pt-BR]
17. Total = 3713, Ave. = 185.65 [fi-FI]
18. Total = 3496, Ave. = 174.8 [pl-PL]
19. Total = 3268, Ave. = 163.4 [de-AT]
20. Total = 2894, Ave. = 144.7 [en-HK]
21. Total = 2590, Ave. = 129.5 [de-CH]
22. Total = 2194, Ave. = 109.7 [cs-CZ]
23. Total = 2181, Ave. = 109.05 [hu-HU]
24. Total = 2129, Ave. = 106.45 [sv-SE]
25. Total = 2095, Ave. = 104.75 [ja-JP]

話が結構それました。改めて、いかにダウンロードしてもらうかという戦略になりますが、まず、ユーザーが当該アプリのダウンロードに至る動線を考えてみます。 ざっと挙げると、以下の様な動線が考えられます。

他にもあるとは思いますが、とりあえず上記の動線について考えます。 それと、マケプレについては端末で見るマケプレと Web サイトのマケプレがあり、見え方など少し異なる部分もありますが、 大まかには同じ構成なので、ここでも同じとして扱います。

まず、マケプレのおすすめ。ここに載ると、大体普段の数倍から 10 倍ぐらいのダウンロード数増加が期待できますが、開発者が直接どうこうすることはできないので、ここでは考慮対象から除外します。 ただ、選考対象として、WP7 アプリらしいアプリ(パノラマ使ってるとかライブタイル使ってるとか)が好まれるらしいので、なるべく WP7 らしいアプリにすることで掲載される可能性が上がる、かもしれません。

[続きはまた後日書きたいと思います]

 

■ 開発者アンロックした後、期限が経過してロックされた時の対処方法について

端末の開発者アンロック後、一年経過すると自動的にロックされてしまうので、引き続きアンロックして使用するためには再度アンロックする必要がありますが、 何度 Windows Phone Developer Registration ツールを使用して登録しても、ツールを起動しなおすとロック状態となっており、端末がアンロックできていない状況になることがあるようです。 その場合、APP HUB 上の登録済みデバイスの一覧に古い期限で登録されたままになっている可能性がありますので、そのような状況になっている場合は、 APP HUB 上で手動で当該端末の登録情報を削除してから Windows Phone Developer Registration ツールでアンロックすることにより、正常にアンロックできるようです。

 

■ おすすめのアプリ

WP7 アプリ開発者御用達というか、must have なアプリは下記。

 

■ おすすめのツール

上述している情報の中で既に案内しているツールではありますが、WP7 アプリを開発する上で、汎用的に使用できる must have なツールは下記。

 

■ おすすめのサイト

 

■ その他参考情報など

開発には直接関係ないですが、たまに計測している情報をまとめてみました。

 

■ WP8 関連 (作業してて気が付いた点を、暫定的に全部まとめて記載)

各種 SDK ツールとデバイス OS の対応は下記。基本的な考え方として、WP 7.1 とホストとの接続は Zune アプリ、 WP 8.0 とホストとの接続は IpOverUsbSvc サービスを使う事と、SDK 7.1 ツールは Zune アプリのみサポート、 SDK 8.0 ツールは Zune と IpOverUsbSvc のどちらもサポートしている違いがある。 まとめると、7.1 SDK ツールは WP 7.1 のみ対象とするが、 8.0 SDK ツールは WP 7.1 と WP 8.0 のどちらもサポートする(8.0 SDK は 7.1, 8.0 どちらの開発も行えるので、当たり前といえば当たり前の結果)。

対象デバイス WP 7.1 WP 8.0 (+ 7.1 apps) WP 8.0 (+ 8.0 apps)
VS2010 (7.1 SDK) ● (using Zune) × ×
VS2012 (8.0 SDK) ● (using Zune) ● (using IpOverUsbSvc) ● (using IpOverUsbSvc)
Application Deployment (7.1 SDK) ● (using Zune) × ×
Application Deployment (8.0 SDK) ● (using Zune) ● (using Zune) ● (using IpOverUsbSvc)
Windows Phone Developer Registration (7.1 SDK) ● (using Zune) × ×
Windows Phone Developer Registration (8.0 SDK) ● (using Zune) ● (using IpOverUsbSvc) ● (using IpOverUsbSvc)

▲画面上へ