一步一步学习使用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,表示一个人可以拥有多个电子邮件地址。
代码如下所示:
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
.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 of string = (
'Daniele',
'Debora',
'Mattia',
'Jack',
'James',
'William',
'Joseph',
'David',
'Charles',
'Thomas'
);
LastNames: array of string = (
'Smith',
'Johnson',
'Williams',
'Brown',
'Jones',
'Miller',
'Davis',
'Wilson',
'Martinez',
'Anderson'
);
Countries: array of string = (
'Italy',
'New York',
'Illinois',
'Arizona',
'Nevada',
'UK',
'France',
'Germany',
'Norway',
'California'
);
HouseTypes: array 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 + ' (' + GetRndCountry + ')';
end;
function GetRndCountry: String;
begin
Result := Countries;
end;
function GetRndFirstName: String;
begin
Result := FirstNames;
end;
function GetRndLastName: String;
begin
Result := LastNames;
end;
end.感谢《Delphi Cookbook》的作者Daniele Spinetti,Daniele Teti,Daniele Teti也是Delphi MVC Framework的开发者,多年前我曾与他有过一次Email来往,在我的博文中,有机会将会详细介绍这个框架。
一点点扩展的思考,对于这个案例可以应用于移动应用,比如在BeforeOpen事件中,从Server端获取JOSN数据,转换成实体对象,也可以在beforePost中将对象转换成JSON,然后发送到Server端进行存储。
下一章,将继续一些深入挖掘LiveBindings的应用,请保持关注哦。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页:
[1]