Dynamic Programming in Web Dynpro ABAP – Part III: Aggregations and DDIC-Binding of ViewElements
After using the Button in most of the previous examples, we will now improve the Group presented in Dynamic Programming in Web Dynpro ABAP – Introduction and Part I: Understanding UI Elements – of course only by using dynamic programming. We will add a header description to it and place the Button inside the Group.
The header of a Group is a ViewElement of type Caption. Everything that makes up the header is stored there. The Group only holds a reference this object. This special kind of relationship is called an aggregation. So the Group aggregates a Caption to display a header.
Each aggregation has a name and a cardinality. The aggregation between the Group and the Caption is called Header. Cardinality here is exactly the same as is used with the context. The values can be any of the following ones: 0..1, 1..1, 0..N or 1..N. The left side determines the minimum and the right side the maximum number of ViewElements that can be aggregated. The most common cardinalities are 0..1 and 0..N. The “Header” aggregation of a Group is 0..1 one, which means that a Group can have a header, but does not require one.
ViewElements can not only aggregate other ViewElements, but also other so called marker interfaces. These are normal ABAP-OO interfaces and allow ViewElements, which implement them, to be used for a certain purpose. The two most important marker interfaces are IF_WD_TABLE_CELL_EDITOR and IF_WD_TOOLBAR_ITEM. As their name already tells, any ViewElement implementing them can by used within a Table or Toolbar.
Changing a ?..1 aggregation can be achieved by calling a SET method. It works as similar to setting a normal property. There is also a corresponding GET method. For ?..N aggregations there are ADD, REMOVE and REMOVE_ALL methods. In addition there are two GET methods – one for getting a single and another one for reading all aggregated ViewElements. There are HAS and NUMBER_OF auxiliary methods as well. The HAS method checks if the ViewElement supplied is part of the aggregation in question, while the NUMBER_OF method returns how many ViewElements are part of it. In contrast to ?..1 aggregations, ?..N aggregations each have a singular and plural name. Depending on the purpose of a method the first or latter one is used. Hence, for the Group the methods are called ADD_CHILD, GET_CHILD, GET_CHILDREN or NUMBER_OF_CHILDREN.
Internally, the Web Dynpro runtime keeps a list of all ViewElements of a View sorted by their ID. This increases the performance of insert, delete and read operations. By using this “secondary index”, a ViewElement can not only be found very quickly, but it is also possible to ensure the uniqueness of an ID during a create or insert operation. This internal table is not directly accessible to an application developer. You might remember on method GET_ELEMENT on interface IF_WD_VIEW from Dynamic Programming in Web Dynpro ABAP – Introduction and Part I: Understanding UI Elements. This method simply reads this special internal table in order to quickly return the requested ViewElement.
In a ViewElement, the aggreated ViewElements are not saved in a sorted fashion. The reason is that for most aggregations, the order of the items is important as well, e.g. the order of columns in a Table UIElement. This has the disadvantage of a lower performance. This means that if you have a View with a single container and many Labels and InputFields inside and you want to access one of the InputFields, using the GET_ELEMENT method will be alot faster than calling GET_CHILD on the container.
Every ViewElement knows its parent ViewElement. The public read-only attribute _PARENT points to it. Of course, the RootUIElementContainer (or any other root UIElement in case you exchange the original one) has no parent. The _PARENT attribute is managed internally by the ViewElement, so an application developer does not need to take care about setting it. Nonetheless, it’s quite useful once you have got to navigate up a ViewElement hierarchy at runtime.
So what exactly happens when a ViewElement is being added to an aggregation? In case it originates from another View, it will be deregistered from there (removed from the previously mentioned ID-sorted internal table). It will not be removed from its old aggregation. This is still your task and has to be done in advance, because this operation would be too expensive for the Web Dynpro runtime. The runtime would have to first determine the aggregation of the parent ViewElement in which the ViewElement in question is stored (there can be more than one) and remove it from there as a second step. Since an application developer has much more knowledge about the old aggregation in which the ViewElement resides and is therefore able to directly access it, manually written coding is the better choice here. That’s basically the reason why the Web Dynpro runtime does not handle such an operation internally as well. The last two steps add the ViewElement to the new aggregation and set the parent attribute.
That concludes the theory. Below you will find the source code that adds the Button and the header to the Group.
method wddomodifyview. data: lr_container type ref to cl_wd_uielement_container, lr_group type ref to cl_wd_group, lr_header type ref to cl_wd_caption, lr_button type ref to cl_wd_button, header_text type string, button_text type string. * let's do it only once check first_time = abap_true. * get a pointer to the RootUIElementContainer lr_container ?= view->get_element( 'ROOTUIELEMENTCONTAINER' ). * create the group and add it to the container lr_group = cl_wd_group=>new_group( ). cl_wd_flow_data=>new_flow_data( element = lr_group ). cl_wd_matrix_layout=>new_matrix_layout( container = lr_group ). lr_container->add_child( lr_group ). * get the text of the header header_text = cl_wd_utilities=>get_otr_text_by_alias( 'SOTR_VOCABULARY_BASIC/CLASSIFICATION' ). * create the header and assign it to the group lr_header ?= cl_wd_caption=>new_caption( text = header_text ). lr_group->set_header( lr_header ). * get the text of the button button_text = cl_wd_utilities=>get_otr_text_by_alias( 'SOTR_VOCABULARY_BASIC/CONFIRM' ). * create the button and add it to the group lr_button = cl_wd_button=>new_button( text = button_text ). cl_wd_matrix_head_data=>new_matrix_head_data( element = lr_button ). lr_group->add_child( lr_button ). endmethod.
Removing a ViewElement
Removing a ViewElement from a ?..1 aggregation is not much different to changing it. You already know how to achieve this from what you learned in the previous chapter: You only need pass a NULL pointer to the SET method of the aggregation and the previously aggregated ViewElement will be removed.
data: lr_group type ref to cl_wd_group, null type ref to cl_wd_caption. lr_group->set_header( null ).
For a ?..N aggregation it can be achieved by calling the REMOVE method and either passing in the ID of the ViewElement in question or its index.
data: lr_group type ref to cl_wd_group, lr_group->remove_child( id = MY_BUTTON).
The REMOVE method returns the removed ViewElement. This is quite useful in case you need the ViewElement for some subsequent operation – like inserting it somewhere else. In such a case there is no need to call a GET method in advance, which slightly improves performance. In addition, there is also a REMOVE_ALL method, which clears the whole aggregation in a single step.
data: lr_group type ref to cl_wd_group, lr_group->remove_all_children( ).
Removing a ViewElement from an aggregation will also deregister it from the View. Originally, a lazy deregistration was planned, but technically not possible since ABAP-OO had no support for class destructors with ABAP coding inside. In case we had implemented it that way, the deregistration would only havehappened while deleting a ViewElement or while inserting it into another View. Nonetheless, the current implementation has its advantage. One can always tell if a ViewElement is aggregated by another ViewElement. In this case (exluding the root element) the _PARENT attribute is set. A last question remains though. When will the allocated memory be released? This will happen whenever there is no reference to the deleted ViewElement or one of its children in your coding. Since the Web Dynpro runtime ensures that it holds no additional pointers to such a ViewElement, the ABAP runtime environment can detect the special state of the object and release the previously allocated memory.
Moving ViewElements Within a View
In the two previous sections we looked at inserting and removing a ViewElement to and from an aggregation. By utilizing this newly aquired knowledge we can combine these two operations to form a new one: moving a ViewElement from one aggregation to another. On a general perspective Web Dynpro offers great support to application developers here, since most of the detail operations, like switching the content of the _PARENT attribute and (de)registering the ViewElement in the corresponding Views is handled internally.
In our next example we will move the Button from one Group to another. Admittingly, the example is too simple to be useful for a real project, but it demonstrate the underlaying principles quite nicely.
The example is implemented like follows: Whenever the Button was clicked, the associated action handler changes an attribute in the ViewController, which marks the direction of the movement. Later on in method wdDoModifyView, the flag is evaluated and the corresponding movement operation is executed. This leads to the following source code:
m_move_direction type i. method wddoinit. wd_this->m_move_direction = 1. endmethod. method wddomodifyview. data: lr_container type ref to cl_wd_uielement_container, lr_group type ref to cl_wd_group, lr_header type ref to cl_wd_caption, lr_button type ref to cl_wd_button, header_text type string, button_text type string. * create the ui during the first roundtrip if first_time = abap_true. * get a pointer to the RootUIElementContainer lr_container ?= view->get_element( 'ROOTUIELEMENTCONTAINER' ). * create the group and add it to the container lr_group = cl_wd_group=>new_group( id = 'UPPER_GROUP' width = '100%' design = cl_wd_group=>e_design-sapcolor ). cl_wd_flow_data=>new_flow_data( element = lr_group ). cl_wd_matrix_layout=>new_matrix_layout( container = lr_group ). lr_container->add_child( lr_group ). * get the text of the button button_text = cl_wd_utilities=>get_otr_text_by_alias( 'SOTR_VOCABULARY_BASIC/CONFIRM' ). * create the button and add it to the group lr_button = cl_wd_button=>new_button( text = button_text on_action = 'ON_ACTION' ). cl_wd_matrix_head_data=>new_matrix_head_data( element = lr_button ). lr_group->add_child( lr_button ). * create the second group and add it to the container lr_group = cl_wd_group=>new_group( id = 'LOWER_GROUP' width = '100%' design = cl_wd_group=>e_design-sapcolor ). cl_wd_flow_data=>new_flow_data( element = lr_group ). cl_wd_matrix_layout=>new_matrix_layout( container = lr_group ). lr_container->add_child( lr_group ). else. * move the button to the other group case wd_this->m_move_direction. when 1. lr_group ?= view->get_element( 'LOWER_GROUP' ). lr_button ?= lr_group->remove_child( index = 1 ). lr_group ?= view->get_element( 'UPPER_GROUP' ). lr_group->add_child( lr_button ). when -1. lr_group ?= view->get_element( 'UPPER_GROUP' ). lr_button ?= lr_group->remove_child( index = 1 ). lr_group ?= view->get_element( 'LOWER_GROUP' ). lr_group->add_child( lr_button ). endcase. endif. endmethod. method onactionon_action. wd_this->m_move_direction = wd_this->m_move_direction * -1. endmethod.
Note the differences to the previous examples. Here we assigned an ID to the two Groups and to the Button for the first time. This is necessary to be able to access those objects later on for performing the move operation
In our next example we will be doing something more meaningful. We will sort the columns of a table according to the choice of a user. There is a difference though compared to all the previous examples: The whole content of the View has been created at design time. You might ask why. First off, while doing some real programming, it is highly advisable to use the workbench as much as possible. Whenever you do dynamic programming you will lose workbench support. This is generally a bad idea. There are so many useful tools with enhanced features like “where used lists” that it would be a waste of resources not to utilize them and to do everything dynamically at runtime. The second reason is quickly explained and related to first one: It’s a great convenience. The reason why we use dynamic programming is not to do the same things we could have achieved by using the workbench as well in the first place. We use it to perform operations that cannot be declared at design time. Changing an aggregation is such an operation. You can’t bind it to the context. Would be an interesting concept though.
In order to keep the example simple we will utilize the flight data model that is shipped with every SAP system. We will display the carriers, which are stored in database table SCARR, in a table. For ease of coding, each column of the table has the same name as the field in the database table. So we can do the whole move operation generically.
Above the table the columns are displayed as a simple list with a “up” and “down” next to each entry. Clicking such a button will move the column by one step. The list itself is implemented by using MultiPane UIElement.
The context contains a node SCARR, which holds the carriers, and COLUMNS, which holds the columns that are displayed in the MultiPane. There are corresponding supply functions S_SCARR and S_COLUMNS. The coding looks as follows:
m_table type ref to cl_wd_table. method onactionon_move_down. data: lr_column type ref to cl_wd_table_column, index type i, new_index type i, lr_node type ref to if_wd_context_node, lr_element type ref to if_wd_context_element, column type if_main=>element_columns. * get the index of the column index = context_element->get_index( ). * don't move downwards if this is already the last column check index < 3. * move the corresponding column lr_column = wd_this->m_table->remove_column( index = index ). new_index = index + 1. wd_this->m_table->add_column( the_column = lr_column index = new_index ). * move the column in the columns node as well context_element->get_static_attributes( importing static_attributes = column ). lr_node = context_element->get_node( ). lr_node->remove_element( context_element ). lr_element = lr_node->create_element( ). lr_element->set_static_attributes( column ). lr_node->bind_element( new_item = lr_element set_initial_elements = abap_false index = new_index ). endmethod. method onactionon_move_up. data: lr_column type ref to cl_wd_table_column, index type i, new_index type i, lr_node type ref to if_wd_context_node, lr_element type ref to if_wd_context_element, column type if_main=>element_columns. * get the index of the column index = context_element->get_index( ). * don't move upwards if this is already the first column check index > 1. * move the corresponding column lr_column = wd_this->m_table->remove_column( index = index ). new_index = index - 1. wd_this->m_table->add_column( the_column = lr_column index = new_index ). * move the column in the columns node as well context_element->get_static_attributes( importing static_attributes = column ). lr_node = context_element->get_node( ). lr_node->remove_element( context_element ). lr_element = lr_node->create_element( ). lr_element->set_static_attributes( column ). lr_node->bind_element( new_item = lr_element set_initial_elements = abap_false index = new_index ). endmethod. method s_columns. data: lt_columns type if_main=>elements_columns, column like line of lt_columns. * get the ddic info the scarr table call function 'DDIF_FIELDINFO_GET' exporting tabname = 'SCARR' tables dfies_tab = lt_columns exceptions not_found = 1 internal_error = 2 others = 3. assert sy-subrc = 0. * remove the mandt and carrid fields delete lt_columns where fieldname = 'MANDT' or fieldname = 'CARRID'. * bind all the elements node->bind_table( new_items = lt_columns ). endmethod. method s_scarr. data: lt_scarr type if_main=>elements_scarr, scarr like line of lt_scarr. * get the carriers select * from scarr into table lt_scarr. * fill the node node->bind_table( new_items = lt_scarr ). endmethod. method wddomodifyview. * get the table once to speed up processing later on check first_time = abap_true. wd_this->m_table ?= view->get_element( 'TABLE' ). endmethod.
This more complex example shows that dynamic programming is a useful addition to its static variant by declaring everything in the workbench. The presented example could not have been implemented without using dynamic programming, because the means of static programming are exhausted when it comes to declare the sorting order of aggregated ViewElements. In the next chapter, we will recap on what we just did by going one step further and moving ViewElements between two different Views.
Moving Viewelements between Views
A Web Dynpro component developed for productive use usually contains more than one View. Lets say you want to offer the possibility to arrange the elements that make up the user interface quite freely. At end this can also mean that ViewElements have to be moved between different Views. In our example we will do that between the two tabs of a TabStrip. Each of them displays a different View.
In order to achieve this the component contains three Views – two for the content of the two tabs and one main View that contains the TabStrip. All Views have access to the data displayed – an entry from database table SPFLI, which contains flight connections from the previously presented flight data model. For our purpose, we simply select the first entry and save it into a context node called SPFLI of cardinality 1..1. The node is defined in the ComponentController and similar named ones map to it from the different Views.
The main view contains two Groups at the right side next to the TabStrip. Each Group shows the fields that are currently part the corresponding Tab. As in the previous example, we use a MultiPane to display the field lists. Next to the description there is a button to move the field to the other Tab.
The coding of the main view MAIN looks as follows:
method onactionon_move_to_a. data: lr_other_node type ref to if_wd_context_node. lr_other_node = wd_context->get_child_node( 'FIELDS_TAB_A' ). move_field( i_from_tab = wd_comp_controller->m_view_b i_to_tab = wd_comp_controller->m_view_a i_context_element = context_element i_other_node = lr_other_node ). endmethod. method onactionon_move_to_b. data: lr_other_node type ref to if_wd_context_node. lr_other_node = wd_context->get_child_node( 'FIELDS_TAB_B' ). move_field( i_from_tab = wd_comp_controller->m_view_a i_to_tab = wd_comp_controller->m_view_b i_context_element = context_element i_other_node = lr_other_node ). endmethod. method s_fields_tab_a. data: lt_fields_tab_a type if_main=>elements_fields_tab_a, fields_tab_a like line of lt_fields_tab_a. call function 'DDIF_FIELDINFO_GET' exporting tabname = 'SPFLI' tables dfies_tab = lt_fields_tab_a exceptions not_found = 1 internal_error = 2 others = 3. assert sy-subrc = 0. delete lt_fields_tab_a where fieldname = 'MANDT'. node->bind_table( new_items = lt_fields_tab_a ). endmethod. method move_field. data: lr_label type ref to cl_wd_label, lr_element type ref to cl_wd_uielement, label_id type string, element_id type string, lr_tab_a type ref to cl_wd_transparent_container, lr_tab_b type ref to cl_wd_transparent_container, dfies type dfies, lr_new_element type ref to if_wd_context_element, lr_source_node type ref to if_wd_context_node. lr_tab_a ?= i_from_tab->get_element( 'ROOTUIELEMENTCONTAINER' ). lr_tab_b ?= i_to_tab->get_element( 'ROOTUIELEMENTCONTAINER' ). i_context_element->get_attribute( exporting name = 'FIELDNAME' importing value = element_id ). concatenate element_id '_LABEL' into label_id. lr_label ?= lr_tab_a->remove_child( id = label_id ). lr_element ?= lr_tab_a->remove_child( id = element_id ). lr_tab_b->add_child( lr_label ). lr_tab_b->add_child( lr_element ). i_context_element->get_static_attributes( importing static_attributes = dfies ). lr_source_node = i_context_element->get_node( ). lr_source_node->remove_element( i_context_element ). lr_new_element = i_other_node->create_element( ). lr_new_element->set_static_attributes( dfies ). i_other_node->bind_element( new_item = lr_new_element set_initial_elements = abap_false ). endmethod.
View TAB_A contains the content of the first Tab. There we only implement method wdDoModifyView. We use it to put the pointer to this view in a corresponding attribute in the ComponentController to access it later from the main view.
method wddomodifyview . * save the pointer to the view check first_time = abap_true. wd_comp_controller->m_view_a = view. endmethod.
View TAB_B contains the second Tab. Similar to TAB_A, we only implement method wdDoModifyView to save the pointer to the View in a corresponding attribute in the ComponentController.
method wddomodifyview. * save the pointer to the view check first_time = abap_true. wd_comp_controller->m_view_b = view. endmethod.
The ComponentController implements the previously mentioned two attributes and a supply function to select an entry from database table SPFLI. The latter is not really required, but the user interface looks more realistic with some real data.
m_view_a type ref to if_wd_view. m_view_b type ref to if_wd_view. method s_spfli. data: splfi type if_componentcontroller=>element_splfi. select single * from spfli into splfi. node->bind_structure( new_item = splfi ). endmethod.
For each of the two movement directions we defined a separate action. The action handler in turn calls a shared method that performs the move operation generically. This is good place to see generic pointers in action. The form does not only contain input fields, but also a DropDownByKey for displaying the fixed values of the connection type. By using base class UIElement the content of the form can be moved in a generic way.
The application can be improved. So it makes not much sense to save the pointers to the Views and to call GET_ELEMENT repeatingly to access the RootUIElementContainers. Instead it would be smarter to save the pointers to the TransparentContainers directly. The reason I did it this way is that I wanted to show you how you can save the pointer to a View as this is something this is asked for alot. The next thing that should be done is reusing the built-in configuration that is available within each component. We would then place the node at a ConfigurationController and use all the nice services and tools.
Binding Attributes to the DDIC
There is on topic left when it comes to handle ViewElements dynamically – DDIC binding. While looking at it from outside there isn’t much difference to binding an attribute of a ViewElement to a context attribute – the target is just different. Now we bind it to a type feature of data element in the data dictionary. The following type features can be used:
- short text
- medium text
- long text
- text header
- matching text
- visual length
“Visual length” always returns an integer number, the other ones return the specified text. DDIC binding can be combined with context binding. This means that you can bind an attribute of a ViewElement to a context attribute, but specify that instead of using the value of this context attribute, the value of a property of the underlying type should be used.
But it does not stop here. DDIC binding has some built-in smartness. In case you do not specify a data element or a context binding, the value is determined from the context binding of the primary property of the current ViewElement. In case there is none and the ViewElement refers to another ViewElement, the context binding of the primary attribute of that other ViewElement is used. We use those two cases in our next example.
The following example is quite simple. There is a Label with an associated InputField. Next to the InputField there is a DropDownByKey, which displays all possible DDIC features. Whenever the user selects a different DDIC feature of this DropDownByKey, we change the DDIC binding dynamically.
Changing a DDIC binding is quite easy. Each ViewElement has a single method for this purpose. It is called SET__DDIC_BINDING. Please note the two underscores. There is also a corresponding GET method. The SET method has four parameters, but most of them are optional. We only have to specify the name of the attribute for which we would like to a change the DDIC binding and the requested DDIC feature. Additionally, we can specify the name of a data element or if an existing context binding should be used. We won’t use those two parameters in our example, because the InputField already binds to the context via its primary attribute “value” and the Label refers to the InputField. So everything has already been specified implicitly
The source code of the example looks as follows:
m_input_field type ref to cl_wd_input_field. m_label type ref to cl_wd_label. method wddomodifyview. data: lr_container type ref to cl_wd_transparent_container, lr_dropdown type ref to cl_wd_dropdown_by_key. * let's do it only once check first_time = abap_true. * get a pointer to the RootUIElementContainer lr_container ?= view->get_element( 'ROOTUIELEMENTCONTAINER' ). * lets create a label and an input field and add both to the container wd_this->m_label = cl_wd_label=>new_label( label_for = 'MY_INPUT_FIELD' ). wd_this->m_input_field = cl_wd_input_field=>new_input_field( id = 'MY_INPUT_FIELD' bind_value = 'VALUE' ). cl_wd_flow_data=>new_flow_data( element = wd_this->m_label ). cl_wd_flow_data=>new_flow_data( element = wd_this->m_input_field ). lr_container->add_child( wd_this->m_label ). lr_container->add_child( wd_this->m_input_field ). * add a dropdown that switches through the DDIC features lr_dropdown = cl_wd_dropdown_by_key=>new_dropdown_by_key( bind_selected_key = 'SELECTED_DDIC_FEATURE' on_select = 'SWITCH_DDIC_FEATURE' ). cl_wd_flow_data=>new_flow_data( element = lr_dropdown ). lr_container->add_child( lr_dropdown ). endmethod. method onactionswitch_ddic_feature. data: selected_ddic_feature type wdy_md_abap_type_feature_enum. * act depending on the selected ddic feature wd_context->get_attribute( exporting name = 'SELECTED_DDIC_FEATURE' importing value = selected_ddic_feature ). if selected_ddic_feature = cl_wdr_view_element=>co_short_text or selected_ddic_feature = cl_wdr_view_element=>co_medium_text or selected_ddic_feature = cl_wdr_view_element=>co_long_text or selected_ddic_feature = cl_wdr_view_element=>co_title_text or selected_ddic_feature = cl_wdr_view_element=>co_auto_text. * lets change the ddic binding of text of the label * we bind it directly to a text property of a data element wd_this->m_label->set__ddic_binding( property = 'TEXT' ddic_field = selected_ddic_feature ). else. * only visual length left - adjust the input field wd_this->m_input_field->set__ddic_binding( property = 'LENGTH' ddic_field = selected_ddic_feature ). endif. endmethod.
Some notes to the coding. In method wdDoModify the Label is created before the InputField, although the Label refers to it. This is not a problem. Such dependencies are checked during rendering time. So feel free to create ViewElements in any order you like. There are two additional context attrbutes. One is called VALUE and is of type “/GC1/DTE_CSN_COMPONENT”. I used this data element, because it has a different short, medium, long and title texts. The other context attribute is called SELECTED_DDIC_FEATURE and of type WDY_MD_ABAP_TYPE_FEATURE_ENUM. The domain of the data element contains the fixed values of all the available DDIC features that can be used for DDIC binding.
This concludes the part of this blog series dealing with dynamic programming of ViewElements. I hope it will prove useful to you. In the next part, we will take a look at how we can perform dynamic programming in the area of the context. We won’t change the data (as this is standard programming), but create and delete nodes and attributes, map nodes to each other in different flavors.
Brian Bernard kindly uploaded the zipped transport files to SDN. You can find them here. These files can be imported into your system if you have not imported a transport with the same ID yet. All examples are contained in package ZWDAA_DYN_PROG_VIEW_ELEMENTS. You need to activate the corresponding ICF services before starting the applications.