この記事の目的は、データバインディングとは何か、それがNativeScriptでどのように機能するのかを説明することです。また、この記事にはサンプルがあります。これは、データバインディングの使用法のさまざまな使用例を示しています。
データバインディングは、アプリケーションのユーザーインターフェイス(UI)をデータオブジェクト(コード)に接続するプロセスです。UIの変更をコードに反映することで変更の伝達を可能にします。
データバインディング設定の一つは、データの流れ方についてです。NativeScriptデータバインディングは次のデータ転送をサポートします。
一般に、ほとんどすべてのUIコントロールはデータオブジェクトにバインドできます(すべてのNativeScriptコントロールはデータバインドを念頭に置いて作成されます)。コードが次の要件を満たした後は、そのままデータバインディングを使用できます。
以下の例は、Label,TextField、およびUIコントロールがバインドされているsourceプロパティで構成されています。 サンプルの目的は、TextFieldの入力を編集しながら、Labelテキストがどのように変更されるかを示すことです。
まず、textSourceプロパティを使用してsourceオブジェクトを作成します。sourceプロパティからLabelへの伝播する変更の一定の流れが必要です。 したがって、コード内のプロパティは、Labelに変更を通知するためにpropertyChangeイベントを発生させる必要があります。 このイベントを発生させるために、この機能を提供する組み込みクラスObservableが使用されます。
const fromObject = require("tns-core-modules/data/observable").fromObject;
const source = fromObject({
textSource: "Text set via twoWay binding"
});
import { fromObject } from "tns-core-modules/data/observable";
const source = fromObject({
textSource: "Text set via twoWay binding"
});
次に、ソースプロパティにバインドするためのターゲットオブジェクトを作成します。このケースでは(すべてのUIコントロールと同様に)Bindableクラスを継承するLabelとTextFieldになります。
// create the TextField
const TextField = require("tns-core-modules/ui/text-field").TextField;
const targetTextField = new TextField();
// create the Label
const Label = require("tns-core-modules/ui/label").Label;
const targetLabel = new Label();
// create the TextField
import { TextField } from "tns-core-modules/ui/text-field";
const targetTextField = new TextField();
// create the Label
import { Label } from "tns-core-modules/ui/label";
const targetLabel = new Label();
その後、ターゲットオブジェクトはソースオブジェクトにバインドされます。 TextFieldは双方向バインディングを使用しているため、ユーザー入力によってコード内のプロパティが変更される可能性があります。 また、コードからUIへの変更のみを伝達するために、Labelのバインディングは一方向に設定されています。
例1:ラベルテキストプロパティのバインド
// binding the TextField
const textFieldBindingOptions = {
sourceProperty: "textSource",
targetProperty: "text",
twoWay: true
};
targetTextField.bind(textFieldBindingOptions, source);
// binding the Label
const labelBindingOptions = {
sourceProperty: "textSource",
targetProperty: "text",
twoWay: false
};
targetLabel.bind(labelBindingOptions, source);
import { BindingOptions } from "tns-core-modules/ui/core/bindable";
// binding the TextField
const textFieldBindingOptions: BindingOptions = {
sourceProperty: "textSource",
targetProperty: "text",
twoWay: true
};
targetTextField.bind(textFieldBindingOptions, source);
// binding the Label
const labelBindingOptions: BindingOptions = {
sourceProperty: "textSource",
targetProperty: "text",
twoWay: false
};
targetLabel.bind(labelBindingOptions, source);
XMLでバインディングを作成するには、ソースオブジェクトが必要です。 これは、上記の例(コード内の双方向バインディング)と同じ方法で作成されます。 その後、バインディングはXMLで記述されます(mustache記法を使用)。 XML宣言では、target: text、source: textSourceの各プロパティの名前のみが設定されます。 ここで興味深いのは、バインディングのソースが明示的に指定されていないことです。 このトピックについての詳細はBindingソースの項を参照してください。
<Page xmlns="http://schemas.nativescript.org/tns.xsd">
<StackLayout>
<TextField text="{{ textSource }}" />
</StackLayout>
</Page>
データバインディングの重要な部分は、ソースオブジェクトを設定することです。 継続的なデータ変更の流れのために、ソースプロパティはpropertyChangeイベントを発行する必要があります。 NativeScriptデータバインディングは、このイベントを発行するすべてのオブジェクトと連携します。 バインディングソースを追加するには、bind(bindingOptions, source)メソッドで2番目のパラメータとして渡します。 このパラメータはオプションであり、省略することもできます。 その場合、BindableクラスのbindingContextという名前のプロパティがソースとして使用されます。 このプロパティの特別な点は、ビジュアルツリー全体に継承できるということです。 つまり、UIコントロールは、明示的に設定されたbindingContextを持つ、最初の親要素のbindingContextを使用できます。 コード内の双方向バインディングの例では、bindingContextはPageインスタンスまたはStackLayoutインスタンスのどちらかに設定でき、TextFieldはそれを "text"プロパティのバインディングの適切なソースとして継承します。
page.bindingContext = source;
//or
stackLayout.bindingContext = source;
page.bindingContext = source;
//or
stackLayout.bindingContext = source;
特定のイベントで実行する機能をバインドするオプションがあります(MVVMコマンドなど)。このオプションはXML宣言を通してのみ利用可能です。 そのような機能を実装するためには、ソースオブジェクトはイベントハンドラ関数を持つべきです。
例2:ボタンタップイベントでのバインディング機能
<Page xmlns="http://schemas.nativescript.org/tns.xsd">
<StackLayout>
<Button text="Test Button For Binding" tap="{{ onTap }}" />
</StackLayout>
</Page>
source.set("onTap", function(eventData) {
console.log("button is tapped!");
});
page.bindingContext = source;
source.set("onTap", function(eventData) {
console.log("button is tapped!");
});
page.bindingContext = source;
非常に一般的なケースは、単純な要素(数字、日付、文字列)のリスト(配列)をListViewアイテムコレクションに提供することです。 上記のすべての例は、UI要素をbindingContextのプロパティにバインドする方法を示しています。 単純なデータしかない場合は、バインドするプロパティがないため、バインドはオブジェクト全体に対するものでなければなりません。 これが、NativeScriptバインディングのもう1つの機能、オブジェクトバインディングまたは値バインディングです。 オブジェクト全体を参照するには(例ではDate())、キーワード$valueを使用する必要があります。
例3:ListViewをbindingContextのプロパティにバインドする
<Page navigatingTo="onNavigatingTo" xmlns="http://schemas.nativescript.org/tns.xsd">
<StackLayout>
<ListView items="{{ items }}" height="200">
<ListView.itemTemplate>
<Label text="{{ $value }}" />
</ListView.itemTemplate>
</ListView>
</StackLayout>
</Page>
const fromObject = require("tns-core-modules/data/observable").fromObject;
function onNavigatingTo(args) {
const list = [];
for (let i = 0; i < 5; i++) {
list.push(new Date());
}
const source = fromObject({
items: list
});
source.set("items", list);
const page = args.object;
page.bindingContext = source;
}
import { EventData, fromObject } from "tns-core-modules/data/observable";
import { Page } from "tns-core-modules/ui/page";
export function onNavigatingTo(args: EventData) {
const list = [];
for (let i = 0; i < 5; i++) {
list.push(new Date());
}
const source = fromObject({
items: list
});
const page = <Page>args.object;
page.bindingContext = source;
}
バインディングを扱う際のもう1つの一般的なケースは、親バインディングコンテキストへのアクセスを要求することです。 それは子のbindingContextとは異なる可能性があり、子が使用しなければならない情報を含む可能性があるためです。 通常、bindingContextは継承可能ですが、要素(アイテム)が何らかのデータソースに基づいて動的に作成される場合は継承されません。 たとえば、ListViewは、要素の外観を表すitemTempleteに基づいて子項目を作成します。 この要素がビジュアルツリーに追加されると、(対応するインデックスを持つ)ListViewitems配列からバインドコンテキストの要素を取得します。 このプロセスは、子項目とその内部UI要素のための新しいバインディングコンテキストチェーンを作成します。 そのため、内側のUI要素は 'ListView'のバインディングコンテキストにアクセスできません。 この問題を解決するために、NativeScriptバインディングインフラストラクチャには、$parentと$parentsという2つの特別なキーワードがあります。 前者は直接の親ビジュアル要素のバインディングコンテキストを示していますが、後者は(数値または文字列インデックスを持つ)配列として使用できます。 これにより、NレベルのUIネストを選択するか、特定のタイプの親UI要素を取得するかを選択できます。 現実的な例でこれがどのように機能するか見てみましょう。
例4:itemTemplateに基づいてListViewの子項目を作成する
<Page navigatingTo="onNavigatingTo" xmlns="http://schemas.nativescript.org/tns.xsd">
<GridLayout rows="*" >
<ListView items="{{ items }}">
<!--Describing how the element will look like-->
<ListView.itemTemplate>
<GridLayout columns="auto, *">
<Label text="{{ $value }}" col="0"/>
<!--The TextField has a different bindingCotnext from the ListView, but has to use its properties. Thus the parents['ListView'] has to be used.-->
<!-- Parent binding to a string property -->
<TextField text="{{ $parents['ListView'].test, $parents['ListView'].test }}" col="1"/>
<!-- Parent binding to a method onTap -->
<Button text="Tap me" tap="{{ $parents['ListView'].onTap, $parents['ListView'].onTap }}" />
</GridLayout>
</ListView.itemTemplate>
</ListView>
</GridLayout>
</Page>
const fromObject = require("tns-core-modules/data/observable").fromObject;
function onNavigatingTo(args) {
const page = args.object;
const viewModel = fromObject({
items: [1, 2, 3],
test: "Test for parent binding!",
onTap: (args) => {
console.log('(func parent binding) Tapped ', args.object);
}
});
page.bindingContext = viewModel;
}
exports.onNavigatingTo = onNavigatingTo;
import { EventData, fromObject } from "tns-core-modules/data/observable";
import { Button } from "tns-core-modules/ui/button";
import { Page } from "tns-core-modules/ui/page";
export function onNavigatingTo(args: EventData) {
const page = <Page>args.object;
const viewModel = fromObject({
items: [1, 2, 3],
test: "Test for parent binding!",
onTap: (args: EventData) => {
const tappedButton = <Button>args.object;
console.log('Tapped ', tappedButton);
}
});
page.bindingContext = viewModel;
}
バインディング用のカスタム式を作成できます。 基になるビジネスデータとロジックを明確に保ちながら、特定のロジックをUIに適用する必要がある場合には、カスタム式が役立ちます。 より具体的に、基本的なバインディング式の例を見てみましょう。 結果は、sourcePropertyの値と、それに続く「なんらかの静的テキスト」文字列を表示するTextField要素になります。
<Page xmlns="http://schemas.nativescript.org/tns.xsd">
<StackLayout>
<TextField text="{{ sourceProperty, sourceProperty + ' some static text' }}" />
</StackLayout>
</Page>
フルバインディング構文には3つのパラメータが含まれています。 1番目のパラメータはsourceプロパティで、これは変更を監視します。 2番目のパラメータは評価される式です。3番目のパラメータは、バインディングが双方向かどうかを示します。 前述のように、XML宣言はデフォルトで双方向バインディングを作成するため、この例では3番目のパラメーターを省略することができます。 他の2つのプロパティを保持するということは、sourcePropertyが変更された場合にのみカスタム式が評価されることを意味します。 最初のパラメータも省略できます。そうすると、bindingContextが変わるたびにカスタム式が評価されます。 したがって、推奨される構文は、この例のように、XML宣言に2つのパラメーター(対象となるプロパティと評価する必要がある式)を含めることです。
NativeScriptは、次のようなさまざまな種類の式をサポートしています。
項目 | 例 | 説明 |
---|---|---|
プロパティへの アクセス |
obj1.obj2.prop1 |
オブジェクト obj2 の prop1 プロパティの値が得られます。
結合式は polymer expressions に基づいています。
これは、ドット(.)チェーン内の値が変更されたときに式を再評価するためのものです。
NativeScriptは(今のところ)バインディングのコンテキストでのみ式を使用するため、バインディングパフォーマンスは、バインディングの sourceProperty が変更された場合にのみ再評価されます(パフォーマンス上の理由から)。
表現部分は遵守しないため、再評価は行われません。 |
配列アクセス | arrayVar[indexVar] |
その配列の有効なインデックス(indexVar)によってアクセスされた配列(arrayVar)内の要素の値を取得します。 |
論理演算子 | !var1 |
オペランドの論理状態を逆にします - 論理notです。 |
単項演算子 | +var1,-var2 |
var1を数値に変換します。var2を数値に変換して符号を反転します。 |
二項演算子 | var1 + var2 |
var2の値をvar1に加算します。サポートされている演算子: +, -, *, /, %。 |
比較演算子 | var1 > var2 |
var1の値がvar2の値より大きいかどうかを比較します。その他のサポートされている演算子: <, >, <=, >=, ==, !=, ===, !==。 |
論理比較演算子 | var1>1 && var2>1. |
var1の値が1より大きく、かつvar2の値が2より大きいかどうかを評価します。サポートされている演算子: &&, ||。 |
三項演算子 | var1 ? var2 : var3 |
var1 の値を評価し、trueの場合はxvar2 を返し、そうでない場合はvar3 を返します。 |
グループ化括弧 | (a + b) * (c + d) |
|
関数呼び出し | myFunc(var1, var2, ..., varN) |
myFuncはバインディングコンテキスト(式のコンテキストとして使用される)またはアプリケーションレベルのリソース内で使用可能な関数です。 var1 からvarN の値がパラメータとして使用されます。 |
フィルター | expression \| filter1(param1, ...) | filter 2 |
フィルタは、式の値に適用されるオブジェクトまたは関数です。 バインディングのコンテキスト内では、この機能はコンバーターとして使用されます。 詳しくは、専用トピックバインディングでのコンバーターの使用を参照してください。 |
" "
' '
< <
> >
& &
双方向バインディングといえば、データの保存と表示で方法が異なるという共通の問題があります。 おそらく最も良い例は、日付と時刻のオブジェクトです。日付と時刻の情報は、数字または数字のシーケンスとして格納されます(インデックス作成、検索、その他のデータベース操作には非常に役立ちます)が、 アプリケーションユーザーに日付を表示するのに最適な選択肢ではありません。 また、ユーザーが日付を入力するときに別の問題があります(以下の例では、ユーザーがTextFieldに入力します)。 ユーザー入力の結果は文字列になり、ユーザーの設定に従ってフォーマットされます。この文字列は正しい日付オブジェクトに変換する必要があります。 NativeScriptバインディングでこれをどのように処理できるかを見てみましょう。
例5:textFieldの日付入力を処理し、設定に従ってフォーマットします。
<Page navigatingTo="onNavigatingTo" xmlns="http://schemas.nativescript.org/tns.xsd">
<StackLayout>
<TextField text="{{ testDate, testDate | dateConverter('DD.MM.YYYY') }}" />
</StackLayout>
</Page>
const fromObject = require("tns-core-modules/data/observable").fromObject;
function onNavigatingTo(args) {
const dateConverter = {
toView(value, format) {
let result = format;
const day = value.getDate();
result = result.replace("DD", day < 10 ? `0${day}` : day);
const month = value.getMonth() + 1;
result = result.replace("MM", month < 10 ? `0${month}` : month);
result = result.replace("YYYY", value.getFullYear());
return result;
},
toModel(value, format) {
const ddIndex = format.indexOf("DD");
const day = parseInt(value.substr(ddIndex, 2), 10);
const mmIndex = format.indexOf("MM");
const month = parseInt(value.substr(mmIndex, 2), 10);
const yyyyIndex = format.indexOf("YYYY");
const year = parseInt(value.substr(yyyyIndex, 4), 10);
const result = new Date(year, month - 1, day);
return result;
}
};
const page = args.object;
const viewModel = fromObject({
dateConverter,
testDate: new Date()
});
page.bindingContext = viewModel;
}
exports.onNavigatingTo = onNavigatingTo;
import { EventData, fromObject } from "tns-core-modules/data/observable";
import { Page } from "tns-core-modules/ui/page";
export function onNavigatingTo(args: EventData) {
const dateConverter = {
toView(value, format) {
let result = format;
const day = value.getDate();
result = result.replace("DD", day < 10 ? "0" + day : day);
const month = value.getMonth() + 1;
result = result.replace("MM", month < 10 ? "0" + month : month);
result = result.replace("YYYY", value.getFullYear());
return result;
},
toModel(value, format) {
const ddIndex = format.indexOf("DD");
const day = parseInt(value.substr(ddIndex, 2), 10);
const mmIndex = format.indexOf("MM");
const month = parseInt(value.substr(mmIndex, 2), 10);
const yyyyIndex = format.indexOf("YYYY");
const year = parseInt(value.substr(yyyyIndex, 4), 10);
const result = new Date(year, month - 1, day);
return result;
}
};
const page = <Page>args.object;
const viewModel = fromObject({
dateConverter,
testDate: new Date()
});
page.bindingContext = viewModel;
}
式内の特殊演算子(|)に注意してください。 上記のコードスニペット(XMLとJavaScriptの両方)は、DD.MM.YYYY形式(toView関数)で日付を表示し、同じ形式で新しい日付が入力されると、有効なDateオブジェクト(toModel関数)に変換されます。 Converterオブジェクトでは、データを変換する必要があるたびに1つまたは2つの関数(toViewおよびtoModel)を実行する必要があります。 toView関数は、データが任意のUIビューの値としてエンドユーザーに表示されるときに呼び出され、toModel関数は編集可能な要素(TextFieldなど)があり、ユーザーが新しい値を入力するときに呼び出されます。 一方向バインディングの場合、ConverterオブジェクトはtoView関数のみを持つことも、関数になることもあります。 すべての変換関数は、最初のパラメーターが変換される値であるパラメーターの配列を持ち、他のすべてのパラメーターはコンバーター定義で定義されたカスタム・パラメーターです。
コンバータは静的なカスタムパラメータだけでなく、bindingContextからの任意の値を受け取ることができます。例えば:
例6:新しい日付入力を有効なDateオブジェクトに変換する
<Page navigatingTo="onNavigatingTo" xmlns="http://schemas.nativescript.org/tns.xsd">
<StackLayout>
<TextField text="{{ testDate, testDate | dateConverter(dateFormat) }}" />
</StackLayout>
</Page>
const fromObject = require("tns-core-modules/data/observable").fromObject;
function onNavigatingTo(args) {
const dateConverter = {
toView(value, format) {
let result = format;
const day = value.getDate();
result = result.replace("DD", day < 10 ? `0${day}` : day);
const month = value.getMonth() + 1;
result = result.replace("MM", month < 10 ? `0${month}` : month);
result = result.replace("YYYY", value.getFullYear());
return result;
},
toModel(value, format) {
const ddIndex = format.indexOf("DD");
const day = parseInt(value.substr(ddIndex, 2), 10);
const mmIndex = format.indexOf("MM");
const month = parseInt(value.substr(mmIndex, 2), 10);
const yyyyIndex = format.indexOf("YYYY");
const year = parseInt(value.substr(yyyyIndex, 4), 10);
const result = new Date(year, month - 1, day);
return result;
}
};
const page = args.object;
const viewModel = fromObject({
dateConverter,
dateFormat: "DD.MM.YYYY",
testDate: new Date()
});
page.bindingContext = viewModel;
}
import { EventData, fromObject } from "tns-core-modules/data/observable";
import { Page } from "tns-core-modules/ui/page";
export function onNavigatingTo(args: EventData) {
const dateConverter = {
toView(value, format) {
let result = format;
const day = value.getDate();
result = result.replace("DD", day < 10 ? "0" + day : day);
const month = value.getMonth() + 1;
result = result.replace("MM", month < 10 ? "0" + month : month);
result = result.replace("YYYY", value.getFullYear());
return result;
},
toModel(value, format) {
const ddIndex = format.indexOf("DD");
const day = parseInt(value.substr(ddIndex, 2), 10);
const mmIndex = format.indexOf("MM");
const month = parseInt(value.substr(mmIndex, 2), 10);
const yyyyIndex = format.indexOf("YYYY");
const year = parseInt(value.substr(yyyyIndex, 4), 10);
const result = new Date(year, month - 1, day);
return result;
}
};
const page = <Page>args.object;
const viewModel = fromObject({
dateConverter,
dateFormat: "DD.MM.YYYY",
testDate: new Date()
});
page.bindingContext = viewModel;
}
bindingContext内でコンバーター関数とパラメーターを設定することは、データの適切な変換を確実にするために非常に役立ちます。 ただし、listview項目をバインドする必要はありません。 問題は、任意のコレクション(配列)の一部でありデータ項目であるlistview項目のbindingContextに、コンバーターを適用する際に発生します。 コンバーターとそのパラメーターはデータ項目に追加する必要がありますが、この際に複数のコンバータインスタンスが発生します。 NativeScriptでこの問題に取り組むのはかなり簡単です。バインディングインフラストラクチャは、適切なコンバータとパラメータを見つけるためにアプリケーションレベルのリソースを探します。 そのため、アプリケーションモジュールのリソースにコンバータを追加できます。より明確にするためには、次の例(XMLとJavaScriptの両方)を参照してください。
例7:アプリケーションモジュールリソースにコンバーターを追加する
<Page navigatingTo="onNavigatingTo" xmlns="http://schemas.nativescript.org/tns.xsd">
<StackLayout>
<ListView items="{{ items }}" height="200">
<ListView.itemTemplate>
<Label text="{{ itemDate | dateConverter(dateFormat) }}" />
</ListView.itemTemplate>
</ListView>
</StackLayout>
</Page>
const appModule = require("tns-core-modules/application");
const fromObject = require("tns-core-modules/data/observable").fromObject;
function onNavigatingTo(args) {
const list = [];
for (let i = 0; i < 5; i++) {
list.push({ itemDate: new Date() });
}
const dateConverter = (value, format) => {
let result = format;
const day = value.getDate();
result = result.replace("DD", day < 10 ? `0${day}` : day);
const month = value.getMonth() + 1;
result = result.replace("MM", month < 10 ? `0${month}` : month);
result = result.replace("YYYY", value.getFullYear());
return result;
};
appModule.getResources().dateConverter = dateConverter;
appModule.getResources().dateFormat = "DD.MM.YYYY";
const page = args.object;
const viewModel = fromObject({
items: list
});
page.bindingContext = viewModel;
}
exports.onNavigatingTo = onNavigatingTo;
import * as application from "tns-core-modules/application";
import { EventData, fromObject } from "tns-core-modules/data/observable";
import { Page } from "tns-core-modules/ui/page";
export function onNavigatingTo(args: EventData) {
const list = [];
for (let i = 0; i < 5; i++) {
list.push({ itemDate: new Date() });
}
const dateConverter = (value, format) => {
let result = format;
const day = value.getDate();
result = result.replace("DD", day < 10 ? "0" + day : day);
const month = value.getMonth() + 1;
result = result.replace("MM", month < 10 ? "0" + month : month);
result = result.replace("YYYY", value.getFullYear());
return result;
};
application.getResources().dateConverter = dateConverter;
application.getResources().dateFormat = "DD.MM.YYYY";
const page = <Page>args.object;
const viewModel = fromObject({
items: list
});
page.bindingContext = viewModel;
}
Bindingオブジェクトは弱い参照を使用するので、通常は明示的にバインドを停止する必要はありません。 これにより、メモリリークが防止されます。ただし、バインドを停止しなければならないシナリオがいくつかあります。 既存のデータバインディングを停止するには、ターゲットプロパティ名を引数としてunbindメソッドを呼び出すだけです。
バインディングのAPIリファレンスはこちら。