Xamarin.Forms+ReactivePropertyでスマホアプリ開発！《MasterDetailPage編》

【記事ツリー】

- [スマホアプリ開発！《本編》](https://qiita.com/YSRKEN/items/0036c6506155b9bf8051)
 - [スマホアプリ開発！《MasterDetailPage編》](https://qiita.com/YSRKEN/items/521d2962ac664fa95ebe)
 - [スマホアプリ開発！《ReactiveProperty編》](https://qiita.com/YSRKEN/items/d6835ac9f1338eeaab05)
 - [スマホアプリ開発！《ファイル読み込み編》](https://qiita.com/YSRKEN/items/36fb3b4ae0c0cf7476db)
 - [スマホアプリ開発！《リリース・署名編》](https://qiita.com/YSRKEN/items/cf22da85013197949742)

# 概要
　[Xamarin.Forms+ReactivePropertyでスマホアプリ開発！《本編》](https://qiita.com/YSRKEN/items/0036c6506155b9bf8051)において、どのようにMasterDetailPageを実装したのかといった話です。正直**一番手こずったパート**ですので、本文も大ボリュームになります！ごめんなさい！

# そもそもMasterDetailPageって？

　アニメーションGIFで示すとこんな感じのページです。
([Xamarin.Forms で MasterDetailPage を使うには - Xamarin 日本語情報](http://ytabuchi.hatenablog.com/entry/2015/07/23/192919)より引用)

![20150723183314.gif](https://qiita-image-store.s3.amazonaws.com/0/29802/0537c5e4-677f-1a28-e626-7d2db3cbc4cb.gif)

　つまり、

- 左上のボタンを押すとメニューページが出てくる
- メニューウィンドウの1つをタップするとそれに対応したページに切り替わる

といった構造になっているわけですね。ここで「メニューページ」「対応したページ」は`<ContentPage>`(普通のページ)や`<TabbedPage>`(タブで中身を切り替えられるページ)で記述しますが、両者を関連付けるページとして`<MasterDetailPage>`を使用します。

# どうやって実装するの？
　戦略としてはこんな感じです。

- 「対応したページ」を1つ以上用意する。こちらは何も工夫する必要はありません
- 「メニューページ」を1つ用意する。例えば`<ListView>`コントロールを使用する場合、「リストの項目を押したか」「押したとしたらどの項目を押したか」を、イベント・プロパティ取得できるようにする必要があります
- 「関連付けるページ」を1つ用意する。これにおける`Master`プロパティは「メニューページ」のインスタンスを持っており、`Detail`プロパティは「対応したページ」のインスタンスを持っている。また、`IsPresented`プロパティがfalseなら、「メニューページ」が引っ込んでいることを表す
- 「関連付けるページ」は、「メニューページ」におけるイベントを監視し、「対応したページ」を切り替えたくなるようなイベントが来た場合、`Detail`プロパティと`IsPresented`プロパティを更新する処理を行う

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29802/0d84f85c-5e18-e51c-0bca-be5c93d1da74.png)

　これを実装するため、最初参考にしたページでは、`x:Name`を多用したコードになっていました。
　　[Xamarin.Forms の MasterDetail を実装してみよう - Qiita](https://qiita.com/NakamuraYoichi/items/cf95748426615b1f6e47)

　しかし、**あまりそういったことをしたくなかった**ので、以下ではViewModelとReactivePropertyを活用した解決方法を考えます。

# 解決策

## 「関連付けるページ」での処理

```Views/MasterDetailPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<MasterDetailPage xmlns="http://xamarin.com/schemas/2014/forms"
	xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
	xmlns:local="clr-namespace:SampleApplication.Views"
	x:Class="SampleApplication.Views.DetailPage">

	<MasterDetailPage.Master>
		<local:MasterPage/>
	</MasterDetailPage.Master>
	<MasterDetailPage.Detail>
		<NavigationPage>
			<x:Arguments>
				<local:Page1/>
			</x:Arguments>
		</NavigationPage>
	</MasterDetailPage.Detail>
</MasterDetailPage>
```

　この辺は割とテンプレですね。ただ、これに結びついたcsを少し工夫しています。

```Views/MasterDetailPage.xaml.cs
using System;
using Xamarin.Forms;
using SampleApplication.ViewModels;
using Xamarin.Forms.Xaml;

namespace SampleApplication.Views
{
	public partial class MasterDetailPage: MasterDetailPage
	{
		public MasterDetailPage()
		{
			InitializeComponent();
			// 選択を切り替えた際の動きを記述する
			var masterPage = this.Master as MasterPage;
			var masterPageViewModel = masterPage.BindingContext as MasterPageViewModel;
			masterPageViewModel.SelectedMenuItem.Subscribe(item => {
				// アイテムがnullでなければ、それに合わせて表示するページを切り替える
				if (item != null) {
					// itemに登録したTargetTypeから表示ページのインスタンスを作成し、代入する
					Detail = new NavigationPage((Page)Activator.CreateInstance(item.TargetType));
					// 選択を解除する
					masterPageViewModel.SelectedMenuItem.Value = null;
					// 選択ページを引っ込める
					IsPresented = false;
				}
			});
		}
	}
}
```

　つまり、「メニューページ」を項目をタップした際に変わる項目名を`SelectedMenuItem`プロパティとして、ReactivePropertyの変更通知機能により、その変化を読み取るわけです。これにより、「関連付けるページ」のXAMLに細工しなくてても、その子である「メニューページ」の変更を通知として拾って利用することができます。
　また、こうして引っ張ってきた`SelectedMenuItem`プロパティを叩くことで、「選択を解除する」操作も同様に実現できます。
　本来なら`SelectedMenuItem`プロパティの`Subcribe`メソッドに登録したいところなのですが、「関連付けるページ」における`Master`プロパティと`Detail`プロパティと`IsPresented`プロパティを叩きやすくするため、コードビハインド部分に書くことになりました。

## 「メニューページ」での処理

```Views/MasterPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
	xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
	x:Class="SampleApplication.Views.MasterPage"
	Padding="0,40,0,0">

	<StackLayout VerticalOptions="FillAndExpand">
		<Label Text="メニュー" FontSize="Large" Margin="10,10,10,10"/>
		<ListView VerticalOptions="FillAndExpand" SeparatorVisibility="None"
				ItemsSource="{Binding MenuList}"
				SelectedItem="{Binding SelectedMenuItem.Value}">
			<ListView.ItemTemplate>
				<DataTemplate>
					<TextCell Text="{Binding Title}" />
				</DataTemplate>
			</ListView.ItemTemplate>
		</ListView>
	</StackLayout>
</ContentPage>
```

　ここで注目したいのはメニュー部分です。Data Bindingを駆使することにより、リストを表示するだけでなく、変更通知も検出できるようになりました(前述の通り)。また、コードビハインドはシンプル極まりない仕様ですが、

```Views/MasterPage.xaml.cs
using SampleApplication.ViewModels;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;

namespace SampleApplication.Views
{
	public partial class MasterPage : ContentPage
	{
		public MasterPage()
		{
			InitializeComponent();
			this.BindingContext = new MasterPageViewModel();
		}
	}
}
```

逆にViewModelは少しリッチになっています。View側での余計な負担を減らしたというわけですね。

```ViewModels/MasterPageViewModel.cs
using SampleApplication.Models;
using SampleApplication.Views;
using Reactive.Bindings;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;

namespace SampleApplication.ViewModels
{
	class MasterPageViewModel : INotifyPropertyChanged
	{
		#pragma warning disable 0067
		public event PropertyChangedEventHandler PropertyChanged;

		// ReactiveProperty
		public ReactiveProperty<MenuItem> SelectedMenuItem { get; set; }
			= new ReactiveProperty<MenuItem>();
		// ReactiveCollection
		public ReadOnlyReactiveCollection<MenuItem> MenuList { get; }

		// コンストラクタ
		public MasterPageViewModel()
		{
			#region ReactiveCollectionを設定
			// MenuList
			{
				var menuList = new List<MenuItem> {
				new MenuItem {Title = "ページ1", TargetType = typeof(Page1) },
				new MenuItem {Title = "ページ2", TargetType = typeof(Page2) },
				new MenuItem {Title = "ページ3", TargetType = typeof(Page3) },
			};
				var oc = new ObservableCollection<MenuItem>(menuList);
				MenuList = oc.ToReadOnlyReactiveCollection();
			}
			#endregion
		}
	}
}
```

```Models/MenuItem.cs
using System;

namespace SampleApplication.Models
{
	public class MenuItem
	{
		public string Title { get; set; }
		public Type TargetType { get; set; }
	}
}
```

# 課題
　このようにすれば`x:Name`せずにMasterDetailPageを表現することができました。一応、「MasterPageViewModelのSelectedMenuItemの`Subcribe`メソッドからメニュー関係のロジックを行うことでMasterDetailPageクラスの負担(コードビハインド)を減らす」ことも考えましたが、MasterプロパティとDetailプロパティをData Bindingすることが難しく、試行錯誤中です……。