祝愿大家身体健康!

 站点注册  找回密码
 站点注册

QQ登录

只需一步,快速开始

查看: 15|回复: 1

[学习笔记] DataWindow 数据内容操作:撤销(Undo)、还原(Revert/Restore)与保存检查(Save Validation)

[复制链接]

[学习笔记] DataWindow 数据内容操作:撤销(Undo)、还原(Revert/Restore)与保存检查(Save Validation)

[复制链接]
pbai

主题

0

回帖

56

积分

PBAI

积分
56
贡献
在线时间
小时
昨天 12:41 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?站点注册

×
DataWindow 数据内容操作:撤销(Undo)、还原(Revert/Restore)与保存检查(Save Validation)

适用版本:PowerBuilder 10(语法兼容 PB 8/9/10 与 Classic 11/12)
关键词:DataWindow 控件、缓冲区、脏数据标记、并发冲突、保存前校验

在 PowerBuilder 的 DataWindow 编程中,“数据内容操作”通常围绕三件事展开:


  • 撤销(Undo)——用户在录入时输错了,想立刻退一步;
  • 还原(Revert / Restore)——改了一半不想要了,把内存里的未保存改动退回原始状态或某个检查点;
  • 保存检查(Save Validation)——点“保存”之前,先确认数据合法、必填项已填、确有改动、且没人并发改过。


这三个能力处在不同的“时间层”:Undo 在工作现场(单元格编辑中),Revert 在一次编辑会话层面,Save Validation 在落库前的最后一道关卡。本文逐项讲清触发条件、适用场景、相互协作关系,并给出明确的功能边界。




0. 先建立共识:DataWindow 的缓冲区模型

要理解撤销/还原/保存检查,必须先理解 DataWindow 内部的 4 个缓冲区行/列状态,它们是所有这些机制的物理基础(详见 datawindow-guide.md 第 4 节)。

缓冲区枚举内容
PrimaryPrimary!当前显示、未被过滤/删除的行
FilterFilter!SetFilter/Filter() 过滤掉的行
DeleteDelete!DeleteRow() 标记删除、尚未 Update 的行
Original每行最初检索到的原始值(Update 生成 WHERE 时使用)


行/列状态(GetItemStatus / SetItemStatus)决定 Update() 会生成什么 SQL:

状态说明Update 行为
New!新插入、尚未改不生成 SQL
NewModified!新插入、已改生成 INSERT
NotModified!未改不生成 SQL
DataModified!已改生成 UPDATE


核心结论:所有“还原”与“脏数据判断”本质上都是在操作这些缓冲区和状态,而不是操作数据库。数据库只在 Update() + COMMIT 之后才被改变。




1. 撤销(Undo)—— 单元格编辑内的“退一步”

1.1 它到底是什么

DataWindow 内置了一个编辑控件(edit control),当用户正在某格输入时,文本就暂存在编辑缓冲里。Undo() 的作用是把当前编辑框内最后一次输入撤销掉。
  1. // 工具栏/按钮 cb_undo.Clicked
  2. if dw_1.CanUndo() then
  3.     dw_1.Undo()   // 撤销当前编辑框内最后一次输入(单元格级、未提交)
  4. end if
复制代码


  • CanUndo():是否还有可撤销的编辑操作(有则返回 True)。
  • EditUndo 事件:用户按 Ctrl+Z(或菜单“编辑 → 撤销”)时触发。其默认行为就是执行一次编辑撤销,绝大多数情况下无需写代码;如需自定义再在该事件里脚本。


1.2 触发条件与适用场景

维度说明
触发时机用户正在编辑某单元格(编辑控件处于活动态)、尚未把值提交进缓冲区
粒度单格、单步(最后一次键入/删除),不是多步历史栈
典型场景输错一个字、多删了几个字符,立刻 Ctrl+Z 或点“撤销”按钮
前提必须 CanUndo() 为真;一旦离开该格并成功提交,编辑缓冲清空,Undo 对该格失效


1.3 关键边界(务必记住)
Undo 只能撤销“尚未提交到缓冲区”的输入。
一旦 itemchanged 返回 0(接受)或焦点移走使编辑被接受,值已进入 Primary 缓冲区,此时 Undo() 再也回不退这个值——你需要改用第 2 节的“还原(Revert)”或重新手动改。

因此 Undo 是最轻、最即时、但作用最浅的能力,它不碰数据库、不碰已提交的数据、也不做业务校验。




2. 还原(Revert / Restore)—— 退回未保存的改动

“还原”解决的是另一个问题:用户在一张表上改了若干行、甚至加了几行、删了几行,现在想全部放弃,回到最初(或某个检查点)的状态

2.1 三种还原路线

路线 A:重新检索(最干净,回到数据库原始态)
  1. // 放弃一切内存改动,从数据库重新取
  2. dw_1.SetTransObject(SQLCA)
  3. dw_1.Retrieve(ls_key)   // 重新检索 → Primary/Delete 缓冲全部重建,改动清零
复制代码


  • 优点:逻辑最稳,新增行、修改行、删除行一次性全部回到数据库真实状态(连被删的行都回来了)。
  • 代价:一次数据库往返;会丢失所有未保存编辑(这正是“还原”的目的);检索参数需与当初一致。


路线 B:用 Original 缓冲区逐列回写(不跑 SQL)

适用于“想还原但不想再查库”、且有 Original 值可参照时:
  1. // 自定义函数 of_RevertData():把未保存改动还原到最初检索值
  2. long ll_row, ll_cnt, ll_status
  3. ll_cnt = dw_1.RowCount()
  4. for ll_row = ll_cnt to 1 step -1
  5.     ll_status = dw_1.GetItemStatus(ll_row, 0, Primary!)
  6.     choose case ll_status
  7.         case DataModified!
  8.             // 已修改的已有行:用 Original 缓冲逐列覆盖
  9.             dw_1.SetItem(ll_row, "cust_name",  dw_1.GetItemString (ll_row, "cust_name",  Primary!, true))
  10.             dw_1.SetItem(ll_row, "age",        dw_1.GetItemNumber (ll_row, "age",        Primary!, true))
  11.             dw_1.SetItem(ll_row, "start_date", dw_1.GetItemDateTime(ll_row, "start_date", Primary!, true))
  12.             dw_1.SetItemStatus(ll_row, 0, Primary!, NotModified!)
  13.         case New!, NewModified!
  14.             // 新插入的行:直接删掉
  15.             dw_1.DeleteRow(ll_row)
  16.     end choose
  17. next
  18. // 可选:把被“删除”的行也还原回来(从 Delete 缓冲移回 Primary)
  19. // dw_1.RowsMove(1, dw_1.DeletedCount(), Delete!, dw_1, 1, Primary!)
  20. dw_1.ResetUpdate()   // 清除全部更新标志(见 2.2)
复制代码

GetItemX(row, col, Primary!, true) 中最后一个参数 true 表示“取原始值(Original 缓冲)”,这是回写还原的关键。

路线 C:清除更新标志 ResetUpdate() —— “撤销保存意图”,不是“还原数据”
  1. dw_1.ResetUpdate()   // 把所有行标记为 NotModified!,Update() 将生成 0 条 SQL
复制代码


  • 作用:让 Update() 认为“没有改动可保存”,于是保存动作变成空操作。
  • 重要边界ResetUpdate() 只清标志、不改显示值。已被用户改动的数值仍显示在界面上,只是不再“脏”。它适合“暂不保存、保留现场”的场景,不要把它和“还原数据”混为一谈


2.2 检查点(Checkpoint)还原

有些业务需要“回到进入明细之前的快照”,而不是回到数据库原始态。可用 DataStore/字符串快照实现任意检查点:
  1. // 进入编辑前打检查点
  2. datastore ids_snap
  3. ids_snap = create datastore
  4. ids_snap.DataObject = dw_1.DataObject
  5. ids_snap.Object.DataWindow.Data = dw_1.Object.DataWindow.Data   // 复制当前全部数据
  6. // 需要时还原检查点
  7. dw_1.Object.DataWindow.Data = ids_snap.Object.DataWindow.Data
  8. destroy ids_snap
复制代码
PFC 的 u_dw 没有内置的“原子级 of_Revert”,建议在项目里按路线 B 自行封装 of_Revert() / of_IsDirty() 等方法,统一团队行为(见 pfc-patterns.md 第 9 节“轻量替代”思路)。

2.3 触发条件与适用场景对照

路线触发场景是否查库能否保留现场数值
A 重新检索“全部作废,回到系统里的样子”否(彻底还原)
B Original 回写“不查库、就地退回原始值”否(数值回退)
C ResetUpdate()“先别保存,但保留我刚改的”是(数值保留)
检查点还原“回到我进入编辑前的快照”视快照而定





3. 保存检查(Save Validation)—— 落库前的最后一道关

保存检查发生在用户点击“保存”、且调用 Update() 之前。它由四块机制组成:数据合法性校验、必填项检查、脏数据标记判断、并发冲突检测。

3.1 数据合法性校验

实时层(编辑时即拦截)——itemchanged 事件:
  1. // dw_1.itemchanged(row, dwo, data)
  2. choose case dwo.Name
  3.     case "qty"
  4.         if Long(data) <= 0 then
  5.             MessageBox("校验", "数量必须大于 0")
  6.             return 1              // 1 = 拒绝本次修改,焦点离开
  7.         end if
  8.     case "email"
  9.         if Pos(data, "@") = 0 then
  10.             MessageBox("校验", "邮箱格式错误")
  11.             return 2              // 2 = 拒绝且焦点停留在本格
  12.         end if
  13. end choose
  14. return 0                          // 0 = 接受
复制代码

data 是用户刚输入的新值(尚未入缓冲),因此 itemchanged 能在非法数据进入缓冲区之前就挡住它,这是最省事的“前置校验”。

设计期校验规则(Validation Rule):在 DataWindow 画板给列设表达式(如 qty > 0),失败时触发 itemerror 事件。配合 itemerror 返回码可定制提示。

批量层(保存前全量扫)——见 3.5 的 of_Validate()

3.2 必填项检查

两种方式叠加使用,最稳妥:


  • 设计期:列 Edit.Required = yes。当用户在必填格内留空并离开时,PB 会触发必填提示(经 itemerror)。
  1.    dw_1.Modify("cust_name.Edit.Required = yes")
  2.    
复制代码

  • 保存前手动扫(更可靠,也能覆盖下拉/只读赋值场景):
  1.    if IsNull(dw_1.GetItemString(ll_row, "cust_name")) or &
  2.       Trim(dw_1.GetItemString(ll_row, "cust_name")) = "" then
  3.        // 必填项为空 → 报错
  4.    end if
  5.    
复制代码
仅靠 Edit.Required 有时不够(例如值由代码 SetItem 写入或下拉选择)。保存前的必填扫描是兜底,建议都做。

3.3 脏数据标记(Dirty Flag)判断

“脏”= 缓冲区与原始态不一致。ModifiedCount() / DeletedCount() 是最常用的整体脏标记:
  1. if dw_1.ModifiedCount() = 0 and dw_1.DeletedCount() = 0 then
  2.     MessageBox("提示", "没有需要保存的改动。")
  3.     return
  4. end if
复制代码


  • ModifiedCount():Primary 缓冲中处于 DataModified!/NewModified! 的行数。
  • DeletedCount():Delete 缓冲中被标记删除的行数。
  • 行/列级状态可用 GetItemStatus(row, col, Primary!) 精细判断(见第 0 节)。

标题栏“* 未保存”之类的提示,就是基于这两个计数实现的。脏标记决定“是否值得保存”,但不校验数据对不对。

3.4 并发冲突检测(Conflict Detection)

DataWindow 生成 UPDATE/DELETE 的 WHERE 时,会用原始值updatewhereclause=yes,列定义里 updatewhereclause=yes)。若另一用户已改过该行,WHERE 匹配 0 行:
  1. // 更新前先不要自动接受改动(acceptchanges = FALSE)
  2. if dw_1.Update(FALSE) = 1 then
  3.     if SQLCA.SQLNRows = 0 then        // 期望更新 N 行,实际 0 行 → 并发冲突
  4.         rollback using SQLCA ;
  5.         MessageBox("冲突", "数据已被其他用户修改,请重新检索后再保存。")
  6.         return                        // 注意:此时改动已被清,需重新 Retrieve
  7.     end if
  8.     commit using SQLCA ;
  9.     dw_1.ResetUpdate()                // 接受改动,清除脏标记
  10. else
  11.     rollback using SQLCA ;
  12.     MessageBox("错误", "保存失败:" + SQLCA.SQLErrText)
  13. end if
复制代码

更稳的乐观并发:在表里加 version / last_update 时间戳列,并将其也设为 updatewhereclause=yes,这样一旦版本不符,UPDATE 直接 0 行,冲突在 WHERE 阶段就被发现。

3.5 统一的保存前检查函数

把校验集中成一个函数,保存按钮只调它:
  1. // of_Validate() returns boolean —— TRUE 通过,FALSE 不通过(已提示)
  2. long     ll_row, ll_cnt
  3. string   ls_msg
  4. datetime ldt_s, ldt_e
  5. ll_cnt = dw_1.RowCount()
  6. for ll_row = 1 to ll_cnt
  7.     // 1) 必填项
  8.     if IsNull(dw_1.GetItemString(ll_row, "cust_name")) or &
  9.        Trim(dw_1.GetItemString(ll_row, "cust_name")) = "" then
  10.         ls_msg = "第 " + String(ll_row) + " 行:客户名称为必填项。"
  11.         goto fail
  12.     end if
  13.     // 2) 数据合法性(业务规则)
  14.     if dw_1.GetItemNumber(ll_row, "age") < 0 then
  15.         ls_msg = "第 " + String(ll_row) + " 行:年龄不能为负。"
  16.         goto fail
  17.     end if
  18.     // 3) 跨列逻辑
  19.     ldt_s = dw_1.GetItemDateTime(ll_row, "start_date")
  20.     ldt_e = dw_1.GetItemDateTime(ll_row, "expire_date")
  21.     if Not IsNull(ldt_e) and ldt_e < ldt_s then
  22.         ls_msg = "第 " + String(ll_row) + " 行:到期日不能早于开始日。"
  23.         goto fail
  24.     end if
  25. next
  26. return TRUE
  27. fail:
  28. MessageBox("保存检查未通过", ls_msg, StopSign!)
  29. return FALSE
复制代码

3.6 完整保存流程(把上述串起来)
  1. // cb_save.Clicked
  2. if dw_1.AcceptText() <> 1 then return        // 关键:把正在编辑的格写入缓冲区
  3. if dw_1.ModifiedCount() = 0 and dw_1.DeletedCount() = 0 then
  4.     MessageBox("提示", "没有需要保存的改动。")
  5.     return
  6. end if
  7. if not of_Validate() then return            // 保存前校验
  8. // ↓ 见 3.4 的 Update + 冲突检测
复制代码

AcceptText() 必须放在最前——它把当前编辑控件的内容提交进缓冲区,否则最后编辑的那格不会被 ModifiedCount() 统计、也不会被 of_Validate() 扫到。




4. 三者的协作关系(时间层与数据流)
  1. 用户开始编辑
  2.    │
  3.    ├─[现场] Undo (Ctrl+Z) ── 单元格级、未提交、单步
  4.    │        ↑ 只能救“输错字”,救不了已提交的值
  5.    │
  6.    ├─[持续守卫] itemchanged / Validation Rule / Edit.Required
  7.    │        → 非法数据在入缓冲前就被挡下(让 Revert 少干活)
  8.    │
  9.    ├─[跟踪] 脏标记 ModifiedCount()/DeletedCount()
  10.    │        → 决定“要不要保存”、标题是否标 *
  11.    │
  12.    ├─[落库前] Save Validation (of_Validate + 必填 + 脏判断)
  13.    │        → 不通过则阻断 Update,保留现场让用户改/撤销/还原
  14.    │
  15.    ├─[持久化] Update() + COMMIT ── 并检查 SQLNRows 做冲突检测
  16.    │
  17.    └─[逃生舱] Revert / Restore(重新检索 / Original 回写 / ResetUpdate)
  18.             → 放弃本次全部未保存改动,回到原始态或检查点
复制代码

一句话协作:设计期校验与 itemchanged 负责“防”,脏标记负责“知”,Save Validation 负责“把关”,Revert 负责“反悔”,Undo 负责“现场微调”。它们互不替代,而是层层兜底。




5. 功能边界(必须划清的红线)

能力作用层级是否涉及数据库能否回退“已 Update 提交”的数据典型误用
Undo单元格编辑内(未提交)否(仅编辑缓冲,提交即失效)以为能退已保存的修改
Revert(重新检索)整窗口数据(未落库)是(重新查)否(已落库需另写回滚/历史)在需保留现场时用它(会清空)
ResetUpdate()更新标志层把它当“还原数据”(其实只清标志,值还在)
Save Validation保存前检查否(检查本身)N/A只在 itemchanged 里校验,漏了保存前全量扫
冲突检测Update 执行时是(看 SQLNRows)N/AUpdate(TRUE) 自动接受后无法再判断 0 行冲突


补充红线


  • Undo ≠ Revert:Undo 是编辑过程中的单步文本撤销;Revert 是整表未保存改动的回退。两者层级完全不同。
  • ResetUpdate() ≠ 还原数据:前者清“脏标记”,后者清“数据”。想保留界面数值但暂不保存用 ResetUpdate();想彻底回到原始态用重新检索或 Original 回写。
  • Validation 不替代冲突检测:必填/合法性校验管“数据对不对”,并发冲突管“别人动没动”。二者解决不同问题,保存流程应同时具备。
  • COMMIT 的数据不在本文范围:本文三能力都只作用于“内存缓冲区”。一旦 COMMIT,要回退得靠数据库事务回滚、历史表或审计日志,不在 DataWindow 客户端机制内。





6. 自检清单(落地时逐条核对)


  • [ ] 撤销按钮先判 CanUndo()Undo();明白它只作用于未提交编辑。
  • [ ] “还原”按业务选路线 A/B/C:要彻底回原始态用重新检索;要就地回退用 Original 回写;要保留现场暂不保存用 ResetUpdate()
  • [ ] 必填项同时用 Edit.Required + 保存前 IsNull/Trim 扫描兜底。
  • [ ] 保存前用 ModifiedCount()+DeletedCount() 判断脏数据,无改动直接返回。
  • [ ] Update() 前务必 AcceptText();冲突检测用 Update(FALSE) + 判断 SQLCA.SQLNRows
  • [ ] 列定义里 updatewhereclause=yes(关键列)以支持 WHERE 生成与冲突识别;乐观并发加 version 时间戳列。
  • [ ] 保存流程:AcceptText → 脏判断 → of_Validate → Update(FALSE) → SQLNRows 冲突判断 → COMMIT / ROLLBACK → ResetUpdate




本文遵循 PB10SKILL 的命名与代码约定(变量前缀 ls_/li_/ll_/ld_/lb_,金额用 decimal,嵌入式 SQL 以 ; 结尾等),可直接作为团队 DataWindow 录入/维护窗口的规范参考。
共享共进共赢
Sharing And Win-win Results
SYBASEBBS - 免责申明1、欢迎访问“SYBASEBBS.COM”,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@sybasebbs.com
smallanntse

主题

0

回帖

1万

积分

论坛元老

积分
10395
贡献
在线时间
小时
昨天 16:51 | 显示全部楼层
写的好,详细。
共享共进共赢
Sharing And Win-win Results
您需要登录后才可以回帖 登录 | 站点注册

本版积分规则

免责声明:
本站所发布的一切破解补丁、注册机和注册信息及软件的解密分析文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。

Mail To:Admin@SybaseBbs.com

客服微信:18669893686
挂谷猜想 · 探索

QQ|Archiver|PowerBuilder(PB)BBS社区 ( 鲁ICP备2021027222号-1 )

GMT+8, 2026-8-14 01:34 , Processed in 0.020791 second(s), 9 queries , MemCached On.

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表