понедельник, 8 февраля 2021 г.

SALV с DropDown кнопками в панели инструментов

Когда мы строим вывод таблицы на базе SALV, мы можем в панель инструментов добавить свои кнопки и потом обработать нажатие на них. Примерно вот так:
DATA lo_alv TYPE REF TO cl_salv_table READ-ONLY.
...
DATA(lo_funcs) = lo_alv->get_functions( ).
lo_funcs->add_function(
  EXPORTING
    name     = 'ZREFRESH'
 	icon     = CONV #( ICON_REFRESH )
	text     = 'Обновить'
	tooltip  = 'Обновить'
	position = if_salv_c_function_position=>left_of_salv_functions
).
...
DATA(lo_event) = lo_alv->get_event( ).
SET HANDLER on_alv_function FOR lo_event ACTIVATION abap_true.
...
METHODS on_alv_function FOR EVENT added_function OF cl_salv_events IMPORTING sender e_salv_function.
...
METHOD on_alv_function.
  CASE e_salv_function.
    WHEN 'ZREFRESH'.
      ...
    WHEN OTHERS.
  ENDCASE.
ENDMETHOD.
Но данный способ не подойдет, если вы хотите добавить не простую кнопку, а кнопку с выпадающими действиями типа такой:
Поскольку в классе CL_SALV_FUNCTIONS_LIST просто нет подходящих методов для этого. Но, поскольку SALV построен внутри на базе CL_GUI_ALV_GRID, а у последнего можно создавать такие кнопки (и не только такие:
), то нужно каким-то образом достучаться до CL_GUI_ALV_GRID внутри SALV и добавить кнопки уже через него. Никакого простого способа получить ссылку на CL_GUI_ALV_GRID из экземпляра CL_SALV_TABLE нет. Под простым я имею в виду конкретный метод или атрибут. Но в принципе есть пара решений на базе доступа через адаптер и с помощью регистрации обработчика AFTER_REFRESH на всех экземплярах GRIDа. С ними обоими проблема в том, что доступ к GRID возможен только после того как вывод произошел и значит toolbar построен. Хотя, в принципе, можно на GRID вызвать метод SET_TOOLBAR_INTERACTIVE для перестройки панели инструментов, это двойная не нужная работа. Кстати, возможно, для случаев выше и простого ФМ GET_GLOBALS_FROM_SLVC_FULLSCR было бы достаточно...

Нам нужен способ получить GRID как можно раньше после вызова CL_SALV_TABLE=>DISPLAY, и повесить на него обработчик события TOOLBAR ну и добавить кнопочки с подэлементами. Разобъем задачу на 2 более простых: получение GRID и управление toolbar.

Задача первая. Получаем GRID.

Когда мы вызываем CL_SALV_TABLE=>DISPLAY, то в итоге будет вызыван метод SET_METADATA на адаптере. Внутри он создает GRID. Причем он может это делать 2мя способами:
  1. Напрямую
  2. Через фабрику строителей
С "напрямую" все понятно, а вот с фабрикой - это уже интересно. В общем логика примерно такая:
  1. Если в параметре cl_alv_z_params=>c_param-alv_gui_instance_builder класса cl_alv_z_params задано не пустое значение, то вызывается метод cl_alv_gui_ist_builder_factory=>get_alv_gui_instance_builder без параметров. Внутри метода:
      method get_alv_gui_instance_builder.
        if mo_alv_gui_instance_builder is not bound.  "Later Admin Table?
          if iv_inst_builder_classname is supplied.
            data(l_instance_builder_name) = iv_inst_builder_classname.
          else.
            l_instance_builder_name = cl_alv_z_params=>get_parameter( cl_alv_z_params=>c_param-alv_gui_instance_builder ).
          endif.
          if l_instance_builder_name is initial.
            ro_instance_builder = new lcl_alv_gui_instance_builder( ).
          else.
            create object ro_instance_builder type (l_instance_builder_name).
          endif.
          mo_alv_gui_instance_builder = ro_instance_builder.
        else.
          ro_instance_builder = mo_alv_gui_instance_builder.
        endif.
      endmethod.
    1. Проверяется, что строитель еще не создан
    2. Если в метод передано имя класса-строителя, берется оно, иначе берется значение параметра cl_alv_z_params=>c_param-alv_gui_instance_builder
    3. Если в итоге имя класса-строителя пустое, создается строитель из локального для фабрики класса lcl_alv_gui_instance_builder (который наследуется от CL_ALV_GUI_INSTANCE_BUILDER)
    4. Иначе создается экземпляр указанного класса (очевидно, он тоже должен наследоваться от CL_ALV_GUI_INSTANCE_BUILDER)
    5. Созданный экземпляр запоминается в статическом атрибуте фабрики и возвращается при следующих вызовах get_alv_gui_instance_builder
  2. У строителя вызывается метод create_alv_grid_at_pbo, который должен вернуть ALV_GRID.
Итак нам нужно создать наследника CL_ALV_GUI_INSTANCE_BUILDER, назовем его ZCL_ALV_GUI_BUILDER. Реализуем его аналогично тому, как реализован класс lcl_alv_gui_instance_builder из фабрики, за исключением того что:
  1. Создавать GRID будем не сами, а используя текущего строителя, которого вернет фабрика. Это позволит работать как прокси, если до вызова нашего кода уже будет зарегистрирован какой-то специфический строитель
  2. При создании GRID будем генерировать соответствующий Event, на который смогут подписаться заинтересованные обработчики
  3. Создадим метод для регистрации нового строителя в фабрике
  4. Создадим метод для разовой регистрации строителя в фабрике. Это позволит регистрировать нашего строителя только на 1 конкретный вызов, после чего устанавливать ранее зарегистрированного строителя
Весь код строителя лежит тут. Рассмотрим некоторые методы подробнее.


Метод REGISTER_BUILDER_ONCE

METHOD register_builder_once.
    DATA(lv_prev_builder_class) = cl_alv_z_params=>get_parameter( cl_alv_z_params=>c_param-alv_gui_instance_builder ).

    ro_builder = register_builder( ).
    ro_builder->mv_prev_builder_class = lv_prev_builder_class.
    ro_builder->mv_restore_on_grid_create = abap_true.
  ENDMETHOD.
Метод статический, без параметров, возвращает экземпляр нашего строителя. Таким образом, вызывающий код может подписаться на событие ON_GRID_CREATE строителя. Внутри метод получает текущее значение параметра cl_alv_z_params=>c_param-alv_gui_instance_builder. Затем получает строителя вызовом метода register_builder и запоминает в экземпляре строителя имя предыдущего класса строителя из параметра и флаг, что его нужно восстановить. Восстановление происходит не сразу, а после первого создания ALV_GRID.


Метод REGISTER_BUILDER

  METHOD register_builder.
    DATA(lo_prev_builder) = cl_alv_gui_ist_builder_factory=>get_alv_gui_instance_builder( ).

    DATA(lo_type) = cl_abap_datadescr=>describe_by_data( VALUE tv_prev_builder( ) ).
    DATA(lv_name) = lo_type->absolute_name.
    REPLACE '\CLASS='  IN lv_name WITH ''.
    FIND FIRST OCCURRENCE OF '\' IN lv_name MATCH OFFSET DATA(lv_offset).
    IF sy-subrc = 0.
      DATA(lv_self_name) = CONV cl_alv_z_params=>y_param_value( lv_name(lv_offset) ).
    ELSE.
      lv_self_name = lv_name. "Hmm
    ENDIF.

    cl_alv_z_params=>set_parameter(
      EXPORTING
        param_name  = cl_alv_z_params=>c_param-alv_gui_instance_builder  " Parameter name
        param_value = lv_self_name
    ).

    ro_builder ?= cl_alv_gui_ist_builder_factory=>new_alv_gui_instance_builder( ).
    ro_builder->mo_prev_builder = lo_prev_builder.
  ENDMETHOD.
Метод статический, без параметров, возвращает экземпляр нашего строителя. Внутри метод получает экземпляр текущего класса-строителя, вычисляет имя собственного класса , заносит его в параметр cl_alv_z_params=>c_param-alv_gui_instance_builder, создает экземпляр собственного класса через вызов метода фабрики new_alv_gui_instance_builder (он принудительно обновляет экземпляр класса строителя в атрибуте фабрики), после чего запоминает в экземпляре собственного строителя ссылку на предыдущий экземпляр строителя.


Метод CREATE_ALV_GRID_AT_PBO

Метод экземпляра, переопределеный метод класса CL_ALV_GUI_INSTANCE_BUILDER, получает кучу параметров, возвращает новый экземпляр CL_GUI_ALV_GRID. Логика метода:
  1. Если аттрибут mo_prev_builder не ссылается на экземпляр строителя, то получить его из фабрики
  2. Вызывать метод CREATE_ALV_GRID_AT_PBO на экземпляре mo_prev_builder, тем самым заставив "работать" логику оригинального строителя по созданию ALV_GRID и запомнить полученный экземпляр GRID в атрибуте mo_alv_grid
  3. Если установлен флаг mv_restore_on_grid_create, вернуть в настройки фабрики прежнего строителя
  4. "Дернуть" событие ON_GRID_CREATE передав в него экземпляр только что созданного ALV_GRID
Итак мы создали класс, который будет генерить событие, когда создается GRID

Задача вторая. Добавляем и обрабатываем DropDown кнопки на панель инструментов.

Идея в следующем.
  1. Мы создаем экземпляр некоего класса (назовоем его ZCL_SALV_DRODOWN_TLB), который регистрирует и получает наш ALV_BUILDER и подписывается на его событие ON_GRID_CREATE
  2. Мы храним в классе структуру наших кнопок (таблица с верхнеуровневыми кнопками и их подпунктами)
  3. В обработчике события ON_GRID_CREATE класс подписывается на событие TOOLBAR (которое вызывается при построении панели инструментов ALV_GRID) и на событие MENU_BUTTON (которое вызывается на нажатие DropDown кнопки). Также производится отписка от данного события, поскольку нас интересует только 1 GRID
  4. В обработчике события TOOLBAR к стандартным кнопками добавляеются зарегистрированные в классе верхнеуровневые кнопки как кнопки типа DropDown
  5. В обработчике события MENU_BUTTON к меню добавляются соответствующие подпункты из нажатой верхнеуровневой кнопки
Также в классе есть пара методов для деактивации и выбора подпунктов. Полный код обработчика лежит здесь

Как это использовать?

Можно примерное так
REPORT ztest_salv_dd.

DATA gt_mara TYPE STANDARD TABLE OF mara.

CONSTANTS:
  BEGIN OF cv_alv_toolbar,
    BEGIN OF action_dd,
      func TYPE salv_de_function VALUE 'ZACTION',
      text TYPE string VALUE 'Action',
      BEGIN OF send,
        func TYPE salv_de_function VALUE 'ZACT_SEND',
        text TYPE string VALUE 'Send',
      END OF send,
      BEGIN OF receive,
        func TYPE salv_de_function VALUE 'ZACT_RECEIVE',
        text TYPE string VALUE 'Receive',
      END OF receive,
    END OF action_dd,
  END OF cv_alv_toolbar.

CLASS lcl_handler DEFINITION.
  PUBLIC SECTION.
    CLASS-METHODS on_alv_function FOR EVENT added_function OF cl_salv_events IMPORTING sender e_salv_function.
ENDCLASS.

CLASS lcl_handler IMPLEMENTATION.
  METHOD on_alv_function.
    CASE e_salv_function.
      WHEN cv_alv_toolbar-action_dd-send-func.
        MESSAGE cv_alv_toolbar-action_dd-send-text TYPE 'I'.
      WHEN cv_alv_toolbar-action_dd-receive-func.
        MESSAGE cv_alv_toolbar-action_dd-receive-text TYPE 'I'.
      WHEN OTHERS.
    ENDCASE.
  ENDMETHOD.
ENDCLASS.

START-OF-SELECTION.
  cl_salv_table=>factory(
    EXPORTING
       r_container = cl_gui_container=>default_screen
    IMPORTING
      r_salv_table   = DATA(lo_alv)

    CHANGING
      t_table        = gt_mara
  ).

  lo_alv->get_functions( )->set_all(
    value = if_salv_c_bool_sap=>true
  ).

  DATA(lo_event) = lo_alv->get_event( ).
  SET HANDLER lcl_handler=>on_alv_function FOR lo_event ACTIVATION abap_true.

  DATA(go_alv_toolbar) = NEW zcl_salv_drodown_tlb( ).

  go_alv_toolbar->add_button(
    is_button = VALUE #(
      function = cv_alv_toolbar-action_dd-func
      text  = cv_alv_toolbar-action_dd-text
      t_subitem = VALUE #(
        ( function = cv_alv_toolbar-action_dd-send-func
          text = cv_alv_toolbar-action_dd-send-text
        )
        ( function = cv_alv_toolbar-action_dd-receive-func
          text = cv_alv_toolbar-action_dd-receive-text
        )
      )
    )
  ).

  lo_alv->display( ).

  CALL SCREEN 100.  
Естественно, кнопки с выпадающими подпунктами можно добавить только для SALV, которые отображаются в контейнере. Полноэкранные SALV используюут GUI Status для панели инструментов, а там такой функциональности не предусмотрено.

Комментариев нет:

Отправить комментарий