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.
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.
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