導入
以前の記事で、Delphi の新しいバインディングフレームワークである SmartBinding の基本的な概念について解説しました。それにはバインディングオブジェクト、リスト、汎用的なデータおよび視覚的コントロールを含み、ナビゲーターの使用法についてもコード例を交えて説明しました。
本稿では、次期 kbmMW リリースに搭載予定の新機能「SmartBinding v2」の詳細を紹介します。
ワンラインでのバインディング
kbmMW SmartBind の知能化を進める目標の一つは、繰り返しのバインディングコードを削除し、開発者が機能要件に集中できるようにすることです。コードによる実行も依然として可能ですが、実装をさらに簡略化するため、以下のようなデモンストレーションをご確認ください。
まず、単純なフォームを作成してください。上記図のように、TLabel、TButton および 2 つの TEdit を含むものとします。
それを起動してみます:
Edit3 に内容を入力すると、残りのコントロールの Caption や Text が自動的に更新されます。この機能については以前の SmartBinding の投稿でも触れました。
過去の投稿では多くの Binding.Bind コード行でコントロールを結んでいましたが、今回の例では TForm の OnCreate イベントで以下の一行を使うだけですみます。
Binding.AutoBind(Self);
これはどのように動くのでしょうか?現在の kbmMW SmartBinding は、オブジェクトおよびコントロールの String プロパティをチェックし、バインディング指示を見つけ出します。
Label.Caption プロパティには明確に `{Edit3.Text}` という指示が含まれています。
ラベルが Edit3.Text プロパティからデータを受け取るために、バインディング内容を Caption 内に入れるだけで済み、これと同様の仕組みでボタンも編集と連動します。二番目の TEdit も Edit3 に連結されますが、ここでは双方向バインディングが行われ、`{Edit3.Text, twoway: true}` のような記述になります。
要約すると、視覚的データがどこから入手されるかを一目で確認できるようになっています。
多様なバインディング
次にデモアプリの 2 枚目のページをご覧いただきます。
これは 1 つの String プロパティに複数のバインディングを指定する方法を示しています。TButton の Caption プロパティには 2 つのバインディングを含む配列を設定されており、Edit1.Text が Label2.Caption と、そして Edit1.Text が Label1.Caption とそれぞれ連動します。Edit1 に入力を行うと期待通りに両方のラベルが更新されます。
既に気づかれているかもしれませんが、ボタンが最終ユーザーにとって友好的に見えないのは、Caption をバインディング配列の保管場所としているためです。しかし、以下のいずれかの方法で簡単に解決できます。
- TButton.Caption を使うのではなく、実行時に見えることのない他の String プロパティを使用する
- TButton.Caption を表示したいコンテンツを生産するものへと連結する
- 値構文を採用する
値構文による定義は以下のようになります。
[
{value: "Button"},
{bind: Edit1.Text, to: Label2.Caption},
{bind: Edit1.Text, to: Label1.Caption}
]
実行後の結果は以下の通りです。
値構文のような手法は、定数文字列を値として使用するため、Button には通常のタイトルが表示されます。ただし kbmMW 設定フレームワークを使用して、kbmMW 設定マネージャーに関する投稿を参照することも可能です。
関連するバインディングセクションに `exprToSrc:"expression"` および/または `exprToDest:"expression"` 文字列プロパティを追加することで、ターゲットとソースの式を指定することもできます。デフォルトで無効にするには `disabled: true` を設定します。
バインディングとデータの分離
実際の本番データをセットする前に GUI を設計できれば理想的ではありませんか。言い換えれば、リアルタイムモデルを開発でき、スイッチ一つで実アプリケーションへと変換可能です。
これがバインディング/データ分離の登場領域です。デモの 3 ページをご覧ください。
このページには 8 つの Label が存在し、そのうち 7 つはそれぞれの意味を成す特定の値を持ったように見えます。
@ 構文を使用することで、kbmMW SmartBinding に `test` という名前のデータプレースホルダーとの連携を指示します。
アプリケーションを実行しますとどうなりますでしょうか?ライブシステムでは、バインディングが `@test` からデータを取得します。`@Test.Value2` と `@test.Value3` は静的データで埋められ、残りのバインディングは無作為に見えるデータを提供します。これは kbmMW の SmartBinding データジェネレーターによるデータ生成のサンプルです。
`AutoBind` を呼び出す前に、使用可能なデータプレースホルダーを定義する必要があります。
var
mockSource: IkbmMWBindingData;
begin
mockSource := TkbmMWBindingDataGenerator.Create('{' +
' mockId:10,' +
' mockText:"abc",' +
' mockScore:88.4,' +
' randomVal: &random {' +
' type:number,' +
' random:true,' +
' min:10000,' +
' max:100000,' +
' step:5,' +
' interval:250' +
' },' +
' randIdx: *random,' +
' randFlag: *random,' +
' selectOpt: {' +
' type: values,' +
' random: true,' +
' values: ["AAA","BBB","CCC","DDD","EEE"]' +
' }' +
'}');
Binding.DefineData('demoCtx', mockSource);
Binding.AutoBind(Self);
end;
`DefineData` を呼び出すことで、`demoCtx`という名前と、生成および受け取り対象となるデータの間に関係性を持たせます。この場合、バインディングは `TkbmMWBindingDataGenerator` のインスタンスになります。
簡略化された YAML を含む文字列を受け取り、様々な命名プロパティに対してどのようなデータが生成されるべきか記述します。各命名プロパティは `mockId` や `mockText` のように定数値、あるいは特定のプロパティのデータ生成方法についてのオブジェクト記述となります。
`randomVal` を見ると、これはオブジェクト(中括弧で始まり終わる)であり、データ生成を定義する多くのプロパティを含んでいます。
タイプは現在以下のいずれかです。
- Simple:静的データ。静的情報のみを持つ命名プロパティは暗黙的に Simple として扱われます。
- number:数のようなものを返すジェネレーター。他のプロパティによって定義されます。
- values:定義済みの値群のうち 1 つを返すジェネレーター。他のプロパティによって定義されます。
numberの場合、以下の追加プロパティが利用可能です。
- random:true / false
true なら min と max の間を step で丸めてランダムに生成します。
false なら min からのインクリメント値(または指定値)を生成し、毎ループで step ずつ増加します。 - min:n
数値 n。このジェネレーターが返す値の下限界を設定します。 - max:n
数値 n。このジェネレーターが返す値の上限界を設定します。 - step:n
数値 n。ランダムまたはインクリメント値におけるステップ幅または丸め基準です。 - interval:n
n は msec 単位の数値。値が変更される頻度を表し、250 は 1 秒あたり最大 4 回の更新を意味します。 - decimals:n
小数点以下の桁数を定義します。デフォルトは 0 です。 - wrap:true / false
true かつランダムでない場合、境界到達時に自動でラップします。
false なら境界到達後の値は固定されます。 - reverse:true / false
true ならデータを逆順で返します。 - value:x
x は返される初期値です。
valuesの場合、以下の追加プロパティが利用可能です。
- random:true / false
true ならランダムに 1 つを選びます。
false なら次の値を選びます。 - step:n
数値 n。インクリメントまたはランダムにおけるステップ幅です。 - wrap:true / false
true かつランダムでない場合、values 配列の境界で自動ラップします。
false なら境界到達後の値は固定されます。 - reverse:true / false
true ならデータを逆順で返します。 - interval:n
n は msec 単位の数値。更新頻度を定義します。 - values:\[...,\...]
この命名プロパティが返せる値の配列です。
kbmMW はカスタムデータジェネレーターの種類の追加もサポートしています。
ここでは `randomVal` における YAML アンカー(`&random`)という特別な構文を使っています。定義されたセクションは他の命名プロパティ値で `*random` 構文を使って参照可能です。これにより `randIdx` と `randFlag` が同様のランダム値を返す理由となっています。
データジェネレーターの定義を理解したところで、開発プロセス中に本番データで代用する方法はどうすればよいでしょうか。
極めて簡単です。例えば以下のようにします。
type
TRealModel = class
private
FRealId: kbmMWNullable<integer>;
FRealText: kbmMWNullable<string>;
FRealScore: kbmMWNullable<double>;
// ... 他プロパティ ...
public
constructor Create;
property RealId: kbmMWNullable<integer> read FRealId write FRealId;
// ... 他プロパティ ...
end;
...
constructor TRealModel.Create;
begin
inherited;
FRealId := 21;
FRealText := 'ACTUAL_VALUE';
// ... 初期値設定 ...
end;
...
begin
modelObj := TRealModel.Create;
Binding.DefineData('demoCtx', modelObj);
end;
このコードを実行すると、データジェネレーターの実体として動的にオブジェクト `modelObj` のデータが利用されます。「Redefine test data」ボタンをクリックすると、下のようになることがわかります。
したがって、私たちは真正の関心点の分離をここに示しました。これは GUI と制御コードの独立設計を可能にし、チーム内でデータレイヤーを個別に開発することを支援します。
リストデータとデータベースの切り替え
デモの最終ページに行きましょう。
これもバインディング分離のケーススタディです。ここではオブジェクトリストを初期バインディングデータとして定義します。
TRow = class
private
FColA: kbmMWNullable<string>;
FColB: kbmMWNullable<integer>;
FColC: kbmMWNullable<double>;
public
property ColA: kbmMWNullable<string> read FColA write FColA;
property ColB: kbmMWNullable<integer> read FColB write FColB;
property ColC: kbmMWNullable<double> read FColC write FColC;
end;
TRows = TObjectList<TRow>;
...
FRows := TRows.Create;
...
var
rowX, rowY, rowZ: TRow;
begin
FRows := TRows.Create;
rowX := TRow.Create;
rowY := TRow.Create;
rowZ := TRow.Create;
rowX.ColA := 'First';
rowX.ColB := 1;
rowX.ColC := 1.1;
rowY.ColA := 'Second';
rowY.ColC := 2.2;
rowZ.ColA := 'Third';
rowZ.ColB := 3;
rowZ.ColC := 3.3;
FRows.Add(rowX);
FRows.Add(rowY);
FRows.Add(rowZ);
Binding.DefineData('listCtx', FRows);
end;
このコードでは、可視コントロールに FRows データをバインドしています。
Binding.Connect(FRows, 'ColA', edInput1, 'Text', [biTwoWay]).Navigator;
Binding.Connect(FRows, 'ColB', edInput2, 'Text', [biTwoWay]);
Binding.Connect(FRows, 'ColC', edInput3, 'Text', [biTwoWay]);
Next および Prev ボタンには以下のようなコードが含まれています。
// Prev ボタンのイベントハンドラ
var
nav: IkbmMWBindingNavigator;
begin
nav := Binding.GetDataNavigator('listCtx');
if Assigned(nav) then
nav.Previous;
end;
// Next ボタンのイベントハンドラ
var
nav: IkbmMWBindingNavigator;
begin
nav := Binding.GetDataNavigator('listCtx');
if Assigned(nav) then
nav.Next;
end;
このコードは以前の投稿で紹介したものとは若干異なります。バインディングとナビゲータを後で使うために保存せず、必要なときに定義されたバインディングの名前で要求しているからです。
実行すると、馴染みのある動作になります。
Prev と Next ボタンを使って値をスクロールできます。
しかし…。もし今、データベースの実際のデータでこのモデルリストを置き換えたい場合はどうでしょうか。
デモのために、メモリテーブルの内容をデータベースのデータとしてシミュレートします。
dtbl := TkbmMemTable.Create(nil);
dtbl.FieldDefs.Add('ColA', ftString, 30);
dtbl.FieldDefs.Add('ColB', ftInteger);
dtbl.FieldDefs.Add('ColC', ftFloat);
dtbl.CreateTable;
dtbl.Open;
dtbl.AppendRecord(['D1', 1, 111.1, 'Extra 1']);
dtbl.AppendRecord(['D2', 2, 222.2, 'Extra 2']);
dtbl.AppendRecord(['D3', 3, 333.3, 'Extra 3']);
dtbl.AppendRecord(['D4', 4, 444.4, 'Extra 4']);
これで TLines ベースのデータではなく「切り替える」だけですみます。「Redefine test2 data」のイベントハンドラは以下のようになります。
Binding.DefineData('listCtx', dtbl);
クリックすると、画面上の表示は即座に変更されます。
以前 FRows インスタンスにリンクしていたすべてのバインディングは、メモリテーブルインスタンスに再接続され、そのまま Prev と Next をクリックしてデータをスクロールし続けることができます。
その他の機能
上記の自動バインディング以外にも、多くの kbmMW サポートされる `$(configpath)` 構文を使用したコンフィグフレームワークからのデータ取得に加え、SmartBinding v2 には以下が含まれます。
- kbmMWNullable タイプのサポート
GroupedBy(..)とNamedBy(..)バインディングメソッド。文字列ベースの自動バインディングでグループ名とバインディング名を指定可能。同じ名とグループを持つバインディングが複数存在可能です。- 共通シングルトン
BindingGeneratorRegistrationsによるカスタムデータ生成器の登録 - フォーカス制御の検出機能改善。タイプライティング時の双方向バインディング処理がより優雅になります。
procedure UnbindBindings(const ABindings: TList <IkbmMWBinding>):バインディングリストの一度きりの解除procedure EnableBindings(const ABindings: TList <IkbmMWBinding>; const AEnable: boolean):リストの有効化または無効化procedure UnbindByGroup(const AGroup: string):グループ名に基づくバインディングの解除procedure UnbindByName(const AName: string):バインディング名に基づく解除procedure EnableByGroup(const AGroup: string; const AEnable: boolean):グループ名による有効化/無効化procedure EnableByName(const AName: string; const AEnable: boolean):バインディング名による有効化/無効化function BindingsByGroup(const AGroup: string): TList <IkbmMWBinding>:グループ名によるバインディングリストの取得function BindingsByName(const AName: string): TList <IkbmMWBinding>:バインディング名によるリスト取得