C# 7 の Tuple は戻り値が複数の時に便利

2017/3/11、「Visual Studio 2017 リリース記念勉強会 & まどすた #2」に参加してきました。
1コマ目のセッションとして、岩永 信之(@ufcpp)さんの「C# 7」セッションを聞かせていただきました。
C# 7の新機能について非常に細かく説明していただきましたが、その中で Tuple についてフォーカスしてブログっておきます。

※以下は、セッションでの解説内容ではなく、それを受けて私が調査した内容になります。

Tuple は戻り値が複数の時に便利

ということで、C# 7 で使い勝手がよくなった Tuple ですが、複数の戻り値があるメソッドで使うのが最も美しい使い方だと思いました。
(Tupleは乱用すると見通しの悪い、属人 巧 のコードが生成される可能性があると思っているので)

では、以下のCompanyクラスをベースに説明を進めます。

// 会社クラス
public class Company
{
  ... 正社員数とパートナー社員数を取得するメソッドを追加していくよ!
}

2つの戻り値を持つ同期メソッドの場合

Companyクラスに以下のメソッドを追加します。

[リスト1]
// Companyクラスに追加するメソッド
// outパラメータを使用して2つの戻り値を返す同期メソッド
static public void GetEmployeeCountSync(
  out int properCount, 
  out int partnerCount)
{
  properCount = 100;
  partnerCount = 30;
}
// リスト1の呼び出し側
Company.GetEmployeeCountSync(out int properCount, out int partnerCount);
Console.WriteLine("proper:{0}  partner:{1}", properCount, partnerCount);

// 出力結果:proper:100  partner:30

2つの戻り値があるので、outパラメータを使うというよくある実装です。
※呼び出し側実装における、outパラメータを事前宣言せず、メソッド呼び出し時に宣言する手法はC# 7の新機能です。

次にリスト1を Tuple戻り値 を使って書き換えます。

[リスト2]
// Companyクラスに追加するメソッド
// Tupleを使用して2つの戻り値を返す同期メソッド
static public (int properCount, int partnerCount) GetEmployeeCountSyncTuple()
{
  int properCount = 100;
  int partnerCount = 30;

  return (properCount, partnerCount);
}
// リスト2の呼び出し側
(int properCount, int partnerCount) = Company.GetEmployeeCountSyncTuple();
Console.WriteLine("proper:{0}  partner:{1}", properCount, partnerCount);
// 出力結果:proper:100  partner:30

または、以下のように呼び出すこともできます。

// リスト2の呼び出し側
var result = Company.GetEmployeeCountSyncTuple();
Console.WriteLine("proper:{0}  partner:{1}", result.properCount, result.partnerCount);
// 出力結果:proper:100  partner:30

戻り値の Tupleオブジェクト を分割して各々の変数に受け取るか、まとめて受け取るかの違いになります。

2つの戻り値を持つ非同期メソッドの場合

この非同期メソッドのケースで Tuple戻り値 の威力がより発揮されると思います。
非同期メソッド、つまり asyncキーワードの付いたメソッドでは、「引数に out / ref キーワードをつけることができません」。これは言語仕様としてそのようになっています。

[リスト3]
// これはコンパイルエラー
static public async Task GetEmployeeCountAsync(
  out int properCount,  // asyncなのにoutはNG
  out int partnerCount)
{
  properCount = 100;
  partnerCount = 30;

  return;
}

その為、複数の戻り値を持つ必要がある場合、それらを集約したクラスを定義する必要があります。
例えば以下のような感じ。

[リスト4]
public class CountResult
{
  public int ProperCount { get; set; }
  public int PartnerCount { get; set; }
}
...
static public async Task<CountResult> GetEmployeeCountAsync()
{
  CountResult result = new CountResult() { properCount = 100, partnerCount = 30 };
  // ...
  return result;
}

しかし、Tupleを利用すれば同期メソッドと同様の実装方式をとることができます。

[リスト5]
// Companyクラスに追加するメソッド
// Tupleを使用して2つの戻り値を返す非同期メソッド
static public async Task<(int properCount, int partnerCount)> GetEmployeeCountAsyncTuple()
{
  int properCount = 100;
  int partnerCount = 30;

  await Task.Delay(1000);

  return (properCount, partnerCount);
}
// リスト5の呼び出し側
(int properCount, int partnerCount) = Company.GetEmployeeCountAsyncTuple().Result;
Console.WriteLine("proper:{0}  partner:{1}", properCount, partnerCount);

// 出力結果:proper:100  partner:30

戻り値がオブジェクト型の場合

前述の例では戻り値の型がリテラル型でした。
オブジェクト型の場合は、非同期メソッドにおいても引数渡しで結果を受け取ることが出来ます(C# 7に関係なく従来の言語仕様的に)。
その部分の振る舞いを見てみたいと思います。

[リスト6]
static public async Task GetEmployeesAsync(
  List<Employee> properEmployees, 
  List<Employee> partnerEmployees)
{
  for (int i = 0; i < 10; i++)
  { // 1秒毎にEmployeeを追加
    await Task.Delay(1000);
    properEmployees.Add(
      new Employee() { Name = "proper" + i.ToString() });
  }

  for (int i = 0; i < 5; i++)
  { // 1秒毎にEmployeeを追加
    await Task.Delay(1000);
    partnerEmployees.Add(
      new Employee() { Name = "partner" + i.ToString() });
  }

  return;
}
// リスト6の呼び出し側
List<Employee> properEmployees = new List<Employee>();
List<Employee> partnerEmployees = new List<Employee>();
var task = Company.GetEmployeesAsync(properEmployees, partnerEmployees);
while (true)
{
  Console.WriteLine("{0} proper:{1}  partner{2}", 
    DateTime.Now.ToString("H:m:s"), 
    properEmployees.Count, 
    partnerEmployees.Count);

  System.Threading.Thread.Sleep(1000);

  if (task.IsCompleted) // Taskの終了を確認
    break;
}

// 出力結果:proper:100  partner:30
17:21:5 proper:0  partner0
17:21:6 proper:1  partner0
17:21:7 proper:2  partner0
17:21:8 proper:3  partner0
17:21:9 proper:4  partner0
17:21:10 proper:5  partner0
17:21:11 proper:6  partner0
17:21:12 proper:7  partner0
17:21:13 proper:8  partner0
17:21:14 proper:9  partner0
17:21:15 proper:10  partner0
17:21:16 proper:10  partner1
17:21:17 proper:10  partner2
17:21:18 proper:10  partner3
17:21:19 proper:10  partner4

非同期メソッド GetEmployeesAsync() を呼出し後、whileループで引き渡したEmployeeオブジェクトを監視&コンソール出力しています。
properEmployees.Count / partnerEmployees.Countが順次カウントアップされているのが確認できます。
GetEmployeesAsync()は1秒毎にEmployeeオブジェクトを追加し、呼び出し元は1秒毎にEmployeeリストオブジェクトの内容を確認しています。
つまり、同一のオブジェクトを別スレッドで互いに参照している状態になっています。これについては、プログラム上のデッドロック等が発生しないように考慮が必要なケースがあるでしょう。

リスト6の実装をTuple戻り値版に書き換えたものが以下になります。

[リスト7]
// リスト6をTuple戻り値版に書き換え
static public async Task<(List<Employee> properEmployees, List<Employee> partnerEmployees)> GetEmployeesAsyncTuple()
{
  List<Employee> properEmployees = new List<Employee>();
  List< Employee > partnerEmployees = new List<Employee>();

  for (int i = 0; i < 10; i++)
  {
    await Task.Delay(1000);
    properEmployees.Add(new Employee() { Name = "proper" + i.ToString() });
  }

  for (int i = 0; i < 5; i++)
  {
    await Task.Delay(1000);
    partnerEmployees.Add(new Employee() { Name = "partner" + i.ToString() });
  }

  return (properEmployees , partnerEmployees);
}
// リスト7の呼び出し側
var (properEmployees, partnerEmployees) = Company.GetEmployeesAsyncTuple().Result;

もしくは

// リスト7の呼び出し側
var task = Company.GetEmployeesAsyncTuple();
(properEmployees, partnerEmployees) = task.Result;

いずれにしても、2つの戻り値(結果)オブジェクトは、GetEmployeesAsyncTuple()非同期メソッド処理が完了してから得られる(処理中には対象オブジェクトへの参照が得られない)為、対象オブジェクトへのスレッドセーフが保証されます。

所感

本投稿の Tuple に限らず、C# 7は「言語仕様としての成熟期に入った」という印象です。
内部関数だったり、「=>」の強化による短縮記述だったり、より洗練された簡潔なコードの記述が可能になっています。
と、同時に思うことは、開発メンバーが非精鋭なチームの場合、洗練された簡潔なコードよりも「冗長だけど昔の技術知識で読み解けるコード」っていうのも重宝されるのかなぁ・・・なんていうネガティブな思いです(笑)。
でも、まあ、LINQなんかもそうだけど、しばらく時が経てば標準的に使われるようになるのかな、とも思います。

参考URL

MS公式 C# 7 の新機能紹介は以下
blogs.msdn.microsoft.com

岩永さんの C# 7 解説記事は以下
ufcpp.net

「2017/3/11 Visual Studio 2017 リリース記念勉強会」岩永さんセッションのスライドは以下
docs.com

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」には魅力的なコントロールが沢山あるので、これからも「自ら学びながら」この場で紹介していけたらと思います。