しっぽを追いかけて

ぐるぐるしながら考えています

Unity と猫の話題が中心   掲載内容は個人の私見であり、所属組織の見解ではありません

Xamarin.Forms で ToolBar によるページ内遷移がしたい

Microsoft Band の接続サンプルを作る上で、ToolBarItem をタップしたら同一画面内で表示遷移させたくなったので挑戦

TabbedPage とかだと同じ Template の表示しかできないので、もっと自由度の高い ToolBar を利用することにしたというわけです

ソースコードの一式は下記にあります!

github.com

※ 順次改修していく予定なので、この記事の内容が現時点のソースより古い可能性があります

まずは 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"
             xmlns:cv="clr-namespace:XamarinBandSample.Converters;assembly=XamarinBandSample"
             xmlns:prismmvvm="clr-namespace:Prism.Mvvm;assembly=XamarinBandSample"
             prismmvvm:ViewModelLocator.AutoWireViewModel="true"
             x:Class="XamarinBandSample.Views.TopPage">
  <ContentPage.Resources>
    <ResourceDictionary>
      <cv:NegativeConverter x:Key="NegativeConverter"/>
    </ResourceDictionary>
  </ContentPage.Resources>
  <ContentPage.Padding>
    <OnPlatform x:TypeArguments="Thickness"
                iOS="0,20,0,0"/>
  </ContentPage.Padding>
  <ContentPage.ToolbarItems>
    <ToolbarItem Text="Basics" Command="{Binding SelectBasicsCommand}" />
    <ToolbarItem Text="Senseor" Command="{Binding SelectSensorsCommand}"/>
  </ContentPage.ToolbarItems>

  <Grid>
    
    <!-- Basics Settings Pain-->
    <ContentView IsVisible="{Binding ShowBasics}">
      <StackLayout Orientation="Vertical"
                   Padding="10"
                   Spacing="10">

        <Label Text="Band Test"
               FontSize="Large"
               HorizontalOptions="Center"
               VerticalOptions="Center"/>

        <StackLayout Orientation="Vertical"
                     Spacing="5">
          <Button Text="Connect Band"
                  FontSize="Medium"
                  HorizontalOptions="Start"
                  VerticalOptions="Center"
                  IsEnabled="{Binding IsConnected, Converter={StaticResource NegativeConverter}}"
                  Command="{Binding ConnectCommand}"/>
          <Grid Padding="20,0">
            <Label Text="{Binding ConnectMessage}"
                   HeightRequest="20"
                   Opacity="0.6"
                   FontSize="Small"/>
          </Grid>
        </StackLayout>

        <StackLayout Orientation="Vertical"
                     Spacing="5">
          <Label Text="Device Name:"
                 FontSize="Medium"/>
          <Grid Padding="20,0">
            <Label Text="{Binding BandName}"
                   HeightRequest="20"
                   FontSize="Small"/>
          </Grid>
        </StackLayout>

        <StackLayout Orientation="Vertical"
                     Spacing="5">
          <Label Text="Hardware Version:"
                 FontSize="Medium"/>
          <Grid Padding="20,0">
            <Label Text="{Binding HardwareVersion}"
                   HeightRequest="20"
                   FontSize="Small"/>
          </Grid>
        </StackLayout>

        <StackLayout Orientation="Vertical"
                     Spacing="5">
          <Label Text="Firmware Version:"
                 FontSize="Medium"/>
          <Grid Padding="20,0">
            <Label Text="{Binding FirmwareVersion}"
                   HeightRequest="20"
                   FontSize="Small"/>
          </Grid>
        </StackLayout>

      </StackLayout>
    </ContentView>

    <!-- Sensor Info Pain-->
    <ContentView IsVisible="{Binding ShowSensors}">
      <Grid Padding="10"
            RowSpacing="10"
            ColumnSpacing="10">
        <Grid.RowDefinitions>
          <RowDefinition Height="Auto"/>
          <RowDefinition Height="Auto"/>
          <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="100"/>
          <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        
        <Label Text="Detecting"
               Grid.Column="0"
               Grid.Row="0"
               VerticalOptions="Center"
               FontSize="Medium"/>
        <Switch IsToggled="{Binding IsSensorDetecting}"
                Grid.Column="1"
                Grid.Row="0" />

        <StackLayout Orientation="Horizontal"
                     Grid.Column="0"
                     Grid.ColumnSpan="2"
                     Grid.Row="1"
                     Spacing="5">

          <Label Text="Accelometer:"
                 FontSize="Medium"/>
          <Label Text=""
                 FontSize="Small"/>

        </StackLayout>

      </Grid>
    </ContentView>
  </Grid>

</ContentPage>

ContentPage.ToolBarItems に2つの ToolBarItem を追加し、同一の画面内に2つの ContentView を持たせました

仕組みとしては ShowBasics や ShowSensors の値によって 2つの ContentView の表示有無が切り替わるようになっています

この画面に合わせて ViewModel を改修

/// <summary>
/// トップ画面の ViewModel
/// </summary>
public class TopPageViewModel : BindableBase
{
    ~ 中略 ~

    #region ShowBasics

    /// <summary>
    /// 基本設定表示フラグ
    /// </summary>
    private bool showBasics = true;

    /// <summary>
    /// 基本設定表示フラグ
    /// </summary>
    public bool ShowBasics
    {
        get { return this.showBasics; }
        set { this.SetProperty(ref this.showBasics, value); }
    }

    #endregion //ShowBasics

    #region ShowSensors

    /// <summary>
    /// センサー情報表示フラグ
    /// </summary>
    private bool showSensors = false;

    /// <summary>
    /// センサー情報表示フラグ
    /// </summary>
    public bool ShowSensors
    {
        get { return this.showSensors; }
        set { this.SetProperty(ref this.showSensors, value); }
    }

    #endregion //ShowSensors

    ~ 中略 ~

    #region SelectBasicsCommand

    /// <summary>
    /// 基本設定表示選択コマンド
    /// </summary>
    public ICommand SelectBasicsCommand { get; private set; }

    #endregion //SelectBasicsCommand

    #region SelectSensorsCommand

    /// <summary>
    /// センサー情報表示選択コマンド
    /// </summary>
    public ICommand SelectSensorsCommand { get; private set; }

    #endregion //SelectSensorsCommand

    ~ 中略 ~

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="manager">Microsoft Band デバイス管理クラス</param>
    [InjectionConstructor]
    public TopPageViewModel(IBandManager manager)
    {
        this.manager = manager;
        this.SelectBasicsCommand = new DelegateCommand(this.SelectBasics, () => { return !this.ShowBasics; });
        this.SelectSensorsCommand = new DelegateCommand(this.SelectSensors, () => { return !this.ShowSensors; });
        this.ConnectCommand = DelegateCommand.FromAsyncHandler(this.Connect);
    }

    /// <summary>
    /// 基本設定表示切替
    /// </summary>
    private void SelectBasics()
    {
        this.ShowSensors = false;
        this.ShowBasics = true;
        ((DelegateCommand)this.SelectBasicsCommand).RaiseCanExecuteChanged();
        ((DelegateCommand)this.SelectSensorsCommand).RaiseCanExecuteChanged();
    }

    /// <summary>
    /// センサー情報表示切替
    /// </summary>
    private void SelectSensors()
    {
        this.ShowBasics = false;
        this.ShowSensors = true;
        ((DelegateCommand)this.SelectBasicsCommand).RaiseCanExecuteChanged();
        ((DelegateCommand)this.SelectSensorsCommand).RaiseCanExecuteChanged();
    }

    ~ 中略 ~
}

ToolBarItem を選択したときの処理と選択可能かどうかを DelegateCommand という Prism.Mvvm のクラスで制御しています

DelegateCommand の場合、new するときの第一引数にコマンド実行するときの処理、第二引数に実行可能かどうかの判定処理を指定することでデータバインディングだけでもこの制御ができるようになります

SelectBasics() や SelectSensors() の選択時の処理のメソッドでは、選択可能状態の更新を ToolBarItem に伝えるために DelegateCommand の RaiseCanExecuteChanged を呼び出しています

これがないと選択状態が更新されないはず

最後に App.cs を変更

/// <summary>
/// アプリケーション基盤クラス
/// </summary>
public class App : Application
{
    /// <summary>
    /// DI コンテナ
    /// </summary>
    public static UnityContainer Container = new UnityContainer();

    /// <summary>
    /// ナビゲーション画面
    /// </summary>
    public static NavigationPage Navigation = null;

    /// <summary>
    /// コンストラクタ
    /// </summary>
    static App()
    {
    }

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public App()
    {
        // ViewModel をインスタンス化するデフォルトメソッドを指定
        ViewModelLocationProvider.SetDefaultViewModelFactory((type) => Container.Resolve(type));

        Navigation = new NavigationPage(new TopPage());
        this.MainPage = Navigation;
    }

    ~ 中略 ~
}

ToolBar を表示する場合には ContentPage.ToolBarItems に ToolBarItem を追加するだけでは不十分で、App.MainPage に NavigationPage を指定しないといけません

これが気づきづらかったり

これで動くはずなので実行してみました

Windows Phone のアイコンが × になるのは、Windows Runtime の OnPlatform タグ対応が来ればなんとかなるからよいとしても・・・iOS だけ選択状態が切り替わらない!!

これたぶん不具合ですかね・・・ToolBarItem は IsEnabled プロパティがないので、Command で選択可能状態を制御するしかないのに機能しないので;

仕方がないので Xamarin Forums で質問しておきました

forums.xamarin.com