Parallelization in Web UI
Part2
4. AJAX
Concept
AJAX (short for asynchronous JavaScript and XML) is a set of web development techniques utilizing many web technologies used on the client-side to create asynchronous Web applications. With AJAX, web applications can send data to and retrieve from a server asynchronously (in the background) without interfering with the display and behavior of the existing page. By decoupling the data interchange layer from the presentation layer, AJAX allows for web pages, and by extension web applications, to change content dynamically without the need to reload the entire page. Data can be retrieved using the XMLHttpRequest object.
AJAX is not a technology, but a group of technologies. HTML and CSS can be used in combination to mark up and style information. The DOM is accessed with JavaScript to dynamically display – and allow the user to interact with – the information presented. JavaScript and the XMLHttpRequestobject provide a method for exchanging data asynchronously between browser and server to avoid full page reloads.
For more information, please refer to the Wiki’ and W3Schools’ pages: https://en.wikipedia.org/wiki/Ajax_(programming) and http://www.w3schools.com/ajax/
AJAX in SAP CRM WebUI
As mentioned above some of the AJAX features are implemented in the standard CRM. One of the nice examples, where you can do a reverse engineering, is the Recent Items in CRM WebUI.
![image005.png]()
View: CRM_BSP_RECOBJ/RecentObjects
In this case, the AJAX call is implemented, right on the BSP page: RecentObjects.htm
<!--this code performs a callback to refresh the items list. Only executed the first time when the BOL objects are not loaded yet--> <script> thAVFMgr.fetch(document.getElementById("<%= controller->COMPONENT_ID %>"),'onRecentItemsRefresh'); </script> |
In this example, it is using relatively old asynchronous value fetch (AVF) functionality. However, in newer releases, after SAP Note: 2181937, the call looks like below:
thShortCutMgr.fetch('<%= controller->COMPONENT_ID %>','CL_CRM_BSP__RECENTOBJECT0_IMPL','display'); |
All the mentioned standard JS functions can be found in one of the JS files of THTMLB_SCRIPTS BSP application, e.g. scripts_areaframe.js, scripts_deltahandling_client.js. You can also search for the needed function right in the browser, e.g. in Chrome, activate Developer Tools (F12) à Sources à Menu à Search all Files (Ctrl + Shift + F).
How it works. A certain function (e.g. thAVFMgr.fetch, thShortCutMgr.fetch and many others) builds a proper AJAX URL, containing all necessary parameters. Then the function thtmlbAJAXCall.callBackend(AJAXUrl,CallBackEvent);is executed. The first parameter is our AJAX URL and the second is the call-back function that is registered to receive the result from the AJAX call.
Typical parameters that you need to provide to the back-end are the following: service handler, callback handler, DOM element id, sometimes type of the element.
Service Handler
Service handler, it’s the ICF service that you call on the backend. WebUI is using the standard service: /default_host/sap/webcuif/uif_callback, which has only one handler - CL_CRM_WEB_UTILITY.
Callback Handler
Callback handler, it’s the class that implements a callback method, which will be called in the AJAX call. This class should implement the interface IF_CRM_WEB_CALLBACK. And the mandatory callback method is IF_CRM_WEB_CALLBACK~HANDLE_REQUEST. For the JS object thAVFMgr, its callbackhandler is hardcoded in the JS file and depend on the callback JS function:
case "onRecentItemsRefresh": if(!trigger) { return; } var ajaxURL = newSessionURL.URL + "/webcuif/uif_callback?crm_handler=CL_CRM_BSP__RECENTOBJECT0_IMPL"; ajaxURL += "&crm_controller=" + encodeURIComponent(trigger.id) + thtmlbBuildRequestTicketQuery(); } |
DOM Element ID
When the AJAX callback handler is executed, it sends the HTML response back to the frontend. On the frontend, a corresponding JS function receives the response (reqObject) and processes it. Finally it needs to find a proper DOM element in order to do some actions with it. That’s when you need a DOM Element ID.
var myelement = document.getElementById(elementID); |
See more: http://www.w3schools.com/js/js_htmldom_elements.asp
AJAX Call
When all parameters are known, you can perform an AJAX call. In WebUI a special THTMLB object is used for this.
thtmlbAJAXCall.callBackend(ajaxURL, ajaxCallbackFunction); |
You should be careful with visibility of the elements through different iframes and also in time.
JS Callback Function
You need a callback function to receive the response from your AJAX call, process it and update the corresponding DOM element. Below you can find an extract of a function used for asynchronous value fetch.
fetch: function(trigger, avf_event, csvSourceFields, rowIndex, customRef, csvTargetFields){ … thtmlbAJAXCall.callBackend(ajaxURL, this.update); … } update: function(reqObject) { … var responseText = reqObject.request.responseText; var responseBlocks = responseText.split(";"); for(var i=0, len=responseBlocks.length; i<len; i++) { if(!responseBlocks[i]) { continue; } var blockParts = responseBlocks[i].split(","); if(blockParts.length < 4) { continue; } var tagclassname = blockParts[0]; var elementid = blockParts[1]; var attributeName = blockParts[2]; var payload = thtmlbDecodeBASE64(blockParts[3]); var payloadValue = thtmlb_decode_html(payload); var targetElement = thtmlbGetElement(elementid); … case 'tooltip': if (targetElement.title != payloadValue) { targetElement.title = payloadValue; } … } |
Storing your JS Files
When developing your JS you need to make sure it is visible for your applications. In case you want to place a simple script to your page, you can do it right on the BSP page.
<!—Here you want to store some Javascript --> <script type="text/javascript"> var MyElement = document.getElementById("<%= controller->component_id %>"); function WindowOpenPopup(varTxt) { alert(varTxt); } </script> |
You can also store it as a MIME object within your BSP application and include it on the page.
<script type="text/javascript" src="/sap/bc/bsp/sap/z<bspcmp>/scripts.js"></script> |
For relatively big applications or if you want your JS code to be visible for the whole WebUI framework, you can store the reference to your JS file in eth table WCFC_ADD_JS.
IMG à Customer Relationship Management à UI Framework à UI Framework Definition à Define Path for JS Files
![image006.png]()
Note: Last option is tested only in SAP CRM 7.0 EHP3.
Simple case
Let us practice first on a simple case, lets update a simple object link (<thtmlb:link>). As usual, we need to create a WebUI component, a view containing the link and a viewset.
![image007.png]()
When working with AJAX in WebUI you have to generate the view content via corresponding FACTORY methods.
<%@page language="abap" %> <%@extension name="thtmlb" prefix="thtmlb" %> <%@extension name="chtmlb" prefix="chtmlb" %> <%@extension name="bsp" prefix="bsp" %> <% data: lv_link_bee type ref to cl_thtmlb_link. if controller->gv_thtmlb_element_link is initial. " Create Link call method cl_thtmlb_link=>factory exporting id = 'dynamic_link' reference = 'javascript:void(0);' onclick = 'ZBTN_CLICK' text = 'Loading...' receiving element = lv_link_bee. " Store the link controller->gv_thtmlb_element_link ?= lv_link_bee. " Post AJAX call to update content %> <!--this code performs a callback to refresh the items list. Only executed the first time when the BOL objects are not loaded yet--> <script type="text/javascript"> var MyElement = document.getElementById("<%= controller->COMPONENT_ID %>"); zDMSHAVFMan.fetch(MyElement,'ZDMSHAJAXCall'); </script> <% endif. " Get teh link lv_link_bee ?= controller->gv_thtmlb_element_link. " Set the text if controller->gv_text is not initial. lv_link_bee->text = controller->gv_text. endif. " Render html element lv_link_bee->if_bsp_bee~render( _m_page_context ). %> </br> </br> |
In this case I just copied the standard JS AVF functionality (object thAVFMgr) into my own namespace and did desired modifications. You can also notice down that our DOM element is identified right on the page and the building of the right URL is a task for the copied JavaScript.
![image008.png]()
Here I am using my view controller class as a callback handler. This means that it must implement callback method: IF_CRM_WEB_CALLBACK~HANDLE_REQUEST. Update JS function (this.update) has not been changed.
method if_crm_web_callback~handle_request. data: lv_link_tag type ref to cl_thtmlb_link, lv_tag_class_name type string, lv_tag_parent_id type string, lv_icon_mode type string, lv_html type string, lv_html_xstr type xstring, lv_response type string, lv_attribute_type type string. data: lr_controller type ref to zl_zdmsh_aj_mainframe_impl. wait up to 3 seconds. if ir_controller is bound. lr_controller ?= ir_controller. lr_controller->gv_text = 'Hello from AJAX !!!'. try. lv_link_tag ?= lr_controller->gv_thtmlb_element_link. lv_link_tag->text = lr_controller->gv_text. lv_tag_class_name = 'CL_THTMLB_LINK'. lv_tag_parent_id = lv_link_tag->m_parent->id. lv_attribute_type = 'something'. lv_html = lv_link_tag->if_bsp_bee~render_to_string( page_context = lr_controller->gv_thtmlb_element_link->m_page_context ). lv_tag_parent_id = lv_link_tag->m_parent->id. call function 'SCMS_STRING_TO_XSTRING' exporting text = lv_html importing buffer = lv_html_xstr. lv_html = cl_http_utility=>encode_x_base64( lv_html_xstr ). concatenate lv_tag_class_name ',' lv_tag_parent_id ',' lv_attribute_type ',' lv_html ',' lv_link_tag->id into lv_response. ir_server->response->set_cdata( exporting data = lv_response ). catch cx_root. endtry. endif. endmethod. |
How does it look like:
3 seconds later… ![image010.png]()
HTTPWatch trace:
![image011.png]()
It takes only 300 ms to build a page and 3 seconds later the link is updated.
Table Rendering
In WebUI most of the data is presented in tables, and therefore the option that is more desirable would be to render such kind of BSP elements. This topic however has been already described on SDN already, so see more at: http://scn.sap.com/community/crm/webclient-ui-framework/blog/2015/09/03/asynchronous-rendering-of-table-views . But here we will consider an option without any modification to SAP standard objects.
So, as usual, we need our WEBUI component, view set and the view. But also we need a context node, a value node in our case.
![image012.png]()
Our page TableView.htm looks like below:
<%@page language="abap" %> <%@extension name="thtmlb" prefix="thtmlb" %> <%@extension name="chtmlb" prefix="chtmlb" %> <%@extension name="bsp" prefix="bsp" %> <% * Conversion Cnode SelectionMode to Tag data: lv_cellerator_selectionmode type string, lv_cellerator_editmode type string, lv_cellerator_selectioncolumn type string. cl_thtmlb_util=>translate_selection_mode( exporting iv_selection_mode = SOMEDATA->SELECTION_MODE iv_all_rows_editable = space importing ev_selection_mode = lv_cellerator_selectionmode ev_edit_mode = lv_cellerator_editmode ev_selection_column = lv_cellerator_selectioncolumn ). " " data: lv_table_bee type ref to cl_chtmlb_config_cellerator. " if controller->gv_config_table is initial. " " Create Table call method cl_chtmlb_config_cellerator=>factory exporting id = controller->gc_table_id downloadToExcel = 'TRUE' editMode = 'NONE' onRowSelection = 'select' personalizable = 'TRUE' selectedRowIndex = somedata->selected_index selectedRowIndexTable = somedata->selection_tab selectionColumn = lv_cellerator_selectioncolumn selectionMode = lv_cellerator_selectionmode table = somedata->table _table = '//SOMEDATA/Table' usage = 'EDITLIST' visibleFirstRow = somedata->visible_first_row_index visibleRowCount = '6' width = '100%' xml = controller->configuration_descr->get_config_data( ) receiving element = lv_table_bee. " " Store the link controller->gv_config_table ?= lv_table_bee. %> <!--this code performs a callback to refresh the table --> <script type="text/javascript"> debugger; thtmlbAJAXCall.callBackend("<%= controller->create_ajax_url( ) %>", thtmlbCCelleratorManager.createFastRowsCallback); </script> <% " endif. " " Get the link lv_table_bee ?= controller->gv_config_table. " " Render html element lv_table_bee->if_bsp_bee~render( _m_page_context ). %> </br> |
You can notice that in our case we perform an AJAX call directly (thtmlbAJAXCall.callBackend); the task to build a proper URL is addressed to ABAP method CREATE_AJAX_URL; and we use a standard callback function thtmlbCCelleratorManager.createFastRowsCallback to process the AJAX response.
How to build a proper URL for AJAX call? If you are going to use the standard AJAX service handler, there are many things that you need to consider: a handler class, a controller id, security session token, etc. But all this is considered in the method CL_CRM_WEB_UTILITY=> CREATE_SERVICE_URL. So finally our method looks very simple:
method create_ajax_url. data: lr_class_desc type ref to cl_abap_typedescr. lr_class_desc = cl_abap_classdescr=>describe_by_object_ref( me ). call method cl_crm_web_utility=>create_service_url exporting iv_handler_class_name = lr_class_desc->get_relative_name( ) iv_controller_id = me->component_id receiving ev_url = rv_url. endmethod. |
In this case, we again use view controller class as a callback handler.
Next what we need is to implement IF_CRM_WEB_CALLBACK~HANDLE_REQUEST method. We do it in a very similar way as described here: http://scn.sap.com/community/crm/webclient-ui-framework/blog/2015/09/03/asynchronous-rendering-of-table-views. However, in our example, we are getting the data we need via parameter IR_CONTROLLER and therefore we do not need to change the standard SAP coding.
method if_crm_web_callback~handle_request. data: lr_controller type ref to zl_zdmsh_aj_tableview_impl. data: lr_somedata type ref to zl_zdmsh_aj_tableview_cn00. data: lr_wrapper type ref to cl_bsp_wd_collection_wrapper. data: lv_html type string. wait up to 3 seconds. if ir_controller is bound. try. lr_controller ?= ir_controller. lr_somedata ?= lr_controller->typed_context->somedata. lr_wrapper ?= lr_somedata->collection_wrapper. if lr_controller is bound and lr_somedata is bound and lr_wrapper is bound. lr_controller->fill_context_node( ). call method create_table_view_html_old exporting ir_server = ir_server ir_controller = ir_controller iv_binding_string = '//SOMEDATA/TABLE' iv_table_id = ir_controller->get_id( gc_table_id ) 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. endif. catch cx_root. endtry. endif. endmethod. |
Method CREATE_TABLE_VIEW_HTML is implemented in exactly the same way as it was described on SDN already. I just repeat it here for consistency.
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. endmethod. |
In the method FILL_CONTEXT_NODE we just populate our context node.
method fill_context_node. data: lt_but000 type table of but000. data: ls_but000 type but000. data: lr_wrapper type ref to cl_bsp_wd_collection_wrapper. data: lr_valuenode type ref to cl_bsp_wd_value_node. data: lr_table_line type ref to but000. lr_wrapper = typed_context->somedata->get_collection_wrapper( ). if lr_wrapper is bound. lr_wrapper->clear( ). select * from but000 into table lt_but000 up to 2 rows where type = 1 and bu_sort1 = 'DMITRY'. loop at lt_but000 into ls_but000. create data lr_table_line. create object lr_valuenode exporting iv_data_ref = lr_table_line. lr_valuenode->set_properties( ls_but000 ). lr_wrapper->add( lr_valuenode ). endloop. endif. typed_context->somedata->build_table( ). endmethod. |
How does it look like:
![image014.png]()
3 seconds after…
![image015.png]()
Note that all your GET-methods are taken into consideration and you can navigate normally as you would do it in the normal WebUI table.
HTTPWatch trace:
![image016.png]()
It takes only 300 ms to build a page and 3 seconds later the table is updated.