Xamarin.FormsでのMasterDetailとバックボタン

de:code2017アプリとかで気になってた事があって

MasterDetail組む時にホーム画面への配慮した方が良いかも。っていう問題。

iOSだと意識しなくて良いのだけどAndroidだとバックボタンがあるのでホーム画面に配慮する必要があると思っています。

例えばGoogle MapであったりGmailアプリであったりで、ハンバーガーメニューで他の画面へ移動してからバックスペースを押すと最初の画面へ戻ると思います。

こうなっていないアプリって結構めんどくさくて

例えば設定画面を開いて設定を変更して元の画面に戻る、みたいなケースを想定した時に

ホーム画面を考慮していないアプリの場合

```
ホーム画面 -> メニューを開く -> 設定画面へ移動 -> 設定操作 -> メニューを開く -> ホームへ戻る
```

というオペレーションになるのだけど

```
ホーム画面 -> メニューを開く -> 設定画面へ移動 -> 設定操作 -> バックボタン
```

っていう操作をしてアプリが閉じちゃう、みたいな事になったりならなかったり。

Xamarin.FormsでCustomRendererを使わずにMasterDetailをホーム画面を意識した感じにはいくつか方法はあるけど、MasterDetailっぽい動作を維持しつつ(開いてるメニューが閉じてDetailが変わる感じ)実現しようとすると、DetailにNavigationPageを二つ重ねる感じで実現できます。

VS2017のXamarinテンプレートのMasterDetailを今回の説明方法で修正したイメージはこんな感じです。

https://sleepyandhungry1984.tumblr.com/post/162319590522/xamarinforms-masterdetail

```
MasterDetail
Master -- ContentPage
Detai -- NavigationPage(outer)
　　　　　　　----NavigationPage(inner)
　　　　　　　　　　　----ContentPage(Home)
```
みたいな感じで構成します。

そして重要な点はinnerナビゲーションの下に追加するページは```NavigationPage.SetHasNavigationBar```でナビゲーションバーを表示しないようにしておきます。

これでメニューから例えばAboutページを選択した場合にはInnerにPushAsyncして

```
NavigationPage(outer)
-------NavigationPage(inner)
-------------ContentPage(Home)
-------------ContentPage(About)
```

という感じにします。

こうするとハンバーガーメニューは残ったままDetailが切り替わります。

でAndroidのバックボタンを押すとAboutがpopしてHomeに戻ると。

Aboutが開いている状態でメニューからHomeを選択した場合にはPopToRootしてあげれば良くて、Aboutが開いている状態で別のメニューを選んだ場合には InnerにPushAsyncした後にAboutをRemovePageする感じでＯＫ。

で、ページ遷移させようとした場合には

```
NavigationPage(outer)
-------NavigationPage(inner)
-------------ContentPage(Home)
-------ContentPage(Newitem)
```

という感じで、outerにPushAsyncすることでNavigationBarのハンバーガメニューが ←矢印に変わってナビゲーションする感じです。

メンドクサイし実装が複雑化するしMVVMフレームワークとかだとできなかったりする場合もあるし、Xamarin.Formsでこの辺にコストをかけるのか？っていう疑問もあるんですけど、でも出来てると良いかも？

そんなお話でした。

コードをちょっと張り付けておきます。

```csharp
#region

using System;
using System.Threading;
using MasterDetail.Views;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;

#endregion

[assembly: XamlCompilation(XamlCompilationOptions.Compile)]

namespace MasterDetail
{
	// XamlになれるとC#コードでも普通に画面書けることに気が付いた。
	// Intellisenseとコード補完最大限に効くので結構書きやすい。
	// 物凄く読みにくいけどね🍣

	public partial class App : Application
	{
		private readonly NavigationPage _innerNavPage;
		private readonly MasterDetailPage _masterDetailPage;
		private readonly NavigationPage _outerNavPage;

		public App()
		{
			InitializeComponent();

			// browse page
			var browsePage = new ItemsPage
			{
				Title = "Browse"
			};
			NavigationPage.SetHasNavigationBar(browsePage, false);

			// aboutPage
			var aboutPage = new AboutPage
			{
				Title = "About"
			};
			NavigationPage.SetHasNavigationBar(aboutPage, false);

			// InnerNavigationPage
			_innerNavPage = new NavigationPage(browsePage)
			{
				Title = browsePage.Title
			};
			_innerNavPage.PropertyChanged += (sender, args) =>
			{
				if (args.PropertyName == NavigationPage.CurrentPageProperty.PropertyName)
					_innerNavPage.Title = _innerNavPage.CurrentPage.Title;
			};

			// OuterNavigationPage
			_outerNavPage = new NavigationPage(_innerNavPage);

			// MenuPage
			Page menuPage = null;
			menuPage = new ContentPage
			{
				Title = "Menu",
				Content = new StackLayout
				{
					Children =
					{
						new ContentView
						{
							Padding = 20,
							Content = new Label {Text = "Browse"},
							GestureRecognizers =
							{
								new TapGestureRecognizer
								{
									Command = new Command(() => { MessagingCenter.Send(menuPage, "Menu", "Browse"); })
								}
							}
						},
						new ContentView
						{
							Padding = 20,
							Content = new Label {Text = "About"},
							GestureRecognizers =
							{
								new TapGestureRecognizer
								{
									Command = new Command(() => { MessagingCenter.Send(menuPage, "Menu", "About"); })
								}
							}
						}
					}
				}
			};


			_masterDetailPage = new MasterDetailPage();
			_masterDetailPage.Master = menuPage;
			_masterDetailPage.Detail = _outerNavPage;

			MainPage = _masterDetailPage;

			MessagingCenter.Subscribe(this, "Navigation",
				new Action<ItemsPage, Page>((s, p) => { _outerNavPage.PushAsync(p); }));

			MessagingCenter.Subscribe(this, "Menu", new Action<Page, string>((p, s) =>
			{
				if (s == "Browse")
				{
					if (!(_innerNavPage.CurrentPage is ItemsPage))
						_innerNavPage.PopToRootAsync();
				}
				else if (s == "About")
				{
					if (_innerNavPage.CurrentPage is AboutPage)
						return;

					if (_innerNavPage.CurrentPage is ItemsPage)
					{
						_innerNavPage.PushAsync(aboutPage);
					}
					else
					{
						var currentPage = _innerNavPage.CurrentPage;
						_innerNavPage.PushAsync(aboutPage)
							.ContinueWith(task =>
							{
								SynchronizationContext.Current.Post(state => { _innerNavPage.Navigation.RemovePage(currentPage); }, null);
							});
					}
				}
				_masterDetailPage.IsPresented = false;
			}));
		}
	}
}
```