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

你的项目结构应该像这样:
一步一步学习使用LiveBindings(6) 实现Master-Detail主从关系的绑定

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。

整体的布局大概如下所示:

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

3. 接下来分别为bsPeople和bsEmails添加字段和指定数据生成器。双击bsPeople,将打开Fields Editor,添加如下所示的字段:
一步一步学习使用LiveBindings(6) 实现Master-Detail主从关系的绑定
双击bsEmails,添加如下所示的字段:
一步一步学习使用LiveBindings(6) 实现Master-Detail主从关系的绑定

4. 右击页面空白处,从弹出的菜单中选择“Bind Visually”进入LiveBindings Designer设计器,按如下步骤完成绑定操作。

虽然看起来LiveBindings是在将数据与UI进行链接,其实到目前为止,所做的工作是在UI与BindSource进行操作,至于BindSource是连接到底层的数据库表还是对象,虽然在本篇中已经说明是对象,但是对于UI控件来说,目前是不清楚底层数据到底是数据库还是对象类型的,也无需顾及。

进入设计器后,可以看到BindNavigator由于指定了DataSource属性,所以设计器已经自动添加了链接。

首先,将bsPeople中的每一个栏位拖动到grdPeople中,不使用*是因为想对每一个列进行调整。而使用*是不可以的。

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

注意:当将每一列拉到TGrid控件上后,TGrid会自动为每一列生成一个TLinkGridToDataSourceColumn,在设计器的Column Editor中可以编辑列宽,指定每一列的自定义显示格式等等。

最后将3个Edit控件也链接上。
一步一步学习使用LiveBindings(6) 实现Master-Detail主从关系的绑定

可以看到,LiveBindings Designer对于TEdit和TGrid都给了以向数据绑定(链接线2边都有箭头)。即用户在UI上的更改也可以更新回底层数据存储。

现在运行程序,可以看到通过BindNavigator,可以对People进行移动,但是相应的Email并不会发生变化。不用担心,底层的数据操作会完成这个功能。

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

5. 现在新建一个实体类,用来存放底存数据和逻辑。如本文开头所述,这里引用了《Delphi Cookbook》中的示例代码,因此将包含示例中的实体类BusinessObjectsU.pas单元引入到了项目中,读者可以新建一个名为BusinessObjectsU.pas的单元,将下面的代码拷进去。
BusinessObjectsU.pas中包含了两个类,TPeople表示是单个个体人,它包含一个泛型的TEmail类型的属性集合Emails,表示一个人可以拥有多个电子邮件地址。

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

代码如下所示:

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. 代码主体大致完工,现在可以预览一下是否如预期。

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

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

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

一步一步学习使用LiveBindings(6) 实现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; 

再看看效果:

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

好了,已经接近预期了,这里还有一些未完工的细节,限于本篇的篇幅,就不再介绍了。

最后附上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的应用,请保持关注哦。

发表评论

评论已关闭。

相关文章