ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LiveBindings对象绑定:TAdapterBindSource实战

LiveBindings对象绑定:TAdapterBindSource实战 如果你在Delphi里用LiveBindings只绑过TDataSet和DBGrid第一次听到TAdapterBindSource时多半和我一样是懵的。数据表天然有字段绑定起来顺理成章但遇到业务层返回一个TUser对象、一个TOrder对象这种内存对象时LiveBindings该怎么绑TDataSetBindSource帮不上忙因为它要的是数据集。这时候就得请出TAdapterBindSource——专门负责把普通对象翻译成LiveBindings认识的字段再和界面控件做绑定。这篇文章我按自己实际踩过的坑来写从单个对象绑到三个Edit开始一步一步做到能绑对象列表中间会解释TAdapterBindSource的字段定义、OnGetFieldValue/OnSetFieldValue事件、Adapter属性分别扮演什么角色。内容不涉及数据库纯内存对象操作适合刚开始接触LiveBindings对象绑定、又不想去啃官方零散文档的人。1. 从数据集到普通对象LiveBindings到底卡在哪LiveBindings这套绑定机制的核心不是对象而是字段。你绑TDataSet为什么舒服因为TDataSet无论连的是SQL Server还是SQLite打开以后都有一堆TField对象字段名、类型都是确定的。TDataSetBindSource拿到这批字段再通过LiveBindings设计器把它们和界面控件挨个连起来数据自然就流过去了。但业务开发里很多数据并不会以数据集形式出现。比如REST接口返回的JSON被解析成了TUser对象或者计算规则生成了一批TOrder内存对象这时候你手上没有TFieldLiveBindings就像一个只认身份证号的人你递给他一张名片他认不出来。TAdapterBindSource就是干这个的它不依赖任何数据访问组件只需要你告诉它我有哪几个字段然后它再通过事件问你这些字段的值是哪来的。你可以把TAdapterBindSource理解成一个翻译官一边把对象的公开属性翻译成字段一边把字段值送到控件上。字段名和属性名可以完全一致也可以不一致这个灵活性后面会讲。TAdapterBindSource的价值就在这儿它让LiveBindings从数据表专用变成了任意对象通用。绑定逻辑不需要改你只要换一个适配器的实现就能把DataSource换成另一个类的实例。这也是为什么很多框架在MVVM、界面与业务解耦时选用它。2. 最小案例让一个TUser对象显示到三个Edit上先不碰列表也不碰数据库我们做一个能跑的最小案例。目标是程序启动时创建一个TUser对象里面放着姓名、年龄、邮箱然后这三个属性分别显示在三个Edit里用户在Edit里改了字对象里的属性也跟着变。2.1 定义有RTTI的对象类在Delphi中要让LiveBindings或后面的TListBindSourceAdapter 能自动识别属性类必须开启RTTI。最省事的方式就是把属性声明成published或者让类继承自TPersistent等带有{$M}的类。这里定义一个TUsertype TUser class private FName: string; FAge: Integer; FEmail: string; published property Name: string read FName write FName; property Age: Integer read FAge write FAge; property Email: string read FEmail write FEmail; end;这个类没有任何数据访问逻辑就是一个普通内存对象。为什么特意写成published如果你后面换用TListBindSourceAdapter 它靠RTTI自动读取属性就必须依赖这个可见性。就算你这次打算用手工事件绑定统一保持published也能少踩坑。2.2 设计期操作添加字段和连接绑定在窗体上放一个TAdapterBindSource名字改成bsUser。在RAD Studio里右键bsUser会看到一个Add Field菜单点击后输入字段名和类型。依次添加三个字段字段名数据类型NameStringAgeIntegerEmailString添加完以后点开Object Inspector里的FieldDefs属性可以看到这三条定义已经进去了。这一步等于把表结构定了下来。TAdapterBindSource没有数据源不能自动分析对象有哪些属性必须手动声明字段这是它的设计方式也是容易让新手困惑的地方。接着放三个TEdit命名edtName、edtAge、edtEmail。然后双击bsUser打开LiveBindings Designer左侧能看到bsUser下的字段树Name、Age、Email三个节点已经挂在那里。把Name字段拖到edtName.Object属性里的Text上Age拖到edtAge.TextEmail拖到edtEmail.Text。拖完以后设计器右边会出现三条绑定链接。这个操作和数据表绑定一模一样区别仅仅是左侧数据源从DataSet换成了TAdapterBindSource。2.3 事件代码对象属性到字段的搬运工现在字段定义有了界面也连上了运行程序会发现三个Edit全是空的。原因很简单没有任何事件告诉TAdapterBindSource字段值从哪里来。这是TAdapterBindSource最有特点、也最容易被忽略的一步——必须实现OnGetFieldValue。先在Form里声明一个FUser字段并在FormCreate里创建和赋值procedure TForm1.FormCreate(Sender: TObject); begin FUser : TUser.Create; FUser.Name : 张三; FUser.Age : 30; FUser.Email : zhangsanexample.com; bsUser.Refresh; end;然后在bsUser的事件里找到OnGetFieldValue写这段代码procedure TForm1.bsUserGetFieldValue(Sender: TObject; const FieldName: string; var Value: TValue; var FieldFound: Boolean); begin FieldFound : True; if FieldName Name then Value : TValue.Fromstring(FUser.Name) else if FieldName Age then Value : TValue.FromInteger(FUser.Age) else if FieldName Email then Value : TValue.Fromstring(FUser.Email) else FieldFound : False; end;注意Value的类型是TValue这是LiveBindings的万能容器。int、string、对象引用都能塞进去。TValue.From 是标准推送方式读出来时用Value.AsString或Value.AsInteger。现在运行三个Edit应该分别显示张三、30、zhangsanexample.com。如果你看到的内容有缺失先检查FieldName是否拼错再看FieldFound是否返回True。2.4 反向写回编辑界面内容同步到对象绑定做到一半只有显示没有写回等于单向通道。要实现双向在bsUser事件里找到OnSetFieldValueprocedure TForm1.bsUserSetFieldValue(Sender: TObject; const FieldName: string; const Value: TValue; var FieldFound: Boolean); begin FieldFound : True; if FieldName Name then FUser.Name : Value.AsString else if FieldName Age then FUser.Age : Value.AsInteger else if FieldName Email then FUser.Email : Value.AsString else FieldFound : False; end;这个事件的触发时机取决于绑定控件属性。TEdit.Text的LiveBindings绑定默认在属性变化时触发不必写提交按钮。你运行后去edtName里把名字改掉然后随便切一下焦点再通过某个按钮弹出一个ShowMessage(FUser.Name)会发现对象里的值已经变了。到这里第一个完整的最小案例跑通了。看到界面跟着对象走、对象跟着界面走说明TAdapterBindSource的基本链路已经建立。3. TAdapterBindSource的运转原理三种角色如何分工成功跑通后我觉得有必要把背后的分工讲清楚否则你换一个类、换一组字段很容易又懵。TAdapterBindSource内部有三个东西在协同工作字段定义、数据事件、Adapter属性。它们各管一段很多人搞混就是因为把三者混为一谈。3.1 FieldDefs如何变成运行时字段你在设计期添加的那三条字段定义并不只是在设计器里画个节点而已。运行到绑定初始化时TAdapterBindSource会依据FieldDefs动态创建出真正的字段对象。这些字段对象和TDataSet里的TField地位类似LiveBindings绑定引擎只认它们后面所有数据传递都要通过字段对象来中转。调试时可以给OnGetFieldValue加一个断点看FieldName的取值范围——它就是字段定义里的名字。如果你在设计期定义了一个叫FullName的字段那么OnGetFieldValue收到的FieldName就是FullName哪怕它对应的对象属性其实叫DisplayName也完全没问题字段名和属性名之间不存在自动关联。你可以把字段定义看成一张映射表结构它只规定LiveBindings能看到哪些数据列不规定这些列背后的数据怎么来。真正决定列的值是什么的是后面的事件或适配器。3.2 OnGetFieldValue/OnSetFieldValue是被谁调用的当界面某个控件需要显示数据时LiveBindings引擎向绑定源请求字段值TAdapterBindSource收到请求后触发OnGetFieldValue。这个事件拿到FieldName然后要你返回一个TValue。如果你没实现这个事件或者FieldFound返回了False引擎就拿不到值界面显示空白。反过来当控件的内容被用户修改后引擎会把新值用OnSetFieldValue送回来让你把这个值写进对象。这里必须理解一点TAdapterBindSource本身不保存任何业务对象它也不在你的FUser和界面之间建立强引用。它只是一座桥桥上的数据全靠事件来搬运。所有状态管理都在你自己的代码里对象是活是死、属性是变是没变TAdapterBindSource一概不关心。这种设计的好处是解耦坏处是你得主动告诉它什么时候该取数据。3.3 Adapter属性在对象列表绑定中的意义单个对象靠两个事件就够用了。但如果要绑定一个TObjectList 还要支持前后翻阅、增删改手写事件会变成一场噩梦你得自己维护当前行号、每次换行都要重新给所有控件赋值。这时候TAdapterBindSource的Adapter属性就派上用场了。Adapter属性的类型是TBaseBindSourceAdapter可以把它理解成一个数据管家。你可以把一个现成的列表适配器塞给TAdapterBindSource比如TListBindSourceAdapter 。装了适配器之后TAdapterBindSource就不再依赖OnGetFieldValue/OnSetFieldValue提供数据而是直接从适配器里取当前行的字段值。适配器内部维护了列表、当前行号、字段映射相当于把第2节里手写的工作全部打包自动化。也就是说三种角色的分工可以概括为FieldDefs定义有哪些字段相当于表结构。OnGetFieldValue/OnSetFieldValue在无适配器时由你手动完成字段和对象属性的映射。Adapter属性在绑列表或需要完整导航时用现成适配器替代手写事件完成同样的映射。4. 五个我踩过的坑以及对应的排查路径TAdapterBindSource的资料少不少问题得靠试错。我在实际项目里踩过几个坑每个都花了不少时间整理出来供你排查。这些坑单看都很小组合在一起足够让人抓狂。症状可能原因解决办法界面全部空白没写OnGetFieldValue或事件里FieldFound没设为True实现事件确保每个字段分支FieldFound : True部分字段没有值字段名拼写不一致或大小写不匹配统一字段名常量检查FieldName字符串值显示出来但类型不对字段类型定义错了导致TValue转换失败核对FieldDefs里的数据类型和OnGetFieldValue里用的一致修改控件后对象没变化OnSetFieldValue没实现或绑定方向是单向补事件检查绑定链接的Direction换了一个对象界面还是旧数据没有触发刷新赋值后调用bsUser.Refresh4.1 界面完全空白一个值都不显示我第一次用TAdapterBindSource就是在这一步卡住字段加好了绑定也拖好了运行出来三个Edit空空如也。检查了半天才发现OnGetFieldValue事件根本没写。你可能会觉得TAdapterBindSource应该自己能从对象拿值吧——不好意思它真的不行。它就是一张嘴你不喂它它什么也说不出来。如果你写了事件还是空白那就给事件入口加个断点看看有没有被触发。没触发说明绑定没建立或者字段定义没生效触发了但返回了FieldFoundFalse就往下一节排查。4.2 字段名大小写或拼写不一致导致FieldFoundFalse我在设计期加字段时图省事把字段名写成了NAME事件里判断却写成Name。运行起来界面空白断点看了半天才发现是字符串比较大小写敏感。后来我学乖了把字段名提取成常量const FIELD_NAME Name; FIELD_AGE Age; FIELD_EMAIL Email;OnGetFieldValue和OnSetFieldValue里都引用常量前端FieldDefs里也保持一致从源头杜绝拼写问题。这个习惯在字段多的时候特别有用。4.3 绑定表达式写错运行时根本没建链LiveBindings Designer里拖字段到控件上如果拖到的是控件对象本身而非具体属性生成不了有效链接。我遇到过拖到TEdit组件上结果什么绑定也没创建。正确做法是拖到右侧对象树的Text属性节点上松开后能看到一条从字段到属性的连线。如果画不上线先检查是不是把同一个字段重复拖了多次有时先删掉旧链接再拖反而更快。4.4 修改界面后对象数据没跟着变这个坑往往是两个原因叠加一是OnSetFieldValue没写二是LiveBindings Designer中绑定的Direction被设成了OneWay。TEdit.Text的默认绑定一般是TwoWay但如果你在属性面板里手动调整过或者从控件往字段拖线而不是从字段往控件拖方向就可能反了。检查绑定链接的属性看到Direction是Out或InTo时改成Bidirectional。4.5 换了个新对象实例旧数据还在界面上程序跑着跑着可能由于业务逻辑给FUser赋了一个新对象。这时候你会想数据变了界面应该跟着变吧不一定。LiveBindings的绑定不会每秒钟轮询你对象属性它需要收到一个刷新的信号。TAdapterBindSource提供了Refresh方法会重新走一遍字段读取流程。我现在的习惯是把切换当前对象和Refresh绑在同一个方法里procedure TForm1.ShowUser(AUser: TUser); begin FUser : AUser; bsUser.Refresh; end;这样每次界面展示的都是新对象的数据不会出现上一页数据残留在Edit里的诡异情况。5. 升级到对象列表用TListBindSourceAdapter 接管一切单个对象绑定只是第一步大部分真实需求其实是对象列表——比如一组订单、一组用户界面上有上下一条按钮当前对象变下面的Edit全部联动。继续手写OnGetFieldValue会非常累正确做法是交给TListBindSourceAdapter 。5.1 为什么需要列表适配器手写事件的方式没有任何位置保存当前是第几个对象。你得自己在Form里加一个Index字段每次切换时重新给bsUser赋值再Refresh代码又散又乱。TListBindSourceAdapter 就是官方提供的解决方案它内部维护了一个对象列表和当前行指针并且能用RTTI自动从当前对象的published属性读取字段值。它的另一个好处是你不需要再写OnGetFieldValue和OnSetFieldValue这两个事件了因为适配器把映射工作接管了。前提是你在FieldDefs里定义的字段名和对象类的published属性名保持一致。如果名字不一致适配器就找不到对应属性界面拿不到值。5.2 TListBindSourceAdapter 的基本用法假设现在的对象是TOrdertype TOrder class private FId: Integer; FTotal: Currency; FStatus: string; published property Id: Integer read FId write FId; property Total: Currency read FTotal write FTotal; property Status: string read FStatus write FStatus; end;窗体上放一个TAdapterBindSource命名为bsOrder设计期添加字段Id、Total、Status类型分别是Integer、Currency、String。然后准备一个TObjectList uses System.Bindings.Helper; var Adapter: TListBindSourceAdapterTOrder; Orders: TObjectListTOrder; begin Orders : TObjectListTOrder.Create(True); Orders.Add(Order1); Orders.Add(Order2); Adapter : TListBindSourceAdapterTOrder.Create(bsOrder, Orders, False); bsOrder.Adapter : Adapter; end;这里需要解释一下Create的第三个参数。如果传True表示适配器销毁时会把Orders也销毁传False列表生命周期由你的代码控制。我建议传False因为列表往往还承担着其他业务作用所有权交给适配器容易在析构时导致双重释放。如果你确认这个列表只给这一个适配器用传True也方便但多数人会在维护期踩到所有权问题。挂上Adapter之后bsOrder就不用写事件了。运行后当前行默认是第一行的Id、Total、Status会被RTTI自动读取显示到绑定好的Edit里。类属性必须是publishedRTTI才能看到这一点在前面强调过。5.3 新增、删除、刷新如何和界面联动列表适配器把当前行和字段值都管理起来剩下的问题就是怎么操作数据并刷新界面。最朴素的思路是直接操作Orders列表操作完调用Adapter.Refresh。我实际用的套路是新增一条订单后让界面跳到新增的那条Orders.Add(NewOrder); Adapter.Refresh;删除当前显示的订单我一般先记住当前索引删掉后让适配器定位到相邻位置再刷新。如果你用的是较新版本RAD Studio适配器提供了针对当前行的删除方法名称可能随版本略有差异但行为等价于先删除、再Refresh。不管用哪种方式最终的刷新动作是关键没有Refresh界面不会有任何变化。对于上下一条导航适配器内部有当前行索引。你只要改变索引然后Refresh界面就会自动重新读取新对象的属性值。自己手写导航逻辑时需要注意越界判断第一行再往前、最后一行再往后不要抛异常。适配器封装里通常会处理CanMoveFirst之类的判断如果你完全自己控制索引就要自己检查。如果哪天你发现绑定的列表数据在后台被修改了比如异步加载了一批新数据最安全的方式是把这些数据先清空再填充然后调用Adapter.Refresh。不要在刷新过程中直接修改正在被绑定的列表对象某些状态下TListBindSourceAdapter内部的记录指针可能因为列表变化而错乱。6. 最后说点个人经验我在把TAdapterBindSource推广到团队内部时发现刚开始大家都很抗拒觉得宁可手动给Edit赋值也不愿意折腾绑定。但用顺之后代码量确实少很多尤其是对象结构变更时——只要FieldDefs和界面的绑定动一下对象类里加一个属性OnGetFieldValue这些映射代码根本不用改。对于绑对象列表的场景TListBindSourceAdapter 的优势更加明显省掉了大量重复的赋值语句。还有一个小技巧想分享如果你只需要单向展示可以完全不管OnSetFieldValue但如果你是做双向绑定的表单建议把OnSetFieldValue里每个字段的值都做一次类型验证比如Age字段要用Value.TryAsType 而不是直接AsInteger不然用户在Edit里输入了非数字内容LiveBindings会在转换时抛异常整个界面都会被卡住。最后提醒一点TAdapterBindSource的FieldDefs是设计期确定的如果你需要在运行时动态增加字段建议在TAdapterBindSource初始化之前、绑定建立之前就完成配置否则已经生成的绑定链路不会自动同步新字段。这个顺序问题我遇到过不止一次每次都是因为偷懒图方便把AddField放在了FormShow之后界面数据就是不出全。先加字段再连绑定最后Refresh这个顺序不会错。
返回列表