LoginSignup
2
2

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-03-25

【記事ツリー】

概要

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

そもそもMasterDetailPageって?

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

20150723183314.gif

 つまり、

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

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

どうやって実装するの?

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

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

image.png

 これを実装するため、最初参考にしたページでは、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することが難しく、試行錯誤中です……。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2