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)に配分される。

「JXUGC #22 最新事例&お前のアプリを説明してもらおうの会」に参加した話

今まで、ユーザーグループやコミュニティのイベントには消極的だったのですが、2017年から色々なところに参加させていただこうかと思い、本日(1/28)の「JXUGC #22 最新事例&お前のアプリを説明してもらおうの会」にも参加させていただきました。
このイベントは比較的大きく、参加者は60人位いたでしょうか。

で、その感想を、長々と・・・会社で長々すると「面倒臭がられるけど」ここは「俺の自由の場だ!」ということで自由に長々します!(笑)。

では各セッション毎の感想を!

「がんばれガンプ ソルバルウを倒せ について」

@hiro128_777 さんのセッションでした。

twitter.com

ゼビウスの派生ゲームで、ソルバルウを倒す側のゲームを現在開発中ということで、それについてのお話でした(公式ゲーで無料公開されるそうです)。
技術的には、「Xamarin Forms」「Cocos Sharp」なんかを使っているとのこと。
Cocos Sharpは、2D / 3Dのゲーム開発のライブラリで、でもFormsの「UI要素(Label等)」と同様に2Dアニメーション要素(例えばソルバルウのキャラ画像)を扱えるとの事。
そして、その仕組みが故、FormsのPCLに実装を持ってくることができ、普通に作ってもコードの90%を(iOS / androidの)共有コードに持ってくることができるそうです。

私は、ゲームの世界は全く概念の異なる異世界のように考えてしまっていたのですが、なんだか身近に感じることができました。
そして、セッション中にもおっしゃられていましたが、ゲーム以外のアプリにもよりリッチなアニメーション効果を追加したい場合には、Cocos Sharpが使えるんじゃないか、と。
そうそう、うちの会社はエンタープライズな自社サービスを作ってるけど、ちょっとリッチな視覚効果とか好きだから使えるかも!?なのではないだろうか、と思いました。

GitHub - mono/CocosSharp: CocosSharp is a C# implementation of the Cocos2D and Cocos3D APIs that runs on any platform where MonoGame runs.

「ゆるふわ Xamarin Tips」

@Santea3173 さんのセッションでした。

twitter.com

[2017/1/29 リンク追記]

bit.ly

サンテアさんは初めて見たのですが、院生だったんですね。あとtwitterのアイコンからもっとチャラいイメージを持っていましたが、全くもって「賢い紳士な大学院生」でした(笑)。
twitterをデータソースとして、SQLを使って検索一覧表示を行うというアプリの紹介でした。
また、そのアプリで使っているライブラリの紹介などもしていただきました。Syncfusionの「Essential Studio For Xamarin」のDataGridやPDFViewerに興味を持ったので、今度自分でも使って見たいと思いました。

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

また「Xamarin Formsを使うべきアプリ」「Xamarin Traditionalを使うべきアプリ」の指針も非常にまとまっていて、会社での技術選定にも役立てることができる内容だったと思います。

Xamarin Component / Nuget / Xamarin Forms Labs の説明も非常に整理されていて良かった印象です。
Xamarin Forms Labsが

This project is no longer maintained. It may not work with newer versions of Xamarin.Forms.

なのは残念に思っているのですが、別のものに置き換わるのでしょうか?

「Reactive ProperrtyでXamarinアプリの作り方が変わった」

@iseebi さんのセッションでした。

twitter.com

実際の開発において既存ネイティブアプリをXamarinで書き換えた経緯や技術の紹介をしていただきました。
最近私は MVVM フレームワークとしては Prism ばかり見ていたので、MVVM Light を使われたお話が逆に新鮮でした。
実は昔、私は Silverlight 開発を行っていたことがあり、その時はMVVM Lightを使っていたのです。
そして Reactive Property は素晴らしいので、もはや今からでは「最新技術を紹介するぞ!」な内容ではありませんが、自分なりの整理をこのブログでも記したいと思っています。
えーと、たしか、いせさんは「フェンリルさんの方でしたよね・・・で、採用の宣伝もしていた・・・」。
フェンリルさんは自社ソフトウェア・自社サービスの開発も行っているようで、私が所属している会社も同じく自社サービス開発企業なので、最近失われていた「こんな面白いアーキテクチャ、こんな面白い技術」を自分の会社にも広めたいな心が、少し発生しました(笑)。

「証券取引アプリ について」

@omanuke さんのセッションでした。

twitter.com

何と言っても 「F#」 っていうのが印象的でした。
証券取引アプリ や Xamarin というよりも、とにかく 「F#愛!F#推し!」 を強調されていましたね。
うん、確かにデモで見せていただいたコードは「F#すげー!C#どんくさいー!」な内容でした。
そう、C#で長々となるロジックがF#だと超簡潔に書けてしまうのです。
これは「型推論の言語仕様の違い」「優秀なパターンマッチング」から来るそうです。
デモの中でも、仕様変更に対して、C#ではコードの全体の各所の修正が必要になるケースでも、F#ではごくわずかな修正で対応できる例を紹介されていました。

でも、すみません!、私はC#を使い続けるでしょう(笑)。真面目な話、F#使ったら、みんな理解できないと思うのです・・・もし使って自社のソフトを開発したら・・・「あれはF#使ってるから分かんねーよ」と、虐げられるソフトウェアになってしまうと思うのです;;

真面目に技術者として気になったのは
「あのF#コードは、コンパイル後にはどのようなILになるのだろう?」
でした。おそらく、普通のIF分岐等に解釈されるものと想像していますが、C#的概念からは不思議を感じそうです。今度確認します!

あとomanukeさんのキャラが可愛かったです^^結構好きです^^

「AzureVM Power Switch について」

@yamamo さんのセッションでした。

twitter.com

Azureの仮想マシンをXamarin Formsアプリから「状態確認・開始・終了」するというアプリの紹介でした。
技術的には、「Active Directory Authentication Library」はXamarinにも対応してたんだあ!と思いました。ADALは以前、仕事で自社サービスをAzureAD認証するのに使ったことがあったので(Webアプリ)。
このアプリは、私の属する会社でも「使えるアプリ」になりそうな気がしました。AzureのREST APIは機能的に豊富なので、超実用的なアプリにブラッシュアップすることが可能な気がしてしまいました(紹介された以外の機能も既にある?かは不明なのですが)。

XAML について」

@okazuki さんのセッションでした。

twitter.com

[2017/1/29 リンク追記] blog.okazuki.jp

この手の話、私は結構好きです。(元MSの萩原さん的な)
実は数年前に私が執筆した本の中でWPFについて書いたとき、本日、かずきさんが語ってくれたことを散々苦労しながら調べたのです(文書にする場合、「使える」以上の明確な理解と整理が必要なので^^;)。

gihyo.jp

以下のような技術要素になります。

  • XAML名前空間(xmlns)とC#名前空間
  • 要素とクラス
  • 属性とプロパティ
  • 添付プロパティ
  • コンテンツプロパティ
  • ディペンデンシープロパティ(今日のセッションでは出なかったけどXamarinだとバインダブルプロパティ)

多分、これらに関する知識が曖昧でもJXUGの場でこんなの作ったよー、って紹介できるし、十分開発できると思います。
でも、何かあった時、これらの基礎技術知識があるとないとでは、問題解決能力の差が出て来ると思うのです。
知っていれば10分で解決、知らないと延々分からない、的な。

「LT枠の皆様」

基本的に皆さん「制限時間内に伝えるべきことを楽しく明確に伝えることができていて」素晴らしい!って感想を持ちました!
年齢層は様々だったと思いますが、若い方、学生の方なんかが、ガンガンXamarinに取り組んでいて、さらに形ある成果物をあげているのが素晴らしいと思いました。
ここに出て来るような人達が、うちの会社にも毎年入社してくれればいいのに!って感じです(笑)

まとめ

今回、懇親会に参加予定だったのですが、所用で参加できずでした。ちょっと、話をしてみたい方も何人かおられたのですが、またの機会にお願いしたいです。

また、JXUGのイベントには大規模なものも小規模なものも参加させていただきたいと思います。

そして、自分自身に思った事は
「もっと手を動かそう!作っちゃおう!」
でした。
私は結構、「学術的に」というか論理的に物事を明確にしたがるので、実用的なものを作るより、「調査」をしてしまっているように思うので、もっと「考える前に作っちゃおう!」と思いました。

あっ、あと「全行ブレークポイントにはふいた!(笑)」

Xamarin Studio(Xamarin Forms)でxUnitする

Xamarin StudioでxUnitを利用する方法についてメモしておきます。

ユニットテストツール xUnit を Xamarin Studioで使用する方法になります。

誰でも出来るように丁寧に画面キャプチャして説明したいと思います。

アドインを追加する

テストランナーを Xamarin Studio IDE に統合する為にアドインを追加します。

Xamarin Studioを起動し「Xamarin Studio→アドイン...」メニューを選択します。

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

「アドイン マネージャー」が表示されます。
「ギャラリー」タブを選択し、右上の検索テキストボックスに「xunit」と入力します。
リストから「xUnit.NET 2 testing framework support」を選択し、「インストール...」ボタンをクリックします。

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

テスト対象のソリューションを作成

「ファイル→新しいソリューション」メニューを選択し、XFormApp1という Xamarin Forms ソリューションを作成します。

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

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

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

完成したソリューションは以下。

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

ユニットテストプロジェクトの追加

XFormsApp1ソリューションに、xUnitプロジェクトを追加します。

「ソリューション」からXFormsApp1をマウス右ボタンクリックし、「追加→新しいプロジェクトを追加」を選択します。

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

「ライブラリ→ポータブル ライブラリ」を選択します。

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

プロジェクト名を「xFormTest」とします。

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

ソリューション→xFormTestの「パッケージ」をダブルクリックします。

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

「パッケージを追加」ウィンドウが表示されるので「xUnit.net」を追加します(Add Packageクリック)。

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

ソリューションツリーから xFormTest の 参照 をダブルクリックし、テスト対象プロジェクト「xFormsApp1」への参照を追加しておきます。

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

ユニットテストコードの追加

xFormTestプロジェクトにユニットテストコードを追加します。
「ソリューション」から xFormTest をマウス右ボタンクリックし「追加→新しいファイル」を選択します。

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

「空のクラス」を選択し、名前に「RunningManTest」と入力します。
(Xamarin Forms PCLプロジェクトに RunningMan クラスを追加予定なので、そのユニットテストクラスを用意する想定です)

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

RunningManクラスは Run_4kmPerHour(int second) / Run_5kmPerHour(int second) / Run_6kmPerHour(int second) といった各時速ごとの走るメソッドを持ち、各々に消費カロリーが記録(計算)されるクラスです。
そのテストメソッドの実装が以下の RunningMaTest クラスになります。

// xFormTest/RunningMantest.cs
using System;
using Xunit;

namespace xFormTest
{
  public class RunningManTest
  {
    [Fact]
    public void TestCase1()
    {
      XFormsApp1.Models.RunningMan runningMan = new XFormsApp1.Models.RunningMan();
      runningMan.Run_4kmPerHour(10); // 4km/hで10分
      runningMan.Run_5kmPerHour(20); // 5km/hで20分

      // 消費カロリーをチェック
      Assert.Equal(runningMan.TotalCal, 2.79 * 10 + 3.90 * 20);
    }

    [Fact]
    public void TestCase2()
    {
      XFormsApp1.Models.RunningMan runningMan = new XFormsApp1.Models.RunningMan();
      runningMan.Run_6kmPerHour(15); // 6km/hで15分

      // 消費カロリーをチェック
      Assert.Equal(runningMan.TotalCal, 5.70 * 15);
    }
  }
}

テスト対象クラスを実装

テスト対象クラス「xFormsApp1/models/RunnnigMan.cs」を追加します。
まず Modelsフォルダ を追加。

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

RunningMan.cs を追加。

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

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

// xFormsApp1/Models/RunningMan.cs
using System;
namespace XFormsApp1.Models
{
  public class RunningMan
  {
    /// <summary>
    /// 合計消費カロリー
    /// </summary>
    /// <value>The total cal.</value>
    public double TotalCal { get; private set;}

    /// <summary>
    /// 時速4kmで走ります。(消費カロリー:2.79kcal/sec)
    /// </summary>
    /// <param name="seconds">継続時間</param>
    public void Run_4kmPerHour(int seconds)
    {
      this.TotalCal += 2.79 * seconds;
    }

    /// <summary>
    /// 時速5kmで走ります。(消費カロリー:3.90kcal/sec)
    /// </summary>
    /// <param name="seconds">継続時間</param>
    public void Run_5kmPerHour(int seconds)
    {
      this.TotalCal += 3.90 * seconds;
    }

    /// <summary>
    /// 時速6kmで走ります。(消費カロリー:5.70kcal/sec)
    /// </summary>
    /// <param name="seconds">継続時間</param>
    public void Run_6kmPerHour(int seconds)
    {
      this.TotalCal += 5.70 * seconds;
    }
  }
}

テスト実行

メニュー「表示→テスト」をクリックします。

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

単体テスト」「テスト結果」ペインが表示されます。
単体テスト」には先ほど実装した単体テストメソッドが表示されています。(テストメソッドへの[Fact]属性から自動的に認識されています)

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

単体テスト」の「すべて実行」ボタンをクリックすると、全ての単体テストが実行されます。
今回実装した2つのテストはともに成功するので、緑の丸がつきます。

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

Visual Studio for Mac で動かないの?

と私自身思いまして、ググったら・・・今のところサポートプランは無いそうです・・・;;

github.com

そもそも、私自身 Xamarin Studioのアドインや Visual Studio for Macの Extensions について、その仕組みやアーキテクチャについて知識がありませんでした。
ということで、これをきっかけに興味を持ったので、暇があったら勉強したいと思います。

プレリリース版のXamarin Formsを使う方法

前回のポストでプレリリース版「Xamarin.Forms 2.3.4.184-pre1」のPickerコントロールについて触れました。

Xamarin Studio や Visual Studio for Mac を利用して新規ソリューション(プロジェクト)を作成した場合、通常、Stable版のXamarin Formsプロジェクトが自動生成されます。

このプロジェクトに対して最新プレリリース版Xamarin Formsを適用する手順について説明します。
以下Xamarin Studioベースで画像キャプチャしていますが、Visual Studio for Macでも同様の手順です。
Visual Studio 2015(windows)における手順も後述します)

Xamarin Studio / Visual Studio for Macの場合

ソリューションの作成

Xamarin Srudio もしくは Visual Studio for Mac で、新規のXamarin Formsアプリケーション ソリューションを作成します。
ここではソリューション名を「PreExample1」としました。Prismも使わないシンプルなXamarin Formsソリューションとしています。

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

パッケージの追加

PCLプロジェクト(PreExample1)、Androidプロジェクト(PreExample1.Droid)、iOSプロジェクト(PreExample1.iOS)の各プロイジェクトの「パッケージ」に対してデフォルトでは、 stable版 の Xamarin.Forms パッケージへの参照が追加されています。
これをプレリリース版に置き換えましょう。
パッケージをマウス右ボタンクリックし「パッケージの追加」メニューを選択します。

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

「パッケージを追加」ウィンドウが表示されます。

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

左下の「プレリリース パッケージを表示する」にチェックを入れ、右上の検索ボックスに「xamarin forms」と入力します。
一覧に表示された「Xamarin.Forms」を選択し、右下のバージョン ドロップダウンリストから利用したいバージョンを選択します(ここでは 2.3.4.184-pre1を選択)。
「OK」ボタンをクリックするとパッケージの追加(更新)が行われます。

パッケージの更新完了後に、「パッケージ→Xamarin.Forms」をマウス右ボタンクリックすると、バージョンが更新されている事を確認することができます。

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

Android / iOSプロジェクトのパッケージも更新

上述で「PCLプロジェクト(PreExample1)」のパッケージが更新されました。
同様の手順で「Androidプロジェクト(PreExample1.Droid)・iOSプロジェクト(PreExample1.iOS)」のパッケージも最新のXamarin.Formsに更新しましょう。

Visual Studio 2015(Windows)の場合

Xamarin Formsプロジェクトを作成します。
ソリューションエクスプローラーからプロジェクトを選択→マウス右ボタン→NuGetパッケージの管理を選択します。

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

以下の「NuGetパッケージマネージャー」が表示されるので、Xamarin.Formsを選択→「プレリリースを含める」にチェック→「バージョン」から最新のプレリリース版を選択→更新ボタンクリック、操作を行います。

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

上記操作をiOS / Android / UWPの各プロジェクトにも行います。

最新版のXamarin Formsで遊べる!

さあ、最新プレリリース版の Xamarin Forms で遊びましょう!

PickerがBindableになるそうだ(Xamarin.Forms 2.3.4.184-pre1)

Xamarin Forms + MVVM開発において最も相性の悪いコントロールの代表である Picker がBindableになるそうです。
現在 Xamarin.Forms 2.3.4.184-pre1 にて実装が提供されています。

blog.xamarin.com

正式版への取り込みは以下のロードマップに記述されている通り「2.4.0 - February 2017」となるそうです。

forums.xamarin.com

そう、従来の(現状の)Pickerコントロールには、ListView.ItemsSourceのようなBindablePropertyが用意されていなかったんですよね。
あと、SelectedItemプロパティも無かったので、 SelectedIndexプロパティから選択オブジェクトを取得するという回りくどい実装が必要でした。

ということでPickerの派生クラスを自前で作ってBindablePropertyを用意するような事をする必要がありました。
以下のような感じで。

github.com

これがネイティブにサポートされる予定です(嬉しい)。

「Xamarin.Forms 2.3.4.184-pre1」のPickerを使ったサンプルを以下に置きました。

XamarinExamples/Prism/Control/UseBindablePicker at master · ryuichi111/XamarinExamples · GitHub

BindablePickerと呼んでいますが、実装自体は「Pickerコントロール」に 「ItemsSourceプロパティ / SelectedItemプロパティ」が追加される形でのアップデートになります。