一步一步学习使用LiveBindings(6) 实现Master-Detail主从关系的绑定

一步一步学习使用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。

你的项目结构应该像这样:

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,表示一个人可以拥有多个电子邮件地址。

代码如下所示:

Pascal 复制代码
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步是添加对实体类单元的引用。

Pascal 复制代码
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区。

Pascal 复制代码
  private
    //代表人员信息的泛型集合类
    FPeople: TObjectList<TPerson>;
    //用来存储人员信息的Adapter类。
    bsPeopleAdapter: TListBindSourceAdapter<TPerson>;
    //用来存储电子邮件地址的Adapter类。
    bsEmailsAdapter: TListBindSourceAdapter<TEmail>;

接下来给bsPeople的OnCreateAdapter添加事件处理代码,主要用来实例化bsPeopleAdapter,然后给ABindSourceAdapter赋值,这个事件在TProtoTypeBindSource实例化后触发,先于FormCreate事件,代码如下所示:

Pascal 复制代码
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。

代码如下所示:

Pascal 复制代码
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的代码:

Pascal 复制代码
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事件中完成的,事件代码如下:

Pascal 复制代码
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代码。

Pascal 复制代码
procedure TMainForm.FormDestroy(Sender: TObject);
begin
  FPeople.Free;   //手动释放FPeople对象
end;

LoadData过程会使用RandomUtilsU.pas单元中定义的随机生成函数,因此建议在Interface区的uses子句中添加RandomUtilsU。

Pascal 复制代码
  //添加对业务实体单元的引用
  uses

  BusinessObjectsU,System.Generics.Collections,RandomUtilsU;

LoadData代码如下:

Pascal 复制代码
  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效果出现了。

如果你单击"+"号,一个新的人员信息 就出现了,邮件列表变为空,很明显UI是进行了数据感知。这是调用到了TPeople的默认的无参数构造函数。

最后来一点锦上添花,当用户单击电子邮件的导航栏的"+"号时,弹出一个输入框,允许用户输入电子邮件。

TBindNavigator有一个OnBeforeAction事件,通过实现这个事件来完成这个需求。

Pascal 复制代码
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的代码:

Pascal 复制代码
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的应用,请保持关注哦。

相关推荐
lincats1 天前
一步一步学习使用LiveBindings(5) 使用TAdapterBindSource实现对象绑定
livebindings·delphi 12.3·firedac·firemonkey
tanqth3 个月前
使用Delphi 和 CrossVcl 开发基于VCL的 macOS 和 Linux 应用程序简介
linux·delphi·crossvcl·vcl开发的linux
我不是代码教父4 个月前
[原创](现代Delphi 12指南): 设置、运行和调试你的第一个macOS应用程序.
macos·delphi
和码说5 个月前
编程考古-忘掉它,Delphi 8 for the Microsoft .NET Framework
delphi·编程考古
ljklxlj6 个月前
rdian是一个结构体,pdian=^Rdian,list泛型做什么用?
delphi
ljklxlj7 个月前
我的杂记一
delphi
月巴月巴白勺合鸟月半7 个月前
以前很常见的一种HTTP操作方式
网络·c++·网络协议·http·delphi
月巴月巴白勺合鸟月半8 个月前
一个小工具
windows·delphi
martian61258 个月前
Delphi Android WebBrowser 加载自定义scheme报错net::ERR_UNKNOWN_URL_SCHEME
delphi