pbai 发表于 6 天前

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

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() 的作用是把当前编辑框内最后一次输入撤销掉。


// 工具栏/按钮 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 录入/维护窗口的规范参考。

smallanntse 发表于 6 天前

写的好,详细。
页: [1]
查看完整版本: DataWindow 数据内容操作:撤销(Undo)、还原(Revert/Restore)与保存检查(Save Validation)

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

Mail To:Admin@SybaseBbs.com