kbmMWのスマートバインディング

kbmMWの最新リリースでは、スマートバインディングが実装されました。スマートバインディングの目標は以下の通りです:

  • 使いやすさ
  • コードの重複を最小限に抑えること
  • 高性能
  • CPUやメモリの消費を低減すること
  • 無限ループを引き起こさないこと
  • 様々なデータやコントロールとのバインディング
  • 柔軟性と拡張性
  • リアルタイムに近い動作
  • リファクタリングの容易さ
  • kbmMWの他の機能との良好な統合
  • kbmMWの他の機能を使用せずに利用可能であること

簡単なプロパティバインディングの例を見てみましょう。

シンプルなバインディング

Binding.Bind(Edit1, 'Text', Label1, 'Caption');
Binding.Bind(Edit1, 'Text', Button1, 'Caption');
Binding.Bind(Edit1, 'Text', Edit2, 'Text', [mwboTwoWay]);

このコードでは、Edit1に入力した内容がLabel1.Caption、Button1.Caption、Edit2.Textに反映されます。最後のバインディングは双方向で、Edit2に変更を加えた場合にもEdit1が更新されます。

BindingインスタンスはkbmMWのスマートバインディングに含まれており、すぐに使用できます。最後の行のオプションパラメータmwboTwoWayは、バインディングが同期的であり、一方の変更が自動的に他方に反映されることを示しています。

基本的には、文字列ブーリアン浮動小数点数int64整数などのプロパティを自動変換してバインドできます。スマートバインディングは必要に応じて異なる型間でデータを自動的に変換します。他の型のデータもバインドできますが、ソースとターゲットのプロパティが同じ型である必要があります(ただし、その他の方法もあります...後述)。

スレッドセーフティについてはどうでしょうか?kbmMWスマートバインディングは、TControlクラスから継承したコンポーネントを自動的に認識するため、アップデートは必ずメインスレッドで行われなければなりません。

次に、レコードのバインディングを見てみましょう。

レコードのバインディング

kbmMWスマートバインディングは、通常のオブジェクトやレコードへのバインディングを実現します。データが利用可能である限り、バインディングは維持されます。データが存在しなくなった場合、kbmMWスマートバインディングを利用してアンバインディングとリバインディングを行うことができます。以下のコードは、TEditとグローバルレコードのバインディングを示しています。

type
  TData = record
    FData1: string;
  end;

var
  data: TData;
...
Binding.Bind(Edit1, 'Text', @data, TypeInfo(TData), 'FData1');
Binding.Bind(@data, TypeInfo(TData), 'FData1', EditN, 'Text');

これにより、Edit1のすべての変更がデータレコードのFData1フィールドに自動的に反映され、データレコードのFData1フィールドの変更も自動的にEditNのTextプロパティに表示されます。

スレッドセーフティについて:

上記の例は単純ですが、両方のバインディングがTControlサブクラスを参照しているため、データレコードのポーリングと更新は必ずメインアプリケーション/GUIスレッドで行われなければなりません。そのため、他のスレッドがdata.FData1にアクセスしない限り、これは安全に動作します。

他のスレッドでレコードのFData1フィールドを変更する場合は、通常のスレッドデータロックが必要です。TkbmMWLockツールを利用してこれを解決できます。

kbmMW SimpleBindingは、データソースまたはターゲットとしての任意のオブジェクトインスタンスの組み合わせをサポートしています。

レコードのバインディングが可能になったので、次にオブジェクトリストのバインディングについて見ていきましょう。

オブジェクトリストのバインディング

次の例では、オブジェクトリストのバインディング方法を示します。まず、いくつかのデータを持つリストを宣言します。

type
  TLine = class
  private
    FName: string;
    FAddress: string;
  public
    constructor Create(const AName: string; const AAddress: string);
    property Name: string read FName write FName;
    property Address: string read FAddress write FAddress;
  end;

  TLines = TObjectList<TLine>;

var
  lines: TLines;
...
lines := TLines.Create;
lines.Add(TLine.Create('Hans', 'Hansvej 1'));
lines.Add(TLine.Create('Jens', 'Jensvej 1'));
lines.Add(TLine.Create('Frederik', 'Frederikvej 1'));
lines.Add(TLine.Create('Jonas', 'Jonasvej 1'));

次に、linesオブジェクトリストをEdit1、Edit2のビジュアルコンポーネントとバインドします。

var
  bnd: IkbmMWBinding;
begin
  Binding.Clear;
  bnd := Binding.Bind(lines, 'Name', Edit1, 'Text');
  Binding.Bind(lines, 'Address', Edit2, 'Text');
  if bnd.Navigator <> nil then
    bnd.Navigator.First;
end;

ここでは、2つの新しい内容を紹介します:

  • Binding.Clearを利用して既存のすべてのバインディングを削除する方法
  • bnd.Navigatorを利用してリストをナビゲートする方法

グローバル変数がIkbmMWBindingを参照している場合、Binding.Clearを呼び出す前にそれをnilに設定する必要があります。特にデータセットをデータソースとして使用する場合(後述)、kbmMWはバインディング間の共有データセットを信頼性高く追跡できなくなります。

Binding.Bindの呼び出しはIkbmMWBindingインターフェースを返します。このインターフェースを利用して特定のバインディングを操作できます。IkbmMWBindingは、IkbmMWBindingNavigator型のNavigatorという便利なプロパティを提供します。Navigatorを利用すると、linesのようなナビゲーション可能なデータソースに簡単にアクセスできます。バインディングのデータソースがリストでない場合は、Navigatorはnilになります。

これで、bnd.Navigator.First/Last/Next/Previousを利用してナビゲートし、ブックマークを取得することができます。

特定のソースに対するすべてのバインディングには、Navigatorは共通で単一です。異なるナビゲーション可能なデータソースにはそれぞれ独自のNavigatorインスタンスがあります。

TDataSetとのバインディング

スマートバインディングは、ビジュアルコンポーネントとTDataSetサブクラスとのバインディングを実現します。TDataSetのサブクラスであれば、バインディングが可能です。ここでは、biolife.csvをTkbmMemTableに読み込んでデータソースを作成します。

var
  mt: TkbmMemTable;
  csv: TkbmCSVStreamFormat;
begin
  csv := TkbmCSVStreamFormat.Create(nil);
  try
    mt := TkbmMemTable.Create(nil);
    mt.LoadFromFileViaFormat('biolife.csv', csv);
  finally
    csv.Free;
  end;

次に、Editコントロールをバインドします。

Binding.Clear;
bnd := Binding.Bind(mt, 'Category', Edit1, 'Text', [mwboTwoWay]);
Binding.Bind(mt, 'Species Name', Edit2, 'Text', [mwboTwoWay]);
if bnd.Navigator <> nil then
  bnd.Navigator.First;

オブジェクトリストのバインディングと同様に、完全に同じバインディング方法を使用します。この例では、双方向バインディングを使用しています。mwboTwoWayオプションは、Edit1とEdit2がDelphiのデータ感知コントロールであるTDBEditコントロールと同じように動作することを目的としています。ナビゲーター(bnd.Navigator)を利用して、データセットを簡単にナビゲートできます。

ここで、Edit1に内容を入力すると、Categoryフィールドに自動的に保存されます。mtの現在レコードが変更されると、Edit1には現在レコードの内容が表示されます。また、コードでデータセットの内容を変更すると、対応するEditコントロールにも表示されます。例えば、次のコードを実行します。

mt.Edit;
mt.FieldByName('Category').AsString := 'テスト!!!';
mt.Post;

Edit1には「テスト!!!」が表示されます。

ListBoxやComboBoxとのバインディング

時折、データソースリスト/データセット(またはその一部)を使用して、TListTComboBox(またはそのサブクラス)を埋めたいことがあります。

この例では、どちらかのコントロールで行われた選択が自動的に他のコントロールに反映されるように同期したいと考えています。

Binding.Clear;
Binding.Bind(mt, 'Species Name', ComboBox1, 'Items');
Binding.Bind(mt, '@', ComboBox1, 'ItemIndex', [mwboTwoWay]);
bnd := Binding.Bind(mt, 'Common_Name', ListBox2, 'Items');
Binding.Bind(mt, '@', ListBox2, 'ItemIndex', [mwboTwoWay]);
if bnd.Navigator <> nil then
  bnd.Navigator.First;

まず、データセットmtSpecies NameフィールドをTComboBoxItemsプロパティにバインドし、その後、別のフィールドCommon_NameTListBoxItemsにバインドします。次に、@を使用してデータソースの位置を表します。これは、kbmMWスマートバインディングがデータソースリストまたはデータセットの位置を参照することを意味します。ここでは、データソースの位置をTListBoxTComboBoxItemIndex(位置)にバインドします。さらに、スマートバインディングに対してmwboTwoWayという2つの方法を指定することで、データソースナビゲーターの位置の変更はTListBoxTComboBoxの位置を更新し、コントロールで何かを選択するとソースリスト/データセットの位置も自動的に更新されます。このように、2つのコントロールが互いに自動的に同期され、ソースの一貫性が保たれます。

グリッドへのバインディング

以下のコードでは、データセットmtをグリッドの特定の列にバインドします。さらに、@を使用してデータセットの現在レコード位置をグリッドの現在行位置とバインドし、また@を使用してデータセット位置をグリッドの最初の列とバインドします。さらに、TEditをグリッドのRowCountプロパティにバインドし、実行時にグリッドの行数(つまり、データセットのレコード数)を表示します。

Binding.Clear;
bnd := Binding.Bind(mt, 'Category', StringGrid1, '#1');
Binding.Bind(mt, 'Species Name', StringGrid1, '#2');
Binding.Bind(mt, '@', StringGrid1, '@', [mwboTwoWay]); // データソースとコントロールの位置を双方向にバインド

// グリッドの最初の列に位置数を表示
Binding.Bind(mt, '@', StringGrid1, '#0');

// ランタイムでの変更を簡単に行うために、RowCountにバインド
Binding.Bind(leRowCount, 'Text', StringGrid1, 'RowCount');

if bnd.Navigator <> nil then
  bnd.Navigator.First;

これもこれまでのバインディング方法と変わりありませんが、今回は#n構文を使用して、データセットのフィールドをグリッドの特定の列(0始まり)にバインドしています。

ナビゲーターを使用することで、グリッドはTDBGridのように動作します。データセットの@をグリッドの@とバインドすることで、ソースデータセットをスクロールするとグリッドは自動的に現在行位置を更新し、グリッドの現在選択行を変更するとデータセットの現在レコード位置も自動的に更新されます。データセットの現在レコードの変更は自動的にグリッドに反映され、双方向バインディングを使用しているため、グリッドの現在行にデータを入力するとそれがデータソースに戻ります。

2019-05-22 追記:筆者は5.09版でテストを行い、上記のバインディング方法を使用してグリッドで行った変更が自動的にデータセットに更新されないことに気付きました。代わりに次の行のコードでバインドを行います。

bnd := Binding.Bind(dataset, 'Category', StringGrid1, '#1', [mwboTwoWay]);

これにより、グリッドの最初の行が正しく表示されません。[mwboTwoWay]を削除すると、正常に表示されます。

匿名関数へのバインディング

バインディングのデータソースまたはターゲットとして、オブジェクト、データ、またはデータセットだけでなく、匿名関数も使用できます。

// Edit1.Textが変更されたときに呼び出す関数を表示する。
Binding.Bind(Edit1, 'Text', procedure(const AProxy: TkbmMWBindingCustomProxy; var AValue: TValue)
  begin
    Log.Debug('バインディングからEdit1への変更: ' + AValue.ToString);
  end);

上の例は基本的に、Edit1.OnChangeイベントハンドラとして匿名関数を登録することを意味します。Edit1.Textの変更が検出され、匿名関数が自動的に実行されます。この例では、kbmMWログフレームワークTkbmMWLogを利用して変更を記録しています。

次に、匿名関数をデータソースとして使用する例を見てみましょう。

Binding.Bind(procedure(const AProxy: TkbmMWBindingCustomProxy; var AValue: TValue)
  begin
    AValue := Random(100);
  end, Edit1, 'Text');

匿名関数が繰り返し呼び出されます。関数が新しい値を返すたびに、Edit1.Textが更新されます。ポーリング関数の頻度は、バインディングを定義するために使用するBindingインスタンスのプロパティUpdateFrequencyの設定によって異なります。デフォルトの頻度は1秒間に10回ですが、必要に応じて頻度を変更できます。

Binding.UpdateFrequency := 1000;

拡張バインディング

あるソースから値を取得するバインディングを定義し、それをラベルに出力したいが、形式が異なる場合はどうすればよいでしょうか?

Binding.Bind関数から返される結果インターフェースで利用可能なToDestinationExpressionメソッドを使用します。

// Edit1.Textの変更を監視し、その表示形式を整形する。
Binding.Bind(procedure(const AProxy: TkbmMWBindingCustomProxy; var AValue: TValue)
  begin
    AValue := Random(100);
  end, Edit1, 'Text')
  .ToDestinationExpression('"こんにちは " + data');

上のコードでは、前の関数バインディングの例を単純に取り上げ、kbmMWスマートバインディングがToDestinationExpression関数で指定された文字列式に基づいてターゲットパスにデータを追加することを求めています。この例では、Edit1.Textが「こんにちは」という値とランダムな数値を含むようになります。

文字列式はkbmMWの他の場所でも使用されている同じ表現処理機能に基づいており、非常に豊富です。これらの機能はkbmMemTablesの機能を活用したSQLパーサーと評価器のものです。この場合、SQL自体ではなく、式の部分のような数学のみをサポートしていますが、期待できるすべての一般的な操作を含む多くの便利な変換、正規表現、数学、条件評価などが使用可能です。

バインディングは双方向であるため、データソースに戻る際に値を整形したり、整形された値を処理したりする必要もあります。そのため、スマートバインディングはToSourceExpression関数も提供しています。

Binding.Bind(Edit1, 'Text', Edit2, 'Text', [mwboTwoWay])
  .ToDestinationExpression('"こんにちは " + data')
  .ToSourceExpression('Mid(data, 8)');

この例では、Edit1.Textの内容をEdit2.Textに入れ、「こんにちは」をプレフィックスとしています。しかし、Edit2.Textで行われた変更を認識し、最初の7文字を削除した後、そのテキストをEdit1.Textに移動させることも可能です。このようなタイプの双方向バインディングは、イベント駆動のバインディングを混乱させる可能性があります。しかし、kbmMWスマートバインディングはこれらのイベントに影響を受けず、最小限のコストで更新を行います。

バインディングの有効化、無効化、アンバインディング、リバインディング

時折、バインディングが作業を行うことを阻止したい場合があります。予防が一時的なものである場合、無効化することが一つの方法です。

var
  bnd: IkbmMWBinding;
begin
  bnd := Binding.Bind(...);
  ...
  bnd.Disable := true;
  ...
  bnd.Disable := false;
end;

永久的に無効化したい場合は、削除することもできます。そのためにUnbindメソッドが存在します。

Binding.UnbindSource(Edit1);

これにより、Edit1がどのバインディングのデータソースとしてもバインドされなくなります。

Binding.UnbindDestination(Edit2);

これにより、Edit2がどのバインディングのターゲットとしてもバインドされなくなります。

Bindメソッド呼び出し時に返されたIkbmMWBindingを利用して、バインディングを解除することもできます。

var
  MyBinding: IkbmMWBinding;
begin
  MyBinding := Binding.Bind(...);
  ...
  Binding.Unbind(MyBinding);
end;

匿名関数にバインドしていない場合、バインディングとまったく同じパラメータを利用してバインディングを解除することも可能です。

Binding.Bind(Edit1, 'Text', Edit2, 'Text');
...
Binding.Unbind(Edit1, 'Text', Edit2, 'Text');

最後に、バインディングを再び有効にする必要があるかもしれません。Rebindは、あるソースまたはターゲットインスタンスから別のソースまたはターゲットインスタンスへのバインディングを変更することができます。これは、一時的なレコードやオブジェクトにバインドする際に特に興味深いです。

Binding.Rebind(@data, @data2);

これにより、レコードやメモリバッファ「data」を参照しているどのバインディングも変更され、これらのバインディングはレコードやメモリバッファ「data2」を参照するように更新されます。

同様に、コントロールを再バインドすることも可能です。

Binding.Rebind(Edit1, NewEdit1);

Edit1を参照しているすべてのバインディングは、今後はNewEdit1を参照するようになります。

結論ランタイムバインディングの構文は一貫しており、シンプルであり、ユーザーインターフェイスやコントロールをリファクタリングする際にバインディングを簡単にリファクタリングすることができます。

以前述べたように、既存のスレッドセーフなバインディングシングルトンの他に、独自のバインディングマネージャインスタンスを作成することもできます。これを行う理由には、異なるバインディングを異なる時間間隔で更新したい場合や、非常に簡単にすべてのバインディングを破棄または再作成したい場合などがあります。たとえば、単純なフレーム内のフレームで、明示的にそれぞれを解除せずに他のフレームで定義されたすべての他のバインディングに影響を与えないようにしたい場合などです。

var
  myBindingMgr: TkbmMWBindings;
begin
  myBindingMgr := TkbmMWBindings.Create(1000);
  ...
  myBindingMgr.Free;
end;

この例では、他のバインディングに影響を与えない別のバインディングマネージャを作成します。これは1秒ごとにポーリングします。不要になったら必ず自分で作成したバインディングマネージャを解放してください。

バインディングをより簡単にし、さらなる機能を追加するためのアイデアはたくさんありますが、これらは次のkbmMWエンタープライズ版のベータ版に含まれます。

kbmMWは、エンコーディング作業を簡素化し、ビジネス機能に焦点を当てて基本的なコードを作成せずに済むように設計されています。なぜなら、最終ユーザーコードを開発する際に基本的な作業を行いたくないからです。実際にこれらのものをあなたのために開発したわけではありませんが、自分自身のために開発しました。これは少し自慢と自私的かもしれませんが、あなたもそれを楽しむことを願っています。

その後のアップデートで、FMX ListViewコントロールへのスマートバインディングが実装されました。

タグ: kbmMW スマートバインディング Delphi データバインディング TDataSet

7月26日 20:47 投稿