多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

SAP ABAP ALV选中数据操作:函数式与面向对象实现详解

SAP ABAP ALV选中数据操作:函数式与面向对象实现详解 做了这么多年ABAP开发我有个很深的体会ALV显示这一关基本上干过一两个项目的顾问都能写出来但真正让用户觉得“这个报表好用”还是“这个报表难用”的往往不是列表本身而是ALV显示完之后那几步。用户勾了几行数据程序能不能稳定、准确地把这些选中数据接住再去做后续的业务处理这才是区分初级开发和能独立解决问题的开发的分水岭。打个比方ALV列表就像食堂的取餐窗口菜端出来了不算完用户指着其中几碟说“我要这几个”后厨还得能准确知道他要的是哪几个不能给他端错。放到SAP ABAP里就是“ALV显示后选择数据操作”这个经典需求。日常开发里最常见的例子就是财务对账ALV里拉出几百条未清项用户在屏幕上勾出其中几十条点一个“过账”或者“输出清单”的按钮程序需要把这几十条的数据准确捞出来做后续处理。这个动作要是做不好轻则按钮没反应重则取错数据、更新错单据直接影响财务月结那就麻烦了。这篇内容我打算从函数式ALV和面向对象ALV两条路线把“选中数据后怎么拿、拿到之后怎么处理、处理完怎么刷新”这一整条链路讲清楚。无论你是刚学会REUSE_ALV的初级开发还是天天跟ALV打交道的自由顾问或者是面试前想梳理知识点的候选人相信都能从中找到能直接用上的东西。1. 先把需求看透ALV选中数据操作的三种形态1.1 财务对账场景里最典型的“勾选-操作”链路先别急着写代码做ABAP最怕的就是需求没想清楚就开干。我见过不少项目开发小哥接到需求说“在ALV后面加个按钮能对选中的行做操作”结果代码写了一堆上线之后用户用起来各种别扭最后还是返工。原因很简单“选中数据操作”这六个字背后其实藏着完全不同的几种技术路径。拿我前面说的财务未清项对账来举例用户进入事务码FBL1N或者自定义报表看到一堆未清项他通常要做的操作有两种第一种是选中一行点“查看明细”第二种是勾选多行点“批量过账”或者“标记为已清”。这两类需求虽然在用户看来都是“选中数据然后操作”但在技术上单行读取和多行读取的写法是有明显区别的。还有一种情况更要小心就是表格本身是可编辑的。用户先在上面改了某些字段比如把备注字段填了一下然后才勾选操作。这个时候你不仅要拿到他勾了哪些行还得把他刚修改的内容也一起同步进程序内表否则你拿到手的数据还是旧值处理出来的结果自然不对。这一类需求我放在第4章单独讲因为它的坑最深。1.2 三种技术形态与选型思路基于我自己的项目经验ALV选中数据的操作大体能分成三种形态第一种是单行读取典型场景是“双击进入明细”或“选中一行后点查看”。这种用USER_COMMAND回调里的自建字段SELFIELD就能拿到当前行号代码量很小。第二种是多行批量读取典型场景是“勾选多行统一打标记”或者“批量打印”。这种就不能只靠SELFIELD了通常需要用到内表里的一个标记字段或者通过Grid对象获取被选中的行集合。第三种是编辑后数据读取典型场景是“ALV里直接维护批次号然后选中、提交”。这种必须先处理数据回写把界面上用户输入的临时值同步到内表里再去做后续操作。选型思路也很直接老项目里大量用函数式ALV也就是REUSE_ALV_GRID_DISPLAY这套代码集中但灵活性差新项目或者需要复杂交互的报表强烈建议用面向对象ALV也就是CL_GUI_ALV_GRID这套虽然没有那么“短平快”但事件处理清晰遇到复杂需求的时候扩展起来真的省很多事。1.3 选择数据操作前必须先想清楚的三件事动手之前我建议你先逼自己回答三个问题回答清楚了方案基本就有谱了。第一个问题用户做这个操作的频率高不高如果是一个高频操作比如每天要用几十次的批量过账那你要重点考虑操作效率选中的方式要顺手比如支持整列选择、支持框选操作后的提示要及时。第二个问题操作能不能回滚如果操作会修改数据库表或者触发BAPI过账那你必须提醒自己这块逻辑要足够健壮不能在USER_COMMAND里糊弄几下就完事。要考虑到重复点击、重复提交的可能性加好锁和状态判断。第三个问题你的ALV内表会不会在显示前被排序或过滤很多报表为了显示好看会给内表做SORT或者过滤掉某些行但这会直接导致“ALV显示行号”和“内表索引”不一致后面读数据的时候如果你还是傻乎乎地按行号去读读到的就是错的数据。这个问题我第5章还会展开讲这里先记住一个结论任何按行号取数据的逻辑都得先确认内表顺序和展示顺序是一致的。2. 函数式ALV老项目里最常用的选中数据实现2.1 先做一个能接收按钮事件的ALV函数式ALV虽然老但存量系统里它的占比依然很大。很多定制报表、增强程序底子都是REUSE_ALV_GRID_DISPLAY。要在这种ALV上实现对选中数据的操作第一步是注册回调FORM。通常你在REUSE_ALV_GRID_DISPLAY函数里会看到这样的参数CALL FUNCTION REUSE_ALV_GRID_DISPLAY EXPORTING i_callback_program sy-repid i_callback_user_command FRM_USER_COMMAND it_fieldcat gt_fieldcat is_layout gs_layout i_save A TABLES t_outtab gt_alv_data EXCEPTIONS program_error 1 OTHERS 2.这个FRM_USER_COMMAND就是核心回调。用户点ALV工具栏上任何一个按钮这个FORM都会被触发按钮的功能码会通过参数传进来。在这里面你就能根据按钮来判断用户想干什么然后去取选中的数据。千万注意回调FORM的名字必须在这个程序里真实存在而且类型要跟ALV框架期望的一致。以前见过有人把回调FORM写成带过多参数或者少参数的直接运行时dump这种错误非常低级但很常见。注册好了之后还需要在内部表里处理一个事件表。如果只关注USER_COMMAND回调其实不需要额外设置事件表因为i_callback_user_command本身就是内置支持的。但如果后面你想做双击、F1帮助、增删行这类事件那就得用it_events构建事件表了这里先不提别把新手绕晕。2.2 用SELFIELD-TABINDEX读取当前选中行FRM_USER_COMMAND这个FORM标准签名长这样FORM frm_user_command USING p_ucomm LIKE sy-ucomm p_selfield TYPE slis_selfield.p_ucomm是用户点的按钮功能码p_selfield是一个结构里面带着ALV当前处理状态的信息其中最常用的就是TABINDEX也就是用户当前光标所在的行号。所以单行读取的逻辑就非常直接了。判断按钮然后用TABINDEX去内表里读那一条记录FORM frm_user_command USING p_ucomm LIKE sy-ucomm p_selfield TYPE slis_selfield. DATA: ls_alv LIKE LINE OF gt_alv_data. CHECK p_ucomm ZVIEW. 用户点了“查看明细”按钮 IF p_selfield-tabindex 0. READ TABLE gt_alv_data INTO ls_alv INDEX p_selfield-tabindex. IF sy-subrc 0. PERFORM frm_show_detail USING ls_alv. ENDIF. ENDIF. ENDFORM.这里有一个最容易踩的坑我必须得说清楚p_selfield-tabindex是ALV显示行号不是内表索引。如果你的内表和ALV展示的顺序完全一致那上面的READ TABLE INDEX没问题但如果你在调用ALV之前对gt_alv_data做了SORT或者中间有过滤那TABINDEX5可就不是内表第5行了读出来的一定是错误数据。解决办法通常两个方向要么保证展示内表顺序和原始内表一致要么不要直接读原始内表而是把传给ALV的那个内表当成“前台展示快照”来读再从快照里的业务主键去找真正的业务数据。这个思路做定制报表的时候特别重要。2.3 用BOX_FIELDNAME实现多选勾选并回写内表单行操作好办但实际业务里大量需求是多选。函数式ALV里要支持多选并拿到所有选中行最稳妥的做法不是去解析SELFIELD而是给内表加一个隐藏的标记字段配合布局参数BOX_FIELDNAME让ALV自动生成复选框列。具体做法分三步。第一步内表结构里增加一个字段通常叫SEL类型是CHAR1。如果你用DDIC结构可以在附加结构里加如果你用的是局部结构直接在定义里加一行TYPES: BEGIN OF ty_alv, sel TYPE char1, 勾选标记 bukrs TYPE bukrs, belnr TYPE belnr_d, gjahr TYPE gjahr, dmbtr TYPE dmbtr, waers TYPE waers, ... END OF ty_alv.第二步布局里指定复选框字段gs_layout-box_fieldname SEL.这样ALV显示的时候就会自动在最前面加一列复选框用户勾选后这个勾选状态会被ALV框架直接回写到内表记录对应的SEL字段里。这就是函数式ALV多选的原理勾选状态实际上和数据绑定在一起了不需要你去额外维护什么选中集合。第三步在USER_COMMAND里直接对内表做筛选把SEL等于X的行抓出来FORM frm_user_command USING p_ucomm LIKE sy-ucomm p_selfield TYPE slis_selfield. DATA: ls_alv LIKE LINE OF gt_alv_data. CHECK p_ucomm ZPOST. LOOP AT gt_alv_data INTO ls_alv WHERE sel X. PERFORM frm_process_selected_data USING ls_alv. ENDLOOP. 处理完后清除勾选状态否则用户会困惑 LOOP AT gt_alv_data ASSIGNING FIELD-SYMBOL(fs_line) WHERE sel X. fs_line-sel . ENDLOOP. ENDFORM.有一个细节要提醒如果你不想让SEL这一列显示给用户看可以在字段目录里把这列设为技术字段也就是在构建字段目录时把某个字段的tech属性设为X或者通过REFESH_FIELD_CATALOG把它过滤掉。但要注意技术字段虽然不显示BOX_FIELDNAME依然能正常回写这个机制很成熟放心用。2.4 换个思路从函数式ALV拿Grid对象再统一处理还有一种情况比较特殊你的程序里既有函数式ALV但你又想用面向对象ALV的灵活方法比如想直接调用GET_SELECTED_ROWS来获取所有选中行。这个需求在函数式ALV框架下其实也能实现只是要绕一个小弯。SAP提供了一个隐藏函数GET_GLOBALS_FROM_SLVC_FULLSCR可以从当前活动的ALV Grid中拿到运行时的Grid对象DATA: go_grid TYPE REF TO cl_gui_alv_grid. CALL FUNCTION GET_GLOBALS_FROM_SLVC_FULLSCR IMPORTING e_grid go_grid.拿到go_grid之后你就能调用面向对象方法了比如GET_SELECTED_ROWS。这种写法在应付一些老程序增强的时候特别好用不用改动原有ALV展示逻辑只需要在用户操作的回调里动态拿Grid对象然后做你想做的事。不过这个方法有前提当前屏幕上必须正好有一个函数式ALV在展示如果函数式的Grid不在当前屏幕上下文里e_grid会返回空。另外要注意SAP官方并没有大力宣传这个函数属于“江湖偏方”在标准SAP版本里基本可用但存在一定的版本兼容性风险用之前最好在你的目标环境里做一个冒烟测试。3. 面向对象ALV更推荐的选中数据操作方式3.1 事件注册和USER_COMMAND写法的差异如果你做的是新开发的报表或者需求里明确要求复杂交互、右键菜单、可编辑单元格、二次刷新这些功能那我建议直接上OO ALV也就是基于CL_GUI_ALV_GRID的方案。OO ALV和函数式ALV最核心的差异在事件处理上。函数式ALV是通过回调FORM来接收事件靠一种“全局函数注册”的机制而OO ALV是通过事件处理器Event Handler来接收你可以在任意类里定义方法然后注册给Grid实例。基本框架搭建通常是这样DATA: go_container TYPE REF TO cl_gui_custom_container, go_grid TYPE REF TO cl_gui_alv_grid. CREATE OBJECT go_container EXPORTING container_name ALV_AREA. CREATE OBJECT go_grid EXPORTING i_parent go_container. 注册事件处理器 SET HANDLER lcl_event_handleron_user_command FOR go_grid.典型的USER_COMMAND事件方法签名如下METHOD on_user_command. CASE e_ucomm. WHEN ZPOST. PERFORM process_selected_lines. ENDCASE. ENDMETHOD.很多老顾问用惯了函数式ALV一开始转到OO ALV会觉得事件这套太啰嗦但实践一段时间后就会发现OO ALV的事件参数非常清晰代码逻辑可控性强尤其是多个ALV实例在同一屏幕上的时候函数式ALV那套回调方案会让人写到怀疑人生OO ALV反而干净利落。3.2 批量选中行获取与业务处理完整代码前面说到函数式ALV做多选靠BOX_FIELDNAMEOO ALV解决同样问题就更优雅了因为USER_COMMAND事件里自带了所有选中行的索引集合。事件方法的完整签名是METHODS on_user_command FOR EVENT user_command OF cl_gui_alv_grid IMPORTING e_ucomm.是的OO ALV的USER_COMMAND事件里标准参数只有e_ucomm一个选中的行集合需要你主动调用Grid的方法GET_SELECTED_ROWS来获取。拿到的是一个行号列表LVC_T_ROW。完整代码逻辑如下METHOD on_user_command. DATA: lt_selected TYPE lvc_t_row, lv_inx TYPE lvc_index, ls_alv LIKE LINE OF gt_alv_data. CHECK e_ucomm ZPOST. 获取所有选中行号 CALL METHOD go_grid-get_selected_rows IMPORTING et_index_rows lt_selected. IF lt_selected[] IS INITIAL. MESSAGE 请先选中需要处理的数据 TYPE S DISPLAY LIKE E. RETURN. ENDIF. 遍历选中行逐行处理 LOOP AT lt_selected INTO lv_inx. READ TABLE gt_alv_data INTO ls_alv INDEX lv_inx. IF sy-subrc 0. PERFORM frm_handle_line USING ls_alv. ENDIF. ENDLOOP. 刷新展示 CALL METHOD go_grid-refresh_table_display. ENDMETHOD.注意几个关键点。第一GET_SELECTED_ROWS获取到的是显示行号如果ALV显示前经过SORT或过滤它和你内表的实际索引可能不一致这个问题和函数式ALV的TABINDEX是同一回事。稳健的做法是保证传给ALV的内表顺序可控或者通过对齐之后用业务主键去定位原表。第二处理完数据后要不要清空选中状态OO ALV不像BOX_FIELDNAME那样有个字段跟着数据走你得主动调用SET_SELECTED_ROWS传入一个空的行号表来清除选中状态。如果没有这个动作用户会看到之前勾选的行仍然是选中状态容易引发误操作。第三整个过程里最好不要在遍历中直接删除内表行如果你的业务需求需要删除某些数据那就先把要删的行号收集起来循环结束后再统一删除并刷新。在遍历内表的同时删行很容易搞乱索引这个坑我踩过一次印象很深。3.3 刷新后再恢复选中状态说到刷新这里有个特别常见的需求场景用户勾了几行点了一个“批量修改状态”的按钮程序处理完之后用户希望刚才勾选的那几行依然保持选中因为他还想再对这几行做其他操作。但现实很骨感ALV执行REFRESH_TABLE_DISPLAY之后选中状态默认会被清空。要实现“刷新后依然保持选中”最直接的办法就是刷新前先记录选中行号刷新后把行号再设置回去。代码大概这样DATA: lt_selected TYPE lvc_t_row. 刷新前保存选中行号 CALL METHOD go_grid-get_selected_rows IMPORTING et_index_rows lt_selected. 刷新 CALL METHOD go_grid-refresh_table_display. 刷新后恢复选中 CALL METHOD go_grid-set_selected_rows EXPORTING it_index_rows lt_selected.这个方案在“数据行数不变、内表顺序不变”的情况下绝对有效。但如果刷新过程中数据变了比如删除了几行行号自然就对不上了那就不能简单恢复得用业务主键重新定位行号后再设置。业务上如果确实有这种动态变动的需求通常我会改用BOX_FIELDNAME的方案因为勾选状态跟数据绑定删除和插入都会自动跟随反而省心。顺便补充一句刷新ALV的时候尽量用SOFT_REFRESH参数这样能减少界面的闪烁用户体验会好很多。如果数据量很大你还能配合SET_FIRST_VISIBLE_ROW控制滚动位置避免刷新后列表跳到最开头。4. 编辑类ALV改完数据再选中的连环坑4.1 DATA_CHANGED事件与修改内容回写现在说说前面反复提到的第三种情况ALV本身是可以编辑的。这种报表的需求往往比纯展示复杂一个量级因为用户改完数据后你拿去用的内表数据可能还停留在“修改前”的状态。OO ALV里可编辑的单元格是通过字段目录里的EDIT属性来控制的。用户改动某个单元格后变更内容会先停留在ALV的“待提交缓冲区”里并不会自动写进你的程序内表。要拿到这些修改必须处理DATA_CHANGED事件并在事件里把缓冲区的内容同步到内表。最简单的同步写法是在事件处理器中调用前置方法METHOD handle_data_changed FOR EVENT data_changed OF cl_gui_alv_grid IMPORTING er_data_changed. DATA: lr_mod_cells TYPE REF TO lvc_t_mod, ls_mod_cell LIKE LINE OF lr_mod_cells-*. 遍历修改过的单元格 LOOP AT er_data_changed-mt_mod_cells INTO ls_mod_cell. READ TABLE gt_alv_data ASSIGNING FIELD-SYMBOL(fs_line) INDEX ls_mod_cell-row_id. IF sy-subrc 0. ASSIGN COMPONENT ls_mod_cell-fieldname OF STRUCTURE fs_line TO FIELD-SYMBOL(fs_value). IF sy-subrc 0. fs_value ls_mod_cell-value. ENDIF. ENDIF. ENDLOOP. ENDMETHOD.写完这个方法后记得要注册事件SET HANDLER lcl_eventshandle_data_changed FOR go_grid.这里有一个容易被人忽略的点就算你写了DATA_CHANGED事件也不能100%保证所有修改都在第一时间同步进内表。比如用户在一个单元格里敲了一串字符但还没回车焦点还在编辑框里这时候你直接点工具栏上的“处理”按钮DATA_CHANGED事件究竟会不会触发和ALV的IREGISTER_EDIT_EVENT设置有关SAP不同版本之间行为还有差异。我自己的经验是处理这类操作前建议先主动调用go_grid-check_changed_data方法它会强制把编辑缓冲区里的内容提交出来。这个方法调用完再读内表拿到的才是最新的值。4.2 先校验再选中还是先选中再校验还有一个流程设计问题经常在需求评审阶段就被忽略了用户一边改数据一边勾选然后点“提交”到底应该先校验还是先操作我强烈建议在正式业务操作之前先把所有修改同步到内表然后统一做校验校验通过后再执行选中数据对应的业务逻辑。为什么因为如果边操作边校验用户勾选路径不同、修改状态不同很容易出现“前面的行改错了后面还在处理”的问题。一次性的校验既好写又不容易出bug。具体流程可以设计成这样用户点提交按钮第一步CHECK_CHANGED_DATA强制缓冲区落库到内表第二步遍历所有选中行检查必填项、数值范围、状态合法性第三步所有检查都通过后统一执行业务操作第四步刷新ALV。如果校验不通过要明确提示用户哪一行哪个字段有问题最好能自动跳转到对应行这个交互细节很提升使用体验。这里还要提一个坑ONLY校验数据时千万别直接更新数据库。很多新人在DATA_CHANGED事件里直接调BAPI或者直接MODIFY数据库表这是非常危险的习惯。DATA_CHANGED事件在用户每次修改单元格时都可能触发频率很高不适合做重业务操作。正确做法是在事件里只做数据收集和同步真正的主业务逻辑放到确定性的动作点比如按钮点击或者流程末尾。5. 常见问题与调试技巧实录5.1 按钮点击了却没触发USER_COMMAND这个问题我在不同项目里遇到不下五次。最常见的原因有两个一是功能码拼写问题你在ADD BUTTON或者EXCLUDING里定义的按钮功能码和USER_COMMAND里判断的字符串不一致比如大小写不同或者多了空格就会静默失效二是按钮被定义成“不是标准Butn”结果被ALV框架吞掉了点击事件。排查时不要瞎猜先在USER_COMMAND第一行打一个断点或者临时加一句MESSAGE显示传入的p_ucomm这样点按钮时就能看到实际传过来的功能码是什么。这个简单的打印动作能解决一大半的“按钮失效”问题。如果功能码传过来了但没进你的CASE分支那就是判断逻辑的问题仔细比对即可。5.2 取到行号却读不到正确数据老生常谈了但我还是要再强调一次ALV的行号不等于内表的索引。如果你对ALV内表做了排序、筛选、甚至合并单元格的操作TABINDEX和GET_SELECTED_ROWS返回的行号都只是“展示层的顺序号”。解决思路是让“数据定位”不依赖行号而是依赖业务主键。展示层内表存上所有必要的主键字段比如公司代码、凭证编号、行项目号然后通过行号先定位到展示层记录取出主键再拿主键去业务表里定位原始数据。这样无论ALV怎么排序数据都不会错。5.3 排序和筛选后选中行索引错位这个和上面相关单独拿出来是因为它隐蔽性更强。比如用户在ALV里按金额降序排序然后再勾选这时候勾选的行号和内表原始位置已经对不上了。如果你按行号直接读内表读到的是另一条记录轻则处理错误重则更新错单号这个后果非常严重。我强烈建议你在开发方案设计阶段就考虑排序问题。要么限制用户排序通过布局参数禁用排序要么保证内表顺序和展示顺序一致要么使用BOX_FIELDNAME这种跟记录绑定的方案。如果所有条件都不允许那至少要做“行号转主键再转行号”的映射把风险兜住。5.4 刷新后勾选状态消失前面讲恢复选中状态的时候已经给了方案这里补充一个细节判断如果刷新后内表行数和顺序都没变用保存选中行号再set_selected_rows的方案没有问题但如果你在刷新前对内表做了SORT原来的行号就全乱套了恢复操作会选错行。最稳妥的做法是无论什么操作刷新前记录下选中行的业务主键刷新后遍历一遍新内表找到主键对应的新行号再批量设置回选中状态。虽然代码多几行但逻辑是完备的。毕竟在客户端交互里“让用户少骂一次”比“少写几行代码”重要得多。5.5 现场调试的经验最后分享一个调ALV事件的小技巧。当遇到“选中数据操作没有反应”的时候不要盯着ABAP调试器发呆先在事件入口打上断点点按钮时看两个关键信息第一个是触发的事件是哪个第二个是传入的功能码是什么。通过这两个信息你就能很快判断是按钮没绑定成功还是事件没注册成功还是判断逻辑出了问题。另外在线调试时如果发现GET_SELECTED_ROWS返回为空先别急着查代码检查一下是不是ALV布局选择了“整行选择”和“单元格选择”的差异。在SEL_MODE配置为多选的情况下用户可能有多个含义的“选中”SAP区分了光标所在单元格和选中行集合如果你把这两个概念搞混了很多诡异问题就会出现。还有一个很实用的小工具思路写报表时顺手做一个“测试按钮”功能码叫ZDEBUG点击时把当前ALV的各种状态选中行数、当前光标行、当前排序字段打印到一个调试内表用屏幕列表输出出来。这个按钮不用在正式版本里删掉代码里留一个隐藏打开条件就行。有了它日后用户说“这里有问题”的时候你能最快搞清楚现场到底发生了什么而不是靠猜。做了这些年项目我最大的感受就是ALV这套东西看着简单实则细节非常多。函数式ALV有它自己的历史包袱OO ALV虽然灵活但也有它绕不开的注册、刷新、事件同步问题。无论哪条路线核心思想都是一样的界面上的“选中”只是一个交互状态你要做的是把这个状态准确、安全地翻译成业务数据然后去执行后续动作。搞清楚内表顺序、展示顺序、选中行号这三者之间的关系再遇到这类需求基本就不会再被难住了。
返回列表