动态

详情 返回 返回

一步一步學習使用LiveBindings(6) 實現Master-Detail主從關係的綁定 - 动态 详情

主從式數據在應用程序的開發中是非常常見的,比如員工和電子郵件地址記錄,一個員工可能對應到多個郵件地址,這就形成了一對多的關係。在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。

你的項目結構應該像這樣:
img

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。

整體的佈局大概如下所示:

img

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

4. 右擊頁面空白處,從彈出的菜單中選擇“Bind Visually”進入LiveBindings Designer設計器,按如下步驟完成綁定操作。

雖然看起來LiveBindings是在將數據與UI進行鏈接,其實到目前為止,所做的工作是在UI與BindSource進行操作,至於BindSource是連接到底層的數據庫表還是對象,雖然在本篇中已經説明是對象,但是對於UI控件來説,目前是不清楚底層數據到底是數據庫還是對象類型的,也無需顧及。

進入設計器後,可以看到BindNavigator由於指定了DataSource屬性,所以設計器已經自動添加了鏈接。

首先,將bsPeople中的每一個欄位拖動到grdPeople中,不使用*是因為想對每一個列進行調整。而使用*是不可以的。

img

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

最後將3個Edit控件也鏈接上。
img

可以看到,LiveBindings Designer對於TEdit和TGrid都給了以向數據綁定(鏈接線2邊都有箭頭)。即用户在UI上的更改也可以更新回底層數據存儲。

現在運行程序,可以看到通過BindNavigator,可以對People進行移動,但是相應的Email並不會發生變化。不用擔心,底層的數據操作會完成這個功能。

img

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

img

代碼如下所示:

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. 代碼主體大致完工,現在可以預覽一下是否如預期。

img

現在可以看到,效果如預期,果然Master-Detail效果出現了。

如果你單擊“+”號,一個新的人員信息就出現了,郵件列表變為空,很明顯UI是進行了數據感知。這是調用到了TPeople的默認的無參數構造函數。

img

最後來一點錦上添花,當用户單擊電子郵件的導航欄的“+”號時,彈出一個輸入框,允許用户輸入電子郵件。

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;

再看看效果:

img

好了,已經接近預期了,這裏還有一些未完工的細節,限於本篇的篇幅,就不再介紹了。

最後附上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的應用,請保持關注哦。

Add a new 评论

Some HTML is okay.