読者です 読者をやめる 読者になる 読者になる

VS2017よりもっと先取りしたい! ~ Visual Studio Preview

2017/3/7に Visual Studio 2017 がリリースされました。
多くの機能が盛り込まれ、そして改善されました。

私を含め皆さんも Visual Studio 2017 の新機能を試して、プロジェクトへの導入の検討をしていることでしょう。

しかし、ギーク達は更なる「おもちゃ」を追い求めることでしょう。

ということで「Visual Studio Preview」です!

Visual Studio Previewとは

Visual Studio Previewは、アーリーアダプターのために最新の機能をいち早く取り込んだ Visual Studio です。
つまり、Stableな製品版 Visual Studio 2017 にはまだ取り込まれていない段階の最新機能が Visual Studio Preview として提供されます。

Visual Studio 2017 と Visual Studio Preview は Side-by-Side 可能

これ、結構嬉しい事だと思います。
「VS2017」と「VS Preview」は、同一環境に並行してインストール可能です。
(とはいっても、仮想環境構築が容易な今の時代的には、VM立てちゃったほうが精神衛生的に良いかもしれないけど・・・でも、公式にサイドバイサイドOKとアナウンスされています。)

Visual Studio Previewをインストールする

では、Visual Studio Previewをインストールしてみましょう!
ここでは、Visual Studio 2017をインストール済みの環境に、Visual Studio Previewを追加でインストールしてみます。
(あっ、でも以下は、実は物理PCではなく、Hyper-V上のWindows10 + VS2017の環境にVS Previewを追加インストールしています(汗))

1.ダウンロード

以下のURLをブラウザで開きます。

www.visualstudio.com

「Download Visual Studio」にマウスを乗せるとエディション一覧がプルダウンされるので、ご希望のエディションをクリックしましょう。
私は Enterpriseを選択しました。
セットアップ用exeがダウンロードされます。

2.セットアップ実行

ダウンロードしたセットアップ用exeを実行します。

Visual Studio 2017セットアップで見慣れた画面が表示されます。
続行をクリックします。

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

インストール対象を選択する画面が表示されますが、「Python 開発」が表示されているのを確認することができます。
これ、StableなVisual Studio 2017にはまだ取り込まれていません(2017/3/10現在)。

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

インストールボタンをクリックすると、インストールが開始されます(ここでは「ワークロード」の機能をすべて選択しました)。

以下の画面をよく見ると、画面左側に「インストール済み Visual Studio 2017 Enterprise」と表示されています。
そして、その右側に「Visual Studio Enterprise 2017(2)」と表示されています(これが、今インストールしているVisual Studio Preview)。

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

3.起動

インストールが終わったら「起動」ボタンをクリックします。

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

バージョン情報を確認してみましょう(メニュー「ヘルプ → Microsoft Visual Studioのバージョン情報」)。
以下のように「Version 15.1(26304.0 Preview)」の文字を確認することが出来ます。

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

4.Pythonプロジェクトを作成してみる

Pythonサポートは、Preview版で先行リリースされている機能です。
これを使ってみようと思います。

メニュー「ファイル → 新規作成 → プロジェクト」を選択します。
以下のように、Pythonテンプレートが表示されました。Python Applicationを選択してOKをクリックします。

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

以下のようなプロジェクトが作成されます。

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

PythonApplication1.pyを適当に以下のように実装します。

print("Hello Python")

CTRL + F5 で実行してみましょう。

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

はい、実行されました。

まとめ

Visual Studio 2017自体がいろいろ盛りだくさんなのですが、Visual Studio Previewなんかの先行リリースも常に軽く眺めておくと時代についていくのが楽になるのではないかと思います。
まとめっちゅうまとめは特にないのですが、興味がある方は Visual Studio 2017 に加えて Visual Studio Preview も入れても面白いのではないかと思います。

参考URL:

blogs.msdn.microsoft.com

Docker Support で ASP.NET Core アプリを作る - Visual Studio 2017

Visual Studio 2017がいよいよ正式リリースされました。
個人的感想としたは「超目玉!!!」な機能は、感じられていないのですが、個々の技術要素は非常に興味深く思っています。
昔に比べて、各情報は小出しにリリースされるので上記のような感想を持つ形になるのでしょう。昔は大きな区切りで一気にいろんな機能がリリース、という流れだったので・・・

で、今回はVisual Studio 2017を利用し、ASP.NET Coreアプリ を Docker Support で開発実行してみようと思います。

Docker for Windowsのインストール

まずは Docker for Windows をインストールします。
以下のURLにアクセスし Stable channel 版をインストールすればOKです。

Install Docker for Windows - Docker Documentation

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

うまくインストールされれば、タスクトレイに例のクジラが現れます。

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

プロジェクトの作成

Visual Studio 2017を起動します。
メニューから「ファイル → 新規作成 → プロジェクト」を選択します。
テンプレートは「Visual C# → Web → ASP.NET Core Webアプリケーション(.NET Core)」を選択し、名前は「HelloAspNetCore」とします。ちなみに場所は「J:\Projects」としました。
OKをクリックします。

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

次に表示されたウィンドウで「.NET Coreのバージョンは1.1」「Web アプリケーション」を選択します。
そして今回最も重要なのは、左下の「Docker サポートを有効にする」にチェックをつけることです。
で、OKをクリックします。

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

自動生成されたソリューションは以下の通りです。
Docker構成(docker-compose)が自動生成されたことが確認できます。

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

Dcokerを設定する

VS上でそのまま実行(CTRL + F5)したいところですが、このままではエラーとなってしまいます。
Docker側の設定が必要です。
タスクトレイのDockerアイコン(クジラ)をマウス右ボタンクリックし「Settings…」を選択します。
Settingsウィンドウが表示されるので、左側のタブから「Shared Drives」を選択し、「システムドライブ(C)」及び「プロジェクトドライブ(J)」を Shared として、Applyボタンをクリックします。

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

更新&Docker再起動まで少し待ちましょう。
Settingsウィンドウの左下の表示が「Uodating Drives…」から「Docker is running」となったらOKです。

実行!

Visual Studio 2017に戻ってプロジェクトを実行しましょう!
(ビルド)ツールバーに「▶Docker」と表示されているので、これをクリックします(「CTRL + F5」でもOK)。

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

すばらしい!
Visual Studio 2017で作成した ASP.NET Coreアプリが、Docker上で実行されました!

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

本当にDockerで実行されたか確認する

あまりに簡単すぎて、これは本当にDokcerで実行されているの?
と、いうことで確認してみましょう。

コマンドプロンプトを開きます。
以下のdockerコマンドを実行してみましょう。

docker images
docker ps -all

結果は以下です。

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

うん、今作成したプロジェクトがdocker上に作成されています。

まとめ

VSのDockerサポートは.NET Coreのみの機能ですが、非常にシームレスに統合されているように感じました。
.NET Framework / .NET Core / Xamarinというマルチプラットフォームの統合は .NET Standard 2.0 であり、もう少し時間がかかりますが、.NET Core + Dcoker という点については、開発において色々な可能性を秘めているのではないでしょうか。

Portability Analyzerを使ってライブラリの.NET Standard準拠を調べよう

Visual Studio 2017のリリースを迎え(るにあたり)、(技術的には、直接的にそこに関連付いているわけではないけれど)「.NET Standard」というキーワードが強く聞こえてくるようになった気がしています。

そうそう、先日、 Xamarin Studio も .NET Standard Library 対応しましたね。

blog.xamarin.com

ということで、今回は既存資産のライブラリ実装が、.NET Standardに準拠しているかどうか調べることができる「Portability Analyzer」というものを使ってみたいと思います。

Githubリポジトリは↓↓↓

github.com

ちなみにGithubからソース取らなくても、以下のGUI操作だけで使えます。

※以下はVisual Studio 2015を使います。明日か明後日にVS2017版に差し替えるかも・・・

.NET Portability Analyzerをインストール

※2017.3.8追記
2017/3/8現在のVisual Studio 2017では「拡張機能と更新プログラム」からだと「.NET Portable Analyzer」が出てこないみたいです。
以下から.vsix ファイルをダウンロードしてダブルクリックでインストールするとVS2017でも使えるようになります。
でも、VS2017が不安定になる可能性があります、と警告文が出るので自己責任でご利用ください(正式対応じゃないってことですね)。私の環境では、特に何かが変になることなく動いていました。

.NET Portability Analyzer - Visual Studio Marketplace


Visual Studio 2015を起動。
メニュー「ツール → 拡張機能と更新プログラム」を選択。

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

「オンライン → .NET Portable Analyzer」を選択してインストール

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

VSの再起動を促されます。

調査対象のライブラリプロジェクトを開く

調査対象のライブラリプロジェクトをVisual Studioで開きます。
今回は以下のような「OmnipotentLibrary」というライブラリプロジェクトを調査対象としました。

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

プロジェクトに含まれるMyLogger.csは以下のような、Reflectionを使ったキビシイ実装です。

// MyLogger.cs
using System;
using System.Security;
using System.Reflection;
using System.Diagnostics;

namespace OmnipotentLibrary
{
  public class MyLogger
  {
    /// <summary>
    /// メソッドの開始時に呼び出します。
    /// </summary>
    /// <returns></returns>
    [DynamicSecurityMethod]
    public static DateTime Start()
    {
      DateTime now = DateTime.Now;

      const int callerFrameIndex = 1;
      StackFrame callerFrame = new StackFrame(callerFrameIndex);
      MethodBase callerMethod = callerFrame.GetMethod();
      Debug.WriteLine(string.Format("start {0}()", callerMethod.Name));

      return now;
    }

    /// <summary>
    /// メソッドの終了時に呼び出します。
    /// </summary>
    /// <param name="start"></param>
    [DynamicSecurityMethod]
    public static void End(DateTime start)
    {
      DateTime now = DateTime.Now;

      TimeSpan ts = now - start;

      const int callerFrameIndex = 1;
      StackFrame callerFrame = new StackFrame(callerFrameIndex);
      MethodBase callerMethod = callerFrame.GetMethod();
      Debug.WriteLine(string.Format("end {0}(): {1}ms", callerMethod.Name, ts.TotalMilliseconds));
    }
  }
}

namespace System.Security
{
  [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
  internal sealed class DynamicSecurityMethodAttribute : Attribute
  {
  }
}

Portability Analyzerの設定を行う

ソリューションエクスプローラでプロジェクトを選択、マウス右ボタンクリック、メニュー「Prtable Analyzer Settings」を選択します。

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

表示された以下のウィンドウで「Target Platforms」を選択します。
「Target Platforms」とは、アナライズ対象のプラットフォームです。調査対象のプロジェクト(OmnipotentLibrary)が、「対象のプラットフォームに準拠しているか?」調べる対象になります。

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

ここでは、.NET Core 1.0 / 1.1 / 2.0、.NET Framework 4.5 / 4.5.1 / 4.5.2 / 4.6 / 4.6.1 / 4.6.2、.NET Standard 1.0 / 1.1 / 1.2 / 1.3 / 1.4 / 1.5 / 1.6 / 2.0、Xamarin Android 1.0.0、Xamarin.iOS 1.0.0.0に準拠しているか?を、アナライズする設定としました。

Portability Analyzerを実行する

ソリューションエクスプローラでプロジェクトを選択、マウス右ボタンクリック、メニュー「Analyzer Azzembly Portability」を選択します。

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

アナライズが完了すると、「Portability Analyze Results」ウィンドウに結果が表示されます。

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

「Open Report」をクリックすると結果のExcelファイルが起動されます。

結果を分析する

アナライズ結果のExcelは、2シート構成です。

1シート目は結果のサマリーになっています。
横長なので折り返し加工していますが、以下のような感じです。

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

表のヘッダ(青い部分)が「ターゲットプラットフォーム」、その下の数値が「分析結果のスコア」です。
スコア100(緑)が準拠OK、スコア100未満(オレンジ)が準拠NG、となります。
このライブラリはReflection系を使用しているので、.NET Core 1.0 / 1.1 や .NET Standard 1.xなどでは結果NGとなりました。

2つ目のシートには、スコアが100未満であった原因の詳細がリストされています。

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

T:System.Diagnostics.StackFrame とか T:System.Runtime.InteropServices.GuidAttributeを使ってるのが悪いよー、Supported: 2.0+じゃないと使えないよー、っていう結果が分かります。

まとめ

ということで

Let’s head to .NET Standard!

Introducing .NET Standard | .NET Blog

docs.microsoft.com

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