NavigationServiceを拡張してちょこっと便利にする
WPFのページ遷移で用いるNavigationServiceクラスですが、微妙に痒いところに手が届かない感じです。
そこで、拡張メソッドで便利メソッドを追加してみました。
ナビゲーションの履歴を一気に消す
NavigationServiceクラスには、ページ遷移履歴をクリアするメソッドはありません。
そこで、こんな拡張メソッドを作っておくと、Pageクラスからページ遷移履歴の削除が簡単にできるようになります。
特定のページに遷移したら、今までの履歴を削除して前のページに戻れないようにしたいときに使えます。
public static void RemoveAllJournals(this NavigationService nav) { while(nav.CanGoBack) { nav.RemoveBackEntry(); } }
使い方はこんな感じ。
private void Page_Loaded(object sender, RoutedEventArgs e) { var nav = this.NavigationService; nav.RemoveAllJournals(); }
Pageクラスのコンストラクタでは、this.NavigationServiceはnullでアクセスできないので、ここは注意が必要。
ナビゲーション完了後に何か処理を行う
たとえば、Navigateメソッドを実行してページ遷移をして、遷移が完了したらページ遷移履歴を削除したい、と思って以下のようなコードを書いたとします。
この場合、nav.RemoveAllJournalsを実行する時は、ページ遷移がまだ完了していないため、次のページに遷移した後も、下記コードを実行したページの履歴が残ってしまいます。
失敗例のコード
private void Button_Click(object sender, RoutedEventArgs e) { var nav = this.NavigationService; nav.Navigate(new Page3()); // ページ遷移後に、履歴削除のつもり // こう書いても、Page3に遷移が完了する前に履歴の削除が行われるため、 // Page3に遷移した時、ここのページ(Page2)が履歴に残ってしまう。 nav.RemoveAllJournals(); }
↓こんな風に履歴が残ってしまう。
もちろん、次のページのLoadedイベントハンドラとかで、履歴削除をすれば対応は可能ですが、、、
ページ遷移して⇒履歴を消して⇒○○をして
というように一連の処理を行うときは、イベントハンドラで分断されたコードではなく、ひとまとまりに書きたいと思うものです。
で、C#にはそんな用途にピッタリなasync/awaitがあります。
ここは時代の流れにそってasync/awaitを用いて書いてみようと思います。
Navigate完了イベントをTaskを用いたパターンに変換する
Navigateメソッドでの画面遷移は非同期で行われますが、遷移の完了はNavigationService.LoadCompletedイベントで通知されます。
asyncやTPL登場以前の、昔のC#スタイルによる非同期処理ですね。
NavigationServiceにはawaitできる形式のメソッドがないので、async/awaitと一緒に使えるように拡張メソッドを追加します。
このようなイベントベースの非同期処理をTaskを用いたパターンに変換する手順は、
以下のページを参考にやってみました。
http://msdn.microsoft.com/ja-jp/magazine/ff959203.aspx
(「イベントベースのパターンを変換する」って箇所)
こんな感じの拡張メソッドをつくりました。
例外処理とかのエラー処理は省いてしまってますが。。。
public static async Task<bool> NavigateAsync(this NavigationService nav, Page next) { return await nav.NavigationAsTask(next); } private static Task<bool> NavigationAsTask(this NavigationService nav, Page next) { var tcs = new TaskCompletionSource<bool>(); nav.LoadCompleted += (sender, args) => { tcs.SetResult(true); }; nav.Navigate(next); return tcs.Task; }
使い方
イベントハンドラにasyncを追加し、ページ遷移は上で追加したNavigateAsync拡張メソッドで行います。
また、NavigateAsync拡張メソッドにはawaitを付けて、ページ遷移履歴の削除は、ナビゲーション完了後に行うようにしています。
private async void Button_Click(object sender, RoutedEventArgs e) { var nav = this.NavigationService; await nav.NavigateAsync(new Page3()); // ↓はページ遷移が完了してから実行される nav.RemoveAllJournals(); }
これで、「ページ遷移完了後に何か処理を行う」ということをスッキリ書けるようになりました。
まとめ
今回作った拡張メソッドはこんな感じ
Extensions.cs
public static class NavigationExtensions { /// <summary> /// ナビゲーションの履歴をすべて削除します /// </summary> /// <param name="nav"></param> public static void RemoveAllJournals(this NavigationService nav) { while(nav.CanGoBack) { nav.RemoveBackEntry(); } } /// <summary> /// 非同期処理としてページ遷移を行います /// </summary> /// <param name="nav"></param> /// <param name="next"></param> /// <returns></returns> public static async Task<bool> NavigateAsync(this NavigationService nav, Page next) { return await nav.NavigationAsTask(next); } /// <summary> /// Navigateメソッドでの画面遷移と、画面遷移完了のLoadCompleteまでの処理を、 /// 非同期タスクに変換します。 /// </summary> /// <param name="nav"></param> /// <param name="next"></param> /// <returns></returns> private static Task<bool> NavigationAsTask(this NavigationService nav, Page next) { var tcs = new TaskCompletionSource<bool>(); nav.LoadCompleted += (sender, args) => { tcs.SetResult(true); }; nav.Navigate(next); return tcs.Task; } }