一步一步学习使用LiveBindings(7) 实现对JSON数据的绑定
本课将介绍如何从JSON中获取绑定数据源,并且将更新也写回JSON。可以设想一下有一台远端服务器提供JSON数据,Delphi客户端可以接收这些JSON数据,然后转换成数据绑定对象,在应用程序中处理完数据后,将更新的数据序列化为JSON传回远端服务器,很多移动应用使用了这种模式处理服务器端的数据。好了废话少说,开始打开Delphi 12.3,建项目吧。
本系列课程具有前后关联性,如果你对LiveBindings的诸多细节没有一个大的概念,请看一步一步学习使用LiveBindings的前几课。
1. 单击主菜单中的 File > New > Multi-Device Application - Delphi > Blank Application ,创建一个新的多设备应用程序。
建议立即单击工具栏上的Save All按钮,将单元文件保存为uMainForm.pas,将项目保存为LiveBinding_BindToJSON.dproj。
你的项目结构应该像这样:
首先新建一个名为CollectionObjects.pas的Unit,右键单击Project Manager中的项目名称,选择"Add New > Unit",保存为CollectionObjects.pas文件名,CollectionObjects.pas包含一个简单的TPerson类,你可以想象为一个业务实体,代码如下:
Pascal
//
unit CollectionObjects;
interface
type
TPerson = class
private
FAge: Integer;
FLastName: string;
FFirstName: string;
public
constructor Create(const FirstName, LastName: String; Age: Integer);
property FirstName: string read FFirstName write FFirstName;
property LastName: string read FLastName write FLastName;
property Age: Integer read FAge write FAge;
end;
implementation
{ TPerson }
constructor TPerson.Create(const FirstName, LastName: String; Age: Integer);
begin
FFirstName := FirstName;
FLastName := LastName;
FAge := Age;
end;
end.
代码过于简洁,无须过多介绍。
2. 在主窗体上,放一个TTabControl控件,为该控件添加2个TabItem,一个指定Text为"Grid",一个指定Text为"JSON",这个名为JSON的Tab页用来演示后台的JSON数据的变化,用户将可以编辑这个JSON,同时在Grid上看到更新。
在名为TabGrid的TabItem上,放置一个TGrid和一个TBindNavigator控件,在名为TabJSON的TabItem上,放置一个TMemo控件用来显示JSON内容。
最后放置一个TDataGeneratorAdapter和一个TAdapterBindSource,指定AdapterBindSource1的Adapter为DataGeneratorAdapter1,BindNavigator1的DataSource属性为AdapterBindSource1。
设计窗口如下图所示:
3. 接下来需要完成绑定工作,目前AdapterBindSource1虽然指向了DataGeneratorAdapter1,但是DataGeneratorAdapter1还没有设置字段和相应的数据生成器。现在右击DataGeneratorAdapter1,从弹出的菜单中选择"Fields Editor"菜单项,根据在CollectionObjects.pas单元中定义的TPerson类的属性来创建3个字段,并分别指定如下图所示的数据生成器。
4. 在TGrid上右击鼠标,从弹出的菜单中选择"Bind Visually",在LiveBinding Designer中,将AdapterBindSource1的字段分别拖放到MainGrid上,可以看到生成的数据会立即显示在Grid上。
注意:拖动单独的列到Grid,可以单独调整Grid列的属性。
到目前为止,就构建了一个具有样例数据的应用程序。
5. 由于示例没有真正的访问远端服务器,为了演示数据绑定的效果,接下来实现AdapterBindSource1的OnCreateAdapter事件,在该事件中添加测试数据,以便在Grid上可以看到"真实"的数据。
首先在Interface区的uses下面添加类引用,并在private区添加一个泛型集合类。
Pascal
unit uMainForm;
interface
uses
System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.TabControl,
Data.Bind.Controls, System.Rtti, FMX.Grid.Style, FMX.Memo.Types,
Data.Bind.Components, Data.Bind.ObjectScope, FMX.Memo,
FMX.Controls.Presentation, FMX.ScrollBox, FMX.Grid, FMX.Layouts,
Fmx.Bind.Navigator, Data.Bind.GenData, Data.Bind.EngExt, Fmx.Bind.DBEngExt,
System.Bindings.Outputs, Data.Bind.Grid, Fmx.Bind.Grid, Fmx.Bind.Editors,
//添加如下的单元引用
CollectionObjects,System.Generics.Collections,REST.Json,System.JSON;
type
TfrmMain = class(TForm)
Tab: TTabControl;
tabGrid: TTabItem;
tabJSON: TTabItem;
NavigatorAdapterBindSource1: TBindNavigator;
mmJSON: TMemo;
DataGeneratorAdapter1: TDataGeneratorAdapter;
AdapterBindSource1: TAdapterBindSource;
grdMain: TGrid;
BindingsList1: TBindingsList;
LinkGridToDataSourceAdapterBindSource1: TLinkGridToDataSource;
private
{ Private declarations }
//添加保存人员信息的泛型列表类
FMyPeople: TObjectList<TPerson>;
public
{ Public declarations }
end;
在单元引用区,可以看到添加了System.JSON和REST.JSON引用,它们将用来处理JSON的解析与生成。
接下来在AdapterBindSource1的OnCreateAdapter事件中添加一些模拟的人员数据,如下代码所示:
Pascal
procedure TfrmMain.AdapterBindSource1CreateAdapter(Sender: TObject;
var ABindSourceAdapter: TBindSourceAdapter);
begin
//用来保存人员数据的集合。
FMyPeople := TObjectList<TPerson>.Create(True);
//添加单个的人员信息
FMyPeople.Add(TPerson.Create('Gomez', 'Addams', 40));
FMyPeople.Add(TPerson.Create('Morticia', 'Addams', 38));
FMyPeople.Add(TPerson.Create('Pugsley', 'Addams', 8));
FMyPeople.Add(TPerson.Create('Wednesday', 'Addams', 12));
FMyPeople.Add(TPerson.Create('Uncle', 'Fester', 55));
FMyPeople.Add(TPerson.Create('Grandmama', 'Frump', 72));
FMyPeople.Add(TPerson.Create('', 'Lurch', 50));
FMyPeople.Add(TPerson.Create('Thing T.', 'Thing', 99));
FMyPeople.Add(TPerson.Create('Cousin', 'Itt', 21));
// 使用TListBindSourceAdapter绑定到集合数据。
ABindSourceAdapter := TListBindSourceAdapter<TPerson>.Create(Self, FMyPeople, True);
end;
现在运行示例,可以看到这些数据已经显示在了Grid上,可以进行上下移动编辑了。
6. 搞定了数据绑定的问题,现在真正要解决的问题是将TObjectList
类型的泛型集合转换成JOSN字符串发送给服务器,或者是在接收到JSON字符串后,转换为TObjectList 类型的泛型集合以更新绑定UI,在这里需要引入2个过程。
在private区域定义2个过程,用来分别将对象转换为JSON以及将JSON转换为对象。
Pascal
private
{ Private declarations }
//添加保存人员信息的泛型列表类
FMyPeople: TObjectList<TPerson>;
//将对象转换为JSON数据
procedure ObjectsToJson;
//将JSON转换为对象
procedure JsonToObjects;
Implementation
procedure TfrmMain.JsonToObjects;
begin
//TODO 将TMemo中的JSON字符串转换为对象
end;
procedure TfrmMain.ObjectsToJson;
begin
//TODO 将TGrid绑定的对象转换为JSON字符串显示在TMemo中。
end;
由于ObjectsToJson和JsonToObjects涉及到一些JSON相关的操作,咱们需要先想想如何实现,但是UI的布局应该是当Tab切换时,如果切换到Grid这个Tab页,则调用JsonToObjects更新Grid上的数据;如果切换到JSON这个Tab页,则调用ObjectsToJson将Grid上的更新序列化为JSON字符串,所以应该是这样的一个效果:
可以看到,JSON与Grid是同步的,两边都可以更改。
7. 在主窗体中选中TTabControl控件,在属性编辑器中切换到Event标签页,找到OnChange事件,添加如下的代码:
Pascal
procedure TfrmMain.TabChange(Sender: TObject);
begin
//如果当前页面是JSON页
if Tab.ActiveTab = tabJSON then
begin
//如果AdapterBindSource处于编辑模式,则提交。
if AdapterBindSource1.Editing then
AdapterBindSource1.Post;
ObjectsToJson; //将对象转换为JSON。
end
else if Tab.ActiveTab = tabGrid then
JsonToObjects; //反之将JSON转换为对象。
end;
8. 在开始这2个核心的过程之前,良好的可重用的代码设计就显得很重要。System.JSON单元封装了JSON对象操作的逻辑,REST.JSON则提供了单一JSON字符串转换为对象或者对象到JSON字符串的转换功能。在这里封装了一个名为TUtils的类,它包含2个类方法:
Pascal
type
TUtils = class
public
//将一个泛型列表对象转换为JSON数组
class function ObjectListToJSON<T: class>(const AObjects: TObjectList<T>): TJSONArray;
//将一个JSON数组转换为泛型列表对象
class function JsonToObjectList<T: class, constructor>(const AText: string): TObjectList<T>;
end;
{ TUtils }
class function TUtils.JsonToObjectList<T>(const AText: string): TObjectList<T>;
var
LObject: T;
LArray: TJSONArray;
LValue: TJSONValue;
LList: TObjectList<T>;
begin
LList := TObjectList<T>.Create;
LArray := nil;
try
LArray := TJSONObject.ParseJSONValue(AText) as TJSONArray;
if LArray = nil then
raise Exception.Create('Invalid JSON');
for LValue in LArray do
if LValue is TJSONObject then
begin
//使用REST.Json提供的TJson类的类方法完成转换
LObject := TJson.JsonToObject<T>(TJSONObject(LValue));
LList.Add(LObject);
end;
Result := LList;
LList := nil;
finally
LArray.Free;
LList.Free;
end;
end;
class function TUtils.ObjectListToJSON<T>(
const AObjects: TObjectList<T>): TJSONArray;
var
LObject: T;
LArray: TJSONArray;
LValue: TPerson;
LElement: TJSONObject;
begin
LArray := TJSONArray.Create;
try
for LObject in AObjects do
begin
//使用REST.Json提供的TJson类的类方法完成转换
LElement := TJson.ObjectToJsonObject(LObject);
LArray.AddElement(LElement);
end;
Result := LArray;
LArray := nil;
finally
LArray.Free;
end;
end;
代码中的TJSONArray,TJSONObject类是由System.Json的供来操纵JSON的,核心部分的TJson类是由REST.Json所提供,顾名思议,这个单元是处理Restful操作的。
9. 现在一切准备就绪,继续完成ObjectsToJson和JsonToObjects这两个过程,代码如下所示:
Pascal
procedure TfrmMain.JsonToObjects;
begin
//使用JsonToObjectList将JSON转换为对象
fMyPeople := TUtils.JsonToObjectList<TPerson>(mmJSON.Text);
//将列表数据重新赋给AdapterBindSource1。
TListBindSourceAdapter<TPerson>(AdapterBindSource1.InternalAdapter).SetList(fMyPeople);
//刷新用户界面
AdapterBindSource1.Active := True;
end;
procedure TfrmMain.ObjectsToJson;
var
LArray: TJSONArray;
begin
//将对象转换为TJSONArray数组
LArray := TUtils.ObjectListToJSON<TPerson>(fMyPeople);
try
//将JSON数组稍稍美经后显示在TMemo控件
mmJSON.Text := PrettyJSON(LArray.ToString);
finally
LArray.Free;
end;
end;
function PrettyJSON(AJson: String): String;
begin
Result := StringReplace(AJson, '},', '},' + sLineBreak, [rfReplaceAll]);
Result := StringReplace(Result, '[{', '[' + sLineBreak + '{', [rfReplaceAll]);
end;
10. 万事皆备,只欠一Run了,按下F9,或者是主菜单的"Run > Run",可以看到JOSN和Grid的数据果然已经同步了。
真实的项目中,JSON生成后,应该是要发送给Server端,或者存储到本地文件,这可以根据需要而定。
本课就讲到这里了,虽然到目前为止笔者还没有深挖TBindingList的内幕,不过可以看到使用LiveBindings Designer已经可以解决不少问题了。当然在实际的项目中还是有很多细节要处理的,比如显式格式的转换,复杂的绑定场景等等。
在本系列的后面的课程中会继续深挖。