Xamarin Formsのデータバインディングを整理する(2)

前回の続き・・・

前回の投稿では、Xamarin Formsにおけるデータバインディングの基本(?)について説明しました。それらは単一コントロール(ターゲット)へのデータバインディングでした。

ryuichi111std.hatenablog.com

今回は「コレクションデータ」(つまり繰り返し要素)のデータバインディングについて説明したいと思います。

コレクションデータのデータバインディング(ListView)

「コレクションデータのデータバインディング」という表現をすると、堅苦しく分かりにくいのですが、要するに具体的なコントロールとしては「ListViewコントロール」です。
つまり一覧表示形式です。
一覧表示を行う、またその一覧からユーザーに選択させる、といったUIはよく利用されるものであると思います。

ListViewコントロールを使う

では早速、ListViewコントロールをとりあえず使ってみたいと思います。
比較的シンプルな使い方のサンプルを紹介します。
で、その後で、個人的に気になる部分を掘り下げて探索していこうと思います。
以下のような画面のアプリを作成します。

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

フルーツクラス(Fruit)オブジェクトのコレクション(配列)を「ソース」とし、ListViewにデータバインディングして、名称(Name)とカロリー(Calory)をリスト表示します。

1.Fruitクラスの作成

データバインディングのソースとなるFruitクラスの実装は以下の通りです。
単純に 名称プロパティ(Nameプロパティ)とカロリープロパティ(Caloryプロパティ)のみを持つモデルクラスです。

[リスト1] fruit.cs
namespace FromExample1
{
  // フルーツクラス
  public class Fruit
  {
    // 名称を取得または設定します。
    public string Name { get; set; }

    // 100gあたりのカロリー数を取得または設定します。
    public int Calory { get; set; }
  }
}

2.フォームへのListViewの配置

ListViewコントロールを配置した、フォーム定義はの通りです。

[リスト2] FormExample1Form.xasml
<?xml version="1.0" encoding="utf-8"?>
<ContentPage 
  xmlns="http://xamarin.com/schemas/2014/forms" 
  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
  x:Class="FromExample1.FromExample1Page">
  <StackLayout Margin="0,20,0,0">
    <ListView x:Name="list1">
      <ListView.ItemTemplate>
        <DataTemplate>
          <ViewCell>
            <StackLayout>
              <Label Text="{Binding Path=Name}" />
              <Label Text="{Binding Path=Calory, 
                            StringFormat='{0} kcal/100g'}" 
                     Margin="30,0,0,0"/>
            </StackLayout>
          </ViewCell>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>
  </StackLayout>
</ContentPage>
  • ListView要素
    単純にStackLayoutの子要素として ListViewコントロール を配置しています。

  • <ListView.ItemTemplate>
    ListViewでは「データバインドされたソースコレクションオブジェクトの各要素を、繰り返し表示するためのアイテムテンプレート」を定義することが出来ます。これが ItemTemplateプロパティ になります。
    ItemTemplateは Xamarin.Forms.DataTemplateクラス型です。

  • DataTemplate
    配下に、リスト表示する各要素の具体的なビュー表現を定義します。
    サンプルでは「ViewCellクラス要素」を使用しています。ListViewコントロールのテンプレート定義には「Xamarin.Forms.Cell派生クラス」を使用する必要があります。

  • Binding構文
    ListViewアイテムテンプレートに対して2つのデータバインディングを指定しています。
    それぞれ Labelコントロール に対するものですが、1つ目は単純に「Nameプロパティ」(つまりFruit.Name)へのバインディングです。 2つ目は「Caloryプロパティ」(つまりFruit.Calory)へのバインディングです。加えて、StringFormat設定を指定しています。フォーマットの指定方式は、C#一般の {0} というプレースホルダを使用することができます。

【補足(+αなメモ)】
  • プロパティ構文
    Xaml構文の話になりますが、ListView要素配下の <ListView.ItemTemplate> は、「プロパティ構文」と呼ばれる記述法であり、「ListViewクラスのItemTemplateプロパティを、自分の子要素で定義する」という意味になります。
    ListView.ItemTemplateプロパティに設定する具象値は、直下の要素「DataTemplate要素オブジェクト」になります。

  • コンテンツプロパティ
    ViewCell要素直下にLabel要素が記述されています。これはXaml構文としては、「コンテンツプロパティ構文」と呼ばれるものになります。
    Xamarinヘルプで ViewCellクラス を確認すると、以下のようにクラス属性として「ContentProperty」が定義されています。Xaml定義上、明示的なプロパティ名を指定しなかった場合に、ContentProperty属性で指定したプロパティへの値設定と認識されます。

[Xamarin.Forms.ContentProperty("View")]
public class ViewCell : Cell

つまり、ViewCellクラス要素をXamlで定義する場合、以下の2つの定義は同じ意味を持ちます。

[リスト3] 
〜プロパティ名を暗黙的に指定〜
<ViewCell>
  <Label Text="{Binding Path=Name}" />
</ViewCell>

↑↓↑↓↑↓ 上と下は同意 ↑↓↑↓↑↓

〜プロパティ名を明示的に指定〜
<ViewCell>
  <ViewCell.View>
    <Label Text="{Binding Path=Name}" />
  </ViewCell.View>
</ViewCell>
  • BindinbgにおけるPath
    データバインディング構文「{Binding}」の中の Path= の記述は省略することが可能です。
    以下の2つの記述は同じ意味を持ちます。
    どちらの構文を使用するかは各プロジェクトで取り決めておくと良いでしょう。
[リスト4] 
<Label Text="{Binding Path=Name}" />

↑↓↑↓↑↓ 上と下は同意 ↑↓↑↓↑↓

<Label Text="{Binding Name}" />

3.データソースの作成とListViewへの割り当て

フォームのコードビハインドクラスでデータソースコレクションオブジェクトを作成し、ListViewコントロールバインドします。

[リスト5] FormExample1Form.xasml.cs
using System.Collections.Generic;
using Xamarin.Forms;

namespace FromExample1
{
  public partial class FromExample1Page : ContentPage
  {
    private List<Fruit> fruits;

    public FromExample1Page()
    {
      InitializeComponent();

      // ListViewにデータバインド
      this.InitializeDataSource();
      this.list1.ItemsSource = this.fruits;
    }

    // データソースオブジェクトを初期化します。
    private void InitializeDataSource()
    {
      this.fruits = new List<Fruit>();

      this.fruits.Add(new Fruit() { Name = "Apple", Calory = 54 }); 
      this.fruits.Add(new Fruit() { Name = "Strawberry", Calory = 34 });
      this.fruits.Add(new Fruit() { Name = "Peach", Calory = 40 });
      this.fruits.Add(new Fruit() { Name = "Pear", Calory = 43 });
      this.fruits.Add(new Fruit() { Name = "Grape", Calory = 59 });
    }
  }
}

ListViewコントロールの「ItemsSourceプロパティ」に対して、データバインディングを行う為のソースオブジェクトを設定します。
コレクション型データバインディングである為、ItemsSourceプロパティの型は「System.Collection.IEnumerable型」になります。

【補足(+αなメモ)】
  • 各コレクション要素の BindingContext
    サンプルにも示したように、ListViewは ItemsSourceプロパティ にデータソースオブジェクトを設定することで、各アイテム要素に対するデータバインディングが行われます。
    Labelコントロール等に対する単一データバインディングを行う場合、ターゲットとなる各コントロール要素の BindingContextプロパティ にソースオブジェクトを設定することで、データバインディングを実現しました。
    ではListViewにおいて、各アイテム要素へのデータバインディングはどのようになっているのでしょうか?
    ということで、ちょっと探索を・・・
    シンプルにListViewに対するデータバインディングを実装した後、デバッグ実行を行い、適当なところでブレークポイントを貼ります(Buttonを配置してClickイベントをハンドルした辺りとか)。で、ウォッチを使って「list -> TemplatedItems[0]」の値を確認します。サンプルでは ViewCell型であり、その BindingContextプロパティ には Fruitオブジェクト が設定されていることが分かります。

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

各繰り返し要素のコントロールに対して、ListView内部でコレクションデータ内の該当するデータ項目が BindingContext に設定されているということであり、データバインディングの基本概念である「ターゲットのDataContextプロパティに、ソースオブジェクトを設定する」の動作が行われています。

以上が、最も基本的なListViewコントロールの使用方法です。

ListView と ItemsView< T > と Cell

前述のサンプルでは、コレクションの各要素を表示する為の DataTemplate定義 に「< ViewCell >」を使用しました。ViewCellの具体的な実装は「Xamarin.Forms.ViewCellクラス」になります。
ListViewコントロールでは、DataTemplate定義に「Xamarin.Forms.Cellクラスの派生クラス」を使用する必要があります(ViewCellクラスは、Cellクラスの派生クラスです)。これは、ListViewコントロールの仕様になります。また、Cellクラス自体は抽象クラスなので、そのまま利用することはできません。

なぜ Xamarin.Forms.Cell 派生クラスを指定する必要があるのか?

ビルドインのコントロールを使用する上では、まあ、そういう仕様だから、ということなのですが。
少しだけXamarin Formsの内部仕様に入り込みたいと思います。
ListViewコントロールクラスは、Xamarinヘルプを見れば分かる通り、ItemsView< T >クラスの派生クラスです。
具体的な定義は以下になります。

public class ListView : ItemsSource< Cell >

また、ItemsViewクラスは「テンプレートリストアイテムを含むビューの抽象基本クラス」です。
< T >は、データバインドされたソース要素を表示する為の型になります。
つまり、ListViewコントロールクラスは「ソースアイテムを表示するビジュアル要素として Cellクラス を使う仕様とした」ということです。また、Cellクラスは Elementクラス の派生クラスであり、Elementクラスはすべてのフォーム要素の基本クラスとなっているクラスになります。

Built-inのCell派生クラス達

ListViewコントロールの各アイテム要素表示には「Cell派生クラス」が利用できることが分かりました。
先程のサンプルでは ViewCellクラス を使用しましたが、これ以外に以下のビルドインCell派生クラスを使用することができます。

  • TextCell
  • ImageCell

※ ViewCellは、Viewクラス をコンテントプロパティとする、開発者が自由なレイアウトを構成する為のCellになります。
※ 開発者が定義したカスタムCellも利用可能です。

TextCellの使用方法

TextCellクラスは、リストアイテムを「Textプロパティ(表示テキスト)」と「Detailプロパティ(詳細テキスト)」によって表示します。
以下がXaml定義の例です。(データソースは前述のサンプルと同様にFruitコレクションを想定しています)

[リスト6]  TextCellの定義(xaml)
<ListView x:Name="list1">
  <ListView.ItemTemplate>
    <DataTemplate>
      <TextCell
            Text="{Binding Path=Name}" 
            Detail="{Binding Path=Calory, StringFormat='{0}kcal/100g'}"
            TextColor="Red"
            DetailColor="Green" />
    </DataTemplate>
  </ListView.ItemTemplate>
</ListView>

Text(string) / Detail(string) / TextColor(Color) / DetailColor(Color) の各プロパティを指定することができます(それぞれの意味は省略・・・)。
1つ特徴として「TextCellは実行時にはネイティブコントロールを使用する為、パフォーマンスに優れている」という点があります(以下Xamarinヘルプより引用)。

TextCells are rendered as native controls at runtime, so performance is very good compared to a custom ViewCell.

そして、実行画面は以下。

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

ImageCellの使用方法

ImageCellクラスは、TextCellに加えて画像を表示する機能を持ちます。
早速サンプルですが、画像を表示するために Fruitクラス に「ImageSource型のImageプロパティ」を追加します。

[リスト7] Fruit.cs(Imageプロパティを追加)
using Xamarin.Forms;

namespace FromExample1
{
  public class Fruit
  {
    public string Name { get; set; }

    public int Calory { get; set; }
    // 画像プロパティを追加
    public ImageSource Image { get; set; }
  }
}

Xamlの定義は以下の通りです。先程のTextCellの使用とほぼ同様です。
テンプレート定義にImageCell要素を使用し、ImageSourceプロパティにはImageプロパティ(ソースオブジェクトである Fruitオブジェクト の Imageプロパティ)をバインド定義します。

[リスト8] ImageCellを使用する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" 
  x:Class="FromExample1.UseImageCellPage">
  <StackLayout Margin="0,20,0,0">
    <ListView x:Name="list1">
      <ListView.ItemTemplate>
        <DataTemplate>
          <ImageCell Text="{Binding Path=Name}" 
                 Detail="{Binding Path=Calory, StringFormat='{0}kcal/100g'}"
                 TextColor="Red"
                 DetailColor="Green"
                 ImageSource="{Binding Path=Image}"/>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>
  </StackLayout>
</ContentPage>

コードビハインドクラスの定義は以下の通りです。
画像ファイル(.png)は、Xamarin Formsプロジェクトにリソースとして登録しています。

[リスト9] 
using System;
using System.Collections.Generic;

using Xamarin.Forms;

namespace FromExample1
{
  public partial class UseImageCellPage : ContentPage
  {
    private List<Fruit> fruits;

    public UseImageCellPage()
    {
      InitializeComponent();

      this.InitializeDataSource();
      this.list1.ItemsSource = this.fruits;
    }

    private void InitializeDataSource()
    {
      this.fruits = new List<Fruit>();


      this.fruits.Add(new Fruit() { 
        Name = "Apple", 
        Calory = 54, 
        Image = ImageSource.FromResource("FromExample1.Resources.apple.png") }); 
      this.fruits.Add(new Fruit() { 
        Name = "Strawberry", 
        Calory = 34,
        Image = ImageSource.FromResource("FromExample1.Resources.strawberry.png")});
      this.fruits.Add(new Fruit() { 
        Name = "Peach", 
        Calory = 40,
        Image = ImageSource.FromResource("FromExample1.Resources.peach.png")
      });
      this.fruits.Add(new Fruit() {
        Name = "Pear",
        Calory = 43,
        Image = ImageSource.FromResource("FromExample1.Resources.pear.png")
      });
      this.fruits.Add(new Fruit() { 
        Name = "Grape", 
        Calory = 59,
        Image = ImageSource.FromResource("FromExample1.Resources.grape.png")
      });
    }
  }
}
画像リソース

画像リソースについて若干の補足をしておきます。
上記サンプルでは画像(png形式)をプロジェクト内に「リソース」として埋め込み登録して利用しています。
つまりビルドにより生成されたアセンブリ(dll)に画像データがリソースとして埋め込んでいます。
サンプルのソリューション構成のキャプチャは以下の通りです。
Xamarin Formsプロジェクトに「Resources」フォルダを作成し、各画像ファイルを追加しています。

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

また追加した各画像ファイルのビルドアクションは「EmbeddedResource」を指定しています。

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

上図プロパティウィンドウは、ソリューションツリーから対象pngファイルを選択し、マウス右ボタンメニュー「プロパティ」で表示されます(Xamarin StudioでもVisual Studio For Macでもほぼ同様のUI)。
プロパティウィンドウの中に「リソースID」という項目があります。このIDによりプログラムから対象リソースを読み込むことができます。
つまり、以下のコードで「リソースIDが FromExample1.Resources.apple.png のリソースをImageSourceとして読み込む」ことができるのです。

[リスト10] 
ImageSource.FromResource("FromExample1.Resources.apple.png")

で、ImageCellを使用したサンプルの実行画面は以下です。

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

  • SwitchCell と EntryCell
    あと「SwitchCell」と「EntryCell」というCell派生クラスがあるのですが、これはTableView用に用意されたものなのでここでは省略。

コードでテンプレートを定義する

ここまでXaml構文を利用してListViewへのデータバインディング・テンプレート定義を行なってきましたが、これらは全て コードで実装することができます。
一般的にはXAMLで定義可能な箇所は、あえてコードではなくXAMLで定義することの方が、可読性なメンテナンス性が高いと思われます。
リスト2 / 5 をコードで実装すると以下のようになります。

[リスト11]
using System.Collections.Generic;
using Xamarin.Forms;

namespace FromExample1
{
  public partial class FromExample1Page : ContentPage
  {
    private List<Fruit> fruits;

    public FromExample1Page()
    {
      InitializeComponent();

      // テンプレートを初期化
      this.InitializeTemplate();

      // ListViewにデータバインド
      this.InitializeDataSource();
      this.list1.ItemsSource = this.fruits;
    }

    // テンプレートを初期化します。
    private void InitializeTemplate()
    {
      DataTemplate dataTemplate = new DataTemplate(() => {
        StackLayout stackLayout = new StackLayout();
          
        Label nameLabel = new Label();
        nameLabel.SetBinding(
          Label.TextProperty, 
          "Name", 
          BindingMode.OneWay, 
          null, 
          null);
        Label caloryLabel = new Label() { Margin = new Thickness(30, 0, 0, 0) };
        caloryLabel.SetBinding(
          Label.TextProperty, 
          "Calory", 
          BindingMode.OneWay, 
          null, 
          "{0}kcal/100g");
        stackLayout.Children.Add(nameLabel);
        stackLayout.Children.Add(caloryLabel);

        return new ViewCell() { View = stackLayout };
      });
      this.list1.ItemTemplate = dataTemplate;
    }

    // データソースオブジェクトを初期化します。
    private void InitializeDataSource()
    {
      this.fruits = new List<Fruit>();

      this.fruits.Add(new Fruit() { Name = "Apple", Calory = 54 }); 
      this.fruits.Add(new Fruit() { Name = "Strawberry", Calory = 34 });
      this.fruits.Add(new Fruit() { Name = "Peach", Calory = 40 });
      this.fruits.Add(new Fruit() { Name = "Pear", Calory = 43 });
      this.fruits.Add(new Fruit() { Name = "Grape", Calory = 59 });
    }
  }
}

DataTemplateをインスタンス化する際のコンストラクタ引数に「テンプレートを作成する為の Func< object >匿名関数」を渡します。
DataTemplateコンストラクタ引数Func< T >自体のジェネリック型は object ですが、ListViewが扱える 項目表示要素は Cell派生クラス である為、これに従います(Cell派生クラス以外をreturnするとコンパイルは通りが実行時エラーとなります)。

まとめ

ListViewはよく使われるメジャーなUIなのですが、こうしてまとめようと思うと何気に多くの技術要素が詰まっているように思いました。
HeaderTemplate とか FooterTemplate についても本投稿では触れていませんし、アイテム選択時の「ItemSelectedイベント」についても触れていません。
同様の記事はたくさんあると思いますが、これを読んで頂いた皆さんにとって、1つでもヒントになるようなことがあれば幸いかと。