做 SAP RAP 开发的朋友应该都有过这种体验:好不容易把一个带参数的 Action 挂到 Fiori Elements 上,用户点完按钮,弹出一个 Action Popup,里面七八个字段,五个要手填。如果这个动作一天要执行几百次,输入量相当可观。之前我在一个采购订单审批增强里就碰到这个问题——动作弹窗要填拒绝原因、补充说明、审批人和日期,每次弹出来全是空白,用户一边填一边吐槽。后来把 RAP 的 Default Values Function 用起来,弹窗一打开默认值就位,用户只需要改一个备注再点确认,实测输入时间降了六成。
这篇文章就把这套做法从原理讲到实操。从最简单的静态预填写起,再到按选中实例动态取数预填,最后整理一份可以直接抄的代码模板和我踩过的几个坑。适合正在用 RAP 做 Fiori 服务端开发的同事,也适合刚从传统 ABAP 转 BTP 环境、想把手上的动作弹窗做得更顺手的同学。
1. 先搞懂 Action Popup 和默认值函数的关系
1.1 带参数的动作,如何变成弹窗
在 RAP(ABAP RESTful Application Programming Model)里,action 是业务对象对外暴露的一个操作,比如审批、拒绝、取消、创建后续凭证。行为定义里每声明一个 action,它就可以被 OData 服务暴露出去,最终以按钮的形式出现在 Fiori 界面上。
只要这个 action 带了参数,Fiori Elements 前端在用户点击按钮时就不会直接执行动作,而是先弹出一个模态框,把参数对应的字段拉出来让用户填。这个模态框就是标题里的 Action Popup。你可以在行为定义中给动作配置一个参数结构,这个结构决定了弹窗里有哪些字段、字段顺序、字段标签。前端本身没有太多逻辑,它只是把后端定义好的"表单"渲染出来。
所以这里有个关键认知:Action Popup 不是一个前端组件,它是后端行为定义到前端渲染的映射结果。弹窗里出现的每一个字段,都对应参数类型里的一个元素。这个认知想清楚,后面你就能理解为什么默认值函数放在后端而不是前端实现。
1.2 没有默认值时,用户到底多做了什么
没有默认值函数的 Action Popup,所有字段都是空白。拿审批场景举例:用户点了"拒绝"按钮,弹窗弹出,需要填拒绝代码、补充说明、审批人、审批日期。但实际使用中,拒绝代码九成情况都是同一个值,审批人就是当前登录用户,日期就是今天,只有补充说明是需要重新输入的。
每个字段都要手选、手填、反复确认,一次动作多花几十秒。高频使用时,比如客服批量拒绝异常订单,一天上百次操作,浪费的时间是肉眼可见的。更麻烦的是,必填字段一多,用户漏填一个,前端直接报校验错误,弹窗重新弹出来,又要从头填一遍。这种体验一旦出现,业务的抱怨就会直接落到你头上。
这类问题表面上是"用户输入习惯"问题,本质上是后端没有给前端提供合理的初始状态。你想想,一个表单如果打开时已经有合理默认值,用户的操作就会从"填写"变成"确认",差之千里。
1.3 默认值函数要解决的核心矛盾
Default Values Function 解决的就是这个矛盾:把输入型弹窗变成确认型弹窗。它在弹窗打开之前执行一次,把推荐值预先塞进字段;用户打开弹窗看到的是已经成型的表单,只需要改自己不认可的部分,然后确认提交。
这不是前端写几行 JS hack,而是后端声明式机制。你只需要在行为定义里用 default 关键字声明一个函数,字段预填就自动继承到所有消费者。不管你用的是标准 Fiori Elements 列表报告,还是预览模式,甚至其他 OData 消费端,默认值都会生效。对开发团队来说,这套机制的成本非常低,不需要写前端扩展,也不需要维护一堆自定义控件。下面开始讲具体怎么落地。
2. 动手前准备:参数类型、行为定义与版本确认
2.1 参数类型用抽象实体,别再用陈旧结构
动作参数的标准做法是在 CDS 里定义一个抽象实体(abstract entity)。抽象实体没有存储映射,不落库、不建表,专门用于行为交互时的数据传递。
@EndUserText.label: 'Reject parameter' abstract entity ZI_REJECT_PARAM { @EndUserText.label: 'Reject Code' reject_code : abap.char(4); @EndUserText.label: 'Description' description : abap.char(80); @EndUserText.label: 'Created By' created_by : abap.char(12); @EndUserText.label: 'Created At' created_at : abap.datn; }抽象实体有两个好处。第一,它不依赖任何持久化表,字段你可以随意增减,不影响数据一致性。第二,它可以在行为定义的参数声明中直接引用,RAP 框架能识别并自动生成对应的 OData 结构。
一个容易被忽略的点:字段命名尽量和业务对象实体的同名保持一致。比如 BO 里供应商字段叫 Supplier,参数实体里就用 supplier。这样后面写默认值函数时,用 CORRESPONDING 或者直接赋值都不会手滑,代码可读性也好。如果字段完全对不上,赋值时就要一个个映射,费时还容易错。
2.2 行为定义挂载默认值函数的正确姿势
行为定义里声明动作参数和默认值函数的语法如下:
action rejectReason parameter ZI_RejectParam default getDefaultReject; action ( features: instance ) createFollowOn parameter ZI_FollowParam default getFollowOnDefault;第一行是静态默认值函数。它的特点是:不管用户选中一行还是多行,这个函数只被调用一次,所有实例共用同一份默认值。适合那些与业务数据无关的固定值:当前用户、当前日期、固定的枚举值等。
第二行加了( features: instance ),这是实例默认值函数。它针对每个选中实例各自调用一次,函数可以拿到当前选中单据的 keys,从而读取单据头字段,把这些数据带进弹窗。比如创建后续订单时,自动带出原订单的供应商、采购组织、公司代码、币别。选择哪种模式,核心取决于默认值依不依赖当前行数据。
两种模式的差异我整理成了表格:
| 维度 | 静态默认值函数 | 实例默认值函数 |
|---|---|---|
| 声明方式 | default getDefaultReject | ( features: instance ) default getFollowOnDefault |
| 调用次数 | 整个动作触发一次 | 每个选中实例各一次 |
| 能否读取选中行 keys | 不能 | 可以 |
| 适用场景 | 当前用户、日期、固定分类 | 从单据头部/明细带出字段 |
| 性能开销 | 低 | 较高,注意控制读取的数据量 |
我见过不少同事在静态默认值函数里尝试用 keys 读数据,编译直接报错,因为静态模式根本不会传 keys 进来。如果你需要带出当前行的字段,一定记得在行为定义里加上( features: instance )。
2.3 版本与运行环境确认:别让编译报错打断你
RAP 默认值函数不是所有版本都支持。我在 SAP BTP ABAP 环境和 S/4HANA 2021 及以后版本上验证过,完整支持default关键字和实例化默认值函数。如果你在较老的 S/4HANA 2020 上写这类代码,行为定义激活时会直接报语法错误,根本走不到运行阶段。
如果环境版本偏老,我的建议是先确认升级计划,不要花时间做兼容方案。因为这个功能是后端声明式能力,和前端、OData 生成深度绑定的,低版本强行模拟的成本很高。另外,如果你写完后编译报错,但代码看起来没错,先去帮你系统的行为定义帮助文档里搜"default value function",看关键词在当前版本是否被识别。
3. 默认值函数实现:从静态到实例再到动态控制
3.1 最简单场景:静态默认值一把梭
静态默认值函数是最容易上手的写法。在行为实现类里补一个方法,方法名和 BD 里声明的一致。我用拒绝审批弹窗举例:
METHOD getDefaultReject. result = VALUE #( ( %cid = 'CID_REJ' reject_code = 'OTHER' description = 'Need More Information' created_by = sy-uname created_at = cl_abap_context_info=>get_system_date( ) ) ). ENDMETHOD.这里sy-uname取当前登录用户,cl_abap_context_info=>get_system_date( )取系统日期。于是用户打开弹窗时,拒绝代码、说明、审批人、日期全部预填好了,他只需要在必要时修改说明。这个方法不需要 keys,不依赖任何业务数据,纯粹是"打开即就位"。
一个值得注意的点:默认值函数只是预填,不是锁定。用户提交动作后,后端动作实现方法收到的参数以用户最终修改后的值为准。如果你希望某些字段不能被用户改,单靠静态赋值是不够的,需要配合后面讲的%control动态控制。
3.2 实例默认值:把单据头部字段带进弹窗
很多场景需要把当前选中单据的信息带到弹窗里。比如做一个"创建后续订单"的动作,弹窗里的供应商、采购组织、公司代码、币别应该直接沿用当前订单的值,用户只需要填一个目标数量即可。
行为定义里声明实例默认值函数后,行为实现方法可以写成这样:
METHOD getFollowOnDefault. READ ENTITIES OF zi_purchaseorder IN LOCAL MODE ENTITY PurchaseOrder FIELDS ( Supplier PurchOrg CompanyCode Currency ) WITH CORRESPONDING #( keys ) RESULT DATA(orders). result = VALUE #( FOR order IN orders ( %cid = keys[ 1 ]-%cid supplier = order-Supplier purch_org = order-PurchOrg company_code = order-CompanyCode currency = order-Currency target_quantity = 1 ) ). ENDMETHOD.这段代码有几个要点。第一,READ ENTITIES IN LOCAL MODE是在行为实现里读取自身业务对象的数据,既可以读自己,也可以读关联实体。第二,FIELDS里只列真正需要的字段,不要贪多,性能会直接体现在弹窗打开速度上。第三,result是一个表,用FOR order IN orders逐行生成默认值,这样多选实例时每行都有对应默认值。
我实际测试下来,实例默认值函数最坑的是 keys 里可能会有多行,而很多示例代码只写了keys[ 1 ]。如果列表上支持多选,你需要想清楚是要"只取第一个选中行做默认值",还是"每行各自按自己的字段预填"。上面示例是按第二种逻辑写的,相对通用。
3.3 %control 动态控制:只读、必填、可见性随心调
预填值有了以后,另一个高频需求是控制字段状态:有些字段要锁死不可改,有些字段用户不用看到但后端必须要,有些字段预填了但还是要强调必填。在 RAP 默认值函数里,这些可以通过%control返回给前端。
METHOD getDefaultReject. result = VALUE #( ( %cid = 'CID_REJ' reject_code = 'OTHER' description = 'Need More Information' created_by = sy-uname created_at = cl_abap_context_info=>get_system_date( ) %control-reject_code-read_only = if_abap_behv=>co-control-read_only-on %control-description-mandatory = if_abap_behv=>co-control-mandatory-on %control-created_by-visible = if_abap_behv=>co-control-visible-off ) ). ENDMETHOD.这段代码做了三件事:拒绝代码只读,描述必填,创建人字段隐藏。实际界面上,用户看不到创建人,也不会被要求修改拒绝代码,弹窗变得非常干净。
这里要注意两个容易出错的地方。第一,%control动态控制需要你的环境支持,如果编译报错,优先确认版本,再去查if_abap_behv下有没有对应的控制常量,Eclipse 里把鼠标放上去按 F2 看常量列表即可。第二,visible-off 的字段如果又是必填,必须先通过默认值把它的值填好,然后再隐藏,否则用户看不到字段却还要填,前端会一直报校验错误。
3.4 默认值函数的边界:哪些坑别踩
默认值函数不是万能工具,它是一个"预填回调",职责非常单一。我在项目上吃过几次亏,总结下来就几句话。
第一,不要在默认值函数里做权限校验或业务校验。这个阶段只是给用户展示推荐值,如果权限不通过就返回错误,体验上等同于打不开弹窗,很突兀。校验请放在动作执行方法里做。第二,不要在这里返回 failed 或 reported。默认值函数不参与动作提交,报错的语义不清晰,前端也不会正确展示到字段上。第三,不要在默认值函数里做复杂的数据库读取或大量循环计算。它每次打开弹窗都会执行,执行时间越长,用户等待越久。第四,取值一定要兜底。实例模式下如果某行某些字段没维护,带出来是空值,不要因此让整个弹窗崩溃,给个合理兜底值或者留空。
4. 一个能跑通的完整案例:拒绝审批动作
4.1 案例设计与 CDS 参数实体
下面用一个完整的拒绝审批动作串一遍整个开发链路。假设业务对象是采购订单审批,用户点击"拒绝"按钮,弹窗里预填拒绝原因、备注、审批人和审批日期。
需要准备的 CDS 对象有两个:一个是业务对象根视图ZI_PURCHASEORDER,一个是抽象实体参数ZI_REJECT_PARAM。
@EndUserText.label: 'Purchase Order' define root view entity ZI_PURCHASEORDER as select from ztp_purchaseorder { key purchase_order_id as PurchaseOrderId, supplier as Supplier, purch_org as PurchOrg, company_code as CompanyCode, currency as Currency }@EndUserText.label: 'Reject reason parameter' abstract entity ZI_REJECT_PARAM { @EndUserText.label: 'Reject Code' reject_code : abap.char(4); @EndUserText.label: 'Description' description : abap.char(80); @EndUserText.label: 'Created By' created_by : abap.char(12); @EndUserText.label: 'Created At' created_at : abap.datn; }参数实体里的字段不需要和 BO 实体字段一一对应,按弹窗需求设计就好。如果你希望调整弹窗字段顺序,可以在抽象实体里加上@UI相关注解控制标签和位置,具体注解强度和版本相关,以你的环境支持为准。
4.2 行为定义和实现类改动
接下来打开行为定义ZI_PURCHASEORDER_BD,在根节点行为定义里增加动作声明:
define behavior for ZI_PURCHASEORDER alias PurchaseOrder persistent table ztp_purchaseorder lock master authorization master ( instance ) { field ( readonly ) PurchaseOrderId; field ( mandatory ) Supplier, PurchOrg, CompanyCode, Currency; action rejectReason parameter ZI_RejectParam default getDefaultReject; mapping for ztp_purchaseorder { PurchaseOrderId = purchase_order_id; } }然后在投影行为定义里把动作暴露出去:
define behavior for ZI_PURCHASEORDER alias PurchaseOrder { use action rejectReason; }激活行为定义后,Eclipse 会提示行为实现类缺少对应方法。你需要在实现类lhc_purchaseorder(局部处理类)里补上默认值函数和动作执行方法的框架:
CLASS lhc_purchaseorder IMPLEMENTATION. METHOD getDefaultReject. result = VALUE #( ( %cid = 'CID_REJ' reject_code = 'OTHER' description = 'Need More Information' created_by = sy-uname created_at = cl_abap_context_info=>get_system_date( ) ) ). ENDMETHOD. METHOD rejectReason FOR MODIFY. " 这里写拒绝动作的实际业务逻辑 " 读取参数、更新状态、写日志等 ENDMETHOD. ENDCLASS.激活之后,这个动作就算完成了。你不需要动任何前端代码,Fiori Elements 会自动渲染 Action Popup,并在用户点击"拒绝"按钮时调用默认值函数。
4.3 Fiori Elements 预览测试步骤
测试这套功能,我习惯直接用 Service Binding 里的预览。右键 Service Binding,选择 Preview,浏览器会打开 Fiori Elements 页面。
第一步,找到列表页或对象页上的"Reject"按钮。第二步,点击按钮,Action Popup 弹出,此时四个字段应该已经预填完毕。第三步,尝试修改说明字段,提交动作,观察后端业务逻辑是否生效。
这里有一个很实用的调试技巧:在getDefaultReject方法里打一个断点,然后在 Fiori 预览里点击按钮。如果 ADT 调试器命中,说明框架确实在弹窗打开前调用了默认值函数。这个方法可以用来确认默认值函数到底有没有被挂上,比看日志快得多。
预览时最常见的困惑是改了默认值函数后,预览页面一直没变化。这通常不是代码问题,而是前端元数据缓存。在 Service Binding 里重新 Publish 一次服务,再在浏览器里强制刷新页面,基本都能解决。
5. 常见问题与排查速查表
5.1 高频问题 Q&A
下面的表格是我把这些功能铺到多个项目后整理的排查思路,挺实用,直接给后来人参考。
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 弹窗打开默认值没出现 | BD 里没挂 default 函数,或前端拿到旧 OData 元数据 | 检查 BD 声明;重新激活并重新 Publish 服务 |
| 默认值只弹一次,多选时不合常理 | 误用了静态默认值函数 | 看 BD 里有没有( features: instance ),没有就补上 |
| 编译报错,default 关键字不识别 | 环境版本过老,不支持默认值函数 | 查环境支持版本;升级系统 |
| 预填字段用户还能改,想锁死 | 没有设置只读控制 | 加%control-字段-read_only = if_abap_behv=>co-control-read_only-on |
| 字段隐藏了但还是必填,前端报错 | visible-off 与 mandatory 冲突 | 先赋默认值再隐藏,或把 mandatory 关掉 |
| 默认值函数里怎么也拿不到 keys | 用了静态默认值函数 | 改成( features: instance )声明,再写实现 |
5.2 三个我真实踩过的坑
第一个坑是把默认值函数当成校验入口用。我有一版代码在默认值函数里读取数据后判断用户权限,权限不够直接 set failed。结果前端弹窗完全打不开,而且错误信息定位不到字段,业务用户一头雾水。后来才想明白,默认值函数是"预填回调"不是"预检回调",校验逻辑统一放到动作方法里才是正解。
第二个坑是字段大小写和对齐问题。曾经为了省事,我在默认值函数里用 CORRESPONDING 把参数实体和 BO 实体整体映射,结果激活不报错、运行也不报错,但弹窗里几个字段全空。原因是两个结构的字段名不完全一致,CORRESPONDING 静默跳过不匹配的字段。之后我改成逐字段赋值,宁可多写两行,也不依赖隐式映射。
第三个坑和缓存有关。有一次改完默认值函数,反复看预览都是旧值,一度怀疑代码没激活。最后发现是浏览器和 OData 服务的双重缓存。我的排查顺序现在固定为:先在 ADT 里看激活状态,再去 Service Binding Publish,最后浏览器开无痕窗口验证。这样做基本能排除大部分"改了没生效"的假象。
我在项目上把这套默认值函数铺开之后,一个明显的感受是用户对系统的信任感上来了。他们不再把弹窗当作一张要填的问卷,而更像一个确认框。建议你在交付高频动作时,把"字段清单 + 默认值 + 只读/隐藏/必填"做成一张表,先给业务确认,再写实现。我后面再做类似功能,都是先让用户画一遍弹窗长什么样,再回来写行为定义,几乎不用返工。
最后再分享一个小技巧:如果你的动作字段多、每个字段都要预填,分两步走效率更高。第一步先做静态默认值函数,把与数据无关的共性字段填上;第二步再针对依赖当前单据的字段,升级成实例默认值函数,用READ ENTITIES带数据。这样每一步的改动都很小,出问题也好定位。默认值函数不是多复杂的技术,但它确实能让高频业务操作的手感完全不一样,值得在项目里认真用起来。