主從式數據在應用程序的開發中是非常常見的,比如員工和電子郵件地址記錄,一個員工可能對應到多個郵件地址,這就形成了一對多的關係。在VCL中,數據控件處理主從式綁定非常方便簡潔,在這個示例中,學習如何使用LiveBindings的TProtoTypeBindSource控件來實現對象間的主從式的數據綁定。
注意:這個示例來自《Delphi Cookbook》中的Using master/details with LiveBindings,需要獲取詳細信息可以參考這本書.
現在請打開Delphi 12.3,按如下的步驟重新實現一個基於主從關係的面向對象的LiveBindings示例。
1. 單擊主菜單中的 File > New > Multi-Device Application - Delphi > Blank Application ,創建一個新的多設備應用程序。
建議立即單擊工具欄上的Save All按鈕,將單元文件保存為uMainForm.pas,將項目保存為LiveBinding_MasterDetail.dproj。
你的項目結構應該像這樣:

2. 在表單上放置兩個 TGrid 組件,並將它們命名為 grdPeople 和 grdEmails 。將兩個組件的 Options.AlternatingRowBackground 屬性設置為 True。將 grdPeople 的 Options.RowSelection 設置為 True。在表單上放置兩個 TPrototypeBindSource 組件,並將它們命名為 bsPeople 和 bsEmails 。
- 在表單上放置一個 TBindNavigator 組件,並將其 DataSource 屬性連接到 bsPeople。
- 在表單上再放置另一個 TBindNavigator 組件,並將其 DataSource 屬性連接到 bsEmails。然後,將其 VisibleButtons 屬性中的所有元素設置為 False,僅將 nbInsert 和 nbDelete 設置為 True(這將允許您從人員中插入或刪除任何電子郵件)。
- 在表單上放置三個 TEdit 組件,並將它們命名為 EditFirstName、EditLastName 和 EditAge。
整體的佈局大概如下所示:

3. 接下來分別為bsPeople和bsEmails添加字段和指定數據生成器。雙擊bsPeople,將打開Fields Editor,添加如下所示的字段:
雙擊bsEmails,添加如下所示的字段:

4. 右擊頁面空白處,從彈出的菜單中選擇“Bind Visually”進入LiveBindings Designer設計器,按如下步驟完成綁定操作。
雖然看起來LiveBindings是在將數據與UI進行鏈接,其實到目前為止,所做的工作是在UI與BindSource進行操作,至於BindSource是連接到底層的數據庫表還是對象,雖然在本篇中已經説明是對象,但是對於UI控件來説,目前是不清楚底層數據到底是數據庫還是對象類型的,也無需顧及。
進入設計器後,可以看到BindNavigator由於指定了DataSource屬性,所以設計器已經自動添加了鏈接。
首先,將bsPeople中的每一個欄位拖動到grdPeople中,不使用*是因為想對每一個列進行調整。而使用*是不可以的。

注意:當將每一列拉到TGrid控件上後,TGrid會自動為每一列生成一個TLinkGridToDataSourceColumn,在設計器的Column Editor中可以編輯列寬,指定每一列的自定義顯示格式等等。
最後將3個Edit控件也鏈接上。

可以看到,LiveBindings Designer對於TEdit和TGrid都給了以向數據綁定(鏈接線2邊都有箭頭)。即用户在UI上的更改也可以更新回底層數據存儲。
現在運行程序,可以看到通過BindNavigator,可以對People進行移動,但是相應的Email並不會發生變化。不用擔心,底層的數據操作會完成這個功能。

5. 現在新建一個實體類,用來存放底存數據和邏輯。如本文開頭所述,這裏引用了《Delphi Cookbook》中的示例代碼,因此將包含示例中的實體類BusinessObjectsU.pas單元引入到了項目中,讀者可以新建一個名為BusinessObjectsU.pas的單元,將下面的代碼拷進去。
BusinessObjectsU.pas中包含了兩個類,TPeople表示是單個個體人,它包含一個泛型的TEmail類型的屬性集合Emails,表示一個人可以擁有多個電子郵件地址。

代碼如下所示:
unit BusinessObjectsU;
interface
uses
System.Generics.Collections;
type
/// <summary>
/// Email實體類,僅簡單的記錄了郵件地址。
/// <summary>
TEmail = class
private
FAddress: String;
procedure SetAddress(const Value: String);
public
//包含重載的構造函數。
constructor Create; overload;
constructor Create(AEmail: String); overload;
property Address: String read FAddress write SetAddress;
end;
/// <summary>
/// 個人實體類,表示單個人,包含多個郵件地址
/// </summary>
TPerson = class
private
FLastName: String;
FAge: Integer;
FFirstName: String;
//定義一個泛型集合類型,用來包含多個TEmail類。
FEmails: TObjectList<TEmail>;
procedure SetLastName(const Value: String);
procedure SetAge(const Value: Integer);
procedure SetFirstName(const Value: String);
function GetEmailsCount: Integer;
public
//包含重載的構造函數,用來初始化屬性值。
constructor Create; overload;
constructor Create(const FirstName, LastName: string; Age: Integer);
overload; virtual;
destructor Destroy; override;
property FirstName: String read FFirstName write SetFirstName;
property LastName: String read FLastName write SetLastName;
property Age: Integer read FAge write SetAge;
property EmailsCount: Integer read GetEmailsCount;
property Emails: TObjectList<TEmail> read FEmails;
end;
implementation
uses
System.SysUtils;
{ TPersona }
constructor TPerson.Create(const FirstName, LastName: string; Age: Integer);
begin
Create;
FFirstName := FirstName;
FLastName := LastName;
FAge := Age;
end;
// 由LiveBindings調用來插入一個新行。
constructor TPerson.Create;
begin
inherited Create;
FFirstName := '<name>';
//初始化郵件列表
FEmails := TObjectList<TEmail>.Create(true);
end;
destructor TPerson.Destroy;
begin
FEmails.Free;
inherited;
end;
function TPerson.GetEmailsCount: Integer;
begin
Result := FEmails.Count;
end;
procedure TPerson.SetLastName(const Value: String);
begin
FLastName := Value;
end;
procedure TPerson.SetAge(const Value: Integer);
begin
FAge := Value;
end;
procedure TPerson.SetFirstName(const Value: String);
begin
FFirstName := Value;
end;
{ TEmail }
constructor TEmail.Create(AEmail: String);
begin
inherited Create;
FAddress := AEmail;
end;
// 由LiveBindings調用來插入一個新行。
constructor TEmail.Create;
begin
Create('<email>');
end;
procedure TEmail.SetAddress(const Value: String);
begin
FAddress := Value;
end;
end.
兩個實體類都包含了重載的構造函數,不帶參數的構造函數將由LiveBindings調用來生成新的行,而帶參數的構造函數將用來生成初始數據,這些數據可以是來自底層的數據庫表,也可以是像示例這樣,使用了一個隨機數單元來生成數據數據。
6. 回到主窗體,開始對主窗體進行編碼了。前面的步驟中在主窗體上放了2個TProtoTypeBindSource控件,這2個控件自帶數據生成器,它就好像是TAdapterBindSource和TDataGeneratorAdapter的結合體。因此它也提供了OnCreateAdapter事件,通過處理這個事件,來將前面創建的實體數據集合橋接給UI控件。
類似於第5課的代碼,首先需要在窗體類的private中添加泛型的集合類FPeople,第1步是添加對實體類單元的引用。
uses
System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, System.Rtti,
FMX.Grid.Style, Data.Bind.Controls, FMX.Layouts, Fmx.Bind.Navigator,
FMX.Controls.Presentation, FMX.ScrollBox, FMX.Grid, Data.Bind.Components,
Data.Bind.ObjectScope, FMX.StdCtrls, FMX.Edit, Data.Bind.GenData,
Data.Bind.EngExt, Fmx.Bind.DBEngExt, Fmx.Bind.Grid, System.Bindings.Outputs,
Fmx.Bind.Editors, Data.Bind.Grid,
//添加對業務實體單元的引用
BusinessObjectsU,System.Generics.Collections;
由於要處理Master-Detail的關係,這裏沒有像第5課那樣直接在OnCreateAdapter事件中創建ABindSourceAdapter的實例,因為要控制ABindSourceAdapter的實例,所以將2個TListBindSourceAdapter的實例定義在了private區。
private
//代表人員信息的泛型集合類
FPeople: TObjectList<TPerson>;
//用來存儲人員信息的Adapter類。
bsPeopleAdapter: TListBindSourceAdapter<TPerson>;
//用來存儲電子郵件地址的Adapter類。
bsEmailsAdapter: TListBindSourceAdapter<TEmail>;
接下來給bsPeople的OnCreateAdapter添加事件處理代碼,主要用來實例化bsPeopleAdapter,然後給ABindSourceAdapter賦值,這個事件在TProtoTypeBindSource實例化後觸發,先於FormCreate事件,代碼如下所示:
procedure TfrmMain.bsPeopleCreateAdapter(Sender: TObject;
var ABindSourceAdapter: TBindSourceAdapter);
begin
//初始化bsPeopleAdapter類,在這裏第2個參數為nil,表示並沒有為其指定列表數據。
bsPeopleAdapter := TListBindSourceAdapter<TPerson>.Create(self, nil, False);
//將bsPeopleAdapter賦給ABindSourceAdapter;
ABindSourceAdapter := bsPeopleAdapter;
//關聯AfterScroll事件,在People切換到下一行時觸發
bsPeopleAdapter.AfterScroll := PeopleAfterScroll;
end;
在這裏構建了一個不帶List的TListBindSourceAdapter實例,然後賦給ABindSourceAdapter,並且有趣的是,還給TListBindSourceAdapter關聯了一個AfterScroll事件,這個事件在VCL的TQuery之類的控件中很常見。
實際上,將它們視為數據集。
所有的適配器類都從TBindSourceAdapter上繼承,TBindSourceAdapter實現了接口IBindSourceAdapter,查看TBindSourceAdapter上公開的方法和屬性,會發現許多與 TDataset 相似或完全相同的方法,例如:
- 一個狀態屬性,類型為 TBindSourceAdapterState,其值有 seInactive、* seBrowse、seEdit 和 seInsert。
- ( BOF 和 EOF 屬性,以及 Next、Prior、First 和 Last 方法。
- Edit、Insert、Append、Post 和 Cancel 方法。
- Insert、Open、Post、Scroll 等事件的前置和後置事件,等等……
實現Master-Detail的核心就是在PeopleAfterScroll過程中,當切換到下一個記錄時,自動給bsEmail控件的ABindSourceAdapter指定List。
代碼如下所示:
procedure TMainForm.PeopleAfterScroll(Adapter: TBindSourceAdapter);
begin
//得到當前選中的人員的Emails列表
bsEmailsAdapter.SetList(bsPeopleAdapter.List[bsPeopleAdapter.CurrentIndex]
.Emails, False);
//將bsEmails.Active設置為True,其實就是在將其內部的InternalAdapter的Active設置為True.
bsEmails.Active := True;
//上位到第1行記錄。
bsEmails.First;
end;
在代碼裏邊,調用bsEmailsAdapter的SetList為bsEmailsAdapter指定了列表值,因為類似於bsPeopleCreateAdapter,它也只是實例化了bsEmailsAdapter,並未給出列表。
然後bsEmails就好像是一個TDataSet開始工作了,指定Active激活,調用其First定位到第1條記錄,其實是通過設置咱們在OnCreateAdapter中指定的Adapter來工作的,也就是説bsEmails有一個InternalAdapter的屬性,它代表在運行時指定的真正的Adapter。
下面是bsEmailsCreateAdapter的代碼:
procedure TMainForm.bsEmailsCreateAdapter(Sender: TObject;
var ABindSourceAdapter: TBindSourceAdapter);
begin
//初始化bsEmailsAdapter類,在這裏第2個參數為nil,表示並沒有為其指定列表數據。
bsEmailsAdapter := TListBindSourceAdapter<TEmail>.Create(self, nil, False);
//將實例賦給 ABindSourceAdapter
ABindSourceAdapter := bsEmailsAdapter;
end;
現在已經給bsEmails給了列表數據,但是bsPeople還沒有指定List,這是在FormCreate事件中完成的,事件代碼如下:
procedure TfrmMain.FormCreate(Sender: TObject);
begin
Randomize; //初始化隨機因子
//創建List實例
FPeople := TObjectList<TPerson>.Create(True);
LoadData; //加載隨機的人員信息
//為bsPeopleAdapter指定List
bsPeopleAdapter.SetList(FPeople, False);
//激活UI的顯示。
bsPeople.Active := True;
end;
由於人員信息是隨機生成的,因此第1行代碼調用了Randomize初始化隨機因子,或什麼其他的叫法,就是確保隨機數很隨機。
然後構建了TObjectList的實例,LoadData是一個私有過程,用來生成隨機的人員信息,請拉到本篇最後進行代碼拷貝。
同樣的給bsPeopleAdapter設置列表。
注意SetList的第2個參數AOwnersObject,指定是否接管這個對象的釋放,在這裏設置為False,表示自己釋放,因此在FormDestroy事件中,要添加對FPeople的Free代碼。
procedure TMainForm.FormDestroy(Sender: TObject);
begin
FPeople.Free; //手動釋放FPeople對象
end;
LoadData過程會使用RandomUtilsU.pas單元中定義的隨機生成函數,因此建議在Interface區的uses子句中添加RandomUtilsU。
//添加對業務實體單元的引用
uses
BusinessObjectsU,System.Generics.Collections,RandomUtilsU;
LoadData代碼如下:
private
{ Private declarations }
//代表人員信息的泛型集合類
FPeople: TObjectList<TPerson>;
//用來存儲人員信息的Adapter類。
bsPeopleAdapter: TListBindSourceAdapter<TPerson>;
//用來存儲電子郵件地址的Adapter類。
bsEmailsAdapter: TListBindSourceAdapter<TEmail>;
procedure PeopleAfterScroll(Adapter: TBindSourceAdapter);
procedure LoadData;
var
frmMain: TfrmMain;
implementation
procedure TfrmMain.LoadData; //加載隨機的人員信息
var
I: Integer;
P: TPerson;
X: Integer;
begin
for I := 1 to 100 do
begin
//創建隨機生成的人員信息
P := TPerson.Create(GetRndFirstName, GetRndLastName, 10 + Random(50));
// 隨機添加1-3個郵件地址
for X := 1 to 1 + Random(3) do
begin
P.Emails.Add(TEmail.Create(P.FirstName.ToLower + '.' + P.LastName.ToLower
+ '@' + GetRndCountry.Replace(' ', '').ToLower + '.com'));
end;
//添加到列表
FPeople.Add(P);
end;
end;
感覺到代碼實在是有點長,請列位看官多多諒解。
7. 代碼主體大致完工,現在可以預覽一下是否如預期。

現在可以看到,效果如預期,果然Master-Detail效果出現了。
如果你單擊“+”號,一個新的人員信息

最後來一點錦上添花,當用户單擊電子郵件的導航欄的“+”號時,彈出一個輸入框,允許用户輸入電子郵件。
TBindNavigator有一個OnBeforeAction事件,通過實現這個事件來完成這個需求。
procedure TfrmMain.bnEmailBeforeAction(Sender: TObject;
Button: TBindNavigateBtn);
var
email: string;
begin
if Button = TNavigateButton.nbInsert then //如果用户單擊插入按鈕。
if InputQuery('Email', '輸入新的郵件地址', email) then
begin
bsEmailsAdapter.List.Add(TEmail.Create(email));
bsEmails.Refresh; // 刷新郵件列表,用來實現UI同步。
bsPeople.Refresh; // 刷新人員列表,用來實現UI同步。
Abort; // 中斷標準的行為
end;
end;
再看看效果:

好了,已經接近預期了,這裏還有一些未完工的細節,限於本篇的篇幅,就不再介紹了。
最後附上RandomUtilsU.pas的代碼:
unit RandomUtilsU;
interface
const
FirstNames: array [0 .. 9] of string = (
'Daniele',
'Debora',
'Mattia',
'Jack',
'James',
'William',
'Joseph',
'David',
'Charles',
'Thomas'
);
LastNames: array [0 .. 9] of string = (
'Smith',
'Johnson',
'Williams',
'Brown',
'Jones',
'Miller',
'Davis',
'Wilson',
'Martinez',
'Anderson'
);
Countries: array [0 .. 9] of string = (
'Italy',
'New York',
'Illinois',
'Arizona',
'Nevada',
'UK',
'France',
'Germany',
'Norway',
'California'
);
HouseTypes: array [0 .. 9] of string = (
'Dogtrot house',
'Deck House',
'American Foursquare',
'Mansion',
'Patio house',
'Villa',
'Georgian House',
'Georgian Colonial',
'Cape Dutch',
'Castle'
);
function GetRndFirstName: String;
function GetRndLastName: String;
function GetRndCountry: String;
function GetRndHouse: String;
implementation
function GetRndHouse: String;
begin
Result := 'Mr.' + GetRndLastName + '''s ' + HouseTypes[Random(10)] + ' (' + GetRndCountry + ')';
end;
function GetRndCountry: String;
begin
Result := Countries[Random(10)];
end;
function GetRndFirstName: String;
begin
Result := FirstNames[Random(10)];
end;
function GetRndLastName: String;
begin
Result := LastNames[Random(10)];
end;
end.
感謝《Delphi Cookbook》的作者Daniele Spinetti,Daniele Teti,Daniele Teti也是Delphi MVC Framework的開發者,多年前我曾與他有過一次Email來往,在我的博文中,有機會將會詳細介紹這個框架。
一點點擴展的思考,對於這個案例可以應用於移動應用,比如在BeforeOpen事件中,從Server端獲取JOSN數據,轉換成實體對象,也可以在beforePost中將對象轉換成JSON,然後發送到Server端進行存儲。
下一章,將繼續一些深入挖掘LiveBindings的應用,請保持關注哦。