一般に、ほぼすべてのUIコントロールをデータオブジェクトにバインドできます(すべてのNativeScriptコントロールは、データバインディングを考慮して作成されます)。 コードが以下の要件を満たしていれば、データバインディングをそのまま使用できます。
Bindable
クラスの後継である必要があります。すべてのNativeScript UIコントロールは、Bindable
クラスから継承します。propertyChange
イベントを発生させる必要があります。この記事では、NativeScriptフレームワークで使用されるアーキテクチャパターン-MVVM(Model-View-ViewModel)を含む、基本的なバインディング手法と高度なバインディング手法について説明します。
Observableオブジェクトを作成し使用するには、tns-core-modules/data/observable
モジュールが必要です。
const Observable = require("tns-core-modules/data/observable").Observable;
const fromObject = require("tns-core-modules/data/observable").fromObject;
const fromObjectRecursive = require("tns-core-modules/data/observable").fromObjectRecursive;
import { fromObject, fromObjectRecursive, Observable, PropertyChangeData } from "tns-core-modules/data/observable";
Observableは、変更が発生したときに通知を受ける場合に使用されます。リスナーを追加/削除するには、on/offメソッドを使用します。
<!-- Using basic string binding and tap event callback binding-->
<Label text="{{ clientName }}" tap="{{ onLabelTap }}" textWrap="true" class="h2" color="red"/>
<!-- Example for using binding with concatenation (text prop) and for using binding to change font-size -->
<Label text="{{ 'font-size:' + mySize }}" textWrap="true" fontSize="{{ mySize }}"/>
<!-- Example demonstrating the boolean property usage with visibility and ternary expression-->
<Label text="{{ isVisible }}" textWrap="true" visibility="{{ isItemVisible, isItemVisible ? 'visible' : 'collapsed' }}"/>
// creating an Observable and setting title propertu with a string value
const page = args.object;
const viewModel = new Observable();
// String binding using set with key-value
viewModel.set("clientName", "Jonh Doe");
// Number binding using set with key-value
viewModel.set("mySize", 26);
// Boolean binding using set with key-value
viewModel.set("isVisible", true);
// Binding event callback using set with key-value
viewModel.set("onLabelTap", (args) => {
// args is of type EventData
console.log("Tapped on", args.object); //
// creating an Observable and setting title propertu with a string value
const viewModel = new Observable();
// String binding using set with key-value
viewModel.set("clientName", "Jonh Doe");
// Number binding using set with key-value
viewModel.set("mySize", 24);
// Boolean binding using set with key-value
viewModel.set("isVisible", true);
// Binding event callback using set with key-value
viewModel.set("onLabelTap", (args) => {
// args is of type EventData
console.log("Tapped on", args.object); // <Label>
console.log("Name: ", args.object.text); // The text value
});
// using get to obtain the value of specific key
console.log(viewModel.get("clientName")); // Jonh Doe
console.log(viewModel.get("mySize")); // 42
console.log(viewModel.get("isVisible")); // true
// bind the view-model to the view's bindingContext property (e.g. the curent view from loaded event)
const view = <Page>data.object;
view.bindingContext = viewModel;
"fromObject"メソッドは、 Observableインスタンスを作成し、指定されたJavaScriptオブジェクトに従ってそのプロパティを設定します。
// fromObject creates an Observable instance and sets its properties according to the supplied JS object
const newViewModel = fromObject({ "myColor": "Lightgray" });
// the above is equal to
/*
let newViewModel = new Observable();
newViewModel.set("myColor", "Lightgray");
*/
// fromObject creates an Observable instance and sets its properties according to the supplied JS object
const newViewModel = fromObject({ "myColor": "Lightgray" });
// the above is equal to
/*
let newViewModel = new Observable();
newViewModel.set("myColor", "Lightgray");
*/
"fromObjectRecursive"メソッドは、 Observableインスタンスを作成し、指定されたJavaScriptオブジェクトに従ってそのプロパティを設定します。 この関数は、指定されたJavaScriptオブジェクトから、ネストされたオブジェクト(配列と関数を除く)ごとに新しいObservableを作成します。
// fromObjectRecursive will create new Observable for each nested object (except arrays and functions)
const nestedViewModel = fromObjectRecursive({
client: "John Doe",
favoriteColor: { hisColor: "Green" } // hisColor is an Observable (using recursive creation of Observables)
});
// the above is equal to
/*
let newViewModel2 = new Observable();
newViewModel2.set("client", "John Doe");
newViewModel2.set("favoriteColor", fromObject( {hisColor: "Green" }));
*/
// fromObjectRecursive will create new Observable for each nested object (except arrays and functions)
const nestedViewModel = fromObjectRecursive({
client: "John Doe",
favoriteColor: { hisColor: "Green" } // hisColor is an Observable (using recursive creation of Observables)
});
// the above is equal to
/*
const newViewModel2 = new Observable();
newViewModel2.set("client", "John Doe");
newViewModel2.set("favoriteColor", fromObject( {hisColor: "Green" }));
*/
"propertyChangeEvent"を使用して、 PropertyChangeData型の引数でプロパティの変更に応答します。
const myListener = viewModel.addEventListener(Observable.propertyChangeEvent, (args) => {
// args is of type PropertyChangeData
console.log("propertyChangeEvent [eventName]: ", args.eventName);
console.log("propertyChangeEvent [propertyName]: ", args.propertyName);
console.log("propertyChangeEvent [value]: ", args.value);
console.log("propertyChangeEvent [oldValue]: ", args.oldValue);
});
const myListener = viewModel.addEventListener(Observable.propertyChangeEvent, (args: PropertyChangeData) => {
// args is of type PropertyChangeData
console.log("propertyChangeEvent [eventName]: ", args.eventName);
console.log("propertyChangeEvent [propertyName]: ", args.propertyName);
console.log("propertyChangeEvent [value]: ", args.value);
console.log("propertyChangeEvent [oldValue]: ", args.oldValue);
});
イベントリスナーは、不要になったときに明示的に削除できます。
viewModel.removeEventListener(Observable.propertyChangeEvent, myListener);
viewModel.removeEventListener(Observable.propertyChangeEvent, myListener);
MVVM(Model-View-ViewModel)は、 NativeScriptフレームワークが構築されるベースパターンです。 MVVMは、ビジネスロジックまたはバックエンドロジック(データモデル)の開発から、グラフィカルユーザーインターフェイスの開発の分離を容易にします。
Observable
と呼ばれるモジュールを提供し、ビューにバインドできるビューモデルオブジェクトの作成を容易にします。モデル、ビュー、ビューモデルを分離する最大の利点は、双方向のデータバインディングを使用できることです。 つまり、モデル内のデータへの変更は即座にビューに反映され、その逆も同様です。 もう1つの大きな利点は、コードを再利用できることです。 これは、多くの場合、モデルを再利用し、ビュー間でモデルを表示できるためです。
以下は、プレーンJavaScriptとTypeScript(Classを使用)を使用したNativeScriptアプリケーションでのMVVMパターンを示す完全な例です。
プレーンJavaScript
const Observable = require("tns-core-modules/data/observable").Observable;
function getMessage(counter) {
if (counter <= 0) {
return "Hoorraaay! You unlocked the NativeScript clicker achievement!";
} else {
return `${counter} taps left`;
}
}
function createViewModel() {
const viewModel = new Observable();
viewModel.set("counter", 42);
viewModel.set("message", getMessage(viewModel.counter));
viewModel.onTap = function () {
this.set("message", getMessage(--this.counter));
};
return viewModel;
}
exports.createViewModel = createViewModel;
const createViewModel = require("./main-view-model").createViewModel;
function onNavigatingTo(args) {
const page = args.object;
// using the view model as binding context for the current page
const mainViewModel = createViewModel();
page.bindingContext = mainViewModel;
}
exports.onNavigatingTo = onNavigatingTo;
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="onNavigatingTo" class="page">
<Page.actionBar>
<ActionBar title="MVVM Pattern"/>
</Page.actionBar>
<StackLayout class="p-20">
<Label text="Tap the button" class="h1 text-center"/>
<!-- using the view model method `onTap` and property `message` -->
<Button text="TAP" tap="{{ onTap }}" class="btn btn-primary btn-active"/>
<Label text="{{ message }}" class="h2 text-center" textWrap="true"/>
</StackLayout>
</Page>
TypeScript
import { Observable } from "tns-core-modules/data/observable";
export class HelloWorldModel extends Observable {
private _counter: number;
private _message: string;
constructor() {
super();
// Initialize default values.
this._counter = 42;
this.updateMessage();
}
get message(): string {
return this._message;
}
set message(value: string) {
if (this._message !== value) {
this._message = value;
this.notifyPropertyChange("message", value);
}
}
public onTap() {
this._counter--;
this.updateMessage();
}
private updateMessage() {
if (this._counter <= 0) {
this.message = "Hoorraaay! You unlocked the NativeScript clicker achievement!";
} else {
this.message = `${this._counter} taps left`;
}
}
}
import { HelloWorldModel } from "./main-view-ts-model";
import { EventData } from "tns-core-modules/data/observable";
import { Page } from "tns-core-modules/ui/page";
export function onNavigatingTo(args: EventData) {
const page = <Page>args.object;
// using the view model as binding context for the current page
const mainViewModel = new HelloWorldModel();
page.bindingContext = mainViewModel;
}
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="onNavigatingTo" class="page">
<Page.actionBar>
<ActionBar title="MVVM Pattern"/>
</Page.actionBar>
<StackLayout class="p-20">
<Label text="Tap the button" class="h1 text-center"/>
<!-- using the view model method `onTap` and property `message` -->
<Button text="TAP" tap="{{ onTap }}" class="btn btn-primary btn-active"/>
<Label text="{{ message }}" class="h2 text-center" textWrap="true"/>
</StackLayout>
</Page>
バインディングを操作するもう1つの一般的なケースは、親バインディングコンテキストへのアクセスを要求することです。
これは、子のbindingContextとは異なる場合があり、子が使用する必要のある情報が含まれる場合があるためです。
一般に、bindingContextは継承可能ですが、要素(アイテム)が一部のデータソースに基づいて動的に作成される場合は継承できません。
たとえば、ListViewは、ListView要素がどのように見えるかを記述するitememplateに基づいて子アイテムを作成します。
この要素がビジュアルツリーに追加されると、(対応するインデックスを持つ)リストビューアイテム配列から要素をバインディングコンテキスト用に取得します。
このプロセスにより、子アイテムとその内部UI要素の新しいバインディングコンテキストチェーンが作成されます。
したがって、内側のUI要素はListView
のバインディングコンテキストにアクセスできません。
この問題を解決するために、NativeScriptバインディングインフラストラクチャには2つの特別なキーワード$parent
と$parents
があります。
最初のものは直接の親ビジュアル要素のバインディングコンテキストを示しますが、2番目のものは配列として使用できます(数値または文字列インデックス)。
これにより、NレベルのUIネストを選択するか、特定のタイプの親UI要素を取得するかを選択できます。
これが実際の例でどのように機能するかを見てみましょう。
<Page navigatingTo="onNavigatingTo" xmlns="http://schemas.nativescript.org/tns.xsd">
<Page.actionBar>
<ActionBar title="Parents Binding"/>
</Page.actionBar>
<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.-->
<TextField text="{{ $parents['ListView'].test, $parents['ListView'].test }}" col="1"/>
</GridLayout>
</ListView.itemTemplate>
</ListView>
</GridLayout>
</Page>
const fromObject = require("data/observable").fromObject;
function onNavigatingTo(args) {
const page = args.object;
const viewModel = fromObject({
items: [1, 2, 3],
test: "Parent binding! (the value came from the `test` property )"
});
page.bindingContext = viewModel;
}
exports.onNavigatingTo = onNavigatingTo;
import { fromObject, EventData } from "tns-core-modules/data/observable";
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: "Parent binding! (the value came from the `test` property )"
});
page.bindingContext = viewModel;
}
非常に一般的なケースは、プレーンな要素(数値、日付、文字列)のリスト(配列)をListView項目コレクションに提供することです。
上記のすべての例は、UI要素をbindingContextのプロパティにバインドする方法を示しています。
プレーンデータしかない場合は、バインドするプロパティがないため、オブジェクト全体にバインドする必要があります。
NativeScriptバインディングのもう1つの機能、オブジェクトまたは値のバインディングがあります。
オブジェクト全体(例ではDate())を参照するには、キーワード$value
を使用する必要があります。
<ListView items="{{ items }}" class="list-group">
<ListView.itemTemplate>
<StackLayout class="list-group-item">
<Label text="Date" class="list-group-item-heading" />
<!-- use $value to bind plain objects (e.g. number, string, Date)-->
<Label text="{{ $value }}" class="list-group-item-text" />
</StackLayout>
</ListView.itemTemplate>
</ListView>
const fromObject = require("tns-core-modules/data/observable").fromObject;
function onNavigatingTo(args) {
const page = args.object;
const list = [];
for (let i = 0; i < 15; i++) {
list.push(new Date());
}
const viewModel = fromObject({
items: list
});
page.bindingContext = viewModel;
}
exports.onNavigatingTo = onNavigatingTo;
import { fromObject, EventData } from "tns-core-modules/data/observable";
import { Page } from "tns-core-modules/ui/page";
export function onNavigatingTo(args: EventData) {
const page = <Page>args.object;
const list = [];
for (let i = 0; i < 15; i++) {
list.push(new Date());
}
const viewModel = fromObject({
items: list
});
page.bindingContext = viewModel;
}
双方向データバインディングは、アプリケーションUIとのバインディング(モデルへのバインディングとUIへのバインディング)を組み合わせるバインディングの方法です。
典型的な例は、モデルから値を読み取り、ユーザー入力に基づいてモデルを変更するTextField
です。
この例は、分離コードによる双方向バインディングを示しています。
TextField
は空の文字列を初期値として受け入れます(同じバインディングがLabel
要素に使用されます)。
次に、ユーザーが新しい文字列をTextField
に入力すると、双方向バインディングによってラベルのテキストプロパティが同時に更新されます。
const observableSource = fromObject({
myTextSource: "" // initial binding value (in this case empty string)
});
// create the TextField
const targetTextField = new TextField();
// create the Label
const targetLabel = new Label();
stackLayout.addChild(targetTextField);
stackLayout.addChild(targetLabel);
// binding the TextField with BindingOptions
const textFieldBindingOptions = {
sourceProperty: "myTextSource",
targetProperty: "text",
twoWay: true
};
targetTextField.bind(textFieldBindingOptions, observableSource);
// binding the Label with BindingOptions
const labelBindingOptions = {
sourceProperty: "myTextSource",
targetProperty: "text",
twoWay: false // we don't need two-way for the Label as it can not accept user input
};
targetLabel.bind(labelBindingOptions, observableSource);
const observableSource = fromObject({
myTextSource: "" // initial binding value (in this case empty string)
});
// create the TextField
const targetTextField = new TextField();
// create the Label
const targetLabel = new Label();
stackLayout.addChild(targetTextField);
stackLayout.addChild(targetLabel);
// binding the TextField with BindingOptions
const textFieldBindingOptions = {
sourceProperty: "myTextSource",
targetProperty: "text",
twoWay: true
};
targetTextField.bind(textFieldBindingOptions, observableSource);
// binding the Label with BindingOptions
const labelBindingOptions = {
sourceProperty: "myTextSource",
targetProperty: "text",
twoWay: false // we don't need two-way for the Label as it can not accept user input
};
targetLabel.bind(labelBindingOptions, observableSource);
XMLでバインディングを作成するには、上記の例と同じ方法で作成されるソースオブジェクトが必要です。 次に、バインディングがXMLで記述されます(mustache記法(訳註:「{{~}}」を用いた値の埋め込み))。 XML宣言では、プロパティの名前のみが設定されます - target: text、および source: textSource です。 ここで興味深いのは、バインディングのソースが明示的に指定されていないことです。 このトピックの詳細については、バインディングソースの記事で説明します。
<Page xmlns="http://schemas.nativescript.org/tns.xsd">
<StackLayout>
<TextField text="" />
</StackLayout>
</Page>