Visual Studio 2017 で Live Unit Testing(xUnit)した感想

3月7日にVisual Studio 2017の正式リリースを控えた週末に、RC版でチョロチョロと遊んでいました。
その中で「Live Unit Testing」について書いてみたいと思います。
この機能は、結構わかりやすい機能で、すでに多くの方がブログ等で紹介されています。
MsTestベース の説明が多そうなので、大して変わりませんが xUnit を使った Live Unit Testing をここでは紹介します。

テスト対象のプロジェクトを作成

プロジェクトの作成

メニュー「ファイル → 新規作成 → プロジェクト」を選択。
テンプレートは「クラスライブラリ(.NET Framework)」、プロジェクト名は「HogeHogeLibrary」とします。

f:id:daigo-knowlbo:20170306004125p:plain
※余談ですが「.NET Core」とか「.NET Standard」とか「ポータブル」とか、きちんと学んでいないと何が何だかになってしまいそうですね・・・3/7版でもこのラインで行くのかな?(まあ、現在の.NETの状況的にはこれが正しい姿ですね)

テスト対象クラスの追加

「Class1.cs」は削除して、テスト対象のクラスを追加します。
ソリューションエクスプローラで HogeHogeLibrary をマウス右ボタンクリック。メニューの「追加 → クラス」を選択します。

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

「Programmer.cs」を追加します。

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

実装は以下の通り。

// HogeHogeLibrary\Programmer.cs
using System;

namespace HogeHogeLibrary
{
  public class Programmer
  {
    public int Stamina { get; set; } = 100;

    public void Work(int hour)
    {
      this.Stamina -= hour * 10;
    }

    public void HaveLunch()
    {
      this.Stamina += 30;
    }

    public void Sleep(int minutes)
    {
      this.Stamina += minutes * 5;
    }
  }
}

彼は
* スタミナがデフォルトで100あります。
* 1時間働くとスタミナが10減ります。
* ランチをとるとスタミナが30増えます。
* 1分居眠りするとスタミナが5増えます。

テストプロジェクトを作成

プロジェクトの作成

では Programerクラス をテストするプロジェクトを作成します。
ソリューションエクスプローラで、ソリューションをマウス右ボタンクリック。メニューの「追加 → 新しいプロジェクト」を選択します。

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

テンプレートは「クラスライブラリ(.NET Framework)」、プロジェクト名は「HogeHogeLibrary.Test」とします。

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

xunitをNuGetから追加

ソリューションエクスプローラからHogeHogeLibrary.Testをマウス右ボタンクリック。メニューの「NuGetパッケージの管理」を選択します。

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

「xunit」をインストールします。

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

「xunit.runner.visualstudio」をインストールします。

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

HogeHogeLibraryへの参照を追加

テスト対象プロジェクト「HogeHogeLibrary」への参照を追加します。
ソリューションエクスプローラで「HogeHogeLibrary.Test → 参照」をマウス右ボタンクリック。メニューの「参照の追加」を選択します。

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

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

テストクラスを追加

「Class1.cs」は削除して、テスト対象のクラスを追加します。
ソリューションエクスプローラで HogeHogeLibrary.Test をマウス右ボタンクリック。メニューの「追加 → クラス」を選択します。

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

「ProgrammerTest.cs」を追加します。

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

実装は以下の通り。

// HogeHogeLibrary.Test\ProgrammerTest.cs
using Xunit;

using HogeHogeLibrary;

namespace HogeHogeLibrary.Test
{
  public class ProgrammerTest
  {
    [Fact]
    public void HardWorkTest()
    {
      var programmer = new Programmer();
      programmer.Work(3);
      programmer.HaveLunch();
      programmer.Work(10);

      Assert.Equal<int>(0, programmer.Stamina);
    }

    [Fact]
    public void NormalWorkTest()
    {
      var programmer = new Programmer();
      programmer.Work(3);
      programmer.HaveLunch();
      programmer.Work(5);

      Assert.Equal<int>(50, programmer.Stamina);
    }
  }
}
  • HardWorkTest()は、プログラマをスタミナ0までめいっぱい働かせます!
  • NormalWorkTest()は、プログラマを健全に働かせます!

一度ビルドしておきましょう。

テストエクスプローラの表示

メニュー「テスト → ウィンドウ → テストエクスプローラ」を選択します。
「すべて実行」をクリックするとテストが実行され、無事、成功の「緑」を得られます。

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

Live Unit Testingを開始

メニュー「テスト → Live Unit Testing → 開始」を選択します。
すると、コードエディタ上に緑のチェックマークがつきます。

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

Liveする!

では、試しにProgrammerクラスのコードをいじってみます。
プログラマ達は、ジムに通いデフォルトスタミナを120に引き上げました。

// HogeHogeLibrary\Programmer.cs
public int Stamina { get; set; } = 120;

すると、ワンテンポの後、以下のように Programmer.cs / ProgrammerTest.cs / テストエクスプローラ の表示が赤くなります。

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

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

仕様が変わったProgrammerに合わせてテストコードを書き直してあげると、緑に戻ります。

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

どのタイミングで動くの?

なんか「カッコいー、未来的ー」って思いますが、どのタイミングで Live Unit Testing は動いているのでしょうか?
見ていると「連続したキー入力が終わって1秒くらい後」には、ビルドが走ってテストが実行されているみたいです。

まとめ(思ったこと)

ということで「Live Unit Testing」を有効にしていると、かなり頻繁に「ビルド→テスト」が繰り返し実行されます。
個人的には「ソースファイルを保存したタイミング」辺りでいいんじゃないかと思ってしまいましたが、製品版でも同様になるのでしょうか(どこかに設定とか有るのでしょうか・・・)。7日に改めて確認してみます。

そしてこのビルドは内部的なもののようで、プロジェクトのbinフォルダのアセンブリは更新されません。

また、Service / Domain / Repositoryなどのように、分離したプロジェクト構成の場合でもきちんと動いてくれます。つまり、Serviceに対するテストコードが、末端のRepositoryソースの変更に対しても反応して Live Unit Testing が行われます。

本投稿のようなシンプルなクラスのテストであれば問題ありませんが、データベースアクセス や 外部WebAPI呼び出し が行われるクラスなんかが絡んでいると、ガンガンアクセスが飛ぶことになると思います。
更新系の処理が予期せず多数走ったり、大き目のチーム開発だとルール化なんかも必要そうかなぁ・・・なんて思いました。

そして、思考が拡散して、そもそも ユニットテストの必要性・TDDの可否・DHH氏のTDD is dead・・・なんかの過去の記事を改めて読み返してしまいました。
個人的には、「細部に至るユニットテスト要らない」「ビジネス的粒度のユニットテスト+自動統合テスト」でおk派です。
(と言いながら、実プロジェクトでテストコードを書かないことが多いのはここだけの話・・・)

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

「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 について、その仕組みやアーキテクチャについて知識がありませんでした。
ということで、これをきっかけに興味を持ったので、暇があったら勉強したいと思います。