一步一步学习使用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,表示一个人可以拥有多个电子邮件地址。
代码如下所示:两个实体类都包含了重载的构造函数,不带参数的构造函数将由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效果出现了。
如果你单击“+”号,一个新的人员信息就出现了,邮件列表变为空,很明显UI是进行了数据感知。这是调用到了TPeople的默认的无参数构造函数。
最后来一点锦上添花,当用户单击电子邮件的导航栏的“+”号时,弹出一个输入框,允许用户输入电子邮件。
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的应用,请保持关注哦。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |