1. 为什么ALV报表里总有人要“勾选”这个功能
用SAP做报表开发的老哥们,十有八九都接到过这样的需求:“这个报表能不能加个复选框,让我把要处理的数据勾上,点个按钮直接批量操作?”说白了,用户不想看数据只是看数据,他们想在报表上直接干活——把要审批的凭证勾出来,把要调整的物料勾出来,甚至把要发给供应商的清单勾出来,然后一键执行后续动作。
ALV就是SAP用来展示和操作数据的标准控件,全称叫ABAP List Viewer。它可以理解成SAP世界里最常用的“表格展示工具”,支持排序、过滤、合计、下钻、导出Excel等等。而复选框是在ALV行数据前面加一列,允许用户用鼠标点击选中或取消选中某一行。这个功能看着简单,但在实际项目里翻车的次数远比想象中多,原因就在于ALV的刷新机制、数据缓冲、事件响应这几个环节如果不搞清楚,做出来的勾选功能会非常难用。
这篇文章就把我在多个项目里做ALV复选框、全选、取消全选、批量操作的经验完整拆一遍。适合刚接触ALV开发的ABAP新手,也适合被“勾选后刷新就丢状态”折磨过的老开发参考。我会从实现方式选型讲起,把两种主流方案、核心代码、常见坑、排错思路一次讲透。
2. 方案选型复盘:自绘复选框字段与标准布局配置怎么取舍
2.1 复选框需求的本质拆解
先想清楚需求到底是什么。很多用户说“我要复选框”,其实背后是这三件事的其中一件或全部:
第一,视觉上能看出哪些行被选中,选中的行要有明显的勾选标记。这是最基础的需求。第二,支持快捷操作,比如全选、取消全选,不需要一格格点过去。第三,勾选状态要能传递到后续处理逻辑,用户勾完以后点“批量处理”,程序得知道到底处理哪些行。
很多开发一上来就纠结“复选框怎么画出来”,但其实真正的难点在第二和第三点,尤其是勾选状态在用户操作后怎么存活下来。如果你只是想在ALV里多一列带勾选框的列,那SAP给了非常简单的配置方案;但如果你要让用户勾选完以后数据还能带回内表、还能批量处理,那就必须理解ALV的输出内表和用户交互之间的数据流。
我把现有项目的需求复盘了一下,典型的场景是这样的:一个物料清单查询报表,ALV里列出了几千行物料,用户需要把其中部分物料勾选出来,然后点一个自定义按钮“批量冻结”,程序对勾选的物料逐条调用BAPI去修改状态。这里如果只是展示勾选框,显然远远不够,勾选信息的收集和回写才是关键。
2.2 方案一:用布局配置指定复选框字段(最省事)
如果ALV的输出结构里本身就有某个字段适合做勾选标记,比如一个CHAR1类型的字段,取值就是“X”或空,你可以在布局中指定这个字段作为复选框字段。做法是在SET_LAYOUT时,给layout这个结构塞一个BOX_FIELDNAME属性,值为那个字段的名字。
代码大概长这样:
DATA: ls_layout TYPE lvc_s_layo. ls_layout-box_fname = 'SEL'. ls_layout-zebra = 'X'. ls_layout-cwidth_opt = 'X'. CALL METHOD alv_grid->set_table_for_first_display EXPORTING i_structure_name = 'GT_ALV' is_layout = ls_layout CHANGING it_outtab = gt_alv[] it_fieldcatalog = gt_fcat[].这个方案的好处是ALV自动把指定字段渲染成复选框,单击单元格就能切换“X”和空值,用户操作体验非常顺。不需要你写任何点击事件处理逻辑,ALV内部的编辑器已经帮你做好了。
缺点是这个字段必须存在于输出内表中,并且用户点击修改的值,只有当数据回传到内表时才有效。如果不触发回传,你在内存里读内表,看到的还是旧值。这是一个非常隐蔽的坑,后面我会单独讲。
2.3 方案二:用字段目录自定义复选框样式(更灵活)
还有一种做法,不依赖布局,直接在字段目录里配置某个字段为复选框。这种方法适合输出结构里没有现成字段、但你想用已有字段来控制勾选状态的场景。
DATA: ls_fcat TYPE lvc_s_fcat. ls_fcat-fieldname = 'SEL'. ls_fcat-ref_table = 'GT_ALV'. ls_fcat-ref_field = 'SEL'. ls_fcat-checkbox = 'X'. ls_fcat-edit = 'X'. APPEND ls_fcat TO gt_fcat.这个方案的核心是给字段打上CHECKBOX和EDIT标记,ALV会把该字段渲染成可编辑的复选框,允许用户单击切换。和方案一的区别在于它的控制粒度更细,你可以单独控制这个列的对齐方式、宽度、是否允许编辑等属性,而不影响其他字段的展示方式。
从实际项目经验看,方案二用得更多。原因是方案一要求整个布局指定一个复选框字段,如果你后续想要在同一个ALV里实现多选模式和单选模式的切换,或者想要动态控制某些行不出现在勾选范围里,方案一会比较僵硬。而方案二因为走的是字段目录,你可以在运行时动态修改字段属性,灵活性大了很多。
2.4 两种方案的核心差异对比
| 对比维度 | 方案一:布局BOX_FNAME | 方案二:字段目录CHECKBOX |
|---|---|---|
| 实现复杂度 | 低,几行代码搞定 | 中,需要配字段目录 |
| 依赖字段 | 必须有专门的SEL字段 | 同样需要SEL字段 |
| 灵活性 | 较差,整个布局只能指定一个复选框字段 | 高,可动态修改属性 |
| 用户交互 | 自动支持单击切换 | 自动支持单击切换 |
| 读取勾选状态 | 需要触发数据回传 | 需要触发数据回传 |
| 适合场景 | 简单报表、快速交付 | 复杂报表、需要批量操作、动态控制 |
这里要特别提醒一点,不管用哪种方案,复选框字段都必须在输出内表中真实存在,并且ALV渲染时能看到它。你要做的核心工作其实不是画这个勾选框,而是保证用户勾选后的“X”值能可靠地回到你的内表里。
3. 核心实现细节拆解:从字段准备到用户交互响应
3.1 输出内表的字段设计
先说输出内表。不管字段目录怎么配,你都需要一个字段来记录勾选状态。实践中我习惯在输出内表的最后加一个名为SEL的字段,类型CHAR1,初始值为空,勾选后为“X”。这样内表里的每一行,都天然携带了“是否被勾选”的信息。
有人会问,能不能直接用行号来记录勾选?技术上可以,但不可靠。因为ALV允许用户排序、过滤,过滤掉的行是不显示的,如果你按行号记录,过滤后行号和原始内表行号对不上,就会出乱子。用SEL字段的好处是它绑定在每一行数据上,无论用户怎么排序、过滤,勾选信息始终跟着那条数据走,不会错位。
另一个容易踩的坑是,如果你用了“行号”那种基于显示位置的方式去记录选中行,一旦ALV刷新或者排序,用户勾选的东西就错位了。所以内表设计时一定要把SEL字段当作普通数据字段对待,并且放在内表结构里,而不是另外建一个辅助表来记录。
3.2 工具栏增加全选、取消全选按钮
标准ALV工具栏其实自带一个“全选/取消全选”功能,用户可以通过拖拽鼠标框选或者Ctrl+A全选。但实际项目里用户总是希望界面上有明确的按钮,告诉他们“一键全选”和“一键取消”。所以我们要在ALV工具栏上增加自定义按钮。
在ALV GRID的事件里有一个TOOLBAR事件,专门用来在工具栏上增加或删除按钮。实现思路是继承CL_GUI_ALV_GRID的事件处理器,重写HANDLE_TOOLBAR方法往里追加按钮。
CLASS lcl_event_receiver DEFINITION. PUBLIC SECTION. METHODS: handle_toolbar FOR EVENT toolbar OF cl_gui_alv_grid IMPORTING e_object e_interactive, handle_user_command FOR EVENT user_command OF cl_gui_alv_grid IMPORTING e_ucomm. ENDCLASS. CLASS lcl_event_receiver IMPLEMENTATION. METHOD handle_toolbar. DATA: ls_toolbar TYPE stb_button. CLEAR ls_toolbar. ls_toolbar-butn_type = '3'. " 分隔符 APPEND ls_toolbar TO e_object->mt_toolbar. CLEAR ls_toolbar. ls_toolbar-function = 'SEL_ALL'. ls_toolbar-icon = icon_select_all. ls_toolbar-quickinfo = '全选'. ls_toolbar-text = '全选'. ls_toolbar-butn_type = '0'. " 普通按钮 APPEND ls_toolbar TO e_object->mt_toolbar. CLEAR ls_toolbar. ls_toolbar-function = 'DESEL_ALL'. ls_toolbar-icon = icon_deselect_all. ls_toolbar-quickinfo = '取消全选'. ls_toolbar-text = '取消全选'. ls_toolbar-butn_type = '0'. APPEND ls_toolbar TO e_object->mt_toolbar. ENDMETHOD. METHOD handle_user_command. CASE e_ucomm. WHEN 'SEL_ALL'. PERFORM select_all. WHEN 'DESEL_ALL'. PERFORM deselect_all. ENDCASE. ENDMETHOD. ENDCLASS.按钮的样子做好了,但真正干活的还是PERFORM select_all和PERFORM deselect_all这两个子程序。它们的核心逻辑很简单,无非就是把内表里的SEL字段全部置成“X”或清空。但这里有一个关键问题——你改完内表以后,必须让ALV界面同步刷新,否则用户看不到勾选变化。
3.3 核心代码实践:全选、取消全选、刷新网格
这里放一段我在实际项目里验证过很多次的完整代码。假设ALV GRID实例名为GO_ALV,输出内表名为GT_ALV。
FORM select_all. DATA: ls_alv TYPE gt_alv. LOOP AT gt_alv INTO ls_alv. ls_alv-sel = 'X'. MODIFY gt_alv FROM ls_alv TRANSPORTING sel WHERE sel <> 'X'. ENDLOOP. go_alv->refresh_table_display( ). ENDFORM. FORM deselect_all. DATA: ls_alv TYPE gt_alv. LOOP AT gt_alv INTO ls_alv. ls_alv-sel = ''. MODIFY gt_alv FROM ls_alv TRANSPORTING sel WHERE sel = 'X'. ENDLOOP. go_alv->refresh_table_display( ). ENDFORM.这里有两个细节值得注意。
第一个细节是MODIFY语句里的TRANSPORTING条件。我用WHERE条件只更新需要变化的那部分行,避免全表逐行MODIFY带来的性能损耗。当数据量几千行时,这个优化不明显,但如果上万行甚至更多,每行都做一次MODIFY和只过滤出需要的行更新,性能差距会很可观。
第二个细节是refresh_table_display之后是否需要重新设置布局。如果你的布局里做了行颜色、特殊格式等定制,刷新时这些都会被保留,不需要重新设置。但如果你在刷新前手动修改了字段目录或某些属性,刷新后就可能不生效,需要调用SET_FRONTEND_FIELDCATALOG重新推一次。
3.4 用户单击复选框后状态回传的实现思路
这是整个功能里最重要、也是最容易出问题的一环。用户单击ALV里的复选框,ALV确实会把界面上的“X”或空值显示出来,但这些变化不会自动写回你的GT_ALV内表。只有当ALV触发数据回传机制时,内表才会被更新。
实现方式有两个:一个是给ALV打开EDIT模式,这样单元格编辑后会自动触发DATA_CHANGED事件,然后你在DATA_CHANGED里处理数据;另一个是在USER_COMMAND里强制触发数据搬运,把ALV当前显示的数据读回内表。
我最常用的做法是DATA_CHANGED事件。先给ALV设置为可编辑模式,然后在事件处理器里实现HANDLE_DATA_CHANGED。这个事件有一个DATA_CHANGED参数,里面存放了用户修改了哪些单元格的信息,包括行号、列名、新值。
CLASS lcl_event_receiver DEFINITION. PUBLIC SECTION. METHODS: handle_data_changed FOR EVENT data_changed OF cl_gui_alv_grid IMPORTING er_data_changed. ENDCLASS. CLASS lcl_event_receiver IMPLEMENTATION. METHOD handle_data_changed. DATA: ls_mod_cells TYPE lvc_s_modi, lv_fieldname TYPE lvc_fname. LOOP AT er_data_changed->mt_mod_cells INTO ls_mod_cells. lv_fieldname = ls_mod_cells-fieldname. IF lv_fieldname = 'SEL'. READ TABLE gt_alv INDEX ls_mod_cells-row_id. IF sy-subrc = 0. IF ls_mod_cells-value = 'X'. gt_alv-sel = 'X'. ELSE. gt_alv-sel = ''. ENDIF. MODIFY gt_alv INDEX ls_mod_cells-row_id. ENDIF. ENDIF. ENDLOOP. ENDMETHOD. ENDCLASS.有了这个事件,用户每次单击复选框,对应内表行的SEL字段就会实时更新。这样当你处理“批量操作”时,直接从GT_ALV内表里筛选SEL = 'X'的行就行了,不会出现界面勾了但程序不知道的情况。
这段代码里有个细节——我用了ROW_ID而不是直接用ROWNUM。这是因为ALV提供了两种行标识方式,ROW_ID是当前显示状态下的行号,而ROWNUM可能受到排序和过滤的影响。为了稳妥,我习惯用ROW_ID定位内表行。
4. 批量操作的完整链路:勾选如何作用于后续业务处理
4.1 从内表筛选勾选行的标准写法
复选框本身不是目的,用户勾完要干事才是目的。在批量操作按钮的处理逻辑里,你需要一个标准的筛选流程。
FORM batch_process. DATA: lt_selected TYPE TABLE OF gt_alv, ls_selected TYPE gt_alv. " 筛选所有勾选行 LOOP AT gt_alv INTO ls_selected WHERE sel = 'X'. APPEND ls_selected TO lt_selected. ENDLOOP. IF lt_selected IS INITIAL. MESSAGE '请先勾选需要处理的数据' TYPE 'S' DISPLAY LIKE 'E'. RETURN. ENDIF. " 逐条处理 LOOP AT lt_selected INTO ls_selected. " 调用后续处理逻辑,比如BAPI、函数、或者更新数据库 PERFORM process_single_data USING ls_selected. ENDLOOP. MESSAGE '批量处理完成' TYPE 'S'. ENDFORM.这个写法的好处是逻辑清晰,筛选和处理分离。筛选出来的LT_SELECTED就是本次操作的准确数据集合,后续无论是逐条处理还是做成批量内表传给函数,都非常方便。而且因为使用了WHERE条件筛选,整个过程的代码量很少,逻辑一眼能看懂。
4.2 批量操作常见的三个陷阱及对策
陷阱一,很多年轻开发在批量操作完成后,直接清空了SEL字段或者刷新了界面,导致用户想再检查一下刚才勾了哪些行都看不到。我建议批量处理完成后,不要把SEL字段清空,而是保留勾选状态,让用户能直观看到“刚才处理了哪些行”。如果用户确认没问题,再点一次“取消全选”即可。
陷阱二,批量处理中间某一行失败时,程序直接报错退出,导致用户不知道哪些处理了、哪些没处理。更稳妥的做法是逐行记录处理结果,比如在输出内表里增加一列“处理结果”,成功写“成功”,失败写“失败原因”。这样一批处理完,用户一目了然。
陷阱三,用户勾选完以后,不小心点了ALV的刷新功能或者重新执行了查询,勾选状态全丢了。这个问题可以通过在查询时主动给SEL字段赋初值,或者把勾选状态和业务数据一起保存来解决。但大多数项目里,用户对这个容忍度比较高,你只需要在界面上给个提示就够了。
4.3 与ALV标准全选功能的关系:如何避免冲突
前面提到ALV标准工具栏自带全选功能,其实它和自定义的“全选”按钮并不冲突。标准ALV的全选操作,只知道哪些行被选中了,但它并不知道这些行对应的SEL字段值是什么。它的选中状态,只是界面层的高亮显示,不会写回你的数据。
所以在实际项目中,很多用户会用鼠标框选、Ctrl+A这种方式去选中多行,然后期望点击批量按钮时程序能识别这些行。这样的需求SAP标准功能是做不到的——它不会把界面选中状态通知你的程序。这时候你要么引导用户用复选框,要么在程序里把界面选中状态的读取逻辑自己写出来。
我遇到过不少项目,用户两种习惯都有。我的做法是在自定义批量按钮的点击逻辑里,优先读取GT_ALV内表SEL='X'的行作为处理对象,然后额外提供一个选项,允许用户先用ALV的框选功能选中行,再点批量按钮时,把这两类选中的行合并去重后处理。这样两种习惯的用户都能顺畅使用。
5. 常见问题与排查技巧实录:我踩过的那些坑
5.1 勾选后刷新界面状态丢失
这应该是ALV复选框开发里最高频的问题。用户的反馈通常是“我明明勾了几行,点了一下别的按钮再回来,勾选没了,又要重新勾”。
这个问题的根源在于勾选状态只存在于界面层,没有回传到数据内表。用户单击复选框后,屏幕上确实显示了“X”,但你内表里的SEL字段可能还是空的。一旦执行了任何引起ALV刷新的操作,界面重新从内表读取数据,那些“X”就全没了。
解决方式有三种:第一,使用DATA_CHANGED事件实时把勾选状态写回内表,这是最推荐的方式,我在3.4里已经写了实现代码;第二,在触发刷新前,强制调用ALV的CHECK_CHANGED_DATA方法把编辑器里的数据回写内表;第三,在业务按钮的点击事件里,调用ALV的GET_CHANGED_DATA得到修改列表,再更新内表。第一种方式最简洁,后两种适合有大量可编辑字段的场景。
" 方式二示例:刷新前强制回写 go_alv->check_changed_data( ). go_alv->refresh_table_display( ).5.2 全选按钮执行了但界面没反应
有朋友跟我说,全选按钮的代码逻辑跑完了,内表SEL也全变成“X”了,但界面上复选框没变化。这个问题我在刚接触ALV时也遇到过一次,原因是忘了刷新。内表数据变了,但ALV控件的显示缓冲区还是旧数据,你不调用refresh_table_display,界面永远不会自己更新。
还有一个类似情况是,你调用了refresh_table_display但布局里设置了不自动刷新标记,导致刷新被吞掉。排查方式是检查layout里AUTO_SET_REF_TABLE或者相关的刷新参数,确保没有刻意禁用刷新。
5.3 复选框字段在ALV中不显示
这个问题的原因有很多,最常见的是字段目录里没有把这个字段加进去,或者字段目录里的NO_OUT属性被误设了。另外还有可能是字段目录里列顺序问题,SEL字段被放到了非常靠后的位置,用户没看到。
我用过最简单的排查方式,就是在得到完整字段目录后,用一个临时ALV把字段目录打出来,直接看SEL字段是否存在、NO_OUT是否为“X”、CHECKBOX是否为“X”。一眼就能定位问题。
5.4 批量处理时勾选行与实际处理行不一致
这个问题是筛选逻辑出了问题。常见原因是你处理时用了错误的行号去内表查找数据,导致取到的行和界面上用户勾选的行不是同一行。
排查思路是检查你读取内表时用的索引是从哪里来的。如果是从ALV的USER_COMMAND事件里拿的,那是有可能拿到显示行号的;如果用ROW_ID,逻辑上就比较可靠。此外,如果你在事件处理前对内表做了排序或删除操作,原来的行索引可能已经失效,也会导致错位。
我的建议是,在批量处理时尽量不用行号,而是直接筛选SEL='X'的行。这样无论界面怎么排序、怎么过滤,只要SEL字段正确,取到的数据就准确。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 解决方式 |
|---|---|---|
| 勾选后刷新消失 | 勾选状态未回写内表 | 使用DATA_CHANGED事件回写 |
| 全选后界面无变化 | 未调用刷新方法 | 调用refresh_table_display |
| 复选框列不显示 | 字段目录缺失或NO_OUT为X | 检查并修正字段目录 |
| 批量处理行数据错位 | 使用了不可靠的行号 | 改用SEL='X'筛选内表 |
| 批量完成后看不到处理结果 | 处理结果未记录到界面 | 内表增加结果字段并刷新 |
| 用户框选但无法批量处理 | 框选状态不在数据层 | 引导使用复选框或合并处理 |
6. 一段能直接抄作业的完整实现骨架
最后我整理一个完整的实现骨架,把上面讲到的所有零部件组装在一起。你拿到这个骨架以后,把内表结构、字段目录、ALV实例名替换成你自己的,基本就能用了。
CLASS lcl_event_receiver DEFINITION. PUBLIC SECTION. METHODS: handle_toolbar FOR EVENT toolbar OF cl_gui_alv_grid IMPORTING e_object e_interactive, handle_user_command FOR EVENT user_command OF cl_gui_alv_grid IMPORTING e_ucomm, handle_data_changed FOR EVENT data_changed OF cl_gui_alv_grid IMPORTING er_data_changed. ENDCLASS. CLASS lcl_event_receiver IMPLEMENTATION. METHOD handle_toolbar. DATA: ls_toolbar TYPE stb_button. CLEAR ls_toolbar. ls_toolbar-butn_type = '3'. APPEND ls_toolbar TO e_object->mt_toolbar. CLEAR ls_toolbar. ls_toolbar-function = 'SEL_ALL'. ls_toolbar-icon = icon_select_all. ls_toolbar-quickinfo = '全选'. ls_toolbar-text = '全选'. ls_toolbar-butn_type = '0'. APPEND ls_toolbar TO e_object->mt_toolbar. CLEAR ls_toolbar. ls_toolbar-function = 'DESEL_ALL'. ls_toolbar-icon = icon_deselect_all. ls_toolbar-quickinfo = '取消全选'. ls_toolbar-text = '取消全选'. ls_toolbar-butn_type = '0'. APPEND ls_toolbar TO e_object->mt_toolbar. ENDMETHOD. METHOD handle_user_command. CASE e_ucomm. WHEN 'SEL_ALL'. PERFORM select_all. WHEN 'DESEL_ALL'. PERFORM deselect_all. ENDCASE. ENDMETHOD. METHOD handle_data_changed. DATA: ls_mod_cells TYPE lvc_s_modi. LOOP AT er_data_changed->mt_mod_cells INTO ls_mod_cells. IF ls_mod_cells-fieldname = 'SEL'. READ TABLE gt_alv INDEX ls_mod_cells-row_id. IF sy-subrc = 0. IF ls_mod_cells-value = 'X'. gt_alv-sel = 'X'. ELSE. gt_alv-sel = ''. ENDIF. MODIFY gt_alv INDEX ls_mod_cells-row_id. ENDIF. ENDIF. ENDLOOP. ENDMETHOD. ENDCLASS.在ALV初始化时注册事件:
CREATE OBJECT go_event_receiver. SET HANDLER go_event_receiver->handle_toolbar FOR go_alv. SET HANDLER go_event_receiver->handle_user_command FOR go_alv. SET HANDLER go_event_receiver->handle_data_changed FOR go_alv.这几段代码串起来,复选框展示、单击选中、工具栏全选取消全选、状态回写、批量处理筛选,整条链路就完整了。
7. 项目落地后的经验沉淀:一些值得记住的细节
做ALV复选框功能,我发现最终决定用户体验好坏的,往往不是某个大功能,而是很多小细节。比如复选框列的列宽,太窄了用户点不准,太宽了又占空间;复选框列是否允许排序,有些业务场景下用户希望按勾选状态排序;复选框列的背景色是否用特殊颜色标记,能让用户更快定位到已勾选的行。
另外,如果报表的数据量特别大,比如几万行起步,你要考虑全选操作的效率。这个时候逐行MODIFY的写法就不是最优解了。可以用一次UPDATE语句直接修改内表中特定条件的行,但ABAP内表操作没有类似UPDATE SET的写法,只能靠循环+条件TRANSPORTING,或者把内表转换成分组内部表后批量处理。
还有一个容易忽略的点是,如果报表里用了子总账或者分类汇总行,比如ALV的SUM BEFORE/SUM AFTER,在汇总行上勾选复选框是没有意义的。你要在程序里对汇总行做特殊处理,比如禁止编辑或者不参与批量处理筛选,否则用户勾了汇总行,后续处理时会出现逻辑混乱。
关于性能,我再多说一句。如果输出内表上万行,DATA_CHANGED事件可能被触发非常多次,每次事件里都做完整的LOOP遍历会拖慢响应。优化方式是只在必要时才去更新内表,其他情况直接退出。实际项目中我甚至见过事件被连续触发上百次的情况,性能优化不是锦上添花而是刚需。
我自己的习惯是把复选框列的标题设置为“选择”,并且在列工具提示里写清楚操作方式。用户停留在列头上时能看到提示,减少了一部分培训成本。项目交付之后,真正让用户觉得这个报表好用的地方,往往就是这些不起眼的小细节。
做这一类功能,我的体会是技术上并不复杂,真正的复杂度在于理解ALV的显示层和数据层的分离机制。把这一点想通了,复选框、全选、批量操作这些功能点,其实都只是围绕这个核心机制展开的排列组合。这也是我每次做ALV报表开发时,都会先和自己的团队强调一遍的内容。