WPFに標準で用意されてるナビゲーション系のクラス類が何かと扱いにくいので、もうちょいシンプルな独自のページ遷移を行うサンプルを書いてみました。
なぜ作った?
WPFには標準で、ナビゲーションを行うための仕組みとして、NavigationWindow、Frame、NavigationServiceなどといったコントロールやクラスが用意されてます。
しかし、まぁこの辺のコントロール類は使いにくい。。
特にイヤなのが以下のような点。
- ナビゲーションの履歴管理が邪魔
- Frame/NavigationWindowを使ったページ遷移では、特に何もしなくてもページ遷移の履歴などが記録されます。
- この履歴がviewクラスの参照を掴んでいて、なかなかGCされなくなる
- ナビゲーション関係のコマンドを受け付けると、勝手にナビゲーション動作をする。。
- F5キー押したらページリロードする・・・
- 一方通行なページ遷移しかさせたくないのに、マウスの戻るボタン押したら前のページに戻ってしまう・・・
- などなど。
- ナビゲーション時に音が出るケースがある
- ナビゲーション先の指定方法が微妙
- NavigationSeriviceの管理する範囲が微妙に扱いにくい
- NavigationServiceは、Frameコントロールの内部に位置する。
- Frame内部からページ遷移するのと、Frame外部からページ遷移するときとで、扱い方が変わる
- ページ遷移用のメニューを作るときなどは、Frame外部からのページ遷移を多用するかと思います。
- ↓参照
- https://msdn.microsoft.com/ja-jp/library/ms750478%28v=vs.110%29.aspx#Navigation_Hosts
- NavigationServiceは、Frameコントロールの内部に位置する。
※補足
このページ遷移で音が鳴るっていう動作は、OSの以下の設定に依存します。(「ナビゲーションの開始」の項目など)
で、このナビゲーション時の音は、基本的にはアプリ側から消すことができません。
細かいことですが、気にし始めると地味にイライラします・・・w
(ちょっとこだわったデザインのUI作った時などは特に・・・)
ということで、以下のような方針で必要最低限なナビゲーションを独自に行うサンプルを書いてみました。
方針
- Frameは使わない
- 前述の通り、なにかと問題があるので、、
- ナビゲーション対象のページはContentControlで表示
- ナビゲーションの履歴とかはいらない
- 履歴も何かと問題になるので、あえて排除
- 履歴が必要、と思ったタイミングでコード足せばいいかな。
- MVVM関係
使い方
概要
独自のナビゲーション処理の起点となる、NavigationServiceExというクラスを添付プロパティとして、任意のコントロールに付けられるようにしています。
使い方の概要は以下の通り。
- NavigationServiceExの設定
- Targetプロパティで、ページ遷移を行う領域を指定
- Startupプロパティで、初期表示に利用するページを指定
- ページ遷移動作の定義
こんなイメージです。
ナビゲーション領域の定義
こんな風に、ナビゲーションを管理したいレイヤーに、NavigationServiceExクラスのTarget/Startup添付プロパティを設定します。
ここではWindowクラスにナビゲーション機能の設定を行い、ナビゲーションを行う領域として「content」という名前のContentControlを指定してます。
<Window x:Class="CustomNavigationSample.Shell":省略:nav:NavigationServiceEx.Target="{Binding ElementName=content}"nav:NavigationServiceEx.Startup="{x:Type view:MainView}"><DockPanel><Grid Background="LightGray"DockPanel.Dock="Top">: 省略 : <ContentControl x:Name="content" /></DockPanel></Window>
ページ遷移させる
ナビゲーション領域の外部からでも内部からでも、同じような書き方でページ遷移できます。
XAML上でのページ遷移定義
<Hyperlink Command="NavigationCommands.GoToPage"CommandParameter="{x:Type view:MainView}">Main Page</Hyperlink>
コードビハインドからのページ遷移
privatevoid button_Click(object sender, RoutedEventArgs e) { // 遷移先ページとなるインスタンスを渡してナビゲーションthis.Navigate(new SubView()); // ↓こんな風に、型情報を指定して遷移も可能//this.Navigate(typeof(SubView)); }
コード
今回追加した独自クラスのコードはこれだけ。
サンプル一式は↓に置いときました。
https://github.com/sourcechord/WPFSamples/tree/master/NavigationSamples
class NavigationServiceEx : DependencyObject { /// <summary>/// ページナビゲーションを行う領域となるContentControlを保持するプロパティ/// </summary>public ContentControl Content { get { return (ContentControl)GetValue(ContentProperty); } set { SetValue(ContentProperty, value); } } // Using a DependencyProperty as the backing store for Content. This enables animation, styling, binding, etc...publicstaticreadonly DependencyProperty ContentProperty = DependencyProperty.Register("Content", typeof(ContentControl), typeof(NavigationServiceEx), new PropertyMetadata(null)); #region ナビゲーションで利用する各種メソッド/// <summary>/// view引数で指定されたインスタンスのページへとナビゲーションを行います。/// </summary>/// <paramname="view"></param>/// <returns></returns>publicbool Navigate(FrameworkElement view) { this.Content.Content = view; returntrue; } /// <summary>/// viewType引数で指定された型のインスタンスを生成し、そのインスタンスのページへとナビゲーションを行います。/// </summary>/// <paramname="viewType"></param>/// <returns></returns>publicbool Navigate(Type viewType) { if (viewType == null) { returnfalse; } var view = Activator.CreateInstance(viewType) as FrameworkElement; returnthis.Navigate(view); } #endregion/// <summary>/// NavigationCommands.GoToPageコマンドに対する応答処理/// </summary>/// <paramname="sender"></param>/// <paramname="e"></param>privatevoid OnGoToPage(object sender, ExecutedRoutedEventArgs e) { var nextPage = e.Parameter as Type; this.Navigate(nextPage); } // 以下、添付プロパティなどの定義 #region ページナビゲーションを行う領域となるContentControlを指定するための添付プロパティ// この添付プロパティで指定した値は、NavigationServiceEx.Contentプロパティとバインドして同期するようにして扱う。publicstatic ContentControl GetTarget(DependencyObject obj) { return (ContentControl)obj.GetValue(TargetProperty); } publicstaticvoid SetTarget(DependencyObject obj, ContentControl value) { obj.SetValue(TargetProperty, value); } // Using a DependencyProperty as the backing store for Target. This enables animation, styling, binding, etc...publicstaticreadonly DependencyProperty TargetProperty = DependencyProperty.RegisterAttached("Target", typeof(ContentControl), typeof(NavigationServiceEx), new PropertyMetadata(null, OnTargetChanged)); privatestaticvoid OnTargetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var element = d as FrameworkElement; var target = e.NewValue as ContentControl; if (element != null&& target != null) { // NavigationServiceExのインスタンスを、添付対象のコントロールに付加する。 var nav = new NavigationServiceEx(); NavigationServiceEx.SetNavigator(element, nav); // ContentプロパティとTargetをバインドしておく。 BindingOperations.SetBinding(nav, NavigationServiceEx.ContentProperty, new Binding() { Source = target }); // ナビゲーション用のコマンドバインディング element.CommandBindings.Add(new CommandBinding(NavigationCommands.GoToPage, nav.OnGoToPage)); var startup = NavigationServiceEx.GetStartup(element); if (startup != null) { nav.Navigate(startup); } } } #endregion #region スタートアップ時に表示するページを指定するための添付プロパティpublicstatic Type GetStartup(DependencyObject obj) { return (Type)obj.GetValue(StartupProperty); } publicstaticvoid SetStartup(DependencyObject obj, Type value) { obj.SetValue(StartupProperty, value); } // Using a DependencyProperty as the backing store for Startup. This enables animation, styling, binding, etc...publicstaticreadonly DependencyProperty StartupProperty = DependencyProperty.RegisterAttached("Startup", typeof(Type), typeof(NavigationServiceEx), new PropertyMetadata(null, OnStartupChanged)); privatestaticvoid OnStartupChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var element = d as FrameworkElement; var startupType = e.NewValue as Type; if (element != null&& startupType != null) { var nav = NavigationServiceEx.GetNavigator(element); nav?.Navigate(startupType); } } #endregion #region 任意のコントロールに対して、NavigationServiceExをアタッチできるようにするための添付プロパティpublicstatic NavigationServiceEx GetNavigator(DependencyObject obj) { return (NavigationServiceEx)obj.GetValue(NavigatorProperty); } // ↓protectedにして外部からは利用できないように。publicstaticvoid SetNavigator(DependencyObject obj, NavigationServiceEx value) { obj.SetValue(NavigatorProperty, value); } // Using a DependencyProperty as the backing store for Navigator. This enables animation, styling, binding, etc...publicstaticreadonly DependencyProperty NavigatorProperty = DependencyProperty.RegisterAttached("Navigator", typeof(NavigationServiceEx), typeof(NavigationServiceEx), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits)); #endregion } publicstaticclass NavigationServiceExtensions { /// <summary>/// view引数で指定されたインスタンスのページへとナビゲーションを行います。/// </summary>/// <paramname="element"></param>/// <paramname="view"></param>/// <returns></returns>publicstaticbool Navigate(this FrameworkElement element, FrameworkElement view) { var navigator = NavigationServiceEx.GetNavigator(element); return navigator.Navigate(view); } /// <summary>/// viewType引数で指定された型のインスタンスを生成し、そのインスタンスのページへとナビゲーションを行います。/// </summary>/// <paramname="element"></param>/// <paramname="viewType"></param>/// <returns></returns>publicstaticbool Navigate(this FrameworkElement element, Type viewType) { var navigator = NavigationServiceEx.GetNavigator(element); return navigator.Navigate(viewType); } }
これでFrameを使わず最低限のページ遷移が行えます。
WPF標準のFrame/NavigationWindowといったコントロール類のイヤな動作は塞ぎつつ、これらを使った時に近い感覚でのページ遷移もできるかと思います。(NavigationCommands.GoToPageでの遷移など)
余力があれば、ページ遷移前後のイベント通知を追加したり、遷移時のアニメーションなどをやってみようかな。