Skip to Content

1 Requirement

In WebUI scenarios specific requests and queries often take a long time until the data is available. In particular, RFC calls to external systems like the ERP can slow down performance. Due to the WebUI framework where visualization and rendering is done after server side processes are finished the user requires dedicated parallel processing. Table Views in WebUI include usually a large amount of different data and therefore different request origins. The ambition is to separate slow requests from the other request in order to fasten the entire visualization.

 

1.1 Preview

Take a look at the asynchronous rendering in action.

 

 

2 Proposal

Using a combination of server implementation and client side scripting Table Views can be rendered asynchronously where required. At first rendering the data is not received but a loading information is displayed instead./wp-content/uploads/2015/09/1_783120.png

After an AJAX-Callback is started, the data is received and the response is sent back to the client in order to display the Table View entries at the moment they’re getting available.

/wp-content/uploads/2015/09/2_783121.png

3 Implementation Steps

Both server side and client side implementation needs to be done in order to achieve this functionality.

3.1 Server Side

The implementation class that handles the callback needs to be the same instance as the one the table view is rendered. Therefore, a modification in the method IF_HTTP_EXTENSION~HANDLE_REQUEST of class CL_CRM_WEB_UTILITY needs to be done. Similar to SAP Note 1968050 where the instance is not created again.

3.1.1 Modification in Web Utility Class

The modification is not critical from my perspective. It might be usefull to integrate a customizing there if more than one class needs to be included for different Table Views.


* -> create handler class
 TRY.
 IF LV_HANDLER_CLASS_NAME = 'CL_CRM_BSP__RECENTOBJECT0_IMPL'
 OR LV_HANDLER_CLASS_NAME = 'ZL_ZWEBUI_B_ZACCOUNTOPPOR_IMPL'.
 lv_handler_class ?= lv_target_controller.
 ELSE.
 CREATE OBJECT lv_handler_class
 TYPE
 (LV_HANDLER_CLASS_NAME).
 ENDIF.
 * -> call back application handler
 lv_handler_class->handle_request(
 ir_server     = server
 ir_controller = lv_target_controller ).
 CATCH cx_root.
 ENDTRY.









3.1.2 HTML

The rendering of the Table View itself must be done manually without using BSP tags to save the created BEE in the respective implementation class. Otherwise, the logic would be called after every roundtrip.


<%
 DATA: lr_config_table TYPE REF TO cl_chtmlb_config_cellerator.

 IF controller->gr_config_table IS INITIAL.

 CALL METHOD cl_chtmlb_config_cellerator=>factory
 EXPORTING
 id             = 'Table'
 table          = controller->typed_context->btqropp->table
 _table         = '//BTQROPP/Table'
 xml            = controller->configuration_descr->get_config_data( )
 width          = '100%'
 selectionmode  = cl_thtmlb_cellerator=>gc_selection_mode_none
 personalizable = 'FALSE'
 RECEIVING
 element        = lr_config_table.

 controller->gr_config_table ?= lr_config_table.
 %>
 <script>
 thtmlbAJAXCall.callBackend("<%= controller->create_ajax_url( ) %>",thtmlbCCelleratorManager.asynchronousRenderingCallback);
 </script>
 <%
 lr_config_table->emptytabletext = cl_wd_utilities=>get_otr_text_by_alias( 'CRM_BSP_UI_FRAME_RECOBJ/LOADING' ).
 ENDIF.
 lr_config_table ?= controller->gr_config_table.
 lr_config_table->if_bsp_bee~render( _m_page_context ).
 %>








3.1.3 Clear on new focus

Depending on the usage it may be required to handle the logic whenever the focus is changed. For instance, in an assignment block that is included in the BP_HEAD. If the business partner is changed the search must be done again. I guess, there can be done some additional investigation to handle the partner change itself and in those cases just take the data out of the BOL. So the performance can be improved even more.

3.1.3.1 Method: IF_BSP_MODEL~INIT

Get the instance of the controller implementation class.


CALL METHOD super->if_bsp_model~init
 EXPORTING
 id    = id
 owner = owner.

 gv_owner ?= owner.






3.1.3.2 Method: ON_NEW_FOCUS

Clear the BSP bee.


CLEAR gv_owner->gr_config_table.






3.1.4 AJAX-URL

The URL which calls the backend via JavaScript can be created in the implementation class directly.


CALL METHOD cl_crm_web_utility=>create_service_url
 EXPORTING
 iv_handler_class_name = 'ZL_ZWEBUI_B_ZACCOUNTOPPOR_IMPL'
 iv_controller_id      = me->component_id
 RECEIVING
 ev_url                = ev_url.






3.1.5 Callback

The implementation class needs the Interface IF_CRM_WEB_CALLBACK in order to receive the previously callback.

3.1.5.1 Method: IF_CRM_WEB_CALLBACK~HANDLE_REQUEST

The method handles the callback, retrieves the data and creates the HTML code for the response.

 


*|-- References
  DATA: lr_result          TYPE REF TO if_bol_bo_col,
 lr_query_service   TYPE REF TO cl_crm_bol_dquery_service.
*|-- Variables
 DATA: lv_partner         TYPE string,
 lv_html            TYPE string.

 *|-- BP number
 lv_partner = me->typed_context->builheader->collection_wrapper->get_current( )->get_property_as_string( iv_attr_name = 'BP_NUMBER' ).
 CHECK lv_partner IS NOT INITIAL.

 TRY.
 *|-- Query Service for Opportunities
 lr_query_service = cl_crm_bol_dquery_service=>get_instance( 'BTQOpp' ).

 *|-- Query-Parameter
 CALL METHOD lr_query_service->add_selection_param
 EXPORTING
 iv_attr_name = 'PROSPECT'
 iv_sign      = 'I'
 iv_option    = 'EQ'
 iv_low       = lv_partner.

 *|-- Result
 lr_result = lr_query_service->get_query_result( ).

 CATCH cx_crm_genil_general_error.
 RETURN.
 ENDTRY.

 *|-- Sorting
 IF lr_result IS BOUND.
 lr_result->sort(
 EXPORTING
 iv_attr_name  = 'EXP_REVENUE'
 iv_sort_order = if_bol_bo_col=>sort_descending ).
 ENDIF.

 *|-- Set Result to Context & build table
 me->typed_context->btqropp->collection_wrapper->set_collection( lr_result ).
 me->typed_context->btqropp->build_table( ).

 *|-- Clear text
 CLEAR me->gr_config_table->emptytabletext.

 *|-- Create HTML
 CALL METHOD create_table_view_html
 EXPORTING
 ir_server         = ir_server
 ir_controller     = ir_controller
 iv_binding_string = '//BTQROPP/TABLE'
 iv_table_id       = me->get_id( 'Table' )
 IMPORTING
 ev_html           = lv_html.

 *|-- Set Response
 ir_server->response->set_cdata( lv_html ).
 ir_server->response->set_header_field( name  = 'content-type'
 value = 'text/xml' ).

 *|-- Invalidate ´content
 CALL METHOD cl_ajax_utility=>invalidate_area_content
 EXPORTING
 ir_controller = ir_controller.








3.1.5.2 Method: CREATE_TABLE_VIEW_HTML


*|-- Constants
 DATA: lc_separator TYPE string VALUE '__'.
 *|-- Variables
 DATA: lv_attribute_path     TYPE string,
 lv_model_name         TYPE string,
 lv_lines              TYPE i,
 lv_string_lines       TYPE string,
 lv_count              TYPE i VALUE 0,
 lv_row_id             TYPE string,
 lv_html               TYPE string,
 lv_template_row_tr_id TYPE string,
 lv_new_row_tr_id      TYPE string,
 lv_rows               TYPE string,
 lv_row_ids            TYPE string,
 lv_fixed_left_rows    TYPE string,
 lv_fixed_right_rows   TYPE string,
 lv_marked_rows        TYPE string.
 *|-- Strucures
 DATA: ls_area_content TYPE crms_tajax_area_content.
 *|-- References
 DATA: lo_page             TYPE REF TO cl_bsp_ctrl_adapter,
 lo_view_manager     TYPE REF TO cl_bsp_wd_view_manager,
 lo_view_controller  TYPE REF TO cl_bsp_wd_view_controller,
 lo_model            TYPE REF TO if_bsp_model_binding,
 lo_context_node     TYPE REF TO cl_bsp_wd_context_node,
 lo_context_node_tv  TYPE REF TO cl_bsp_wd_context_node_tv.
 *|-- Field Symbols
 FIELD-SYMBOLS: <fs_page>  TYPE bsprtip.

 *|-- Create page instance
 READ TABLE cl_bsp_context=>c_page_instances
 WITH KEY page_name = cl_bsp_wd_appl_controller=>appl_controller_name
 ASSIGNING <fs_page>.

 *|-- Rendering
 IF sy-subrc IS INITIAL AND <fs_page>-instance IS BOUND.
 lo_page            ?= <fs_page>-instance.
 lo_view_manager    ?= lo_page->m_adaptee.
 lo_view_controller ?= ir_controller.
 lo_view_manager->render( iv_root_view = lo_view_controller ).
 ENDIF.

 *|-- Get model
 CALL METHOD cl_bsp_model=>if_bsp_model_util~split_binding_expression
 EXPORTING
 binding_expression = iv_binding_string
 IMPORTING
 attribute_path     = lv_attribute_path
 model_name         = lv_model_name.
 TRY.
 lo_model ?= ir_controller->get_model( lv_model_name ).
 lo_context_node ?= lo_model.
 lo_context_node_tv ?= lo_model.
 lv_lines = lo_context_node->collection_wrapper->size( ).
 CATCH: cx_root.
 EXIT.
 ENDTRY.

 WHILE lv_count < lv_lines.
 "Create AJAX content
 lv_count = lv_count + 1.
 lv_string_lines = lv_count.
 CONDENSE lv_string_lines NO-GAPS.
 CONCATENATE iv_table_id '__' lv_string_lines '__1' INTO lv_row_id.
 CALL METHOD lo_view_controller->retrieve_ajax_area_content
 EXPORTING
 iv_area_id         = lv_row_id
 iv_page_id         = ir_controller->component_id
 IMPORTING
 es_content_info    = ls_area_content
 er_used_controller = lo_view_controller.
 "Covert HTML
 IF ls_area_content-area_content IS NOT INITIAL.
 lv_html = cl_thtmlb_util=>escape_xss_javascript( ls_area_content-area_content ).
 ENDIF.
 CLEAR ls_area_content.
 "Build table
 lo_context_node_tv->build_table( ).
 "Create Response
 IF lv_rows IS INITIAL.
 CONCATENATE `'` lv_html `'` INTO lv_rows.
 CONCATENATE `'` '' `'` INTO lv_fixed_left_rows.
 CONCATENATE `'` '' `'` INTO lv_fixed_right_rows.
 CONCATENATE `'` lv_row_id `'` INTO lv_row_ids.
 CONCATENATE `'` '' `'` INTO lv_marked_rows.
 ELSE.
 CONCATENATE lv_rows `,'` lv_html `'` INTO lv_rows.
 CONCATENATE lv_fixed_left_rows `,'` '' `'` INTO lv_fixed_left_rows.
 CONCATENATE lv_fixed_right_rows `,'` '' `'` INTO lv_fixed_right_rows.
 CONCATENATE lv_row_ids `,'` lv_row_id `'` INTO lv_row_ids.
 CONCATENATE lv_marked_rows `,'` '' `'` INTO lv_marked_rows.
 ENDIF.
 ENDWHILE.

 CONCATENATE `{ "rows": [ ` lv_rows ` ],  "fixedLeftRows": [ ` lv_fixed_left_rows ` ], "fixedRightRows": [ ` lv_fixed_right_rows
 ` ], "markedRows": [ ` lv_marked_rows ` ],  "tableId": [ '` iv_table_id `' ], "rowIds": [ ` lv_row_ids ` ]}` INTO ev_html.








3.2 Client Side

The response that is created on the server side now must be handled on the client side for direct rendering without the need of a round trip.

Therefore the JavaScript scripts_.oo_tableview.js of BSP application THTMLB_SCRIPTS must be modified.

3.2.1 Modification in JavaScript

Basically the logic of the new Javascript function is the same as the function createFastRowsCallback where the system creates new empty rows in a Table View.

3.2.2 Function: asynchronousRenderingCallback

This function has following changes compared to the fast row creation logic:

  • Change the loading text of the no result line to take cases without any results into consideration
  • Hide the no result line in cases where we get at least one result
3.2.2.1 Change text

var noResultRow = document.getElementById(values.tableId[0]+"_noresult");
 var regexExp = new RegExp ("<%= cl_wd_utilities=>get_otr_text_by_alias( 'CRM_BSP_UI_FRAME_RECOBJ/LOADING' ) %>","g");
 var noResultText = "<%= cl_wd_utilities=>get_otr_text_by_alias( 'BSP_DYN_CONFIG_TAG_LIB/NO_RESULT_FOUND' ) %>";
 noResultRow.firstChild.innerHTML = noResultRow.firstChild.innerHTML.replace(regexExp,noResultText);








3.2.2.2 Hide line

for (var i = 0, length = values.rows.length; i < length; i++) {
 <%-- Hide no result line --%>
 noResultRow.className += ' th-displayNone' ;
 <%-- check if the row already exists in the case where the table has inserted empty rows --%>
 rowFound = true;
 row = document.getElementById(values.rowIds[i]);
 if (values.fixedLeftRows[i] != "")
 rowFixedLeft = document.getElementById(values.rowIds[i]+"_left");
 if (values.fixedRightRows[i] != "")
 rowFixedRight = document.getElementById(values.rowIds[i]+"_right");








4 Summary

This logic provides a good opportunity in scenarios where dedicated requests take a long time and therefore the entire visualization is slow. However, the modification in the JavaScript is highly critical. One mistake leads to heavy errors in the WebUI as this file is used in almost every view. As a result, I can’t recommend this for productive systems. I hope, that this can help the SAP to create an improment note to support this functionality.

Cheers,

Sebastian

To report this post you need to login first.

5 Comments

You must be Logged on to comment or reply to a post.

  1. Dmitrii Sharshatkin

    Hi, nice post!

    Modification of the standard class in p.3.1.1. is not really necessary as you anyway will receive your correct instance in IR_CONTROLLER. You just go away from singleton logic, but still should have an access to your global variables.

    Look at call: “thAVFMgr.fetch(document.getElementById(“<%= controller->COMPONENT_ID%>”),’onRecentItemsRefresh’)” in CRM_BSP_RECOBJ/RecentObject.htm

    That’s how you get your object.

    BR, Dima

    (0) 
    1. Sebastian Seiler Post author

      Hi Dima,

      yes, the modification is not necessary anymore. In an earlier version I tried it by creating a custom BSP object instance. Unfortunately, that didn’t help me for the end solution. 😉

      Best regards,

      Sebastian

      (0) 
  2. Dmitrii Sharshatkin

    Hi, I noticed that CREATE_AJAX_URL is not mentioned here. It could look as below:

      method create_ajax_url.

        data: lr_class_desc    type ref to cl_abap_typedescr.   

        data: lv_class_name    type string.   

       lr_class_desc = cl_abap_classdescr=>describe_by_object_ref( me ).   

       lv_class_name = lr_class_desc->get_relative_name( ).   

      call method cl_crm_web_utility=>create_service_url      

        exporting       

           iv_handler_class_name = lv_class_name

           iv_controller_id               = me->component_id

         receiving

            ev_url                = rv_url.

      endmethod.

    BR, Dima

    (0) 

Leave a Reply