做SAP项目这么多年,要说被业务方追问最多的报表功能,复选框、全选、取消全选一定排得上前三。业务方的话术都很统一:“就像Excel那样,左边一列小框,上面一个全选按钮,勾完点批量打印就完事了。”听起来很简单,真动手的时候,很多刚接触ALV开发的新人会卡在第一步:ABAP ALV里到底应该用哪种方式实现这个“小框”?搜索热词里还能经常看到el-table、selection-change这类前端术语,说明不少同事会下意识把前端表格那套交互逻辑搬过来,但ALV的机制和前端完全两回事。ALV没有现成的checkbox控件,全选按钮也不在默认工具栏里,一切要靠字段目录、布局、事件处理和刷新机制配合才能实现。
这篇文章就围绕“基于ALV表报实现复选框、全选、取消全选等功能”这条主线,把从数据准备、字段目录配置、工具栏自定义按钮,到勾选数据获取和常见坑位完整捋一遍,适合正在做ALV报表增强、或者准备接手类似需求的开发同事参考。我尽量把每一步背后的原理也讲清楚,免得大家照着写完了,换个需求又不会了。
1. 别急着写代码:先搞清楚ALV“复选框”到底是个什么机制
1.1 ALV的复选框不是控件,而是“可编辑单元格”
前端开发里,复选框就是一个独立的input控件,有状态、有事件、有选中值回调。ALV和这个思路完全不同:ALV GRID里的复选框本质上是“一个允许编辑的字符字段,在画面渲染时以勾选框的形式展现”。换句话说,你给这一列设置一个字符串字段,比如SEL,长度1个字符,然后在字段目录里告诉ALV“我这列要显示成复选框”,ALV就会自动在单元格里画一个框,勾上时字段值为“X”,取消勾选时值为空。
这个底层机制决定了后面很多事:既然复选框是可编辑单元格,那它就要遵循ALV编辑模式的那套规则。整张报表是不是处于可编辑状态,这一列允不允许编辑,运行时哪些行允许勾、哪些行不允许勾,全都能做精细控制,但也全是坑。
1.2 复选框、行选中、编辑态:三种并存的状态
ALV里有三个概念非常容易混:
- 复选框勾选:SEL字段的值变成“X”,这是数据层面的变化;
- 行高亮选中:用户用鼠标点击行带来的选中效果,比如整行变蓝,这属于ALV界面状态;
- 单元格编辑态:用户点进某个单元格准备输入,这个单元格会处于可输入状态。
这三个状态互相独立。行高亮不等于复选框被勾上;复选框勾上了也不代表这行被“选中”。判断“用户勾了哪些数据”时,如果拿行高亮那套逻辑去遍历SEL字段,或者反过来,数据一定对不上。这个我在第4章会专门展开。
1.3 为什么“全选”不在ALV默认工具栏里
ALV GRID内置按钮有不少,比如排序、合计、筛选、导出Excel等,但唯独没有“全选”“取消全选”这种按钮。原因也不复杂:ALV默认的全选概念是“选中所有行”,而且用鼠标拖拽、Ctrl+A之类的操作也能实现,它没必要专门做一个工具栏按钮。但业务方要的是“把复选框这一列全部勾上”,这属于数据操作的业务功能,需要更新内表,ALV框架猜测不到你要更新哪一列、按什么规则过滤,所以只能由开发者自己在工具栏上添按钮、写逻辑。搞清楚这层逻辑,就不会一上来就到处找“ALV全选函数”了。
2. 把复选框从“看不见”调到“能勾上”需要配齐的三件事
2.1 内表里先种下一个SEL字段
实现复选框的第一步,是在输出内表里增加一个字段。实战里这个字段命名很随意,但大多数项目都习惯用SEL或者BOX,我一般固定用SEL,结构如下:
TYPES: BEGIN OF ty_alv, sel TYPE c LENGTH 1, " 复选框标记 doc_no TYPE vbeln, " 单据号 bukrs TYPE bukrs, " 公司代码 status TYPE char30, " 状态描述 END OF ty_alv. DATA: gt_alv TYPE TABLE OF ty_alv, gs_alv TYPE ty_alv.这个字段没有数据字典参考,就是纯内表字段。SEL放在第一个位置只是习惯,不重要,重要的是后面字段目录配置时必须把它指认出来。
2.2 字段目录里让这一列“长得像复选框”
字段目录的设置是核心。如果是手动往gt_fieldcat里追加,逻辑是这样的:
CLEAR gs_fieldcat. gs_fieldcat-fieldname = 'SEL'. gs_fieldcat-ref_table = '/'. " 无字典引用,可以留空 gs_fieldcat-checkbox = 'X'. gs_fieldcat-edit = 'X'. gs_fieldcat-col_pos = 1. gs_fieldcat-no_out = ''. APPEND gs_fieldcat TO gt_fieldcat.如果是从DDIC结构自动生成的字段目录,更常见的是先调LVC_FIELDCATALOG_MERGE生成,再循环修改SEL那一行:
CALL FUNCTION 'LVC_FIELDCATALOG_MERGE' EXPORTING i_structure_name = 'ZALV_STR' CHANGING ct_fieldcat = gt_fieldcat. LOOP AT gt_fieldcat INTO gs_fieldcat. CASE gs_fieldcat-fieldname. WHEN 'SEL'. gs_fieldcat-checkbox = 'X'. gs_fieldcat-edit = 'X'. gs_fieldcat-col_pos = 1. WHEN 'DOC_NO'. gs_fieldcat-emphasize = 'C310'. ENDCASE. MODIFY gt_fieldcat FROM gs_fieldcat. ENDLOOP.关键点就这么几个:
- checkbox = 'X':告诉ALV这一列渲染成复选框样式;
- edit = 'X':允许这一列进入编辑状态,没有这个,框画出来了你也勾不了;
- ref_table、ref_field:SEL是纯内表字段,不需要引用数据字典字段,可以留空,但如果你的SEL有参考字段,最好把ref_field配出来,避免部分ALV版本显示异常。
我遇到过不止一次:checkbox设了,也确认了数据源里有SEL字段,但界面上就是不出复选框,最后发现是edit没设成'X'。ALV的机制就是这样,复选框本质上就是可编辑单元格的一种呈现,它和edit属性绑定得很死。
2.3 Layout配置与样式控制
字段目录设置完,还要看Layout里怎么配。如果整张报表允许编辑,直接在Layout里打开全局编辑开关:
gs_layout-edit = 'X'.这个开关一打开,所有字段目录里edit='X'的列都能编辑。但问题来了:很多时候报表里只有SEL需要可编辑,别的字段如单据号、金额应当是只读的。如果只靠全局edit='X'粗暴放开,用户可能误改关键数据。
更好的做法是:Layout不要全局打开edit,改用STYLE做“只允许指定列可编辑”的精细控制。
gs_layout-stylefname = 'FIELD_STYLE'. " 填充内表时,拼装这个字段对应的样式 DATA: ls_style TYPE lvc_s_styl, lt_style TYPE lvc_t_styl. CLEAR ls_style. ls_style-fieldname = 'SEL'. ls_style-style = cl_gui_alv_grid=>mc_style_enabled. APPEND ls_style TO lt_style. gs_alv-field_style = lt_style. gs_alv-sel = ''. APPEND gs_alv TO gt_alv.用stylefname方案时,Layout的edit可以保持初始值。字段目录里SEL列仍然要设edit='X',因为style只是“运行时把某个单元格置为可编辑”,而字段目录的edit是“这列具备编辑能力”。两层配合:字段目录负责能力,style负责具体哪些单元格放开。这样其他字段在界面上还是只读,只有SEL能点。实测下来这个方案最稳,尤其报表里还有其他业务字段的时候。
2.4 运行时某些行不能勾选怎么办
真实业务里经常有“已过账不能勾”“已删除不能勾”这类需求。Style同样能解决:对不允许勾选的行,把SEL字段的style设成mc_style_disabled,这一行的复选框会变成灰色,点不动。
LOOP AT gt_alv ASSIGNING FIELD-SYMBOL(<fs_alv>). CLEAR lt_style. IF <fs_alv>-status = '已过账'. ls_style-fieldname = 'SEL'. ls_style-style = cl_gui_alv_grid=>mc_style_disabled. APPEND ls_style TO lt_style. <fs_alv>-field_style = lt_style. <fs_alv>-sel = ''. " 历史数据强制不带勾 ENDIF. ENDLOOP.注意一个细节:如果数据刷新后,原本不允许勾选的行变成了允许勾选,style要重新组装,不能沿用上一次的lt_style,否则会“怎么点都点不动”。我踩过一次这个坑,排查了半天才发现是复用内部表导致style一直叠加了disabled状态。
3. 全选、取消全选:从按钮到事件再到刷新的一条链路
3.1 在ALV工具栏上挂上自定义按钮
ALV GRID要加按钮,标准做法是注册TOOLBAR事件,往事件对象里追加按钮。
METHOD handle_toolbar. " 先加一个分隔符 CLEAR gs_toolbar. gs_toolbar-butn_type = '3'. APPEND gs_toolbar TO e_object->mt_toolbar. " 全选按钮 CLEAR gs_toolbar. gs_toolbar-function = 'SEL_ALL'. gs_toolbar-icon = icon_select_all. gs_toolbar-quickinfo = '全选'. gs_toolbar-text = '全选'. gs_toolbar-butn_type = '0'. APPEND gs_toolbar TO e_object->mt_toolbar. " 取消全选按钮 CLEAR gs_toolbar. gs_toolbar-function = 'DESEL_ALL'. gs_toolbar-icon = icon_deselect_all. gs_toolbar-quickinfo = '取消全选'. gs_toolbar-text = '取消全选'. gs_toolbar-butn_type = '0'. APPEND gs_toolbar TO e_object->mt_toolbar. ENDMETHOD.butn_type有几种常见值,新人容易忽略:'0'是普通按钮,'1'是菜单按钮,'2'是分割线,'3'是分隔符。全选和取消全选用'0'就行。如果希望按钮只显示图标不带文字,text留空即可;如果图标找不到合适的,text可以当按钮文字用。我习惯图标和文字都给,因为业务方经常不知道哪个图标是啥意思,有文字更友好。
3.2 USER_COMMAND事件里写全选逻辑
按钮加好了,必须注册USER_COMMAND事件处理点击。事件里的参数e_ucomm就是按钮的function code。
METHOD handle_user_command. CASE e_ucomm. WHEN 'SEL_ALL'. PERFORM select_all_rows. WHEN 'DESEL_ALL'. PERFORM deselect_all_rows. ENDCASE. ENDMETHOD.FORM实现核心逻辑:
FORM select_all_rows. FIELD-SYMBOLS: <fs_alv> LIKE LINE OF gt_alv. LOOP AT gt_alv ASSIGNING <fs_alv>. <fs_alv>-sel = 'X'. ENDLOOP. go_grid->refresh_table_display( ). ENDFORM.取消全选就是把这行赋值改成空:
FORM deselect_all_rows. FIELD-SYMBOLS: <fs_alv> LIKE LINE OF gt_alv. LOOP AT gt_alv ASSIGNING <fs_alv>. CLEAR <fs_alv>-sel. ENDLOOP. go_grid->refresh_table_display( ). ENDFORM.这段代码看起来平平无奇,但需要注意两件事。
第一遍新手写的时候经常用工作区加MODIFY,这样写也没错,但数据量大时会产生大量结构体拷贝,性能明显差。用FIELD-SYMBOLS直接改内表行,性能好很多,代码也简洁。内表几万行时体感差异比较明显。
第二件事就是下面要说的refresh。
3.3 改了内表数据,为什么必须refresh_table_display
ALV GRID在显示时,界面渲染和底层内表不是完全同步的。你在USER_COMMAND里改了gt_alv的SEL字段,但界面上的复选框状态不会自动更新。必须调用refresh_table_display,ALV才会根据最新内表数据重新绘制一次。这个原理和SAP的事务隔离类似:视图层有自己的一份渲染缓存,数据层只是被绑定到一起。
所以全选按钮的完整链路是:
- 用户点击工具栏的“全选”;
- USER_COMMAND事件触发,e_ucomm='SEL_ALL';
- 程序LOOP更新内表SEL字段;
- 调用refresh_table_display刷新界面;
- ALV重新读取内表,所有行的复选框变成勾选状态。
少了第4步,界面纹丝不动,很多新手在这卡半天以为是按钮事件没触发,单步调试发现逻辑走了,但界面就是不变,其实就是忘了刷新。
3.4 报表启用了筛选时,“全选”到底该选哪些行
这个属于进阶问题,但真的遇到就头疼。
ALV的行筛选、排序都是界面层的操作,不改变内表数据。用户筛了“只显示状态=未过账”,点全选,程序LOOP的是全部gt_alv,结果把已过账的数据也一起勾上了。这显然不符合预期。
要精确满足“把当前筛选结果全部选中”,有几个思路:
- 方案一:在数据源准备阶段,自己也按同样的业务规则过滤一遍,填入一个“当前可见数据”标记字段,全选时只更新这些行。这个方案最稳,不依赖ALV内部API,缺点是筛选条件要维护两份,一旦业务条件复杂容易漏。
- 方案二:使用CL_GUI_ALV_GRID提供的FILTER相关方法,获取当前筛选后可见的行范围,再更新对应行。这个方案省事,但不同版本的方法名和返回结构差异较大,需要针对项目SAP版本验证。
- 方案三:干脆在按钮逻辑里把筛选条件重新判断一遍,前端记忆一个last_filter条件,隐藏在程序逻辑里。
我个人的项目习惯是方案一,因为标准ALV的筛选条件匹配规则(比如文本模糊匹配、数字区间匹配)跟业务需求里的规则很可能不是一回事,与其倒推ALV的筛选状态,不如自己掌控视图数据集合。如果你的需求只是“全选所有数据”,那直接LOOP就行。
不过这里要提醒:如果用户先手动筛选,再点全选,方案二和方案三的结果是一样的,都是“可见的全选”。方案一则需要你在取数时就维护好“当前可见”的标记。反正不管用哪种,别把“全选”和“全选可见”混为一谈,先跟需求方确认清楚。
4. 勾选之后的数据读取:别再拿行高亮当勾选
4.1 用SEL字段取数还是GET_SELECTED_ROWS
前面我们用了SEL字段存储复选框状态。用户勾选完后,点击“批量打印”这类后续按钮时,肯定要把勾选的数据收集出来处理。最简单的做法:
LOOP AT gt_alv INTO gs_alv WHERE sel = 'X'. " 在这里收集主键,或调用业务函数 lv_count = lv_count + 1. ENDLOOP. IF lv_count = 0. MESSAGE '请先勾选需要处理的单据' TYPE 'W'. RETURN. ENDIF.这段代码是通用套路,重点是想再次强调:SEL字段存储的是复选框状态,和ALV的行高亮选中没有任何关系。如果业务逻辑明确用复选框,那判断条件就用sel = 'X';如果要使用用户“鼠标高亮选中的行”,那要调用:
CALL METHOD go_grid->get_selected_rows IMPORTING et_index_rows = lt_index_rows et_row_no = lt_row_no.然后根据行号读内表。两套逻辑不要混着用。
我见过一个项目,Layout里没关行高亮选择,用户用鼠标点了一行,然后代码里去读sel字段,发现是空,就找我说“勾选框有bug”。实际上用户只是“高亮选中”,根本没勾选复选框。后来我把sel_mode设为不选行,只保留复选框列,这个问题才彻底消失。所以项目里如果只要复选框,建议把行选模式关掉,避免交互歧义。
Layout的sel_mode取值需要记一下:
- 'A':单选行;
- 'B':多选行,不按Ctrl也能连续选中;
- 'C':多选行,按Ctrl才生效;
- 'D':类似B;
- 留空:不启用行选择。
如果只想要复选框勾选,可以把sel_mode留空。但注意,一些旧代码在刷新时会依赖行选中模式,改动前最好自己做一轮完整回归。
4.2 check_changed_data:PAI前最容易漏掉的一行
复选框是编辑单元格。用户勾选后,ALV内部有一个编辑缓存,这个缓存不会立刻写回绑定的内表,而是等焦点离开单元格、或者触发某些事件后才提交。最经典的坑是:
- 用户勾了几行复选框;
- 没有点回车或Tab,直接点了“批量打印”按钮;
- PAI里读gt_alv,发现sel全是空的;
- 业务方炸了:“我明明勾了!”
原因就是ALV的编辑数据还没有提交。解决办法是在读取数据前,强制让ALV做一次“数据变更检查”:
IF go_grid IS NOT INITIAL. CALL METHOD go_grid->check_changed_data. ENDIF.这个方法会把ALV内部尚未提交的单元格修改发到绑定的内表上,之后再读gt_alv才是真实结果。这个调用建议放在所有需要读取ALV数据的PAI逻辑最前面,不管是全选、批量操作还是导出Excel,都先执行这句。
别问我怎么知道的,这个坑我帮同事排查了很多次,每次都是最后加的这行代码。后来我干脆写成了模板,凡是用ALV GRID的程序,PAI里统一先check_changed_data。
4.3 DATA_CHANGED事件里做联动
除了事后读取,很多时候需要“用户一勾选,界面立刻有反应”,比如勾选后状态栏显示数量、合计金额变化、某些按钮置灰。这时要注册DATA_CHANGED事件。
METHOD handle_data_changed. DATA: ls_mod_cell TYPE lvc_s_mod. LOOP AT e_mod_cells->* INTO ls_mod_cell. IF ls_mod_cell-fieldname = 'SEL'. " 勾选框状态变化,可以在这里做联动处理 PERFORM update_selected_count. ENDIF. ENDLOOP. ENDMETHOD.e_mod_cells是一个lvc_t_mod结构的内表,每条记录表示一个发生变化单元格的行号、字段名、新旧值。注意,这个事件每次单元格变化都会触发,如果业务逻辑对性能有要求,不要在这里写重操作,比如不要在这里刷新整张ALV或者调用BAPI。我一般只在里面更新一下工具栏的“已选N条”文本,或者少量计算合计。
这里还要提一个细节:只要用户手动修改了单元格,DATA_CHANGED事件触发后,修改结果最终还是要靠check_changed_data在PAI里确认写入内表。如果用户只是勾选但没有离开单元格,即使事件触发了,内表里可能还是旧值。这也解释了为什么PAI里check_changed_data是刚需。
5. 常见踩坑与我的处理习惯
5.1 全选按钮和数据勾选不是一回事
A、B两个按钮都在工具栏,但一个操作的是ALV行选择状态(高亮),另一个操作的是SEL字段。如果用户点了全选工具栏按钮,高亮可能没变化,但复选框都勾上了;如果用户按Ctrl+A,复选框可能不会变化,但行高亮全选上了。这些都是正常现象,不是bug。关键是想清楚需求要的是哪种“全选”。脚本这种交互层面的歧义,建议在开发前就跟业务方确认,别憋在代码里。
5.2 大数据量全选的性能与内表高频更新
几万行内表做全选,用FIELD-SYMBOLS循环几乎没压力;但如果你还在用MODIFY逐条更新,能明显感到界面卡顿。再多一个数量级,几十万行的时候,ALV GRID本身渲染就会有压力,全选功能已经不是首要矛盾。这种量级建议考虑分页展示或者做数据预筛选,而不是让用户一口气看几十万行再去全选。
另外,全选后refresh整表,滚动条会跳回顶部,体验很差。刷新时可以指定模式保持位置:
CALL METHOD go_grid->refresh_table_display EXPORTING is_stable = VALUE lvc_s_stbl( row = 'X' col = 'X' ).is_stable结构里row=‘X’表示保持当前滚动行,col=‘X’表示保持当前滚动列。这个我在长报表里实测很有效。
5.3 “勾选保存后清空”和“刷新后保留勾选”
业务场景不同,对勾选状态的要求也不同。
有些场景希望点完“批量打印”后勾选清空,防止重复打印。这个简单,在打印处理完后,再循环一次清空sel并refresh即可。
另一些场景,用户切走了界面再回来,希望之前勾选的还保留。这就要保证:从ALV显示到重新取数之间,不要因为任何重新查询把gt_alv整个重建。如果你在屏幕PBO里每次进入都重新SELECT覆盖内表,那勾选必然丢。正确做法是:把复选框状态按主键存一份(比如全部行塞到一个HASHED KEY内表里,键是DOC_NO,值是SEL),重建数据后再根据主键回填。网上总有人问“复选框怎么保留勾选”,其实ALV只要绑定内表对象不换、不重新赋值,状态天然保留;麻烦的是自己把内表重建了。
5.4 老项目Function模式ALV如何实现同样的按钮与全选
上面写的都是CL_GUI_ALV_GRID面向对象方式。老项目里大量使用的是REUSE_ALV_GRID_DISPLAY函数,这也能做全选,就是写法上绕一点。
复选框字段和字段目录配置完全一致,checkbox+edit。全选按钮不能通过TOOLBAR事件加,而是要在GUI Status里自己画一个按钮,function code叫SEL_ALL或类似名字。然后在I_CALLBACK_USER_COMMAND注册的FORM里处理:
FORM user_command USING r_ucomm TYPE sy-ucomm rs_selfield TYPE slis_selfield. CASE r_ucomm. WHEN 'SEL_ALL'. LOOP AT gt_alv ASSIGNING FIELD-SYMBOL(<fs_alv>). <fs_alv>-sel = 'X'. ENDLOOP. " 刷新ALV显示 CALL FUNCTION 'REUSE_ALV_GRID_DISPLAY' EXPORTING i_callback_program = sy-repid ... TABLES t_outtab = gt_alv. WHEN 'DESEL_ALL'. LOOP AT gt_alv ASSIGNING FIELD-SYMBOL(<fs_alv2>). CLEAR <fs_alv2>-sel. ENDLOOP. CALL FUNCTION 'REUSE_ALV_GRID_DISPLAY' EXPORTING i_callback_program = sy-repid ... TABLES t_outtab = gt_alv. ENDCASE. ENDFORM.Function模式里重新调用一次DISPLAY就是刷新。这里有个项目经验:刷新时最好把i_save设为'X',让ALV记住用户调整过的列宽、排序等界面布局,否则每次刷新列都恢复默认,业务方会以为系统坏了。
其实我这个习惯基本是照着老项目模板来的:能用Function模式的项目多半都是老掉牙的代码库,继续沿用比重构更稳妥。但新开发的报表,能上OO ALV的就别再用Function模式了,后期维护差距很大。
5.5 提升体验的小技巧:工具栏显示“已选N条”
业务方对勾选数量非常敏感,批量操作前总想知道自己选了几条。与其让他们数,不如在USER_COMMAND里更新一个动态文本按钮。
思路是在TOOLBAR事件里追加一个function code为‘SEL_COUNT’的按钮,text设成‘已选0条’,平时不响应点击。每次全选、取消全选、DATA_CHANGED事件触发后,重新统计并刷新工具栏文本:
METHOD update_sel_count. DATA: lv_count TYPE i. lv_count = REDUCE i( INIT c = 0 FOR ls_alv IN gt_alv WHERE ( sel = 'X' ) NEXT c = c + 1 ). CALL METHOD go_grid->set_toolbar_interactive EXPORTING ip_field = 'SEL_COUNT'. ENDMETHOD.set_toolbar_interactive本身是用来设置光标位置的,想动态改工具栏按钮文本,需要通过改变自身变量并刷新工具栏。细节比较多,但思路就是:统计数量,更新按钮文本,调用刷新。这个小功能不值多少工作量,但能给业务方留下“这开发靠谱”的好印象。
5.6 复选框列别塞到最后,放最左边才符合直觉
我见过不少项目把SEL字段放进内表时顺手放了最后,字段目录也是按默认顺序排,结果复选框跑到报表最右侧。业务方几乎统一的反馈是:勾选框应该在最左边,方便鼠标快速点选。所以在设置col_pos时,一定要把SEL的列位置固定到1。
如果用的是LVC_FIELDCATALOG_MERGE自动生成的字段目录,循环里对SEL明确指定col_pos = 1,并对其他字段做一次位置迁移。ALV的col_pos是按数字排序的,两个字段同值时会按内表顺序排,所以只要SEL是1,其他字段从2开始排就稳定了。
6. 我的最终建议:把“复选框+全选”做成模板复用
写到这里,再分享一点个人习惯。ALV报表里做复选框、全选、取消全选,来来回回就是字段目录、Layout、TOOLBAR事件、USER_COMMAND事件、check_changed_data这么一整套。我后期做项目,会把这套逻辑封装成一个独立的类或者通用FORM,参数就传内表引用和过滤条件。新报表要加复选框,十分钟就能搞定,还能避免每次重写带来的低级错误。
更要紧的是,每次开发前先想清楚三件事:
- 第一,这报表是要用“SEL字段复选框”还是“ALV行高亮选中”,或者两个都要;
- 第二,全选是针对全部数据还是当前筛选后的数据;
- 第三,数据读取前有没有强制check_changed_data。
这三件事确认清楚了,剩下的就是拼接模板代码的事。
复选框在ALV里不是复杂功能,但任何一个链路节点踩坑都可能让业务方误以为功能有问题。希望这篇内容能帮正在做ALV报表的同事少走点弯路。