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


【記事ツリー】

概要

 Xamarin.Forms+ReactivePropertyでスマホアプリ開発!《本編》において、どのようにMasterDetailPageを実装したのかといった話です。正直一番手こずったパートですので、本文も大ボリュームになります!ごめんなさい!

そもそもMasterDetailPageって?

 アニメーションGIFで示すとこんな感じのページです。
(Xamarin.Forms で MasterDetailPage を使うには - Xamarin 日本語情報より引用)

 つまり、

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

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

どうやって実装するの?

 戦略としてはこんな感じです。

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

 これを実装するため、最初参考にしたページでは、x:Nameを多用したコードになっていました。
  Xamarin.Forms の MasterDetail を実装してみよう - Qiita

 しかし、あまりそういったことをしたくなかったので、以下では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することが難しく、試行錯誤中です……。