Syncfusion SfCalendarを使う(Xamarin Forms)

1. はじめに

Xamarin Formsで、Syncfusion SfCalendarを使って以下のようなサンプル実装を行いました。

f:id:daigo-knowlbo:20180328013715p:plain:w300
f:id:daigo-knowlbo:20180328020349p:plain:w300

  • 月カレンダー形式で予定を表示する
  • 日をクリックすると、対象日のスケジュール詳細(件名)が表示される
  • スワイプもしくはボタンクリックで前後の月に移動できる
  • Prismを使ってMVVMアーキテクチャとする

2. 実装手順

2.1. VS 2017でプロジェクト作成

Visual Studio 2017でPrism Blank App(Xamarin.Forms)プロジェクトを作成。
プロジェクト名は「UseSfCalendarWithPrism」としました。

f:id:daigo-knowlbo:20180328014042p:plain

2.2. Nugetパッケージ管理でSyncfusion SfCalendarを追加

XamarinNugetパッケージ管理で「Syncfusion.Xamarin.SfCalendar」をインストール。

f:id:daigo-knowlbo:20180328014001p:plain

2.3. 実装

で、実装は以下に置きました。。。。

github.com

以下にサンプル実装のポイントを・・・

ポイント1 カレンダーの月変更イベントをViewModelにバインド

カレンダーの月変更イベントは SfCalendar.MonthChanged です。
MVVMとしているのでイベントをViewMode(MainPageViewModel)で受け取りたいです。
その為、Prismの「EventToCommandBehavior」を利用して、イベントをView→ViewModelにコマンドとして伝播させています。

[MainPage.xaml]
<ContentPage ...省略
                        xmlns:b="clr-namespace:Prism.Behaviors;assembly=Prism.Forms" />
<SfCal:SfCalendar x:Name="calendar" 
                  ShowInlineEvents="True"
                  MinDate="{Binding MinDate}"
                  MaxDate="{Binding MaxDate}"
                  BlackoutDates="{Binding BlackoutDates}"
                  DataSource="{Binding CalendarEventCollection}"
                  SelectionMode="{Binding SelectionMode, Mode=OneWay,Converter={StaticResource SelectionModeConverter}}">
    <SfCal:SfCalendar.Behaviors>
        <b:EventToCommandBehavior EventName="MonthChanged" 
                              Command="{Binding MonthChangedCommand}"
                              EventArgsParameterPath="args.CurrentValue" />
    </SfCal:SfCalendar.Behaviors>
</SfCal:SfCalendar>
[MainPageViewModel.cs]

public class MainPageViewModel : ViewModelBase
{
  ...省略
  public ICommand MonthChangedCommand => new Command<DateTime>((currentDate) =>
  {
    this.UpdateEvents(currentDate);
  });
}

ポイント2 カレンダーにバインドするイベントコレクション作成

SfCalendarに表示するイベントは「SfCalendar.DataSourceプロパティ」に設定しますが、PrismでViewModelにバインドしているので「MainPageViewModel.CalendarEventCollectionプロパティ」にデータバインドしています。
カレンダーの月変更イベントに対して当該月+前後一週間のイベントをバインドデータに設定しています。

[MainPageViewModel.cs]
  public class MainPageViewModel : ViewModelBase
  {
    
    /// <summary>
    /// イベントを更新します。
    /// </summary>
    /// <remarks>
    /// 今月のイベントを表示するために、対象月+前後一週間のイベントをコレクションに設定します。
    /// 1ヶ月のカレンダーの前後に 前月・次月 の日付が表示されるため、前後1週間のイベントを設定します。
    /// </remarks>
    /// <param name="calendarDate"></param>
    private void UpdateEvents(DateTime calendarDate)
    {
      // バインド対象のthis.CalendarEventCollectionを直接、繰り返しAdd()するとパフォーマンスが著しく落ちるのでテンポラリにデータコレクションを用意して差し替える
      CalendarEventCollection newCalendarEventCollection = new CalendarEventCollection();

      DateTime dt = new DateTime(calendarDate.Year, calendarDate.Month, 1);
      int thisMonthLastDay = dt.AddMonths(1).AddDays(-1).Day;
      for (int i = -7; i < thisMonthLastDay+7; i++)
      { // イベントはサンプルなので適当に2日に1回散歩と仕事、毎日のランチを設定
        DateTime eventDt = dt.AddDays(i);

        if (eventDt.Day % 2 == 1)
        {
          //this.calendarEventCollection.Add(
          newCalendarEventCollection.Add(
          new CalendarInlineEvent()
          {
            Subject = $"{eventDt.Day}日 散歩",
            StartTime = eventDt.AddHours(10),
            EndTime = eventDt.AddHours(11),
            Color = Color.Green
          });
        }
        //this.calendarEventCollection.Add(
        newCalendarEventCollection.Add(
          new CalendarInlineEvent()
          {
            Subject = $"{eventDt.Day}日 ランチ",
            StartTime = eventDt.AddHours(12),
            EndTime = eventDt.AddHours(13),
            Color = Color.Orange
          });
        if(eventDt.Day % 2 == 0)
        {
          //this.calendarEventCollection.Add(
          newCalendarEventCollection.Add(
            new CalendarInlineEvent()
            {
              Subject = $"{eventDt.Day}日 仕事",
              StartTime = eventDt.AddHours(10),
              EndTime = eventDt.AddHours(19),
              Color = Color.Blue
            });
        }
      }
      this.CalendarEventCollection.Clear();
      this.CalendarEventCollection = newCalendarEventCollection;
    }
  }

つまずいた点

2018/3/28現在版 Syncfusion SfCalendar(android)に不具合がありました。
イベントを設定しているのに画面上で、対象日をクリックすると「No Appointments」と表示されてしまう不具合でした。
twitterでつぶやいたところSyncfusionの方よりリプライを頂き、即座にパッチアッセンブリを頂き不具合の修正を確認できました。
アップデートとしては2018年3月末の「2018 Vol 1 SP1」で対応されるとのことです。

SfCalendarへの要望

表示形式が「YearViewとMonthView」の2つなのですが、Weeklyとかandroidのカレンダーみたいな形式とか、色々なバリエーションがあればもっと嬉しいなぁ、と思いました。

※今日のブログ、雑ですね。。。まあ、動くソースをgithubに置いたので、「おかしいぞ!分からんぞ!」と思った方はコメント頂ければと思いますm( )m

Xamarin FormsでCarouselViewControlを使う

1. はじめに

Xamarin Formsで以下のようなUIを作りたくて、Carouselコントロールについて あたふた したのでブログにメモっておきます。

f:id:daigo-knowlbo:20180322003924p:plain:w300 f:id:daigo-knowlbo:20180322004001p:plain:w300 f:id:daigo-knowlbo:20180322004009p:plain:w300

2. いくつかのCarousel実装

久しぶりにXamarin Formsアプリ作り始めたのですが、そんな自分にとってはカルーセルがカオスに見えました。。。
どれ 使えばいいの?と・・・
ググったら公式のと非公式のと色々出てきまして、今回は「alexrainman/CarouselView」を使用しました。
一応公式の2つと合わせて以下の3つについて書いておこうと思います。

  • Xamarin.Forms..CarouselPage
  • Xamarin.Forms.CarouselView
  • alexrainman/CarouselView

2.1. Xamarin.Forms.CarouselPage

Xamarin Formsの標準クラスです。
名前からも分かるようにページクラスです。
クラス図でいうと以下のような感じ。なので、ページ内の1要素として配置することは出来ないですね。

f:id:daigo-knowlbo:20180321234555p:plain

2.2. Xamarin.Forms.CarouselView

公式から出された奴ですね。
なんか一時期、CarouselPageはdeprecatedしてCarouselViewに移行する的な話を聞いた気がするのですが、Nugetしようとしたら、2016/7/28のプレリリースで止まっている。。。これはタヒってるのかな。。。

f:id:daigo-knowlbo:20180321235212p:plain

2.3. alexrainman/CarouselView

と、上記公式の2つが使えそうにないので、ググったら良さげなものがありました。

「alexrainman/CarouselView(https://github.com/alexrainman/CarouselView)」

こちらは以下のようにXamarin.Forms.Viewクラスの派生クラスとして実装されているのでPage内の1要素として利用することができます。
※「alexrainman/CarouselView」の実際に配置するコントロールクラス名は CarouselViewControlクラス になります。

f:id:daigo-knowlbo:20180322000036p:plain

3. 実装

では実装します。
(超簡単なサンプルだけど、alexrainman/CarouselViewを使った説明は日本語に少なげだったので、書いておこうと思いました。)
あと、一応 Prism の上で実装します。

3.1. ソリューション・プロジェクトの作成

Visual Studio 2017を起動します。
メニュー「ファイル→新規作成→プロジェクト」を選択。
ここでは、以下の設定で作成します。

プロジェクトテンプレート:「Prism→Xamarin.Forms→Prism Blank App(Xamarin.Forms)」
名前:UseAlexCarouselViewApp

f:id:daigo-knowlbo:20180322000444p:plain

ターゲットは android / iOS としました。

f:id:daigo-knowlbo:20180322000621p:plain

3.2. Nugetパッケージの追加

UseAlexCarouselViewAppプロジェクト(Formsの共通実装のプロジェクト)のNugetパッケージマネージャを表示して、CarouselView.FormsPluginを検索し、インストールします(これが alexrainman/CarouselView になります)。

f:id:daigo-knowlbo:20180322000906p:plain

3.3. モデルクラスの追加

せっかくPrismを使用しているので、ページ情報を表すモデルクラスを追加します。
UseAlexCarouselViewApp/Models/PageInfo.cs を追加します。

f:id:daigo-knowlbo:20180322001119p:plain

// UseAlexCarouselViewApp\Models\PageInfo.cs

using Xamarin.Forms;

namespace UseAlexCarouselViewApp.Models
{
  public class PageInfo
  {
    public string Name { get; set; }
    public Color ForeColor { get; set; }
    public Color BackColor { get; set; }
  }
}

3.4. ViewModelにPageInfoコレクションを追加

デフォルトで作られたMainPageViewModelクラスにPageInfoコレクションプロパティを追加します。
後でPageに配置するCarouselViewControlのページにバインドするプロパティになります。

// UseAlexCarouselViewApp\ViewModels\MainPageViewModel.cs

using System.Collections.ObjectModel;
using Prism.Navigation;
using Xamarin.Forms;
using BlankApp6.ViewModels;

namespace UseAlexCarouselViewApp.ViewModels
{
  public class MainPageViewModel : ViewModelBase
  {
    // CarouselViewControlにバインドするページ情報コレクション
    public ObservableCollection<PageInfo> CarouselPageInfo { get; set; }

    public MainPageViewModel(INavigationService navigationService) 
      : base (navigationService)
    {
      Title = "Main Page";

      // コレクション初期化
      this.CarouselPageInfo = new ObservableCollection<PageInfo>
      {
        new PageInfo
        {
          Name = "Page1",
          ForeColor=Color.Yellow,
          BackColor= Color.Red
        },
        new PageInfo
        {
          Name = "Page2",
          ForeColor=Color.Black,
          BackColor= Color.Yellow
        },
        new PageInfo
        {
          Name = "Page3",
          ForeColor=Color.Gold,
          BackColor= Color.Green
        }
      };
    }
  }
}

3.5. PageにCarouselViewControlを追加

MainPage.xamlにCarouselViewControlを配置して、MainViewModel.PageInfoをデータバインディングします。

UseAlexCarouselViewApp\Views\MainPage.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:CarouselView.FormsPlugin.Abstractions;assembly=CarouselView.FormsPlugin.Abstractions"
       x:Class="UseAlexCarouselViewApp.Views.MainPage"
       Title="{Binding Title}">

  <StackLayout>
    <Label Text="Hello" />
    <Editor BackgroundColor="Yellow" />

    <cv:CarouselViewControl
      VerticalOptions="FillAndExpand"
      HorizontalOptions="FillAndExpand"
      Position="0"
      ShowIndicators="True"
      ShowArrows="True"
      ItemsSource="{Binding CarouselPageInfo}">
      <cv:CarouselViewControl.ItemTemplate>
        <DataTemplate>
          <StackLayout VerticalOptions="FillAndExpand"
                 HorizontalOptions="FillAndExpand"
                 BackgroundColor="{Binding BackColor}">
            <Label Text="{Binding Name}" TextColor="{Binding ForeColor}"/>
          </StackLayout>
        </DataTemplate>
      </cv:CarouselViewControl.ItemTemplate>
    </cv:CarouselViewControl>
  </StackLayout>

</ContentPage>

ポイントは以下の通り。

  • コントロールを使うのでページにnamespaceを追加する
    以下により cvタグプレフィックス で CarouselViewControl が使えるようになります。
xmlns:cv="clr-namespace:CarouselView.FormsPlugin.Abstractions;assembly=CarouselView.FormsPlugin.Abstractions"
  • CarouselViewControlを配置
    親要素(ContentPage)で名前空間定義したタグプレフィックcvを使ってコントロールを配置定義しています。
    ItemsSourceとかDataTemplate定義は普通のデータバインド対応コントロールと同じテーストです。
<cv:CarouselViewControl
...
  • CarouselViewControlはView派生クラス
    CarouselViewControlはView派生クラスなのでStackLayoutの子要素としてLabelやEditorと並べて配置可能です。

3.6. ViewRendererの初期化を追加

最後に重要な「ViewRendererの初期化」処理とiOS/androidそれぞれのプロジェクトに追加します。

androidは、UseAlexCarouselViewApp.Android\MainActivity.csに「CarouselViewRenderer.Init();」を追記します。

// UseAlexCarouselViewApp.Android\UseAlexCarouselViewApp.Android\MainActivity.cs
...
using CarouselView.FormsPlugin.Android;  // ←これも追記
...
namespace UseAlexCarouselViewApp.Droid
{
  ...
  public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
  {
    ...
    global::Xamarin.Forms.Forms.Init(this, bundle);
    CarouselViewRenderer.Init();  // ←追記
    ...
  }
}

iOSは、UseAlexCarouselViewApp.iOS\AppDelegate.csに「CarouselViewRenderer.Init();」を追記します。

// UseAlexCarouselViewApp.OS\UseAlexCarouselViewApp.iOS\AppDelegate.cs
...
using CarouselView.FormsPlugin.iOS;  // ←これも追記
...
namespace UseAlexCarouselViewApp.iOS
{
  ...
  public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
  {
    ...
    public override bool FinishedLaunching(UIApplication app, NSDictionary options)
    {
      global::Xamarin.Forms.Forms.Init();
      CarouselViewRenderer.Init();  // ←追記
      ..
    }
  }
}

※ViewRenderer
ViewRendererは「Formsとネイティブコントロール要素との繋ぎ役」みたいな役割を持っています。
Forms上でのXXコントロールandroidでは○○コントロールとして表示し、iOSでは△△コントロールとして表示する。また、各ネイティブで発生したアクション・イベントをFormsコントロールのイベントに結び付けるような、そんな役割を持っています。

4. ソースはココ

サンプル実装は↓↓↓↓↓に置いときました。

github.com

<2017年8月版>Xamarin + Prism 超入門(とりあえず動かしてみよう!)

最近の本ブログへのトラフィックのうち、以前に書いた以下の記事へのものが未だに多いようです。

ryuichi111std.hatenablog.com

ただし、上記記事では開発環境が Visual Studio 2015 であり、少し古いバージョンをベースとしています。

その為、[2017年8月版]として最新の開発環境である「Visual Studio 2017(Windows)」をベースに「Xamarin + Prism 超入門(とりあえず動かしてみよう!)」を書き起こしたいと思います(厳密には Xamarin Forms + Prism です)。

1 前提条件

本編に入る前・・・以下を前提条件とします。

  1. Visual Studio 2017がインストールされている事(Windowsの場合)
  2. Visual Studio 2017において「.NETによるモバイル開発」がインストールされている事
  3. iOS / Android / UWP(VS2017のみ) 実行環境が整っている事

2 はこれ↓です。

f:id:daigo-knowlbo:20170803012230p:plain

3 については本エントリーの主題ではありませんが、Xamarin開発を始める際に引っかかる事が多い点なので、いくつかのポイントを以下に説明しておこうと思います。

1.1 iPhoneシミュレータ(iOS)実行環境設定について

Mac側およびVisual Studio 2017側で設定を行います。

1.1.1 Mac側セキュリティ設定の変更

まず、Mac側でセキュリティに関する設定を行う必要があります。
「システム環境設定」ウィンドウで「共有」をクリックします。

f:id:daigo-knowlbo:20170803012705p:plain

「共有」ウィンドウで「リモートログイン」を有効に設定します。

f:id:daigo-knowlbo:20170803012732p:plain

これにより Visual Studio 2017側 から Mac側 へのアクセスが可能になります。
(Macとの接続がとられていないと、iOSアプリの実行・デバッグが行えません)

1.1.2 Visual Studio 2017からの接続設定

Visual Studio 2017のメニュー「ツール → iOS → Xamarin Mac Agent」を選択します。
「Xamarin Mac Agent」ウィンドウが表示されるので、「次へ」をクリックします。

f:id:daigo-knowlbo:20170803012839p:plain

Macが同一ネットワーク上に存在すればリストに表示されます。
接続したいMacを選択して「接続」ボタンをクリックします。

f:id:daigo-knowlbo:20170803012929p:plain

ログイン情報を入力して「ログイン」ボタンをクリックします。

f:id:daigo-knowlbo:20170803013024p:plain

以上で、Visual Studio 2017からMacへの接続設定が完了しました。

1.2 Androidエミュレータ実行環境設定について

初期設定として行うべきことが多く躓きやすいと思います(私もXamarinやり始めのころかなり躓きました・・・)。
まず前提として、Androidエミュレータは HAXM(Intel Hardware Accelerated Execution Manager) を使って実行します。そうでないと使い物にならないほど激遅です。
で、HAXMを動作させるには「Hyper-Vを無効化」しておかなければなりません。

1.2.1 Hyper-Vの無効化

Windowsの機能として「Hyper-V」をインストールしていなければ気にする必要はありません。
Hyper-Vがインストールされている場合には、以下のコマンドによりHyper-Vを無効化します。

bcdedit /set hypervisorlaunchtype off

※コマンド実行後は、再起動により設定が反映されます。

逆にHyper-V機能を有効化するには以下のコマンドを実行します(要再起動)。

bcdedit /set hypervisorlaunchtype auto

1.2.2 Google APIs Intel x86 Atom System Image と HAXM のインストール

エミュレータイメージである「Google APIs Intel x86 Atom System Image」と、「HAXM」をインストールします。
Visual Studio 2017のメニューから「ツール → AndroidAndroid SDK マネージャー」を選択します。

f:id:daigo-knowlbo:20170803013723p:plain

表示された「Android SDK Manager」から、以下を選択して「Install xx packages..」ボタンをクリックします。

f:id:daigo-knowlbo:20170803024247p:plain
f:id:daigo-knowlbo:20170803024306p:plain

HAXMの方は「Not compatible with Windows」と表示されてインストールできない場合があります。
その場合は以下から直接ダウンロードしてインストールを行います。

software.intel.com

zipファイルをダウンロードし、解凍して生成された「intelhaxm-android.exe」を実行することでHAXMのインストールが行われます。

f:id:daigo-knowlbo:20170803013951p:plain

1.3 UWP実行環境設定について

Windows 10の「スタートメニュー → 設定 → 更新とセキュリティ → 開発者向け」を選択します。
「開発者向け機能を使う」項目から「開発者モード」を選択します。

f:id:daigo-knowlbo:20170803014052p:plain

しばらく待つとインストールが完了します。

2 Visual Studio 2017 で Xamarin + Prism 開発

やっと本題です。

Visual Studio 2017を起動します。

2.1 「Prism Template Pack」のインストール

メニュー「ツール → 拡張機能と更新プログラム」を選択します。

f:id:daigo-knowlbo:20170803014331p:plain

拡張機能と更新プログラム」ウィンドウが表示されます。

「オンライン」を選択し、右上の検索ボックスに「Prism Template Pack」と入力します。
一覧に表示された「Prism Template Pack」を選択し「ダウンロード」ボタンをクリックします。
ダウンロードが完了したら、「閉じる」ボタンをクリックします。

f:id:daigo-knowlbo:20170803014426p:plain

Visual Studio 2017を終了させます。

「Prism Template Pack」のインストール画面が表示されます。

f:id:daigo-knowlbo:20170803014512p:plain

インストール画面の指示に従うことで、インストールが完了します。

f:id:daigo-knowlbo:20170803014526p:plain

2.2 Prismプロジェクトの作成

Visual Studio 2017を起動します。
メニュー「ファイル → 新規作成 → プロジェクト」を選択します。

f:id:daigo-knowlbo:20170803014613p:plain

プロジェクトテンプレート(カテゴリー)として「Prism」が追加されていることを確認することができます。

f:id:daigo-knowlbo:20170803014657p:plain

ここでは「Prism → Xamarin.Forms」を選択します。以下の4つのテンプレートがリストされます。

  • Prism Autofac App(Xamarin.Forms)
  • Prism Dryloc App(Xamarin.Forms)
  • Prism Ninject App(Xamarin.Forms)
  • Prism Unity App(Xamarin.Forms)

名称から分かるように、テンプレートごとに「利用するDIコンテナ」に違いがあります。
お好みで選択してOKですが、ここでは「Prism Autofac App(Xamarin.Forms)」を選択することにします。
名前はデフォルトのまま「PrismAutofacApp1」としました。
「OK」をクリックします。

「PRISM PROJECT WIZARD」ウィンドウが表示されます。

f:id:daigo-knowlbo:20170803014805p:plain

ターゲットとするプラットフォームの選択になります。ここでは iOS / ANDROID / UWP の3つすべてを選択します。

Macとの接続設定が未設定の場合は「Xamarin Mac Agentの指示」ウィンドウが開きます。
ネットワーク内に開発に使えるMacがある場合は接続します。
Macとの接続設定が済んでいる環境では、このステップは省略されます)

f:id:daigo-knowlbo:20170803015005p:plain

「新しいユニバーサル Windows プロジェクト」ウィンドウが表示されます。

f:id:daigo-knowlbo:20170803015130p:plain

UWPのターゲットバージョンの指定になりますが、ここではデフォルトのまま「OK」ボタンをクリックすることとします。

以上でプロジェクトの作成が完了します。

2.3 実行

では、ソースは何もいじらずに、ウィザードによって生成されたHello World的なプログラムを実行してみます。

2.3.1 Androidエミュレータで実行

VS上部のツールバー(?)から「スタートアッププロジェクト」を「PrismAutofacApp1.Droid」を選択します。
すぐ右のドロップダウンでシミュレータデバイスを選択します。ここでは「Visual Studio_android-23_x86_phone(Android 6.0 - API 23)」を選択します。

f:id:daigo-knowlbo:20170803020110p:plain

F5キークリックでデバッグ実行を行います。
ビルド&デプロイが行われ、以下のエミュレータ画面が表示されます。

f:id:daigo-knowlbo:20170803020132p:plain

※私の環境ではデプロイが完了しない場合があります。そんな場合は、一度ビルドのキャンセルを行い、再度F5をクリックするとうまく動作したりするようです。

2.3.2 iPhoneシミュレータ(iOS)で実行

VS上部のツールバー(?)から、「ソリューション プラットフォーム」を「iPhoneSimulator」に、「スタートアッププロジェクト」を「PrismAutofacApp1.iOS」に設定します。
すぐ右のドロップダウンでシミュレータデバイスを選択します。ここでは「iPhone 7 iOS 10.3」」を選択します。

f:id:daigo-knowlbo:20170803020549p:plain

F5キークリックでデバッグ実行を行います。
ビルド&デプロイが行われ、シミュレータ画面が表示されます。

Visual Studio 2017の「リモート Simulator から Windows へ」機能が有効になっている場合は、Windows側の画面にシミュレータが表示されます。この機能が無効になっている場合には、Mac側にシミュレータ画面が表示されます。

[Windows側にシミュレータ表示]
f:id:daigo-knowlbo:20170803022544p:plain

[Mac側にシミュレータ表示]
f:id:daigo-knowlbo:20170803022450p:plain

「リモート Simulator から Windows へ」機能の設定変更は以下の通りです。
Visual Studio 2017のメニュー「ツール → オプション」を選択し、「オプション」ウィンドウを開きます。

f:id:daigo-knowlbo:20170803022835p:plain

左側のカテゴリーから「Xamarin → iOSの設定」を選択します。右側に表示される「リモート Simulator から Windows へ」チェックボックスで有効・無効の設定を切り替えます(当該機能はVisual Studio Enterpriseのみの機能になります)。

2.3.3 UWPで実行

VS上部のツールバー(?)から、「ソリューション プラットフォーム」を「x64(もしくはx86)Any CPU」に、「スタートアッププロジェクト」を「PrismAutofacApp1.UWP(Universal Windows)」に設定します。
すぐ右のドロップダウンでシミュレータデバイスを選択します。ここでは「ローカル コンピューター」を選択します。  

f:id:daigo-knowlbo:20170803074041p:plain

F5キークリックでデバッグ実行を行います。
以下のようにUWPアプリが起動します。

f:id:daigo-knowlbo:20170803023310p:plain

※2017/8/3 かずき@69.7kg (@okazuki) | Twitterさんより「ソリューションプラットフォーム」を AnyCPU ではなく x64 もしくは x86 を明示的に選択すれば「配置」の必要がないことをご指摘いただきました。「2.3.3 UWPで実行」に関する 以下の取り消し線部分を上記に修正しました!かずきさんありがとうございます!

配置→実行の手順となります。

(1) 配置

ソリューションエクスプローラから「PrismAutofacApp1.UWP」を選択し、マウス右ボタンクリックでポップアップメニューを表示、「配置」メニューを選択します。

f:id:daigo-knowlbo:20170803023005p:plain

ビルドが行われ、配置処理が行われます。

(2) 実行

VS上部のツールバー(?)から、「ソリューション プラットフォーム」を「Any CPU」に、「スタートアッププロジェクト」を「PrismAutofacApp1.UWP(Universal Windows)」に設定します。
すぐ右のドロップダウンでシミュレータデバイスを選択します。ここでは「ローカル コンピューター」を選択します。  

f:id:daigo-knowlbo:20170803023228p:plain

F5キークリックでデバッグ実行を行います。
以下のようにUWPアプリが起動します。

f:id:daigo-knowlbo:20170803023310p:plain

まとめ

ということで、コーディング0の「Xamarin + Prism 超入門」でした。
Xamarinまわりは、以前と比べてかなり改善されていますが、初期設定関連の取り掛かりの部分が躓きとなる可能性がある部分だと思います。
ということで、Xamarin + Prism開発、更にはその手前の初期設定に関して少しでも本エントリーが参考になればと思います。

Syncfusion SfAutoComplete を使ってみる - Xamarin Forms

前回の投稿に引き続いて Syncfusion の Essential Studio for Xamarin を使ってみたいと思います。

↓↓↓前回↓↓↓

ryuichi111std.hatenablog.com

今回は SfAutoComplete を使ってみます。
SfAutoComplete は、テキストボックスへのユーザーの入力に対して、候補をオートコンプリート表示(&選択)するコントロールです。

f:id:daigo-knowlbo:20170220042709p:plain

ここでは、以下の2つの実装を紹介します。

  • 1.シンプルな実装
     → 最もシンプルに、とりあえずSfAutoCompleteを使ってみます。

  • 2.かな入力 → 漢字で候補表示 & Prism で使ってみる
     → 駅名を「ひらがな」で入力。オートコンプリートでは「漢字の駅名」が候補表示される。オートコンプリートされた「漢字駅名」を選択すると、オートコンプリート内にも「漢字の駅名」が設定される。さらに Prism を使用。というサンプルを実装します。

で、以下で紹介するソースはGithubにあげてあります。

github.com

1. シンプルな実装

まず初めに、シンプルに SfAutoComplete を使ってみます。
ソリューションを新規作成します。
シンプルに「Cross-Platform → Blank Xaml App(Xamarin.Forms.Portable)」とします。
プロジェクト名は「AutoCompleteExample1」としました。

f:id:daigo-knowlbo:20170220003408p:plain

Nugetで SfAutoComplete への参照を追加

※SyncfusionのNugetを追加する方法についてはこちらの「Nugetソースの追加」の項を参照してください。

AutoCompleteExample1(PCLプロジェクト)には以下の参照をNugetで追加します。

  • Syncfusion.Xamarin.SfAutoComplete

AutoCompleteExample1.Droid(androidプロジェクト)には以下の参照をNugetで追加します。

  • Syncfusion.Xamarin.SfAutoComplete
  • Syncfusion.Xamarin.SfAutoComplete.Android

AutoCompleteExample1.iOSiOSプロジェクト)には以下の参照をNugetで追加します。

  • Syncfusion.Xamarin.SfAutoComplete
  • Syncfusion.Xamarin.SfAutoComplete.IOS

iOSプロジェクトに初期化処理を追加

AutoCompleteExample1.iOS プロジェクト→AppDelegate.cs内のFinishedLaunching()メソッドに「SfAutoCompleteRenderer」をインスタンス化する処理を追記します。
この記述がないと、iOSでは実行時に、画面に配置した SfAutoComplete が何の描画も行われません。

// AppDelegate.cs

public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
  // 以下を追記
  new Syncfusion.SfAutoComplete.XForms.iOS.SfAutoCompleteRenderer();

  global::Xamarin.Forms.Forms.Init();
  LoadApplication(new App(new iOSInitializer()));

  return base.FinishedLaunching(app, options);
}

フォームに配置

フォーム(MainForm.xaml)に SfAutoComplete コントロールを配置します。

<!-- MainPage.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:local="clr-namespace:AutoCompleteExample1"
  xmlns:sf="clr-namespace:Syncfusion.SfAutoComplete.XForms;assembly=Syncfusion.SfAutoComplete.XForms"
  x:Class="AutoCompleteExample1.MainPage">
  
  <StackLayout Margin="0,20,0,0">

    <sf:SfAutoComplete x:Name="AutoComplete1" />

  </StackLayout>

</ContentPage>

ポイントは以下の通り。

① 「xmlns:sf」の指定
ルート要素 に、SfAutoCompleteをXaml内で定義するためにXML名前空間を定義します。

xmlns:sf=“clr-namespace:Syncfusion.SfAutoComplete.XForms;assembly=Syncfusion.SfAutoComplete.XForms

上記により、Syncfusion.SfAutoComplete.XFormsアセンブリに実装されたSyncfusion.SfAutoComplete.XForms名前空間を、「sf」として利用可能になります。

② SfAutoCompleteの配置
<sf:SfAutoComplete>要素により SfAutoComplete コントロールをフォームに配置します。

候補文字列を追加

フォームに配置したSfAutoCompleteコントロールに候補文字列を追加します。
ここではコードビハインド上に以下のように実装を追加します。

// MainPage.xaml.cs
using System.Collections.Generic;
using Xamarin.Forms;

namespace AutoCompleteExample1
{
  public partial class MainPage : ContentPage
  {
    public MainPage()
    {
      InitializeComponent();

      // 候補文字列リストを作成
      List<string> Stations = new List<string>();
      Stations.Add("Tokyo");
      Stations.Add("Osaka");
      Stations.Add("Nagoya");
      Stations.Add("Nagatachou");
      Stations.Add("Ebisu");
      Stations.Add("Sinagawa");

      // SfAutoCompleteに候補文字列リストをソースとして設定
      this.AutoComplete1.AutoCompleteSource = Stations;
    }
  }
}

実行

実行すると以下のような画面が表示されます。

f:id:daigo-knowlbo:20170220014345p:plain

「Na」と入力。Nagoya / Nagatachou が候補として表示される。

f:id:daigo-knowlbo:20170220014602p:plain

「O」と入力。Osaka が候補として表示される。

f:id:daigo-knowlbo:20170220014615p:plain

ちょっと実装を追加

引き続き・・・フォームに「ボタン」と「ラベル」を追加します。
ボタンクリック時に SfAutoComplete に入力されている値をラベルに表示してみようと思います。

<!-- MainPage.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:local="clr-namespace:AutoCompleteExample1"
  xmlns:sf="clr-namespace:Syncfusion.SfAutoComplete.XForms;assembly=Syncfusion.SfAutoComplete.XForms"
  x:Class="AutoCompleteExample1.MainPage">
  
  <StackLayout Margin="0,20,0,0">

    <sf:SfAutoComplete x:Name="AutoComplete1" />

    <Label x:Name="Label1" />
    
    <Button Text="入力項目チェック" Clicked="CheckClicked" />

  </StackLayout>

</ContentPage>
// MainPage.xaml.cs

using System.Collections.Generic;
using Xamarin.Forms;

namespace AutoCompleteExample1
{
  public partial class MainPage : ContentPage
  {
    public MainPage()
    {
      InitializeComponent();

      // 候補文字列リストを作成
      List<string> Stations = new List<string>();
      Stations.Add("Tokyo");
      Stations.Add("Osaka");
      Stations.Add("Nagoya");
      Stations.Add("Nagatachou");
      Stations.Add("Ebisu");
      Stations.Add("Sinagawa");

      // SfAutoCompleteに候補文字列リストをソースとして設定
      this.AutoComplete1.AutoCompleteSource = Stations;
    }

    // ボタンクリックイベントハンドラ
    private void CheckClicked(object sender, System.EventArgs e)
    {
      // ラベルにSfAutoCompleteに入力された値を設定
      this.Label1.Text = this.AutoComplete1.Text;
    }
  }
}

さあ、実行!

「To」と入力して「Tokyo」が候補に挙がった。

f:id:daigo-knowlbo:20170220015734p:plain

「Tokyo」を選択。

f:id:daigo-knowlbo:20170220015807p:plain

「入力項目チェック」ボタンクリック。

f:id:daigo-knowlbo:20170220015901p:plain

2.かな入力 → 漢字で候補表示 & Prism で使ってみる

次に、もう少し面白みのあるサンプルを実装してみます。
要件は以下の通りです。

  • 電車の駅名を入力するUIを想定する
  • ユーザーは、駅名を「ひらがな」で入力する
  • 「ひらがな」の入力に対して「漢字の駅名」を候補としてオートコンプリートする
  • オートコンプリート候補の中から「漢字の駅名」を選択したら、SfAutoComplete内の表示も「漢字の駅名」となる

実行画面は以下のようになります。

f:id:daigo-knowlbo:20170220033030p:plain

ひらがな で「え」と入力すると、漢字の駅名が候補で表示される。

f:id:daigo-knowlbo:20170220033049p:plain

候補から「恵比寿」を選択。

f:id:daigo-knowlbo:20170220033142p:plain

「検索」ボタンをクリックすると、SfAutoCmopleteの入力内容が、下のラベルに表示される。

f:id:daigo-knowlbo:20170220033223p:plain

では、以下に実装を・・・

ソリューションを新規作成

「Prism Xamarin Forms → Prism Unity App(Xamarin.Forms)」とします。
プロジェクト名は「AutpCompleteWithPrism1」としました。

f:id:daigo-knowlbo:20170220023747p:plain

Nugetからの SfAutoComplete 参照の追加、iOSプロジェクト AppDelegate.cs へのコード追加は先程と同様。

Event to Command の下準備

本サンプルでは、Prism(MVVM)を使用します。
そして、SfAutoCompleteの「SelectionChangedイベント」を、ViewModelで捕捉したいです。
さらに、SelectionChangedはCommandとして提供されていません。
Viewのコードビハインドにコードを書きたくないし、MVVM的には「すべきではない」から。
という事で、「Event To Command」な実装が必要になります。

nuits.jpさんが以下の投稿で、いい感じの実装を提供してくれているので、これを拝借させていただきました。

www.nuits.jp

以下の2ソースをPCLプロジェクトに追加しました。

Stationモデルクラスを作成

先程のシンプルな実装では List をソースとしましたが、今回は Stationモデルクラス をソースとします。
漢字名・ひらがな名を属性として持った駅クラスを用意するためです。

// Models/Station.cs

using System;

namespace AutpCompleteWithPrism1.Models
{
  // 駅モデルクラス
  public class Station
  {
    /// <summary>
    /// 漢字駅名を取得または設定します。
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// ひらがな駅名を取得または設定します。
    /// </summary>
    public string Kana { get; set; }
  }
}

View(MainPage.xaml)を作成

ビュークラスは以下のように実装します。ポイントは後述。

<!-- Views/MainPage.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:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
  xmlns:common="clr-namespace:AutpCompleteWithPrism1.Common;assembly=AutpCompleteWithPrism1"
  xmlns:sf="clr-namespace:Syncfusion.SfAutoComplete.XForms;assembly=Syncfusion.SfAutoComplete.XForms"
  prism:ViewModelLocator.AutowireViewModel="True"
  x:Class="AutpCompleteWithPrism1.Views.MainPage"
  Title="MainPage">
  
  <StackLayout HorizontalOptions="Center" VerticalOptions="Center">

  <sf:SfAutoComplete 
    DataSource="{Binding Stations}" 
    DisplayMemberPath="Kana" 
    Text="{Binding InputText}">
    <sf:SfAutoComplete.ItemTemplate>
    <DataTemplate>
      <Label Text="{Binding Name}" />
    </DataTemplate>
    </sf:SfAutoComplete.ItemTemplate>
    <sf:SfAutoComplete.Behaviors>
    <common:EventToCommandBehavior 
      EventName="SelectionChanged" 
      Command="{Binding SelectionChangedCommand}"  />
    </sf:SfAutoComplete.Behaviors>
  </sf:SfAutoComplete>

  <Button Text="検索" Command="{Binding SearchCommand}" />
  <Label Text="{Binding Message}" />
  </StackLayout>
</ContentPage>

ポイントは以下の通り。

①SfAutoComlete / Button / Label の配置
SfAutoCompleteコントロールを配置します。
検索ボタン・ラベルを配置します。
検索ボタンクリック時には、ラベルに「SfAutoCompleteに入力された値」を表示します。

②SfAutoComplete.DataSourceの指定
カスタムクラスのコレクションを、オートコンプリート表示候補としてバインドする際には、DataSourceプロパティにデータソースを設定します。

③DisplayMemberPathの指定
オートコンプリート候補となるオブジェクト「Station」の、「どのプロパティ」を「表示項目(候補検索項目)」とするのかを指定します。

④ItemTemplateの指定
「ひらがなで入力、漢字名で候補を表示」の要件を満たすために、先のDisplayMemberPathには候補検索用としてKana(Station.Kane)を、漢字名表示用にカスタムなItemTemplate(Station.Nameを表示するように設定)を指定しています。 このItemTemplate の指定が無いと、候補の駅名が「ひらがな」で表示されてしまいます(DisplayMemberPathがKanaの為)。

⑤Event To Command Behavior の使用
以下のXML名前空間を定義。

xmlns:common=“clr-namespace:AutpCompleteWithPrism1.Common;assembly=AutpCompleteWithPrism1”

そして、SfAutoComplete の SelectionChangedイベント に Behavior を追加。

ViewModelを作成

ビューモデルクラスの実装は以下になります。

// ViewModels/MainPageViewModel.cs

using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Input;

using Xamarin.Forms;

using AutpCompleteWithPrism1.Models;

namespace AutpCompleteWithPrism1.ViewModels
{
  public class MainPageViewModel : BindableBase, INavigationAware
  {
    /// <summary>
    /// SfAutoCompleteにデータバインドする駅情報データソース
    /// </summary>
    public List<Station> Stations { get; set; } = new List<Station>();

    /// <summary>
    /// 検索ボタンコマンド
    /// </summary>
    public ICommand SearchCommand { get; }

    /// <summary>
    /// SfAutoComplete選択項目変更コマンド
    /// </summary>
    public ICommand SelectionChangedCommand { get; }

    /// <summary>
    /// SfAutoComplete入力テキスト
    /// </summary>
    private string inputText = "";
    public string InputText
    {
      get
      {
        return this.inputText;
      }
      set
      {
        this.SetProperty(ref this.inputText, value);
      }
    }

    /// <summary>
    /// メッセージテキスト
    /// </summary>
    private string message = "";
    public string Message
    {
      get
      {
        return this.message;
      }
      set
      {
        this.SetProperty(ref this.message, value);
      }
    }

    public MainPageViewModel()
    {
      // データソース作成
      this.Stations.Add(new Station() { Name = "東京", Kana = "とうきょう" });
      this.Stations.Add(new Station() { Name = "恵比寿", Kana = "えびす" });
      this.Stations.Add(new Station() { Name = "江戸橋", Kana = "えどばし" });
      this.Stations.Add(new Station() { Name = "品川", Kana = "しながわ" });
      this.Stations.Add(new Station() { Name = "新宿", Kana = "しんじゅく" });

      // SfAutoComplete.SelectionChangedイベントに対応したCommand
      SelectionChangedCommand = new Command((param) =>
      {
        this.InputText = this.Stations.First(s => s.Kana == ((Syncfusion.SfAutoComplete.XForms.SelectionChangedEventArgs)param).Value).Name;
      });

      // 検索ボタンクリック時のCommand
      SearchCommand = new Command(() => { this.Message = this.InputText; });
    }

    public void OnNavigatedFrom(NavigationParameters parameters)
    {

    }

    public void OnNavigatedTo(NavigationParameters parameters)
    {
    }
  }
}

ポイントは以下の通り。

①SfAutoComleteのデータソースを用意
List型プロパティ「Stations」をクラスプロパティとして用意し、コンストラクタ内で初期化しています。

②SelectionChangedCommand の実装
SfAutoCompleteの「候補」が選択されたときに発生するイベントをBehavior経由でSelectionChangedCommandとして取得します。
その際には、パラメータ「Syncfusion.SfAutoComplete.XForms.SelectionChangedEventArgs型 Param 」から、選択された値「ひらがな」名を取得し、該当するStationモデルクラスのName(漢字駅名)をInputTextプロパティに設定しています。
InputTextプロパティは、ViewにおいてShAutoComplete.Textにバインドされています。
つまり、以下のような動作が行われます。

SfAutoCompleteにユーザーが「ひらがな」入力 ↓↓↓
候補として「ひらがな」に該当する Kanaプロパティ を持つ Stationオブジェクト の漢字駅名が候補一覧される ↓↓↓
ユーザーが漢字駅名を選択
↓↓↓
SelectionChangedCommandが発生
↓↓↓
ViewModelのInputTextプロパティ値に、選択されたStationモデルクラスに該当する「漢字駅名」を設定
↓↓↓
SfAutoComplete.Textに、ViewModel.InputText値が反映される(つまり漢字駅名)

③SearchCommand の実装
検索ボタンコマンドの実装です。
InputTextの値(つまりSfAutoCompleteの入力値)を、Messageプロパティに設定します。
Messageプロパティはビューのラベルにバインドされています。

まとめ

ということで「Syncfusionを使ってみる シリーズ(?)」第2回でした。
SfAutoCompleteは、すごくシンプルで僅かな英語力であっても、以下のヘルプを読めばすぐに理解できました。

help.syncfusion.com

が、「ひらがな入力→漢字表示」みたいな、非英語圏(日本)のちょっとした要件を満たす部分には、若干の調査が必要でした・・・
ちょっとイレギュラーな利用方法をしているような気がするので、その使い方だと「こんな時におかしくなるよー」とかありましたらご指摘お願いいたします。
ということで、「Syncfusionを使ってみる」は引き続き、本ブログでシリーズ化していきたいな、と思っています!

Syncfusion SfTreeMap を使ってみる - Xamarin Forms

最近ちょろちょろと Syncfusion の Essential Studio for Xamarin で遊んでおりました。
ということで、各コンポーネントの使い方なんかをブログにあげていこうかな、と思います。
まずは SfTreeMap の使い方について書きたいと思います。

Syncfusion Essential Studio for Xamarin とは?

iOS / android / Forms用の便利なコントロール群のパッケージです。

Introducing Essential Studio for Xamarin : Feature-rich data visualization and file format components

これの良いところ(?)は、個人開発者や売り上げが $1 million に満たない企業であれば無償で利用できるコミュニティライセンスというものが提供されていることです。

800+ Free controls and frameworks for .NET (Windows Forms, WPF, ASP.NET MVC, ASP.NET Web Forms, LightSwitch, Silverlight, Windows Phone, WinRT, Windows 8), iOS, Android, Xamarin and JavaScript platforms

SfTreeMapコントロール

では早速 SfTreeMap を使ってみたいと思います。
SfTreeMapは以下のようにタイル状に子要素を並べるUIコントロールです。

f:id:daigo-knowlbo:20170213013403p:plain
※上記画像はSyncfusionサイトより拝借m( )m

「タイル状の子要素」と表現しましたが、これは正式には「Leaf Node」と呼ばれます(Treeに対する子要素としてのLeaf、ですね)。

Leaf Node要素は「重み付け」を行うことができ、その「重み付け」に基づいてLeaf Node要素のサイズ(専有面積)が決まります。
国別の人口の大小、会社毎の売り上げの大小、人気の大小・・・などをビジュアルな一覧で表現することができます。

ステップ1(シンプルにデータバインド)

作るサンプルは以下のようなものです。

f:id:daigo-knowlbo:20170213013306p:plain

F1チームの2016年 年間リザルトを一覧します。Leaf Nodeの領域重み付けは獲得ポイント数で行います。

Nugetソースの追加

Essential Studio for Xamarin コントロールは、Nuget経由でプロジェクトに追加することができます。
まずは Essential Studio for Xamarin 用の Nugetソース を追加します。
VS2015では、メニュー「ツール→オプション」を選択。

f:id:daigo-knowlbo:20170213013732p:plain

表示されたオプションダイアログから「NuGetパッケージマネージャー→パッケージソース」を選択。

f:id:daigo-knowlbo:20170213013802p:plain

プロジェクトの用意

本サンプルは、Prismを使ったプロジェクトとします。
TreeMapExampleというプロジェクト名とします。

SfTreeMapをNugetから追加します。

f:id:daigo-knowlbo:20170213013831p:plain

PCLプロジェクトには「Syncfusion.Xamarin.SfTreeMap」を追加します。
iOSプロジェクトには「Syncfusion.Xamarin.SfTreeMap」「Syncfusion.Xamarin.SfTreeMap.Android」を追加します。
androidプロジェクトには「Syncfusion.Xamarin.SfTreeMap」「Syncfusion.Xamarin.SfTreeMap.IOS」を追加します。
それぞれ、先程追加した Syncfusion Nugetソース からの追加になります。

iOSプロジェクトに初期化コードを追加

iOSプロジェクトの「AppDelegate.cs」の「FinishedLaunching()メソッド」に以下の実装を追加します。

// AppDelegate.cs

public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{ 
  // 以下の1行を追加
  new Syncfusion.SfTreeMap.XForms.iOS.SfTreeMapRenderer();

  global::Xamarin.Forms.Forms.Init();
  LoadApplication(new App(new iOSInitializer()));
  return base.FinishedLaunching(app, options);
}

データモデルクラスの用意

SfTreeMapにバインドするデータモデルクラスを用意します。

// F1TeamResult.cs
using System;

namespace TreeMapExample.Models
{
  /// <summary>
  /// F1チームの年間リザルト
  /// </summary>
  public class F1TeamResult
  {
    /// <summary>
    /// チーム名を取得または設定します。
    /// </summary>
    /// <value>The name.</value>
    public string Name { get; set; }

    /// <summary>
    /// 国籍を取得または設定します。
    /// </summary>
    /// <value>The country.</value>
    public string Country { get; set; }

    /// <summary>
    /// 勝利数を取得または設定します。
    /// </summary>
    /// <value>The window.</value>
    public int Win { get; set; }

    /// <summary>
    /// 獲得ポイントを取得または設定します。
    /// </summary>
    /// <value>The points.</value>
    public int Points { get; set; }

    /// <summary>
    /// コンストラクタです。
    /// </summary>
    /// <param name="name">チーム名</param>
    /// <param name="country">国籍</param>
    /// <param name="win">勝利数</param>
    /// <param name="points">獲得ポイント</param>
    public F1TeamResult(string name, string country, int win, int points )
    {
      this.Name = name;
      this.Country = country;
      this.Points = points;
      this.Win = win;
    }
  }
}

XAMLにSfTreeMapを追加

XAMLに SfTreeMap を追加します。

<!--  MainPage.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:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms" 
  prism:ViewModelLocator.AutowireViewModel="True" 
  x:Class="TreeMapExample.Views.MainPage" 
  xmlns:sf="clr-namespace:Syncfusion.SfTreeMap.XForms;assembly=Syncfusion.SfTreeMap.XForms"
  Title="MainPage">
  <StackLayout HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" Margin="0,20,0,0">
    
    <sf:SfTreeMap x:Name="TreeMap" 
      DataSource="{Binding F1TeamResults}"
      VerticalOptions="FillAndExpand" 
      HorizontalOptions="FillAndExpand"
      WeightValuePath="Points">

      <sf:SfTreeMap.LeafItemSettings>
        <sf:LeafItemSettings ShowLabels="true" LabelPath="Name" >
        </sf:LeafItemSettings>
      </sf:SfTreeMap.LeafItemSettings>

    </sf:SfTreeMap>

  </StackLayout>
</ContentPage>
  • ポイント①
    Syncfusion SfTreeMapの名前空間を定義。 以下により sf:XXXX という形式でコントロールを定義できるようにxmlnsを宣言します。

xmlns:sf=“clr-namespace:Syncfusion.SfTreeMap.XForms;assembly=Syncfusion.SfTreeMap.XForms

<sf:SfTreeMap x:Name=“TreeMap” …>

  • ポイント③
    SfTreeMapコントロールのデータバインド設定を行います。データバインドには DataSourceプロパティ を利用します。

DataSource=“{Binding F1TeamResults}”
「F1TeamResults」はビューモデル「MainPageViewModelクラス」のプロパティになります。

  • ポイント④
    「LeafItemSettings」により、Leaf Nodeの表示設定を行います。
    「ShowLabels=“True"」によりLeaf Node領域にラベル表示を行うことを示します。
    「LabelPath="Name"」により、ラベルとして表示するデータへのパスを指定します。つまり、バインドしたデータ「F1TeamReault.Name」となります。

ViewModelを作成

XAMLに対応したビューモデルを実装します。
ObservableCollection型としてビューにデータバインドするオブジェクトを公開します。
実装の大半を占める LoadData() は固定のコレクションデータを作成する処理になります.。

// MainPageViewModel.cs
using Prism.Mvvm;
using Prism.Navigation;
using System.Collections.ObjectModel;

using TreeMapExample.Models;

namespace TreeMapExample.ViewModels
{
  public class MainPageViewModel : BindableBase, INavigationAware
  {
    /// <summary>
    /// F1チームリザルト(SfTreeMapにデータバインドするコレクションオブジェクト)
    /// </summary>
    /// <value>The f1 team results.</value>
    public ObservableCollection<F1TeamResult> F1TeamResults { get; set; } = new ObservableCollection<F1TeamResult>();

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public MainPageViewModel()
    {
      this.LoadData();
    }

    /// <summary>
    /// データをロードします。
    /// </summary>
    public void LoadData()
    {
      this.F1TeamResults.Add(
        new F1TeamResult(
          "メルセデス",
          "ドイツ",
          19,
          765
        ));

      this.F1TeamResults.Add(
        new F1TeamResult(
          "レッドブルレーシング",
          "オーストリア",
          2,
          468
        ));

      this.F1TeamResults.Add(
        new F1TeamResult(
          "フェラーリ",
          "イタリア",
          0,
          398
        ));

      this.F1TeamResults.Add(
        new F1TeamResult(
          "フォースインディア",
          "インド",
          0,
          173
        ));

      this.F1TeamResults.Add(
        new F1TeamResult(
          "ウィリアムズ",
          "イギリス",
          0,
          138
        ));

      this.F1TeamResults.Add(
        new F1TeamResult(
          "マクラーレン",
          "イギリス",
          0,
          76
        ));

      this.F1TeamResults.Add(
        new F1TeamResult(
          "トロ・ロッソ",
          "イタリア",
          0,
          63
        ));

      this.F1TeamResults.Add(
        new F1TeamResult(
          "ハース",
          "アメリカ",
          0,
          29
        ));

      this.F1TeamResults.Add(
        new F1TeamResult(
          "ルノー",
          "フランス",
          0,
          8
        ));

      this.F1TeamResults.Add(
        new F1TeamResult(
          "ザウバー",
          "スイス",
          0,
          2
        ));

      this.F1TeamResults.Add(
        new F1TeamResult(
          "マノー",
          "イギリス",
          0,
          1
        ));
    }

    public void OnNavigatedFrom(NavigationParameters parameters)
    {
    }

    public void OnNavigatedTo(NavigationParameters parameters)
    {
    }
  }
}

以上の実装で、「ステップ1(シンプルにデータバインド)」が完成しました。

ステップ2(色をカスタマイズ)

Leaf Node領域の色を「値によって領域サイズを決定する」のと同様に「値によって色を変える」事ができます。
以下が実装になります。

<?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:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms" 
  prism:ViewModelLocator.AutowireViewModel="True" 
  x:Class="TreeMapExample.Views.MainPage" 
  xmlns:sf="clr-namespace:Syncfusion.SfTreeMap.XForms;assembly=Syncfusion.SfTreeMap.XForms"
  Title="MainPage">
  <StackLayout HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" Margin="0,20,0,0" >
    
    <sf:SfTreeMap x:Name="TreeMap" 
      DataSource="{Binding F1TeamResults}"
      VerticalOptions="FillAndExpand" 
      HorizontalOptions="FillAndExpand"
      WeightValuePath="Points"
      ColorValuePath="Points">  <!-- ←←← 追加 -->
      
      <!-- ↓↓↓追加↓↓↓ -->
      <sf:SfTreeMap.LeafItemColorMapping>
        <sf:RangeColorMapping>
          <sf:RangeColorMapping.Ranges>
            <sf:Range LegendLabel="1" From="0" To="50" Color="Blue" />
            <sf:Range LegendLabel="2" From="51" To="100" Color="Maroon" />
            <sf:Range LegendLabel="3" From="101" To="200" Color="Gray" />
            <sf:Range LegendLabel="4" From="201" To="300" Color="Navy" />
            <sf:Range LegendLabel="5" From="301" To="500" Color="Green" />
            <sf:Range LegendLabel="6" From="501" To="1000" Color="Red" />
          </sf:RangeColorMapping.Ranges>
        </sf:RangeColorMapping> 
      </sf:SfTreeMap.LeafItemColorMapping>
      <!-- ↑↑↑追加↑↑↑ -->

      <sf:SfTreeMap.LeafItemSettings>
        <sf:LeafItemSettings ShowLabels="true" LabelPath="Name" >
        </sf:LeafItemSettings>
      </sf:SfTreeMap.LeafItemSettings>

    </sf:SfTreeMap>

    <Button Text="Grouping" Clicked="Handle_Clicked" x:Name="Button1"/>
  </StackLayout>
</ContentPage>
  • ポイント
    SfTreeMapのプロパティ「ColorValuePath」を指定する。
    バインドされたオブジェクトの「Pointsプロパティ」の値を評価して色を変えることを設定します。
    SfTreeMap.LeafItemColorMappingを指定する。
    Rangeオブジェクトを指定します。ColorValuePathプロパティの値(つまり、サンプルではF1Result.Point)の値を評価し、From - Toの値に合致したらColorプロパティ値を適用します。
    実行画面が以下の通りです。

f:id:daigo-knowlbo:20170213014559p:plain

ステップ3(Leaf Nodeのテンプレートをカスタマイズ)

次に、各Leaf Nodeの表示についてカスタマイズしてみます。
完成イメージは以下の通り。
Leaf Node内には、チーム名とチームマシン画像を表示します。

f:id:daigo-knowlbo:20170213012403p:plain
※ちょっと画像ちっちゃいけど、細かなレイアウトはご勘弁m( )m

画像リソースの追加

アプリケーション内で画像を使用するために jpgファイル をプロジェクトに追加します。
PCLプロジェクトに「Resourcesフォルダ」を作成して配下に jpgファイル を追加します。
「ビルドアクション」は「埋め込みリソース」とします。

f:id:daigo-knowlbo:20170213012228p:plain

モデルクラスの調整

カスタムテンプレート定義によって、画像を表示しようと思います。その為、モデルクラスにImageSourceプロパティを追加します。

// F1TeamResult.cs(ImageSource追加)
using System;

namespace TreeMapExample.Models
{
  /// <summary>
  /// F1チームの年間リザルト
  /// </summary>
  public class F1TeamResult
  {
    /// <summary>
    /// チーム名を取得または設定します。
    /// </summary>
    /// <value>The name.</value>
    public string Name { get; set; }

    /// <summary>
    /// 国籍を取得または設定します。
    /// </summary>
    /// <value>The country.</value>
    public string Country { get; set; }

    /// <summary>
    /// 勝利数を取得または設定します。
    /// </summary>
    /// <value>The window.</value>
    public int Win { get; set; }

    /// <summary>
    /// 獲得ポイントを取得または設定します。
    /// </summary>
    /// <value>The points.</value>
    public int Points { get; set; }

    /// <summary>
    /// イメージを取得または設定します。
    /// </summary>
    public ImageSource ImageSource { get; set; }

    /// <summary>
    /// コンストラクタです。
    /// </summary>
    /// <param name="name">チーム名</param>
    /// <param name="country">国籍</param>
    /// <param name="win">勝利数</param>
    /// <param name="points">獲得ポイント</param>
    public F1TeamResult(string name, string country, int win, int points )
    {
      this.Name = name;
      this.Country = country;
      this.Points = points;
      this.Win = win;
      
      this.ImageSource = ImageSource.FromResource(String.Format("TreeMapExample.Resources.{0}.jpg", name));
    }
  }
}

XAML定義の修正

XAMLの定義を以下のように修正します。
「<sf:SfTreeMap.ItemTemplate>」の定義が肝となります。
Template内の定義ではデータバインディング項目は「Data.xxx」となります(つまり「Data.ImageSource」は「F1TeamResult.ImageSource」に対応します)。

<?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:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms" 
  prism:ViewModelLocator.AutowireViewModel="True" 
  x:Class="TreeMapExample.Views.MainPage" 
  xmlns:sf="clr-namespace:Syncfusion.SfTreeMap.XForms;assembly=Syncfusion.SfTreeMap.XForms"
  Title="MainPage">
  <StackLayout HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" Margin="0,20,0,0" >
    
    <sf:SfTreeMap x:Name="TreeMap" 
      DataSource="{Binding F1TeamResults}"
      VerticalOptions="FillAndExpand" 
      HorizontalOptions="FillAndExpand"
      WeightValuePath="Points">

      <sf:SfTreeMap.LeafItemSettings>
        <sf:LeafItemSettings ShowLabels="true" LabelPath="Name" >
        </sf:LeafItemSettings>
      </sf:SfTreeMap.LeafItemSettings>

      <sf:SfTreeMap.ItemTemplate>
        <DataTemplate>
          <Grid BackgroundColor="Navy" >
            <Label Margin="5,5,0,0"  
                   FontSize="11" 
                   Text="{Binding Data.Name}" 
                   TextColor="White" 
                   HeightRequest="50"
                   WidthRequest="100" 
                   HorizontalOptions="Start" 
                   VerticalOptions="Start"/>
            <Image HorizontalOptions="Center" 
                   VerticalOptions="Center" 
                   HeightRequest="132" 
                   WidthRequest="58" 
                   Source="{Binding Data.ImageSource}" />
          </Grid>
      </DataTemplate>
      </sf:SfTreeMap.ItemTemplate>
    </sf:SfTreeMap>
  </StackLayout>
</ContentPage>

以上で「Leaf Nodeのテンプレートをカスタマイズ」の実装が完了しました。

まとめ

本投稿のソースコードは後でGithubにアップしようと思います。
SfTreeViewについては、まだ他にもいくつかの機能があるのですが、はじめの一歩として本投稿が参考になればと思います。
また、SfTreeViewに限らず「Syncfusion Essential Studio for Xamarin」には魅力的なコントロールが沢山あるので、これからも「自ら学びながら」この場で紹介していけたらと思います。

Xamarin Formsでカスタムレイアウトを作ってみる

Xamarin Formsでは Grid / StackLayout / RelativeLayout / AbsoluteLayout などいくつものLayoutが用意されています。
これら以外に、開発者が独自に カスタムレイアウト を作成する事が出来ます。

という事で、カスタムレイアウトの勉強として「ShuffleLayout」というものを作ってみました。
仕様としては「追加された子要素(子View)をシャッフルして配置する。」レイアウトです。
以下のような画面での利用を想定しています。

f:id:daigo-knowlbo:20170205161658p:plain

上記は、出題された英単語の日本語訳を選択肢から答える画面になっています。
「選択肢」を表示している赤枠の部分が、今回作成する「ShuffleLayout」です。
選択肢は Button として ShuffleLayout の子要素として登録します。
ShuffleLayout は登録された選択肢子要素(Button)をランダムにシャッフルして表示します。

ShuffleLayout の実装

まず実装をシンプルにするため、以下のような仕様とします。

  • 子要素は配置処理の度にランダムでシャッフルする
  • 子要素は必ず縦に並べる
  • 子要素の幅(Width)は画面いっぱいのサイズとする

実装は以下の通り。

// ShuffleLayout.cs
using System;
using System.Linq;
using System.Collections.Generic;

using Xamarin.Forms;

namespace App1
{
  /// <summary>
  /// シャッフルレイアウトです。
  /// </summary>
  public class ShuffleLayout : Layout<View>
  {
    /// <summary>
    /// コンストラクタです。
    /// </summary>
    public ShuffleLayout() : base()
    {
    }

    /// <summary>
    /// Method that is called when a layout measurement happens.
    /// </summary>
    /// <param name="widthConstraint"></param>
    /// <param name="heightConstraint"></param>
    /// <returns></returns>
    protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
    {
      double lastY;
      var layout = NaiveLayout(widthConstraint, heightConstraint, out lastY);

      return new SizeRequest(new Size(widthConstraint, lastY));
    }

    /// <summary>
    /// Positions and sizes the children of a Layout.
    /// </summary>
    /// <param name="x"></param>
    /// <param name="y"></param>
    /// <param name="width"></param>
    /// <param name="height"></param>
    /// <remarks>
    /// Implementors wishing to change the default behavior of a Layout should override this method.
    /// It is suggested to still call the base method and modify its calculated results.
    /// </remarks>
    protected override void LayoutChildren(double x, double y, double width, double height)
    {
      double lastY;
      List<Tuple<View, Rectangle>> layout = NaiveLayout(width, height, out lastY);

      foreach (var t in layout)
      {
        var location = new Rectangle(t.Item2.X + x, t.Item2.Y + y, t.Item2.Width, t.Item2.Height);
        t.Item1.Layout(location);
      }
    }

    /// <summary>
    /// 子要素のレイアウトを行います。
    /// 子要素を配置する場所を決定し、場所をRectangleで表します。
    /// </summary>
    /// <param name="width"></param>
    /// <param name="height"></param>
    /// <param name="lastY"></param>
    /// <returns></returns>
    private List<Tuple<View, Rectangle>> NaiveLayout(double width, double height, out double lastY)
    {
      var result = new List<Tuple<View, Rectangle>>();

      double startY = 0;
      double right = width;
      double nextY = 0;

      lastY = 0;

      // シャッフル実行
      List<View> shuffleChildren = new List<View>(); 
      shuffleChildren.AddRange(Children.OrderBy(i => Guid.NewGuid()).ToArray());

      foreach (var child in shuffleChildren)
      {
        SizeRequest sizeRequest = child.Measure(double.PositiveInfinity, double.PositiveInfinity);

        var paddedHeight = sizeRequest.Request.Height;

        startY += nextY;

        result.Add(new Tuple<View, Rectangle>(child, new Rectangle(0, startY, width, sizeRequest.Request.Height)));

        lastY = Math.Max(lastY, startY + paddedHeight);

        nextY = Math.Max(nextY, paddedHeight);
      }

      return result;
    }
  }
}

Layout < View > を継承

カスタムレイアウトを実装する際の、各種基本実装を備えたクラスが「Xamarin.Forms.Layout」になります。
子要素リスト「Childrenプロパティ」なども Layoutクラス で実装されています。
更に Layoutの派生クラスとして「Xamarin.Forms.Layout < T > 」が用意されており、 < T > によって子要素として受け入れる型を明示する事が出来ます。
一般的にレイアウトは子要素としてUI要素を受け入れるので「Layout < View > 」を継承してカスタムレイアウトを定義します。

OnMeasure()メソッド

このレイアウト(ShuffleLayout)の「サイズ測定」が必要な場合に呼び出されます。
後述する NaiveLayout() により、子要素を含めたサイズを測定し SizeRequest オブジェクトを返却します。
引数 widthConstraint / heightConstraint には、親によって許容される width / height が設定されて呼び出されます。

LayoutChildren()メソッド

レイアウト上に子要素を実際に配置する必要がある場合に呼び出されます。
LayoutChildren()メソッドが未実装だと、レイアウト上には何も配置(表示)されません。
NaiveLayout()メソッドの呼び出しにより、子要素の配置座標を計算します。
配置座標の計算結果は List<Tuple<View, Rectangle>> 型で返却されます。子要素分のリスト要素が作成され、1つの要素には「子要素オブジェクト」「配置座標を表す Rectangle」がタプルで格納されています。
引数 x / y / width / height には、子要素を配置可能な境界が設定されています。例えば ShuffleLayoutのPaddingプロパティが(20,20,20,20)と設定されていた場合、x と y は 20 になります。
各子要素は Layout()メソッド の呼び出しにより配置しています。
(子要素の VerticalOption / HorizontalOption を有効にした、配置を行うレイアウトを作成する場合には、Layout()メソッドではなく、Layout.LayoutChildIntoBoundingRegion()メソッドを利用します)

NaiveLayout()メソッド

各子要素の配置ポジションを計算しています。
同時にこのメソッド内で子要素のシャッフルを行っています。
子要素に対し「child.Measure()」を呼び出すことで、子要素が表示に必要とするサイズを取得しています。
本レイアウトでは width は「強制的に画面幅いっぱい」としているため、Measure()結果のheightのみ利用しています。

ShuffleLayoutの利用

問題と選択肢を表すモデルクラスを用意します。

// EnglishWordQA.cs
using System.Collections.Generic;

namespace App1
{
  /// <summary>
  /// 英単語問題クラス。
  /// </summary>
  public class EnglishWordQA
  {
    /// <summary>
    /// 問題の単語。
    /// </summary>
    public string QuestionWord { get; set; }

    /// <summary>
    /// 回答選択肢。
    /// </summary>
    public List<AnserChoice> AnswerChoices { get; set; } = new List<AnserChoice>();
  }

  /// <summary>
  /// 回答選択肢。
  /// </summary>
  public class AnserChoice
  {
    /// <summary>
    /// コンストラクタです。
    /// </summary>
    /// <param name="answerText"></param>
    /// <param name="isCorrect"></param>
    public AnserChoice(string answerText, bool isCorrect) {
      this.AnswerText = answerText;
      this.IsCorrect = isCorrect;
    }

    /// <summary>
    /// 選択肢。
    /// </summary>
    public string AnswerText { get; set; }

    /// <summary>
    /// 正解フラグ。
    /// </summary>
    public bool IsCorrect { get; set; }
  }
}

画面フォームは以下の通り。

<!-- MainPage.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:local="clr-namespace:App1"
  x:Class="App1.MainPage">
  <StackLayout>
    <Button Text="次の問題" Clicked="nextButtonClicked" />
    <Label x:Name="message" TextColor="Red" HorizontalTextAlignment="Center" />
    <Label Text="単語の意味を答えてください。" HorizontalTextAlignment="Center" />
    <Label x:Name="questionWord" HorizontalTextAlignment="Center" FontAttributes="Bold"/>
    <local:ShuffleLayout x:Name="shuffleLayout" />
  </StackLayout>
</ContentPage>

画面フォームのコードビハインドは以下の通り。

// MainPage.xaml.cs
using System;
using System.Collections.Generic;
using Xamarin.Forms;

namespace App1
{
  public partial class MainPage : ContentPage
  {
    /// <summary>
    /// 英単語問題リスト
    /// </summary>
    private List<EnglishWordQA> englishWordQA = null;

    /// <summary>
    /// 現在回答中の問題のインデックス
    /// </summary>
    private int currentQaIndex = -1;

    /// <summary>
    /// コンストラクタです。
    /// </summary>
    public MainPage()
    {
      InitializeComponent();

      // 英単語問題を初期化
      this.englishWordQA = this.InitializeQuestions();

      // 
      this.SetNextQuestion();
    }

    /// <summary>
    /// 次の問題を表示します。
    /// </summary>
    private void SetNextQuestion()
    {
      this.message.Text = "";

      this.currentQaIndex++;
      if (this.currentQaIndex >= this.englishWordQA.Count)
      {
        this.message.Text = "おしまい";
        return;
      }

      this.shuffleLayout.Children.Clear();

      this.questionWord.Text = this.englishWordQA[this.currentQaIndex].QuestionWord;

      foreach (var qa in this.englishWordQA[this.currentQaIndex].AnswerChoices)
      {
        var button = new Button() { Text = qa.AnswerText };
        button.Clicked += (sender, e) => {
          if (qa.IsCorrect)
            this.message.Text = "正解";
          else
            this.message.Text = "不正解";
        };

        
        this.shuffleLayout.Children.Add(button);
      }
    }

    /// <summary>
    /// 「次の問題を表示」ボタンクリックイベントハンドラ。
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    public void nextButtonClicked(object sender, System.EventArgs e)
    {
      this.SetNextQuestion();
    }

    /// <summary>
    /// 問題を初期化します。
    /// </summary>
    /// <returns></returns>
    private List<EnglishWordQA> InitializeQuestions()
    {
      List<EnglishWordQA> result = new List<EnglishWordQA>();

      result.Add(new EnglishWordQA()
      {
        QuestionWord = "Book",
        AnswerChoices = {
          new AnserChoice("本", true),
          new AnserChoice("花", false),
          new AnserChoice("空", false),
          new AnserChoice("机", false)
        }
      });

      result.Add(new EnglishWordQA()
      {
        QuestionWord = "Language",
        AnswerChoices = {
          new AnserChoice("科学", false),
          new AnserChoice("墓", false),
          new AnserChoice("私", false),
          new AnserChoice("言語", true)
        }
      });

      result.Add(new EnglishWordQA()
      {
        QuestionWord = "Phone",
        AnswerChoices = {
          new AnserChoice("画面", false),
          new AnserChoice("電話", true),
          new AnserChoice("赤", false),
          new AnserChoice("電車", false)
        }
      });

      result.Add(new EnglishWordQA()
      {
        QuestionWord = "Pickle",
        AnswerChoices = {
          new AnserChoice("リンゴ", false),
          new AnserChoice("ピクルス", true),
          new AnserChoice("針", false),
          new AnserChoice("杭", false)
        }
      });

      return result;
    }
  }
}

回転したとき

スマホを回転させた場合の動きについて補足しておきます。
デフォルト表示を「縦」と想定します。
以下手順で順に操作を行った場合の動きについて見ていきます。


①初期起動時
「ShuffleLayout.OnMeasure()呼び出し → ShuffleLayout.LayoutChildren()呼び出し」が4回繰り返されます。
これは「4つの回答選択肢」が ShuffleLayout.Children.Add() される度に、再レイアウトが必要と判断され、OnMeasure() / LayoutChildren()が繰り返し呼び出されるためです。

②「縦」→「横」に回転させる
ShuffleLayout.OnMeasure() が呼び出され、続いて ShuffleLayout.LayoutChildren() が呼び出されます。

③「横」→「縦」に回転させる
「横」→「縦」に回転させます。
この場合、ShuffleLayout.LayoutChildren() のみが呼び出されます。

③「次の問題」をクリック
「ShuffleLayout.OnMeasure()呼び出し → ShuffleLayout.LayoutChildren()呼び出し」が8回繰り返されます。
これは既存の「4つの回答選択肢」の削除毎、及び、次の問題の「4つの回答選択肢の追加毎に、OnMeasure() / LayoutChildren()が繰り返し呼び出されるためです。

※ 上記から分かるように、冗長な処理が行われています。計算した子要素サイズをキャッシュしたり、バッチレンダリング処理を加えたり、パフォーマンスチューニングが必要な場合は、もう少し複雑な実装が必要になります。


上記で「③」において「ShuffleLayout.OnMeasure()」が呼び出されない事は少し不思議に思います。
Xamarin Formsのソースを確認すると、OnMeasure()は「VisualElement.GetSizeRequest(double widthConstraint, double heightConstraint)」内から呼び出されています。また、VisualElement.GetSizeRequest(..)の実装は以下のようになっています。

[Obsolete("Use Measure")]
 public virtual SizeRequest GetSizeRequest(double widthConstraint, double heightConstraint)  
 {  
   SizeRequest cachedResult;  
   var constraintSize = new Size(widthConstraint, heightConstraint);  
   if (_measureCache.TryGetValue(constraintSize, out cachedResult))  
   {  
     return cachedResult;  
   }  
   ...省略  
   SizeRequest result = OnMeasure(widthConstraint, heightConstraint);  
   ...省略  
   var r = new SizeRequest(request, minimum);
   if (r.Request.Width > 0 && r.Request.Height > 0)
   {
      _measureCache[constraintSize] = r;
   }
   ...省略  
 }  

「widthConstraint / heightConstraint」をキーとして _measureCacheフィールドにサイズ情報をキャッシュしています。
その為、対象レイアウトにおいて1度目の領域測定時のみ OnMeasure() が呼び出されます。

Github上の VisualElement.cs の実装は↓↓↓
Xamarin.Forms/VisualElement.cs at ae59382c9046501edb37882ad1c065aacce60319 · xamarin/Xamarin.Forms · GitHub

ただし、この辺りの実装は SizeRequest() が Obsolete であることを含め、修正が入りそうですね。

まとめ

カスタムレイアウトについてはあまり情報が見つからず、また、上記説明でも Invalidateフロー周りはスルーしているので、改めて整理できれば、と思っています。
また、英語情報では、Jason Smith氏がいろいろ説明してくれています。

developer.xamarin.com

xfcomplete.net

Xamarin Forms - StackLayoutのVerticalOptions(Expands)について

(非常にピンポイントな話題ですが)Xamarin FormsのStackLayoutにおける、子コントロールの配置で、「VerticalOptions(HorizontalOptions)」指定時の動きについて纏めたいと思います(特にExpandsに焦点を当てて)。

このネタ、先日の「Xamarin.Forms本 邦訳読書会 #2」にて私が「どんな動きでしたっけ?」と言って、皆さんを(闇に引き込み)こんな動きだよーと口頭で説明していただいたものなんです(でも、誰が説明しても分かりにくい的な話もあり、やはり、厳密には自分でコーディング&実行で検証する必要がありました)

xamarinformsbookreading.connpass.com

VerticalOptions / HorizontalOptions プロパティ

「VerticalOptions / HorizontalOptions プロパティ」は、Xamarin.Forms.Viewクラスの LayoutOptions構造体型 プロパティです。
Viewクラスは、Label や Button、StackLayout や Grid などのすべてのUI要素の基本クラスです。
これらのプロパティを指定することで、対象コントロール(View要素)のLayout上での配置(サイズやパディング)に影響を与えます。

LayoutOptions構造体

LayoutOptionsは構造体で、以下の2つのプロパティを持ちます。

  • public LayoutAlignment Alignment { get; set; }
    配置方法についていかから選択します。
    Center - 中央寄せ
    Start - 開始点寄せ(横方向であれば Left、縦方向であれば Top)
    End - 終了点寄せ(横方向であれば Right、縦方向であれば Bottom)
    Fill - 領域全体を満たす

  • public Boolean Expands { get; set; }
    親の領域が余った場合、「領域を与えてもらうか?(サイズを拡張してもらうか?)」のフラグを指定します。

コードによる指定

動作については後で説明するとして、指定の仕方は C# コードからでは以下のようになります。

this.label1.VerticalOptions = new LayoutOptions(LayoutAlignment.Center, false);

this.button1.VerticalOptions = new LayoutOptions(LayoutAlignment.Fill, true);

XAMLによる指定

上記 cs と同様の設定をXAMLで行うと以下のようになります。

<Label Text="Label1" VerticalOptions="Center" />
<Button Text="ボタン" VerticalOptions="FillAndExpand" />

“Center"とか"FillAndExpand"とかは、「LayoutOptionsConverter型コンバータ」によって「LayoutOptions.Alignment / LayoutOptions.Expands」に変換設定されます。

・・・分かりにくいし想像つかないですね・・・

なので、以下のいくつかのパターンの例を・・例は話を単純にするために「StackLayoutを利用。OrientationはVertical。」とします。

サンプルで試す

ex1) シンプルに

StackLayoutにLabelを5つ配置します。
配置された領域を分かりやすくするために、各要素に BackgroundColor を設定しています。
また、VerticalOptionsプロパティに何も設定していません。この場合、デフォルトの「Fill」となります。

<?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:local="clr-namespace:LayoutExample1" 
  x:Class="LayoutExample1.LayoutExample1Page">

  <StackLayout x:Name="stack1" Orientation="Vertical" BackgroundColor="Silver">
    <StackLayout.Margin>
      <!-- iOSはトップに 20 の余白が必要 -->
      <OnPlatform x:TypeArguments="Thickness"
        iOS="0, 20, 0, 0"
        Android="0, 0, 0, 0"
        WinPhone="0, 0, 0, 0" />
    </StackLayout.Margin>

    <Label Text="Label1" BackgroundColor="Green" />
    <Label Text="Label2" BackgroundColor="White" />
    <Label Text="Label3" BackgroundColor="Yellow" />
    <Label Text="Label4" BackgroundColor="Aqua" />
    <Label Text="Label5" BackgroundColor="Gray" />
  </StackLayout>
</ContentPage>

実行画面は以下の通りです。

f:id:daigo-knowlbo:20170129122305p:plainf:id:daigo-knowlbo:20170129122529p:plain

ex2) Label間のスペース

ex1)の例では、5つのLabelの間には「わずかなスペース」が存在します。
StackLayoutのBackgroundColorである Silver が Label間に見えています。
これは親ビュー側である「StackLayoutのSpacingプロパティ」で調整可能です。
StackLayoutのSpacingに 0 を設定します。

<?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:local="clr-namespace:LayoutExample1" 
  x:Class="LayoutExample1.LayoutExample1Page">

  <StackLayout x:Name="stack1" Spacing="0" Orientation="Vertical" BackgroundColor="Silver">
    <StackLayout.Margin>
      <!-- iOSはトップに 20 の余白が必要 -->
      <OnPlatform x:TypeArguments="Thickness"
        iOS="0, 20, 0, 0"
        Android="0, 0, 0, 0"
        WinPhone="0, 0, 0, 0" />
    </StackLayout.Margin>

    <Label Text="Label1" BackgroundColor="Green" />
    <Label Text="Label2" BackgroundColor="White" />
    <Label Text="Label3" BackgroundColor="Yellow" />
    <Label Text="Label4" BackgroundColor="Aqua" />
    <Label Text="Label5" BackgroundColor="Gray" />
  </StackLayout>
</ContentPage>

実行画面は以下です。

f:id:daigo-knowlbo:20170129122923p:plainf:id:daigo-knowlbo:20170129122940p:plain

ex3) FillAndExpand を指定する

5つのLabelのVerticalOptionsに「FillAndExpand」を設定してみます。

<?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:local="clr-namespace:LayoutExample1" 
  x:Class="LayoutExample1.LayoutExample1Page">

  <StackLayout x:Name="stack1" Spacing="0" Orientation="Vertical" BackgroundColor="Silver">
    <StackLayout.Margin>
      <!-- iOSはトップに 20 の余白が必要 -->
      <OnPlatform x:TypeArguments="Thickness"
        iOS="0, 20, 0, 0"
        Android="0, 0, 0, 0"
        WinPhone="0, 0, 0, 0" />
    </StackLayout.Margin>

    <Label Text="Label1" VerticalOptions="FillAndExpand" BackgroundColor="Green" />
    <Label Text="Label2" VerticalOptions="FillAndExpand" BackgroundColor="White" />
    <Label Text="Label3" VerticalOptions="FillAndExpand" BackgroundColor="Yellow" />
    <Label Text="Label4" VerticalOptions="FillAndExpand" BackgroundColor="Aqua" />
    <Label Text="Label5" VerticalOptions="FillAndExpand" BackgroundColor="Gray" />
  </StackLayout>
</ContentPage>

こうすると・・・以下の実行結果となります。

f:id:daigo-knowlbo:20170129123054p:plainf:id:daigo-knowlbo:20170129123110p:plain

「おー!」なんか画面いっぱいに広がって「いい感じ(?)になりました!」。
子要素に「Expand」が指定されている場合、
親は、各要素を配置し、余った領域について「等分に、(Expandsが指定された)各要素に配分」します。

f:id:daigo-knowlbo:20170129123608p:plain

ex4) 次にこんなXAML定義を

StackLayoutの子として2つのStackLayoutを配置します。
1つ目の子のStackLayoutには、5つのラベルを配置します。
2つ目の子のStackLayoutには、2つのラベルを配置します。
各VerticalOptionsには FillAndExpand を指定します。

<?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:local="clr-namespace:LayoutExample1" 
  x:Class="LayoutExample1.LayoutExample1Page">

  <StackLayout Orientation="Vertical" Spacing="0" BackgroundColor="Red">
    <StackLayout.Margin>
      <!-- iOSはトップに 20 の余白が必要 -->
      <OnPlatform x:TypeArguments="Thickness"
        iOS="0, 20, 0, 0"
        Android="0, 0, 0, 0"
        WinPhone="0, 0, 0, 0" />
    </StackLayout.Margin>

    <StackLayout Orientation="Vertical" BackgroundColor="Navy" Spacing="0" VerticalOptions="FillAndExpand">
      <Label Text="Label1" BackgroundColor="White" VerticalOptions="FillAndExpand" />
      <Label Text="Label2" BackgroundColor="Green" VerticalOptions="FillAndExpand" />
      <Label Text="Label3" BackgroundColor="Yellow" VerticalOptions="FillAndExpand" />
      <Label Text="Label4" BackgroundColor="Blue" VerticalOptions="FillAndExpand" />
      <Label Text="Label5" BackgroundColor="Gray" VerticalOptions="FillAndExpand" />
    </StackLayout>

    <StackLayout Orientation="Vertical" BackgroundColor="Fuchsia" Spacing="0" VerticalOptions="FillAndExpand">
      <Label x:Name="labelA" Text="LabelA" BackgroundColor="White" VerticalOptions="FillAndExpand" />
      <Label x:Name="labelB" Text="LabelB" BackgroundColor="Green" VerticalOptions="FillAndExpand" />
    </StackLayout>
  </StackLayout>
</ContentPage>

以下が実行画面です。

f:id:daigo-knowlbo:20170129124006p:plainf:id:daigo-knowlbo:20170129124015p:plain

予想通りだったでしょうか?
ポイントは「1つ目の子StackLayout と 2つ目のStackLayout の高さは異なる」という点です。

以下のようなサイズ調整になります。

f:id:daigo-knowlbo:20170129135528p:plain A = ①+③
B = ②+④

ex5) 上記に至るパターンの解析(1)

ex4) の結果が想像できた方は、VerticalOptionsの Extends の動作を理解されていることでしょう。
頭の中に「??」が浮かんだ方の為に、更に以下に幾つかのサンプルを紹介します。
ex4)のサンプルの FillAndExpand を Fill に変更します。

<?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:local="clr-namespace:LayoutExample1" 
  x:Class="LayoutExample1.LayoutExample1Page">

  <StackLayout Orientation="Vertical" Spacing="0" BackgroundColor="Red">
    <StackLayout.Margin>
      <!-- iOSはトップに 20 の余白が必要 -->
      <OnPlatform x:TypeArguments="Thickness"
        iOS="0, 20, 0, 0"
        Android="0, 0, 0, 0"
        WinPhone="0, 0, 0, 0" />
    </StackLayout.Margin>

    <StackLayout Orientation="Vertical" BackgroundColor="Navy" Spacing="0" VerticalOptions="Fill">
      <Label Text="Label1" BackgroundColor="White" VerticalOptions="Fill" />
      <Label Text="Label2" BackgroundColor="Green" VerticalOptions="Fill" />
      <Label Text="Label3" BackgroundColor="Yellow" VerticalOptions="Fill" />
      <Label Text="Label4" BackgroundColor="Blue" VerticalOptions="Fill" />
      <Label Text="Label5" BackgroundColor="Gray" VerticalOptions="Fill" />
    </StackLayout>

    <StackLayout Orientation="Vertical" BackgroundColor="Fuchsia" Spacing="0" VerticalOptions="Fill">
      <Label Text="LabelA" BackgroundColor="White" VerticalOptions="Fill" />
      <Label Text="LabelB" BackgroundColor="Green" VerticalOptions="Fill" />
    </StackLayout>
  </StackLayout>
</ContentPage>

実行画面は以下です。各ラベルに必要な領域が取られ、下に領域が余ります。余った領域には親のStackLayoutのBackgroundColorが見えています。

f:id:daigo-knowlbo:20170129125042p:plainf:id:daigo-knowlbo:20170129125127p:plain

ex6) 上記に至るパターンの解析(2)

ex5)に対して「1つ目の子StackLayoutのVerticalOptionsを Fill → FillAndExpand」に修正します。

<?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:local="clr-namespace:LayoutExample1" 
  x:Class="LayoutExample1.LayoutExample1Page">

  <StackLayout Orientation="Vertical" Spacing="0" BackgroundColor="Red">
    <StackLayout.Margin>
      <!-- iOSはトップに 20 の余白が必要 -->
      <OnPlatform x:TypeArguments="Thickness"
        iOS="0, 20, 0, 0"
        Android="0, 0, 0, 0"
        WinPhone="0, 0, 0, 0" />
    </StackLayout.Margin>

    <StackLayout Orientation="Vertical" BackgroundColor="Navy" Spacing="0" VerticalOptions="FillAndExpand">
      <Label Text="Label1" BackgroundColor="White" VerticalOptions="Fill" />
      <Label Text="Label2" BackgroundColor="Green" VerticalOptions="Fill" />
      <Label Text="Label3" BackgroundColor="Yellow" VerticalOptions="Fill" />
      <Label Text="Label4" BackgroundColor="Blue" VerticalOptions="Fill" />
      <Label Text="Label5" BackgroundColor="Gray" VerticalOptions="Fill" />
    </StackLayout>

    <StackLayout Orientation="Vertical" BackgroundColor="Fuchsia" Spacing="0" VerticalOptions="Fill">
      <Label Text="LabelA" BackgroundColor="White" VerticalOptions="Fill" />
      <Label Text="LabelB" BackgroundColor="Green" VerticalOptions="Fill" />
    </StackLayout>
  </StackLayout>
</ContentPage>

実行画面は以下です。

f:id:daigo-knowlbo:20170129125359p:plainf:id:daigo-knowlbo:20170129125408p:plain

ex5)では「5つのLabelを含むStackLayout と 2つのLabelを含むStackLayout」を配置した後の余った領域は、そのままにされていました(親StackLayoutのBackgroundColorが前面に見えている状態になる)。
ex6)では1つ目のStackLayoutが Expands 指定されています。レイアウトの仕組みとして「余った領域は、Expands指定された子要素に等分に分配される」ので、ここではExpand指定された唯一のStackLayoutに領域配分されます(BackgroundColor=Navyで領域を確認することができます)。

ex7) 上記に至るパターンの解析(3)

次に ex6) に以下の修正を加えます。
「1つ目のStackLayoutの子Labelの VerticalOptions を FillAndExpand に修正」

<?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:local="clr-namespace:LayoutExample1" 
  x:Class="LayoutExample1.LayoutExample1Page">

  <StackLayout Orientation="Vertical" Spacing="0" BackgroundColor="Red">
    <StackLayout.Margin>
      <!-- iOSはトップに 20 の余白が必要 -->
      <OnPlatform x:TypeArguments="Thickness"
        iOS="0, 20, 0, 0"
        Android="0, 0, 0, 0"
        WinPhone="0, 0, 0, 0" />
    </StackLayout.Margin>

    <StackLayout Orientation="Vertical" BackgroundColor="Navy" Spacing="0" VerticalOptions="FillAndExpand">
      <Label Text="Label1" BackgroundColor="White" VerticalOptions="FillAndExpand" />
      <Label Text="Label2" BackgroundColor="Green" VerticalOptions="FillAndExpand" />
      <Label Text="Label3" BackgroundColor="Yellow" VerticalOptions="FillAndExpand" />
      <Label Text="Label4" BackgroundColor="Blue" VerticalOptions="FillAndExpand" />
      <Label Text="Label5" BackgroundColor="Gray" VerticalOptions="FillAndExpand" />
    </StackLayout>

    <StackLayout Orientation="Vertical" BackgroundColor="Fuchsia" Spacing="0" VerticalOptions="Fill">
      <Label Text="LabelA" BackgroundColor="White" VerticalOptions="Fill" />
      <Label Text="LabelB" BackgroundColor="Green" VerticalOptions="Fill" />
    </StackLayout>
  </StackLayout>
</ContentPage>

実行画面は以下です。

f:id:daigo-knowlbo:20170129125748p:plainf:id:daigo-knowlbo:20170129125755p:plain

ex6)では1つ目のStackLayoutに割り当てられた領域がそのまま余っていましたが、このex7)では、そのStackLayoutの5つの子LabelのVerticalOptionsがFillAndExpandになった為、余った領域を等分した高さが、5つのLabelに配分されています。

ex8)上記に至るパターンの解析(4)

もういい加減しつこいですが・・・
ex7)に対して以下の修正を加えます。
2つ目のStackLayoutのVerticalOptionsをFillAndExpandに修正。

<?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:local="clr-namespace:LayoutExample1" 
  x:Class="LayoutExample1.LayoutExample1Page">

  <StackLayout Orientation="Vertical" Spacing="0" BackgroundColor="Red">
    <StackLayout.Margin>
      <!-- iOSはトップに 20 の余白が必要 -->
      <OnPlatform x:TypeArguments="Thickness"
        iOS="0, 20, 0, 0"
        Android="0, 0, 0, 0"
        WinPhone="0, 0, 0, 0" />
    </StackLayout.Margin>

    <StackLayout Orientation="Vertical" BackgroundColor="Navy" Spacing="0" VerticalOptions="FillAndExpand">
      <Label Text="Label1" BackgroundColor="White" VerticalOptions="FillAndExpand" />
      <Label Text="Label2" BackgroundColor="Green" VerticalOptions="FillAndExpand" />
      <Label Text="Label3" BackgroundColor="Yellow" VerticalOptions="FillAndExpand" />
      <Label Text="Label4" BackgroundColor="Blue" VerticalOptions="FillAndExpand" />
      <Label Text="Label5" BackgroundColor="Gray" VerticalOptions="FillAndExpand" />
    </StackLayout>

    <StackLayout Orientation="Vertical" BackgroundColor="Fuchsia" Spacing="0" VerticalOptions="FillAndExpand">
      <Label Text="LabelA" BackgroundColor="White" VerticalOptions="Fill" />
      <Label Text="LabelB" BackgroundColor="Green" VerticalOptions="Fill" />
    </StackLayout>
  </StackLayout>
</ContentPage>

実行画面は以下です。

f:id:daigo-knowlbo:20170129131701p:plainf:id:daigo-knowlbo:20170129131709p:plain

余った領域を 1つ目のStackLayout と 2つ目のStackLayout で等分に分け合いました。

これで ex4) の動作の振る舞いにつながったはずです!

コントロール(ビュー要素)が必要とするサイズとは?

上記で使用した StackLayout や Label は「必要な領域を計算し、Expands指定がある場合には余った領域を分配する」という動きをしました。
では、「必要な領域」とは何か?
それは「Xamarin.Forms.VisualElement.OnMeasure()」メソッドで行われています。
LabelやButtonやその他の各UI要素自身が、自らを表示するのに必要な領域(サイズ)をこのメソッドで返却します。
つまりStackLayoutなどの子要素を持つUI要素は、子に必要サイズを聞いて回り、結果として子を含む自らの必要サイズを呼び出し元に返却します。

OnMeasure()を使ったサンプル

ではLabelを継承した MyLabel を以下のように実装してみます。
OnMeasure()メソッドをオーバーライドし、強制的に必要サイズを Width=200 / Height=100 とします。

using System;
namespace LayoutExample1
{
  public class MyLabel : Xamarin.Forms.Label
  {
    public MyLabel()  : base()
    {
    }

    protected override Xamarin.Forms.SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
    {
      return new Xamarin.Forms.SizeRequest(
        new Xamarin.Forms.Size(200, 100), 
        new Xamarin.Forms.Size(200, 100));
    }
  }
}

MyLabelを使用した画面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:local="clr-namespace:LayoutExample1" 
  x:Class="LayoutExample1.LayoutExample1Page">

  <StackLayout x:Name="parentStack" Orientation="Vertical" Spacing="0" BackgroundColor="Red" VerticalOptions="Fill">
    <StackLayout.Margin>
      <!-- iOSはトップに 20 の余白が必要 -->
      <OnPlatform x:TypeArguments="Thickness"
        iOS="0, 20, 0, 0"
        Android="0, 0, 0, 0"
        WinPhone="0, 0, 0, 0" />
    </StackLayout.Margin>

      <local:MyLabel Text="Label1" BackgroundColor="White" VerticalOptions="Fill" />
      <local:MyLabel Text="Label2" BackgroundColor="Green" VerticalOptions="Fill" />
      <local:MyLabel Text="Label3" BackgroundColor="Yellow" VerticalOptions="Fill" />
      <local:MyLabel Text="Label3" BackgroundColor="Blue" VerticalOptions="Fill" />
  </StackLayout>
</ContentPage>

実行すると以下のように Label が Height 100で表示されていることを確認することができます。

f:id:daigo-knowlbo:20170129130335p:plainf:id:daigo-knowlbo:20170129130349p:plain

(おまけ)画面から溢れるほどUIが配置されている場合

画面に収まりきらないほどUIが配置されていた場合、そのまま画面から溢れ、表示・操作できない状態になります。

<?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:local="clr-namespace:LayoutExample1" 
  x:Class="LayoutExample1.LayoutExample1Page">

  <StackLayout Orientation="Vertical" Spacing="0" BackgroundColor="Red">
    <StackLayout.Margin>
      <!-- iOSはトップに 20 の余白が必要 -->
      <OnPlatform x:TypeArguments="Thickness"
        iOS="0, 20, 0, 0"
        Android="0, 0, 0, 0"
        WinPhone="0, 0, 0, 0" />
    </StackLayout.Margin>
    
    <Label Text="Label1" BackgroundColor="White" VerticalOptions="FillAndExpand" />
    <Label Text="Label2" BackgroundColor="Green" VerticalOptions="FillAndExpand" />
    <Label Text="Label3" BackgroundColor="Yellow" VerticalOptions="FillAndExpand" />
    <Label Text="Label4" BackgroundColor="Blue" VerticalOptions="FillAndExpand" />
    <Label Text="Label5" BackgroundColor="Gray" VerticalOptions="FillAndExpand" />
    <Label Text="Label6" BackgroundColor="White" VerticalOptions="FillAndExpand" />
    <Label Text="Label7" BackgroundColor="Green" VerticalOptions="FillAndExpand" />
    <Label Text="Label8" BackgroundColor="Yellow" VerticalOptions="FillAndExpand" />
    <Label Text="Label9" BackgroundColor="Blue" VerticalOptions="FillAndExpand" />
    <Label Text="Label10" BackgroundColor="Gray" VerticalOptions="FillAndExpand" />
    <Label Text="Label11" BackgroundColor="White" VerticalOptions="FillAndExpand" />
    <Label Text="Label12" BackgroundColor="Green" VerticalOptions="FillAndExpand" />
    <Label Text="Label13" BackgroundColor="Yellow" VerticalOptions="FillAndExpand" />
    <Label Text="Label14" BackgroundColor="Blue" VerticalOptions="FillAndExpand" />
    <Label Text="Label15" BackgroundColor="Gray" VerticalOptions="FillAndExpand" />
    <Label Text="Label16" BackgroundColor="White" VerticalOptions="FillAndExpand" />
    <Label Text="Label17" BackgroundColor="Green" VerticalOptions="FillAndExpand" />
    <Label Text="Label18" BackgroundColor="Yellow" VerticalOptions="FillAndExpand" />
    <Label Text="Label19" BackgroundColor="Blue" VerticalOptions="FillAndExpand" />
    <Label Text="Label20" BackgroundColor="Gray" VerticalOptions="FillAndExpand" />
    <Label Text="Label21" BackgroundColor="Gray" VerticalOptions="FillAndExpand" />
    <Label Text="Label22" BackgroundColor="White" VerticalOptions="FillAndExpand" />
    <Label Text="Label23" BackgroundColor="Green" VerticalOptions="FillAndExpand" />
    <Label Text="Label24" BackgroundColor="Yellow" VerticalOptions="FillAndExpand" />
    <Label Text="Label25" BackgroundColor="Blue" VerticalOptions="FillAndExpand" />
    <Label Text="Label26" BackgroundColor="Gray" VerticalOptions="FillAndExpand" />
    <Label Text="Label27" BackgroundColor="Gray" VerticalOptions="FillAndExpand" />
    <Label Text="Label28" BackgroundColor="White" VerticalOptions="FillAndExpand" />
    <Label Text="Label29" BackgroundColor="Green" VerticalOptions="FillAndExpand" />
    <Label Text="Label30" BackgroundColor="Yellow" VerticalOptions="FillAndExpand" />
    <Label Text="Label31" BackgroundColor="Blue" VerticalOptions="FillAndExpand" />
    <Label Text="Label32" BackgroundColor="Gray" VerticalOptions="FillAndExpand" />
    <Label Text="Label33" BackgroundColor="Gray" VerticalOptions="FillAndExpand" />
    <Label Text="Label34" BackgroundColor="White" VerticalOptions="FillAndExpand" />
    <Label Text="Label35" BackgroundColor="Green" VerticalOptions="FillAndExpand" />
    <Label Text="Label36" BackgroundColor="Yellow" VerticalOptions="FillAndExpand" />
    <Label Text="Label37" BackgroundColor="Blue" VerticalOptions="FillAndExpand" />
    <Label Text="Label38" BackgroundColor="Gray" VerticalOptions="FillAndExpand" />
    <Label Text="Label39" BackgroundColor="Gray" VerticalOptions="FillAndExpand" />
    <Label Text="Label40" BackgroundColor="White" VerticalOptions="FillAndExpand" />
    <Label Text="Label41" BackgroundColor="Green" VerticalOptions="FillAndExpand" />
    <Label Text="Label42" BackgroundColor="Yellow" VerticalOptions="FillAndExpand" />
    <Label Text="Label43" BackgroundColor="Blue" VerticalOptions="FillAndExpand" />
    <Label Text="Label44" BackgroundColor="Gray" VerticalOptions="FillAndExpand" />
  </StackLayout>
</ContentPage>

実行画面は以下です。

f:id:daigo-knowlbo:20170129131109p:plainf:id:daigo-knowlbo:20170129131118p:plain

まとめ

余った領域は、Expands指定されたUI要素の数で等分され、各UI要素(Expands)に配分される。