
ABAP in Practice - Creating a Setup App
No more SAP GUI? How do we then create a static setup app while still remaining flexible on the ABAP side for further implementation? Here's a practical example.
Table of contents
In this article, we'll look at how to create a simple static setup app to automate various configuration steps, for example.
Introduction
In our current Clean Core Measurement (CCM) project, there are several steps to consider and perform during installation. Since these steps are numerous, ranging from setup and customization to the configuration of applications and technical settings, we decided to provide a simple setup application to automate these various steps. We have no way of providing a report; instead, we must work with a Fiori Elements application.
Task
The task now is to provide and design the actual application and choose a suitable pattern for it. We have several requirements for our application that we want to implement:
- First, the application should have a static list of setup steps, which are predefined in a specific order and do not allow the user to sort, filter, or hide them; these steps are fixed.
- Second, the application should be modular. This means that the individual setup steps should be mapped in separate logic modules, which, however, implement as many similar steps as possible, such as checking whether the step was successful, or correspondingly outputting a log, messages, and a status indicating which settings are still missing.
- Finally, automation is also needed so that the user can click "Execute" in the frontend, can click to start the setup and perform the necessary steps in the system.
Hint: In the next section, we will discuss the solution. If you would like to complete the task independently first, you should pause here.
Solution
In this chapter, we will address the solution and look at various points and challenges to consider when implementing the application.
Foundation
The foundation here will primarily be a custom It's important to define an entity because we'll be encapsulating the main logic in classes to create various components within our framework or to call standard SAP APIs. The simplest approach is to define a custom entity, as it doesn't have any underlying data or tables. For this, we can use the Custom Pattern, which we'll describe later in this article.
Entity
As a foundation, we now define the structure we want to display to the user so they can interact with the setup. For this, we need an ID for the actual step, which identifies which step was called and later, for example, performs the instantiation of the correct entity. The step will have a description, a criticality indicator so we can see at a glance whether the step is green or if further work is needed, and a status message indicating the current state. We'll also add navigation options. These will later allow you to jump to the corresponding destination with a single click, for example, if you want to perform manual configurations or a review. Each row could then navigate to a different app.
@EndUserText.label: 'Setup Steps'
@ObjectModel.query.implementedBy: 'ABAP:ZCL_BC_CCM_SETUP_QUERY'
define root custom entity ZBC_R_CCMSetupSteps
{
key StepID : abap.char(2);
StepDescription : abap.sstring(60);
StatusCriticality : abap.int1;
StatusMessage : abap.sstring(250);
NavigationObject : abap.sstring(200);
NavigationAction : abap.sstring(50);
}
Framework
As a basis for our work, we use a factory that generates the appropriate implementation for us based on an identifier. The advantage is that later we only need to call the factory, pass an identifier, and then call the actual implementation via a corresponding interface. You can find more information about this pattern in the linked article. Therefore, the first step is to define a common interface that will then be called later when we execute the action.
INTERFACE zif_bc_ccm_setup_step
PUBLIC.
TYPES step_type TYPE c LENGTH 2.
TYPES:
BEGIN OF ENUM steps STRUCTURE step BASE TYPE step_type,
placeholder VALUE IS INITIAL,
setting VALUE 'SE',
provider_config VALUE 'PC',
comm_arrangement VALUE 'CA',
jobs VALUE 'JO',
cluster VALUE 'CL',
role VALUE 'RO',
END OF ENUM steps STRUCTURE step.
METHODS check
RETURNING VALUE(result) TYPE check_result.
METHODS execute
IMPORTING cid_ref TYPE abp_behv_cid
RETURNING VALUE(result) TYPE execute_result.
METHODS get_description
RETURNING VALUE(result) TYPE ZBC_R_CCMSetupSteps-StepDescription.
METHODS get_step_id
RETURNING VALUE(result) TYPE ZBC_R_CCMSetupSteps-StepID.
METHODS get_navigation
RETURNING VALUE(result) TYPE navigation_result.
METHODS execute_save
RETURNING VALUE(result) TYPE REF TO zif_bc_ccm_mini_log.
ENDINTERFACE.
The interface contains certain components that we will also need later during processing. We store all possible steps as an enumeration. This also ensures that an evaluation takes place automatically as soon as we create a new implementation.
- We implement CHECK as a method. This is where the check is performed to determine whether a step was successfully executed or completed.
- We have an EXECUTE method that is executed when the user clicks "Execute".
- Additionally, we create an EXECUTE step for SAVE, which is called in the Save Sequence if we have steps that are not possible in the actual action but must first be called in the Save Sequence.
- Then we have other methods that, for example, return the description, the step as a character field, or navigate to the corresponding target app, which can be different for each step.
In the Factory, we then create a method with the configuration that we could call centrally. Here we need the logic in two steps: one for generating the data in the query class and one for generating the instances via the factory. Therefore, we create a small mapping table here, which holds the step ID internally, i.e., the enumeration, and an external ID that we consume via the service, as well as the actual instance of the implementation.
METHOD get_step_configuration.
result = VALUE #( ( step_id = zif_bc_ccm_setup_step=>step-role
instance = NEW zcl_bc_ccm_step_role( ) )
( step_id = zif_bc_ccm_setup_step=>step-setting
instance = NEW zcl_bc_ccm_step_setting( ) )
( step_id = zif_bc_ccm_setup_step=>step-provider_config
instance = NEW zcl_bc_ccm_step_provider( ) )
( step_id = zif_bc_ccm_setup_step=>step-comm_arrangement
instance = NEW zcl_bc_ccm_step_comm_arr( ) )
( step_id = zif_bc_ccm_setup_step=>step-jobs
instance = NEW zcl_bc_ccm_step_jobs( ) )
( step_id = zif_bc_ccm_setup_step=>step-placeholder
instance = NEW zcl_bc_ccm_step_placeholder( ) )
( step_id = zif_bc_ccm_setup_step=>step-cluster
instance = NEW zcl_bc_ccm_step_cluster( ) ) ).
LOOP AT result REFERENCE INTO DATA(step).
step->external_id = step->instance->get_step_id( ).
ENDLOOP.
ENDMETHOD.
This creates the foundation for flexibly adding new steps later and maintaining them in only one place. The configuration is then called in the class or in the factory, making it centrally manageable for us.
Service
To build the service, we first need to create the data foundation. Since we are working in a custom scenario, we create a new query class to retrieve the data. Essentially, we have to make several assumptions that will later prove true. For example, we want to display all steps completely, not allow filtering, and not sort the entries. Basically, some prerequisites are already in place: Because we're using a custom entity, the fields are virtual and must first be manually activated in the UI.
The first step we take is to call the required methods from the request, otherwise we'll get an error during implementation. Then we also read the filter from the step ID and return it as a filter. This is because, for example, when executing an action, the current line is always read before the action is carried out. Therefore, we need at least the step ID, i.e., the key that we can use as a filter.
request->get_sort_elements( ).
request->get_paging( ).
TRY.
DATA(odata_filter) = request->get_filter( )->get_as_ranges( ).
result = CORRESPONDING #( odata_filter[ name = 'STEPID' ]-range ).
CATCH cx_rap_query_filter_no_range cx_sy_itab_line_not_found.
CLEAR result.
ENDTRY.
In the next section of the implementation, we can then iterate through the configuration, taking into account the filter we were passed. This creates a list of the steps that will later be available in our application. We also execute some initial components via the interface, such as performing a check to obtain the result and store it in the line. We also retrieve information like the step ID, description, and current status from the step to have some initial information available when the application is first loaded.
LOOP AT configs INTO DATA(config) WHERE step_id IN step_filter.
DATA(table_line_number) = sy-tabix.
IF io_request->is_data_requested( ).
DATA(check_result) = config-step->check( ).
ENDIF.
DATA(navigation) = config-step->get_navigation( ).
INSERT VALUE #( StepID = config-step->get_step_id( )
StepDescription = |{ table_line_number }. { config-step->get_description( ) }|
StatusCriticality = check_result-status
StatusMessage = check_result-message
NavigationObject = navigation-object
NavigationAction = navigation-action )
INTO TABLE steps.
ENDLOOP.
In the final step, we only need to return the steps if they are requested. Either we return the data itself or the number of data points if the UI service requests this.
IF io_request->is_data_requested( ).
io_response->set_data( steps ).
ENDIF.
IF io_request->is_total_numb_of_rec_requested( ).
io_response->set_total_number_of_records( lines( steps ) ).
ENDIF.
Behavior
When defining the behavior, we set an unmanaged scenario because we are working with a custom entity. We set all CRUD operations to "internal" because we don't want to create any new data records about the app, but only display existing ones. We then create a new action for checking and executing each step. These are instance-based because they should be performed step by step.
unmanaged implementation in class zbp_bc_ccm_setup_steps unique;
strict ( 2 );
define behavior for ZBC_R_CCMSetupSteps alias StetupSteps
lock master
authorization master ( instance )
{
internal create;
internal update;
internal delete;
action CheckStep;
action ExecuteStep;
side effects {
action ExecuteStep affects $self;
}
field ( readonly ) StepID;
}
During implementation, we then have to implement each action individually, but we can do this relatively generically, so that, for example, we can iterate over the different keys that are addressed and have the factory create the appropriate instance based on the step ID. We then call the "EXECUTE" or "CHECK" method via the interface and receive the result in the form of messages.
LOOP AT keys INTO DATA(key).
DATA(step) = zcl_bc_ccm_setup_step_factory=>create_step( CONV #( key-StepID ) ).
DATA(step_result) = step->execute( key-%cid_ref ).
INSERT LINES OF step_result-log->get_all_messages( ) INTO TABLE reported-%other.
INSERT step INTO TABLE lcl_buffer=>instances.
ENDLOOP.
Since we also need to remember that we have to access this instance in the Save Sequence, for example, we store the currently applied step in a temporary buffer and then call the second method in the Save Sequence. The implementation then has to handle this.
UI
For the UI, we then need to add additional UI annotations. Since we are in a custom scenario, we also have to add these to the Custom Entity, along with the actual definitions. To do this, we define a Criticality at the row level to highlight the steps with color. We deactivate the filter on the corresponding column and additionally define two actions that we want to display inline for faster access, so that these actions are not visible in the bar at the top.
@UI.lineItem : [
{ position : 10, criticality: 'StatusCriticality' },
{ position : 30, type: #FOR_ACTION, label: 'Check', dataAction: 'CheckStep', inline: true },
{ position : 40, type: #FOR_ACTION, label: 'Execute', dataAction: 'ExecuteStep', inline: true }
]
@ObjectModel.text.element: [ 'StepDescription' ]
@UI.textArrangement:#TEXT_ONLY
@Consumption.filter.hidden: true
@EndUserText.label: 'Step'
key StepID : abap.char(2);
As a result, we then get the steps with the step sequence and highlights if a step is not fulfilled, for example. In the latter part, we find the two actions, which are displayed inline to facilitate access and execution.
Using an additional annotation for the filter, we can also exclude it from the list of filterable fields. This prevents the user from hiding certain steps or pre-filling variants with filters. This also ensures that the entries are no longer sortable, filterable, or groupable (also due to the custom entity).
@Consumption.filter.hidden: true
Generation
We use an OData v4 UI service. We can then run the Fiori Elements generator "from Template" on this service to generate our standard Fiori Elements application. Further information on generating the application can be found in the following article linked below. For generation, we use the standard template "List Report"; otherwise, we have not made any further customizations. If you want to deploy the application later, you should also create a deployment configuration, as well as a Launchpad configuration if you want to integrate the application into Launchpad later.
Object Page
In the next step, after generation, we want to disable navigation to the Object Page. To do this, we switch to the Page Map and can delete the Object Page that was automatically generated. This makes the navigation path disappear in the back, and we can no longer navigate, so we always remain on the List Report.
Filter
When we load our application in preview for the first time, we will notice that the filter bar is still available at the top. Here we can resize the area and add elements. However, because we have deactivated all fields for the filter, no further criteria are offered. Therefore, this area no longer makes any sense.
To do this, we switch to the edit mode of the List Report in the Page Map and click on the Filter Bar. On the right side, we should see further settings for the Filter Bar. There, we can hide the Filter Bar completely by setting the value to "true".
The preview will then reload, and we can take another look at the settings in detail. We see that the variant management is still there, as are the additional actions. The area for the filter bar has now been hidden, as have the additional buttons.
Test
Let's now test our final and deployed application. We have already implemented some steps that are necessary for the setup, but that wasn't part of the actual task. After loading the list, we see the various steps that are loaded by the factory configuration. We receive a status if certain things are not yet fully completed, and status messages, which are also highlighted in color. In the next step, we perform a check using the CHECK. This calls our actual implementation for the settings and returns a result in the form of messages. This way, we know exactly which steps are not yet fully defined. Finally, we call the actual system using EXECUTE. The configuration is processed, the parameters are created in the configuration, and a check is automatically performed. The settings are now green, and all parameters are created.
In this case, after the Execute, we also call a side effect. This ensures that the line is reloaded. This also provides the user with automatic feedback when they select an action and the status of a step changes.
Challenge
There were also some challenges in implementing the application that needed to be considered. Basically, we now have a modular framework that encapsulates the various setup steps, allowing us to make small implementations without having to rebuild the entire application each time.
However, there are also challenges in using the APIs. For example, not all the APIs we use, such as those for creating the Communication Arrangement, creating business roles, or assigning to business users, are automatically suitable for RAP. Here you ultimately have to check and see which API is suitable in which scenario. In some cases, we also had to move an API to a background process using bgPF so that it ran smoothly and didn't disrupt the RAP flow. This was usually due to a COMMIT in the process, which is forbidden in STRICT mode.
Complete Example
We already use the app in our Clean Core Measurement (CCM) project. In the GitHub repository you will find the complete project and in the ZBC_CCM_APP_SETUP directory the setup app.
Conclusion
Implementing the setup app is not necessarily difficult, but it does require some steps that need to be considered in order to implement the application. An important lesson here is how we can deactivate various mechanisms in Fiori Elements to provide the user with the simplest possible list. Secondly, we learn a great deal about which frameworks and elements we can call in which situations and where we need to find workarounds.





