Xamarin Formsのデータバインディングを整理する(2)
前回の続き・・・
前回の投稿では、Xamarin Formsにおけるデータバインディングの基本(?)について説明しました。それらは単一コントロール(ターゲット)へのデータバインディングでした。
今回は「コレクションデータ」(つまり繰り返し要素)のデータバインディングについて説明したいと思います。
コレクションデータのデータバインディング(ListView)
「コレクションデータのデータバインディング」という表現をすると、堅苦しく分かりにくいのですが、要するに具体的なコントロールとしては「ListViewコントロール」です。
つまり一覧表示形式です。
一覧表示を行う、またその一覧からユーザーに選択させる、といったUIはよく利用されるものであると思います。
ListViewコントロールを使う
では早速、ListViewコントロールをとりあえず使ってみたいと思います。
比較的シンプルな使い方のサンプルを紹介します。
で、その後で、個人的に気になる部分を掘り下げて探索していこうと思います。
以下のような画面のアプリを作成します。
フルーツクラス(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オブジェクト が設定されていることが分かります。
各繰り返し要素のコントロールに対して、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.
そして、実行画面は以下。
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」フォルダを作成し、各画像ファイルを追加しています。
また追加した各画像ファイルのビルドアクションは「EmbeddedResource」を指定しています。
上図プロパティウィンドウは、ソリューションツリーから対象pngファイルを選択し、マウス右ボタンメニュー「プロパティ」で表示されます(Xamarin StudioでもVisual Studio For Macでもほぼ同様のUI)。
プロパティウィンドウの中に「リソースID」という項目があります。このIDによりプログラムから対象リソースを読み込むことができます。
つまり、以下のコードで「リソースIDが FromExample1.Resources.apple.png のリソースをImageSourceとして読み込む」ことができるのです。
[リスト10] ImageSource.FromResource("FromExample1.Resources.apple.png")
で、ImageCellを使用したサンプルの実行画面は以下です。
- 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つでもヒントになるようなことがあれば幸いかと。
Xamarin Formsのデータバインディングを整理する(1)
昨今の開発言語やフレームワークにおいて「データバインディング」は非常に重要で有益な技術となっています。
Xamarin Formsにおいても データバインディング がサポートされており、これは WPF / Silverlight などから引き継がれた技術になります(といっても、詳細についてはWPF / Silverlight / Xamarinでは実装内容・実装レベルが異なりますが・・・WPFではDependencyPropertyであったものが Xamarin FormsではBindablePropertyであったり・・・)。
データバインディングは、Xamarin Formsアプリを開発するすべてのエンジニアが利用する技術だと思います。
そして、手軽に利用することもできるし、結構深い技術であるとも思っています。
自分自身の知識の整理も含めて、これから何回かに渡って「Xamarin Formsのデータバインディング」について書いていこうと思います。
※あくまでブログであり、書籍とかじゃないから体系的なまとめはあまり気にしないで、基本+自分の興味の赴くままの技術探求をしていこうかと思います。
データバインディングの基本(ソース と ターゲット)
概念として、データバインディングにおける「元データのオブジェクト側」=「ソース」、「データバインドされる側」 = 「ターゲット」と呼びます。
データバインディングを行う上では、「ソースオブジェクト」と「ターゲットオブジェクト」の2つを結びつける必要があります。その具体的な方法は以下になります。
- ターゲットオブジェクトの「BindingContextプロパティ」に「ソースオブジェクト」を設定する
シンプルなデータバインディング例
早速、以下にシンプルなデータバインディングの例を示します。
ソースは「Personオブジェクト」、ターゲットはフォームに配置した「Labelコントロール」です。
Personは単純なPOCOです。
PersonオブジェクトのNameプロパティの値を、LabelコントロールのTextプロパティにデータバインドします。
Personクラスの定義
ソースとなるPersonクラスの定義です。
// リスト1 person.cs namespace Example1 { public class Person { public string Name { get; set; } } }
フォームの定義
ターゲットとするLabelコントロールを配置したフォームの定義です。
// リスト2 Example1Page.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="Example1.Example1Page"> <StackLayout Margin="0,20,0,0"> <Label x:Name="Label1" Text="{Binding Path=Name}" /> </StackLayout> </ContentPage>
LabelコントロールのTextプロパティに対してデータバインドを行なっています。
「 {Binding Path=Name} 」がXAMLにおけるバインディング構文になります。
ターゲットオブジェクトに対するソースオブジェクトの設定は、この後に紹介するフォームのコードビハインドクラス上で行なっています。
ここでは、以下の2点を {Bindng} 構文によって定義しています。
- ターゲットオブジェクトであるLabelの「Textプロパティ」に対してデータバインドを行う
- Path=Nameによって、ソースオブジェクトのNameプロパティ値をバインドすることとする(つまりPersonオブジェクトのNameプロパティ値)
ソースとターゲットの結び付け定義
ソースとなるPersonオブジェクトを作成して、ターゲットとなるLabelコントロールのBindingContextプロパティに設定します。
// リスト3 Example1Page.xaml.cs using Xamarin.Forms; namespace Example1 { public partial class Example1Page : ContentPage { private Person person; public Example1Page() { InitializeComponent(); // Personオブジェクト作成 this.person = new Person(); person.Name = "ryuichi daigo"; // ターゲットのラベルコントロールにソースを設定 this.Label1.BindingContext = person; } } }
実行結果画面は以下の通りです。
BindingContextプロパティ
前述のように「ソースとターゲットの結び付け」は、「ターゲットオブジェクトのBindingContextプロパティにソースオブジェクトを設定する」事で行います。
BindingContextプロパティをXamarinヘルプで確認すると、「Xamain.Forms.BindableObjectクラスで定義されたObject型のプロパティ
」であることが分かります。
つまり、データバインディングにおけるターゲットとなりうるオブジェクトとは、BindableObjectを継承したクラスであると言う事が出来ます。
そして、Labelなどの多くのUIコントロールクラスは Viewクラス を継承しています。さらにViewクラスから継承関係を辿ると、以下の図のようなクラス継承関係となっています。
このことから、Xamarin Formsにおける殆どのUIコントロールが、データバインディングにおける「ターゲット」となる事ができると言う事、及びその意味が分かります。
BindableProperty
ターゲットオブジェクトに対してソースオブジェクトをデータバインドする事が分かりました。
ただ、上述の例でも示したように、ターゲット自体ではなくターゲットオブジェクトのプロパティ(LabelコントロールクラスのTextプロパティ)に対して、ソースオブジェクトのプロパティ(PersonクラスのNameプロパティ)をデータバインドします。
これはごく当たり前の自然なお話ですね。
ただし、ここにも技術的仕組み上の あるルール が存在します。
ターゲットオブジェクトの中で「データバインドの対象とする事ができるプロパティ」とは、「BindableProperty」と呼ばれる特殊なプロパティである必要があります。
Labelクラスのヘルプページを参照すると、Textプロパティの項目には以下のような記述があるはずです(https://developer.xamarin.com/api/type/Xamarin.Forms.Label/)。
String. Gets or sets the text for the Label. This is a bindable property.
そしてBindablePropertyは少し特殊な定義の仕方をする必要があり、例えばstring型のTextプロパティをBindablePropertyとして定義する場合、以下のような実装を行うことになります。
// リスト4 BindablePropertyの実装方法(カスタムコントロール実装時などに使う) public static readonly BindableProperty TextProperty = BindableProperty.Create("Text", typeof(string), typeof(Example1Page), default(string), propertyChanged: (bindable, oldValue, newValue) => ((Example1Page)bindable).Text = (string)newValue); public string Text { get { return (string)GetValue(TextProperty); } set { SetValue(TextProperty, value); } }
そう、以下のような単純なプロパティ定義では、データバインド可能なプロパティにはなりません。
// リスト5 こんな定義ではデータバインド可能なプロパティとはならない・・・ public string Text { get; set; }
この辺りの詳細は、カスタムコントロールを実装する際に必要になります。
ひとまず、提供されているコントロールはこのような実装を行なっている、そしてBindablePropertyがデータバインド可能なプロパティである、と理解しておけばまずは良いのではないかと思います。
バインディングの方向
データバインディングとは「ターゲットオブジェクトとソースオブジェクト」の結び付けです。
そして、バインディングには方向の概念があります。
以下の3つの方向の方式があります。これは、Xamarin.Forms.BindingMode列挙体で定義されています(WPFと異なりOneTimeという設定がなくなっています)。
OneWay
「ソース → ターゲット」の単方向に対してデータバインディングが行われます。
プログラムロジック上でソースとなるモデルオブジェクトの値を設定・変更し、そのソース値をターゲット(UIコントロール等)にバインドする事で画面表示を切り替えるような用途が想定されます。OneWayToSource
「ターゲット → ソース」の単方向に対してデータバインディングが行われます。
UI上の入力項目(例えばEntryコントロール)をターゲットとし、画面上で入力された値をソースオブジェクトにバインディングさせるような用途が考えられます。TwoWay
「ターゲット ←→ ソース」の両方向に対してデータバインディングが行われます。
プログラムロジック上でのソースオブジェクトの変更をターゲットであるUIに反映させる、また、逆にターゲットであるUI上での値変更をソースオブジェクトに反映させる、という相互でのデータバインディングが行われます。
3つのバインディングモードを使用したサンプル
3つのバインディングモードの動作の違いを確認できるサンプルプログラムを作成します。
ソースオブジェクト
データバインディングのソースオブジェクトとして ValueModelクラス を用意します。
単純に Value1 / Value2 / Value3 というstring型のプロパティを3つ持ちます。
いくつか補足が必要な実装が含まれていますが、ソース下部で説明を行います。
// リスト6 ValueModel.cs using System; using System.ComponentModel; namespace Example2 { /// <summary> /// ソースオブジェクトとして利用するオブジェクト /// </summary> public class ValueModel : INotifyPropertyChanged { /// <summary> /// INotifyPropertyChangedインターフェイスの実装 /// </summary> public event PropertyChangedEventHandler PropertyChanged; // データを保持するフィールド private string value1; private string value2; private string value3; // Value1プロパティ public string Value1 { get { return this.value1; } set { if (this.value1 != value) { this.value1 = value; OnPropertyChanged("Value1"); } } } // Value2プロパティ public string Value2 { get { return this.value2; } set { if (this.value2 != value) { this.value2 = value; OnPropertyChanged("Value2"); } } } // Value3プロパティ public string Value3 { get { return this.value3; } set { if (this.value3 != value) { this.value3 = value; OnPropertyChanged("Value3"); } } } // プロパティ値の変更を通知します protected virtual void OnPropertyChanged(string propertyName) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } } }
上記 ValueModelクラス の実装に関して「INotifyPropertyChangedインターフェイスの実装」について補足説明を行う必要があります。
INotifyPropertyChangedインターフェイスとは、その名称の通り「プロパティの変更を通知する」インターフェイスです。
INotifyPropertyChangedインターフェイスを実装する事で、「ソースオブジェクトのプロパティ値に変更があった際に、ターゲットオブジェクトへの通知」を行う事ができます。
つまり、OneWay / TwoWayモードのデータバインディングにおいてソースオブジェクトの変更がリアルタイムにターゲットに伝達されます。
フォームコントロール(ターゲット)の定義
フォーム上に3つの Entryコントロール を配置します。それぞれ Value1へのOneWayモードバインディング / Value2へのOneWayToSourceモードバインディング / Value3へのTwoWayモードバインディング とします。
また、3つのボタンを用意し、それぞれ、クリックされたらプログラム側で Value1 / Value2 / VAlue3 の値を変更します。各ソース値の変更が、ターゲット(Entryコントロール)に反映される様子を確認します。
もう1つ、「show data」ボタンを配置します。これはクリックすると、Value1 / Value2 / Value3 の値をアラートダイアログで表示します。これにより、ターゲット(Entryコントロール)からソースの値への変更の反映を確認します。
// リスト7 Example2Page.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="Example2.Example2Page"> <StackLayout x:Name="stack1" Margin="0,20,0,0"> <Label Text="OneWay(ソース→ターゲット)" /> <Entry x:Name="entry1" Text="{Binding Path=Value1,Mode=OneWay}"/> <Label Text="OneWayToSource(ターゲット→ソース)" /> <Entry x:Name="entry2" Text="{Binding Path=Value2,Mode=OneWayToSource}"/> <Label Text="TwoWay(ソース←→ターゲット)" /> <Entry x:Name="entry3" Text="{Binding Path=Value3,Mode=TwoWay}"/> <Button Clicked="onClickedModifyValue1" Text="ソースのValue1値を変更"/> <Button Clicked="onClickedModifyValue2" Text="ソースのValue2値を変更"/> <Button Clicked="onClickedModifyValue3" Text="ソースのValue3値を変更"/> <Button Clicked="onClickedShowData" Text="show data"/> </StackLayout> </ContentPage>
以下がフォームのコードビハインドクラスになります。
ソースオブジェクトは適当な値で生成しています。
stack1(StackLayoutコントロール)の BindingContextプロパティ にソースオブジェクトを設定しています。
実際にデータバインディングを行うのは「StackLayoutの子コントロールである 3つのEntryコントロール」です。しかし、親であるStackLayoutコントロールにソースオブジェクトを設定しています。これは「BindingContextによるデータソースのバインド情報は、子要素に継承される」というデータバインディングの特徴を生かしています。
// リスト8 Example2Page.xaml.cs using Xamarin.Forms; namespace Example2 { public partial class Example2Page : ContentPage { // ソースオブジェクト private ValueModel valueModel; public Example2Page() { InitializeComponent(); // ソースオブジェクト(Valuemodel)を生成します this.valueModel = new ValueModel(){ Value1 = "default value 1", Value2 = "default value 2", Value3 = "default value 3" }; // データバインディングのターゲットオブジェクトとソースオブジェクトを紐付けます this.stack1.BindingContext = this.valueModel; } public void onClickedModifyValue1(object sender, System.EventArgs e) { // Value1データ値をプログラムロジックで変更します this.valueModel.Value1 = "Modify1:" + System.DateTime.Now.ToString("yyyy/MM/dd"); } public void onClickedModifyValue2(object sender, System.EventArgs e) { // Value2データ値をプログラムロジックで変更します this.valueModel.Value2 = "Modify2:" + System.DateTime.Now.ToString("yyyy/MM/dd"); } public void onClickedModifyValue3(object sender, System.EventArgs e) { // Value3データ値をプログラムロジックで変更します this.valueModel.Value3 = "Modify3:" + System.DateTime.Now.ToString("yyyy/MM/dd"); } public async void onClickedShowData(object sender, System.EventArgs e) { // ソースオブジェクトの値をポップアップ表示します await DisplayAlert( "info", string.Format("Value1: {0} /" + "Value2: {1} /" + "Value3: {2}", this.valueModel.Value1, this.valueModel.Value2, this.valueModel.Value3), "close"); } } }
実行してみる
ここではiOSシミュレータで実行します。
①実行
実行直後の画面が以下です。1つ目のEntry(OneWay) および 3つ目のEntry(TwoWay)にはソースの値が反映されています。2つ目のEntryはOneWayToSourceモードである為、ソース→ターゲットへのデータバインディングは行われません。
②次に、3つのEntryにUI上から「変更」の文字を追記します。
③ここで、「show data」ボタンをクリックし、ソースオブジェクトの値を確認します。
Value2 / Value3にはEntryコントロールUI上での「変更」文字の追記が反映されました。この2つはそれぞれ、OneWayToSource / TwoWayモードでデータバインディングされている為、「ターゲット→ソース」へのデータ反映が行われました。
④次に「ソースのValue1値を変更」「ソースのValue2値を変更」「ソースのValue3値を変更」ボタンをクリックします。
これにより、プログラム上で Value1 / Value2 / Value3 の値が変更されます。
以下がその結果画面です。1つ目、3つ目のEntryコントロールの値が変更されました。OneWay / TwoWayモードデータバインディングである為、「ソース→ターゲット」へのデータ変更反映が行われました。
1つ重要な点は、ソース→ターゲットの変更反映を行う為に、ソースオブジェクト(ここではValueModelクラス)に対してINotifyPropertyChangedインターフェイス の実装が必要であるということです。
⑤最後にもう1度「show data」ボタンをクリックします。
④での操作により、ターゲットであるEntryコントロールへのデータ変更の反映の有無に関わらず、ソースオブジェクトの値自体は変更されていることを確認することができます。
コードでデータバインディングを定義
実際には多くの場合、XAMLのデータバインド構文を利用することになりますが、同じことをコードから行う事が出来ます。
先程のリスト1 / リスト2を、コードによるデータバインディング定義で書き換えると次のようになります。
(Xamlへの {Binding Path=Name} の書き換えになります)
// リスト8 「リスト2 Example1Page.xaml」をコードで書き換えると this.Label1.BindingContext = person; this.Label1.SetBinding( Label.TextProperty, "Name", BindingMode.OneWay, null, null);
SetBinding()メソッドは「BindableObjectの拡張メソッド」として定義されています。
BindableObjectとは、つまりデータバインディングのターゲットとなりうる(多くの)UIコントロールが該当します。
Label1の SetBinding() メソッドを呼び出します。
第1引数は、バインディング対象の BindableProperty を指定します。前述でバインド可能なプロパティは「通常のプロパティではなくBindablePropertyという特殊なプロパティである」と説明しました。Xaml定義上では「暗黙的にTextプロパティへの {Binding} 定義」を行いますが、コード上では「明示的にLabel.TextPropertyというBindableProperty型」を指定します。
第2引数は、Pathの指定になります。ソースはオブジェクト Person の Nameプロパティ がバインド対象なので「"Name"」となります。
まとめ & つづく・・・
ということで、Xamarin Formsにおけるデータバインディングの基本をまとめてみました。
基本といっても、まだまだ情報不足な部分もありますが、それでも結構長くなってしまったので、本投稿ではここまでとします。
今後のポストではコレクションコントロールへのデータバインディングとかにも触れていこうと思っています。
【追記】続きはこちら↓↓↓
Xamarin + Prism 超入門(とりあえず動かしてみよう!)
[2017/8/3追記] Visual Studio 2017をターゲットとした焼き直し記事を書きました。
本エントリーはVisual Studio 2015をベースとしています。
Visual Studio 2017での作業手順は上記「<2017年8月版>Xamarin + Prism 超入門(とりあえず動かしてみよう!)」が最新となります。
今回はXamarinで本格的な(?)開発を行う際の「gettting started」的な記事を書きたいと思います。
要点としては「Xamarin + Prism」で開発する為の「はじめの一歩」の記事となります。
「Macで Xamarin Studio を利用するケース」、「Windowsで Visual Studio を利用するケース」毎に、誰にでも分かる様に画面キャプチャ付きで手順を説明したいと思います。
同様のブログがすでにいくつも書かれていると思いますが、自分自身の整理と、本記事がどなたかのお役に立てればと思います。
Prismって何?
Xamarinに限らず、ある程度しっかりしたシステムの開発を行う際には、レイヤー化アーキテクチャが導入されます。
Web開発だと「MVC (Model-View-Controller」や「MVP (Model-View-Presenter」などがありますね。
Wikipediaによると、MVCに関する最初の論文が発表されたのが1988年のことだそうです。そう、このようなソフトウェアアーキテクチャは、遥か昔から存在しています。
で、Xamarinにおけるメジャーなレイヤー化アーキテクチャは「MVVM (Model-View-ViewMode)」となります。
まあ、Xamarinというか、XAML関連技術においてMVVMが標準技術として利用されてきました。
Xamlの始まりはWPFですね。続いてSilverlight、Windowsストアアプリ、UWP、とXaml技術は継続的進化を果たしています(私の様なベテランエンジニアに対しては、優しい、積み上げ型の知識が非常に役に立つ正常進化です)。
これらにMVVMアーキテクチャを適用するためのライブラリ(フレームワーク)がいくつか世の中には存在します。
代表的な(そしてXamarinに対応している)フレームワークとして以下のようなものがあります。
- Prism
- MVVM Light
今回紹介するのは Prism になります。
ネット上の情報を見る限り Prism がディファクトスタンダードとなりつつあるように感じます。
Xamarinの神様(Miguel de Icaza氏)が、Prism使おうよって仰ってますしね。
Prism公式サイトは以下です。
何故 Prism を使うのか?
それは Prism 公式サイトの以下の言葉に集約されています。
Prism is a framework for building loosely coupled, maintainable, and testable XAML applications in WPF, Windows 10 UWP, and Xamarin Forms.
ここでは、これ以上深く踏み込まず、次に進みます・・・
Xamarin Studio で Prism を使おう(Mac)
(1) Prism Template Pack のインストール
Xamarin Studioを起動します。
メニュー「Xamarin Studio Community→アドイン」を選択します。
以下の「アドインマネージャー」ウィンドウが表示されます。
「ギャラリー」タブを選択、右上の検索テキストボックスに「Prism」と入力、アドインリストから「Prism Template Pck」を選択し、右下の「インストール」ボタンをクリックします。
[2016/12/31 追記]
アドインマネージャーで検索して Prism Template Pack が表示されない場合は、Prism Template Packから直接アドインをダウンロードして、アドインマネージャーウィンドウ左下の「ファイルからのインストール」ボタンからインストールできます。
(2) Prismプロジェクトの作成
メニュー「ファイル→新しいソリューション」を選択します。
プロジェクトテンプレートとして「Xamarin.Forms→Prism Unity App」を選択を選択して、「次へ」ボタンをクリックします。
App Name等を適当に設定します。ここではApp Nameは PrismExample、Organization Identifierは jp.co.knowlbo としました。
続いてプロジェクトの構成を設定します。プロジェクト名・ソリューション名は共に PrismExample としました。
しばらく待っているとソリューション・プロジェクトが構成されます。
作成されたソリューションは以下の様になります。
通常のXamarin Formsと基本的には似た構成になります。
- PCLとしてのXamarin Formsプロジェクトである「PrismExampleプロジェクト」。
- Android固有実装用プロジェクトである「PrismExample.Droidプロジェクト」。
- iOS固有実装用プロジェクトである「PrismExample.iOSプロジェクト」。
1つ大きく異なるのはPrismExampleプロジェクトに ViewModels / Views フォルダがあることです。
まあ、PrismはMVVMフレームワークですからこれがキモですね。
(3) ビルドエラーが出るから修正
自動生成されたソリューションは、いわゆる HelloWorld 的な実装が出力されています。
とりあえずビルドして実行してみよう!ということで、メニュー「ビルド→すべてリビルド」を選択します。
が・・・2016/12/11段階のPrism Template Packにおいては、ビルドエラーにその道を阻まれます(泣)
ビルドエラーは以下の通りです。
エラーメッセージは以下ですね。
CS0246: The type or namespace name `IPlatformInitializer' could not be found. Are you missing `Prism.Unity' using directive? CS0246: The type or namespace name `IUnityContainer' could not be found. Are you missing `Microsoft.Practices.Unity' using directive?
「型か名前空間が見つからないんだけど・・・usingディレクティブ足りてないんじゃね?」って言ってますね。
なのでエラーが出ている「PrismExample.Droid¥MainActivity.cs」と「PrismExample.iOS¥AppDelegate.cs」に、以下の using を追加します。
using Prism.Unity; using Microsoft.Practices.Unity;
ビルドは無事通るはずです。
※これはそもそも提供されているPrism Templata Packの問題かな?皆さんが利用する際には治っているかも・・・
(4) 実行してみる
メニュー「実行→デバッグなしで開始」を選択します。デフォルトだと iOS emulator での実行が構成されています。
実行結果は以下の通りです。
(5) せっかくなので、ちょっといじろうか・・・
ただ実行するだけでは、あまりにもつまらないので、以下の様な実装を付け加えようと思います。
以下の様な入力フォームとします。
名前を入れて・・・
決定ボタンを押すと、メッセージが表示されます。
実装コードは以下の通りです。
--- 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="PrismExample.Views.MainPage" Title="MainPage"> <StackLayout HorizontalOptions="Center" VerticalOptions="Center"> <Label Text="あなたの名前" /> <Entry Text="{Binding YourName}" /> <Button Text="決定" Command="{Binding DecisionCommand}" /> <Label Text="{Binding Message}" /> </StackLayout> </ContentPage>
--- MainPageViewModel.cs --- using Prism.Commands; using Prism.Mvvm; using Prism.Navigation; using System; using System.Collections.Generic; using System.Linq; namespace PrismExample.ViewModels { public class MainPageViewModel : BindableBase, INavigationAware { // 名前入力Entry項目にバインドします private string _yourName; public string YourName { get { return _yourName; } set { SetProperty(ref _yourName, value); } } // メッセージ表示Label項目にバインドします private string _message; public string Message { get { return _message; } set { SetProperty(ref _message, value); } } // 決定ButtonのCommandにバインドします。 private DelegateCommand _decisionCommand; public DelegateCommand DecisionCommand { get { return this._decisionCommand = this._decisionCommand ?? new DelegateCommand(DecisionCommandExecute); } } public MainPageViewModel() { } private void DecisionCommandExecute() { this.Message = string.Format("{0}さん こんにちは", this.YourName); } public void OnNavigatedFrom(NavigationParameters parameters) { } public void OnNavigatedTo(NavigationParameters parameters) { } } }
実装をずらずらと載せてしまいましたが、要点は以下になります。
(すみません、実装を載せておきながら、なのですが、中身の説明は本投稿の軸となる趣旨からはずれるので、要点のみの概略で、PrismやXamarin Formsの詳細技術は別投稿で取り上げさせていただきたいと思います。m( _ _ )m)
MainPage.xamlに 名前入力用 Entry 要素を追加
EntryコントロールのTextプロパティの値、つまり入力された値は {Binding} 構文により MainPageViewModelクラス の YourNameプロパティ にデータバインディングします。MainPage.xamlに 決定 Button 要素を追加
Buttonコントロールの Commandプロパティ に、MainPageViewModelクラスの DecisionCommandプロパティ をデータバインドします。
Button.Clickedイベントではなく、Commandにバインドする理由は「BindablePropertyが・・・・」などの技術的バックボーンがありますが、このお話はまた別のところで・・・MainPage.xamlに メッセージ Label 要素を追加
LabelコントロールのTextプロパティの値に、MainPageViewModeクラスの Messageプロパティ をデータバインディングします。
MainPageViewModel側でMessageプロパティを変更すると、自動的にUIの表示も切り替わります。MainPageViewModelにDelegateCommandプロパティを追加
MainPageViewModelクラスに、DelegateCommand型のDecisionCommandプロパティを追加し、イベントハンドラとしてDecisionCommandExecute()メソッドを実装します。
以下の様にMessageプロパティ・YourNameプロパティを扱うことでロジックを処理しています。xaml側とのデータバインディング処理によってViewModelクラス上でのロジック処理結果がUIに自動的に反映されます。
--- MainPageViewModel.csのコードスニペット --- private void DecisionCommandExecute() { this.Message = string.Format("{0}さん こんにちは", this.YourName); }
こんな感じでView - ViewModelが疎結合に実装されました。
Visual Studio 2015 で Prism を使おう(Windows)
(1) Prism Template Pack のダウンロード & インストール
Visual Studio を起動し、メニュー「ツール→拡張機能と更新プログラム」を選択します。
以下の「拡張機能と更新プログラム」ウィンドウが表示されます。
左のツリーから「オンライン」を選択し、右上の検索テキストボックスに「Prism」と入力します。中央のリストに「Prism Template Pack」が表示されるので「ダウンロード」ボタンをクリックします。
ダウンロードが完了すると以下のインストールウィンドウが表示されるので「インストール」ボタンをクリックします。
インストールが完了すると、Visual Studioの再起動を求められますので、再起動を行います。
(2) Prismプロジェクトの作成
メニュー「ファイル→新規作成→プロジェクト」を選択します。
「新プロジェクト」ウィンドウが表示されるので、プロジェクトテンプレートとして「Prism→Prism Unity App(Xamarin.Forms)」を選択します。
プロジェクト名は「PrismExample」としました。
プロジェクトに含めるプラットフォーム選択ウィンドウが表示されます。今回は「ANDROID / iOS / UWP」を選択することにします。
MacのXamarin studioでは作成する事は出来なかった「UWP / STORE 8.1 / PHONE 8.1」が選択可能です。逆にiOSアプリはMacに接続しないとデバッグ・実行する事が出来ません。
プロジェクトの用意が完了するとMacへの接続情報設定ウィンドウが表示されますが、ここではそのまま設定せずに進みます。
次にUWPのターゲットバージョン選択ウィンドウが表示されます。ここでは、デフォルトのままOKをクリックする事とします。
以上でプロジェクトの生成が完了しました。
(3) ビルド
メニュー「ビルド→ソリューションのリビルド」を選択します。
しばらく時間がかかると思いますが、ビルドは正常終了するはずです。
(4) 実行
せっかくWindows環境なので、UWPアプリケーションとして実行してみましょう。
ソリューションエクスプローラ上で、「PrismExample.UWP」をマウス右クリックし、「スタートアッププロジェクトに設定」を選択します。
実行する前にアプリケーションを配置する必要があります。
メニュー「ビルド→PrismExample.UWPの配置」を選択します。
対象のPCが開発者モードになっていなかった場合、以下のウィンドウが表示されます。
UWPとして実行する場合、Windows上での実行になりますので、Windows設定を開発者モードにする必要があります。
「開発者モード」を選択しましょう。
改めて、メニュー「ビルド→PrismExample.UWPの配置」を選択します。
配置が完了したら、ツールバーから「▶︎ローカルコンピューター」をクリックしましょう。
デバッグモードで PrismExample.UWP が実行されました。
まとめ
Xamarinに限らずアーキテクチャを考慮せず、ただただコードビハインドにロジックを書き連ねるような実装方法が行われる事があります。
レイヤー化アーキテクチャに限らず、testableである事、maintainableである事、適切な責務の分離が行われている事などは非常に重要です。
XamarinにおけるPrismの導入に限らず、システムソリューションを構築する際は、一度立ち止まってベストプラクティスなアーキテクチャを検討したいものです。
今回は Xamarin + Prism の入り口の説明でしたので、今後はより技術的に踏み込んだトピックも取り上げていければと思います。