马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
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 节)。
| 缓冲区 | 枚举 | 内容 | | Primary | Primary! | 当前显示、未被过滤/删除的行 | | Filter | Filter! | 被 SetFilter/Filter() 过滤掉的行 | | Delete | Delete! | 被 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() 的作用是把当前编辑框内最后一次输入撤销掉。
- // 工具栏/按钮 cb_undo.Clicked
- if dw_1.CanUndo() then
- dw_1.Undo() // 撤销当前编辑框内最后一次输入(单元格级、未提交)
- 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:重新检索(最干净,回到数据库原始态)
- // 放弃一切内存改动,从数据库重新取
- dw_1.SetTransObject(SQLCA)
- dw_1.Retrieve(ls_key) // 重新检索 → Primary/Delete 缓冲全部重建,改动清零
复制代码
- 优点:逻辑最稳,新增行、修改行、删除行一次性全部回到数据库真实状态(连被删的行都回来了)。
- 代价:一次数据库往返;会丢失所有未保存编辑(这正是“还原”的目的);检索参数需与当初一致。
路线 B:用 Original 缓冲区逐列回写(不跑 SQL)
适用于“想还原但不想再查库”、且有 Original 值可参照时:
- // 自定义函数 of_RevertData():把未保存改动还原到最初检索值
- long ll_row, ll_cnt, ll_status
- ll_cnt = dw_1.RowCount()
- for ll_row = ll_cnt to 1 step -1
- ll_status = dw_1.GetItemStatus(ll_row, 0, Primary!)
- choose case ll_status
- case DataModified!
- // 已修改的已有行:用 Original 缓冲逐列覆盖
- dw_1.SetItem(ll_row, "cust_name", dw_1.GetItemString (ll_row, "cust_name", Primary!, true))
- dw_1.SetItem(ll_row, "age", dw_1.GetItemNumber (ll_row, "age", Primary!, true))
- dw_1.SetItem(ll_row, "start_date", dw_1.GetItemDateTime(ll_row, "start_date", Primary!, true))
- dw_1.SetItemStatus(ll_row, 0, Primary!, NotModified!)
- case New!, NewModified!
- // 新插入的行:直接删掉
- dw_1.DeleteRow(ll_row)
- end choose
- next
- // 可选:把被“删除”的行也还原回来(从 Delete 缓冲移回 Primary)
- // dw_1.RowsMove(1, dw_1.DeletedCount(), Delete!, dw_1, 1, Primary!)
- dw_1.ResetUpdate() // 清除全部更新标志(见 2.2)
复制代码
GetItemX(row, col, Primary!, true) 中最后一个参数 true 表示“取原始值(Original 缓冲)”,这是回写还原的关键。
路线 C:清除更新标志 ResetUpdate() —— “撤销保存意图”,不是“还原数据”
- dw_1.ResetUpdate() // 把所有行标记为 NotModified!,Update() 将生成 0 条 SQL
复制代码
- 作用:让 Update() 认为“没有改动可保存”,于是保存动作变成空操作。
- 重要边界:ResetUpdate() 只清标志、不改显示值。已被用户改动的数值仍显示在界面上,只是不再“脏”。它适合“暂不保存、保留现场”的场景,不要把它和“还原数据”混为一谈。
2.2 检查点(Checkpoint)还原
有些业务需要“回到进入明细之前的快照”,而不是回到数据库原始态。可用 DataStore/字符串快照实现任意检查点:
- // 进入编辑前打检查点
- datastore ids_snap
- ids_snap = create datastore
- ids_snap.DataObject = dw_1.DataObject
- ids_snap.Object.DataWindow.Data = dw_1.Object.DataWindow.Data // 复制当前全部数据
- // 需要时还原检查点
- dw_1.Object.DataWindow.Data = ids_snap.Object.DataWindow.Data
- 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 事件:
- // dw_1.itemchanged(row, dwo, data)
- choose case dwo.Name
- case "qty"
- if Long(data) <= 0 then
- MessageBox("校验", "数量必须大于 0")
- return 1 // 1 = 拒绝本次修改,焦点离开
- end if
- case "email"
- if Pos(data, "@") = 0 then
- MessageBox("校验", "邮箱格式错误")
- return 2 // 2 = 拒绝且焦点停留在本格
- end if
- end choose
- return 0 // 0 = 接受
复制代码
data 是用户刚输入的新值(尚未入缓冲),因此 itemchanged 能在非法数据进入缓冲区之前就挡住它,这是最省事的“前置校验”。
设计期校验规则(Validation Rule):在 DataWindow 画板给列设表达式(如 qty > 0),失败时触发 itemerror 事件。配合 itemerror 返回码可定制提示。
批量层(保存前全量扫)——见 3.5 的 of_Validate()。
3.2 必填项检查
两种方式叠加使用,最稳妥:
- 设计期:列 Edit.Required = yes。当用户在必填格内留空并离开时,PB 会触发必填提示(经 itemerror)。
- dw_1.Modify("cust_name.Edit.Required = yes")
-
复制代码
- 保存前手动扫(更可靠,也能覆盖下拉/只读赋值场景):
- if IsNull(dw_1.GetItemString(ll_row, "cust_name")) or &
- Trim(dw_1.GetItemString(ll_row, "cust_name")) = "" then
- // 必填项为空 → 报错
- end if
-
复制代码仅靠 Edit.Required 有时不够(例如值由代码 SetItem 写入或下拉选择)。保存前的必填扫描是兜底,建议都做。
3.3 脏数据标记(Dirty Flag)判断
“脏”= 缓冲区与原始态不一致。ModifiedCount() / DeletedCount() 是最常用的整体脏标记:
- if dw_1.ModifiedCount() = 0 and dw_1.DeletedCount() = 0 then
- MessageBox("提示", "没有需要保存的改动。")
- return
- 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 行:
- // 更新前先不要自动接受改动(acceptchanges = FALSE)
- if dw_1.Update(FALSE) = 1 then
- if SQLCA.SQLNRows = 0 then // 期望更新 N 行,实际 0 行 → 并发冲突
- rollback using SQLCA ;
- MessageBox("冲突", "数据已被其他用户修改,请重新检索后再保存。")
- return // 注意:此时改动已被清,需重新 Retrieve
- end if
- commit using SQLCA ;
- dw_1.ResetUpdate() // 接受改动,清除脏标记
- else
- rollback using SQLCA ;
- MessageBox("错误", "保存失败:" + SQLCA.SQLErrText)
- end if
复制代码
更稳的乐观并发:在表里加 version / last_update 时间戳列,并将其也设为 updatewhereclause=yes,这样一旦版本不符,UPDATE 直接 0 行,冲突在 WHERE 阶段就被发现。
3.5 统一的保存前检查函数
把校验集中成一个函数,保存按钮只调它:
- // of_Validate() returns boolean —— TRUE 通过,FALSE 不通过(已提示)
- long ll_row, ll_cnt
- string ls_msg
- datetime ldt_s, ldt_e
- ll_cnt = dw_1.RowCount()
- for ll_row = 1 to ll_cnt
- // 1) 必填项
- if IsNull(dw_1.GetItemString(ll_row, "cust_name")) or &
- Trim(dw_1.GetItemString(ll_row, "cust_name")) = "" then
- ls_msg = "第 " + String(ll_row) + " 行:客户名称为必填项。"
- goto fail
- end if
- // 2) 数据合法性(业务规则)
- if dw_1.GetItemNumber(ll_row, "age") < 0 then
- ls_msg = "第 " + String(ll_row) + " 行:年龄不能为负。"
- goto fail
- end if
- // 3) 跨列逻辑
- ldt_s = dw_1.GetItemDateTime(ll_row, "start_date")
- ldt_e = dw_1.GetItemDateTime(ll_row, "expire_date")
- if Not IsNull(ldt_e) and ldt_e < ldt_s then
- ls_msg = "第 " + String(ll_row) + " 行:到期日不能早于开始日。"
- goto fail
- end if
- next
- return TRUE
- fail:
- MessageBox("保存检查未通过", ls_msg, StopSign!)
- return FALSE
复制代码
3.6 完整保存流程(把上述串起来)
- // cb_save.Clicked
- if dw_1.AcceptText() <> 1 then return // 关键:把正在编辑的格写入缓冲区
- if dw_1.ModifiedCount() = 0 and dw_1.DeletedCount() = 0 then
- MessageBox("提示", "没有需要保存的改动。")
- return
- end if
- if not of_Validate() then return // 保存前校验
- // ↓ 见 3.4 的 Update + 冲突检测
复制代码
AcceptText() 必须放在最前——它把当前编辑控件的内容提交进缓冲区,否则最后编辑的那格不会被 ModifiedCount() 统计、也不会被 of_Validate() 扫到。
4. 三者的协作关系(时间层与数据流)
- 用户开始编辑
- │
- ├─[现场] Undo (Ctrl+Z) ── 单元格级、未提交、单步
- │ ↑ 只能救“输错字”,救不了已提交的值
- │
- ├─[持续守卫] itemchanged / Validation Rule / Edit.Required
- │ → 非法数据在入缓冲前就被挡下(让 Revert 少干活)
- │
- ├─[跟踪] 脏标记 ModifiedCount()/DeletedCount()
- │ → 决定“要不要保存”、标题是否标 *
- │
- ├─[落库前] Save Validation (of_Validate + 必填 + 脏判断)
- │ → 不通过则阻断 Update,保留现场让用户改/撤销/还原
- │
- ├─[持久化] Update() + COMMIT ── 并检查 SQLNRows 做冲突检测
- │
- └─[逃生舱] Revert / Restore(重新检索 / Original 回写 / ResetUpdate)
- → 放弃本次全部未保存改动,回到原始态或检查点
复制代码
一句话协作:设计期校验与 itemchanged 负责“防”,脏标记负责“知”,Save Validation 负责“把关”,Revert 负责“反悔”,Undo 负责“现场微调”。它们互不替代,而是层层兜底。
5. 功能边界(必须划清的红线)
| 能力 | 作用层级 | 是否涉及数据库 | 能否回退“已 Update 提交”的数据 | 典型误用 | | Undo | 单元格编辑内(未提交) | 否 | 否(仅编辑缓冲,提交即失效) | 以为能退已保存的修改 | | Revert(重新检索) | 整窗口数据(未落库) | 是(重新查) | 否(已落库需另写回滚/历史) | 在需保留现场时用它(会清空) | | ResetUpdate() | 更新标志层 | 否 | 否 | 把它当“还原数据”(其实只清标志,值还在) | | Save Validation | 保存前检查 | 否(检查本身) | N/A | 只在 itemchanged 里校验,漏了保存前全量扫 | | 冲突检测 | Update 执行时 | 是(看 SQLNRows) | N/A | 用 Update(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 录入/维护窗口的规范参考。 |