読者です 読者をやめる 読者になる 読者になる

SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

NavigationServiceを拡張してちょこっと便利にする

C# WPF

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();
        }

↓こんな風に履歴が残ってしまう。
f:id:minami_SC:20140107011713p:plain


もちろん、次のページの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;
        }
    }