Preface
I'll start with a fact that the whole last month I've been heavily participating in some strange and interesting Web Dynpro application development. What was the interesting in that development?
The first thing is the Java application was completely based on ABAP backend. The application loads all the necessary data from the backend, user works on the data, modifies it and sends it back to R3.
Nothing unusual till the moment. Right?... But, there was the serious restriction that I was not allowed to change anything in the backend logic, I could not request change in the BAPI interface or implementation. It was just a black box for me.
Horizontal Table, but Vertical Data
One of the requirements was to design a horizontal table. I mean the table which displays a data in row manner. The peculiar feature of such table is a horizontal column expansion with the column scroll bar:

Horizontal table with scrollable columns
I began working on the table with enthusiasm, but almost immediatelly realized that I did not have such BAPI which would return me the data exactly as I'd like to display... No, Wrong... I had a BAPI, but its output format was in usual table form, not as I required. So, since I could not obtain another BAPI I had to convert the BAPI output (see below) into a horizontal representation and display it on UI:
| 1/30/2004 |
1,210 |
93,243 |
| 1/30/2005 |
1,789 |
68,041 |
| 1/30/2006 |
1,437 |
18,815 |
| 1/30/2007 |
1,861 |
48,302 |
| 1/30/2008 |
1,123 |
28,766 |
Existing Solutions
After a while searching throught SDN forums I found the following. Here http://forumsa.sdn.sap.com/thread.jspa?messageID=5268774 mentioned two solutions:
- Purely read-only table can be achieved by programatical creating a table layout. Actually you can create not exactly Web Dynpro table, but just a set of cells (TextView controls) placed inside Grid or Matrix container. Umm... Not my case: I needed editable table.
- Rearranging the vertical context node into a horizontal one and binding it to the table. Good idea, but here we have to solve additionally two main problems: Data synchronization between the Vertical and Horizontal nodes and support dynamically the number of columns, because the number is not known on design time.
So I decided to enhance the second solution and overcome the mentioned issues.
My Approach
Generic Idea
I used standard Table control partly designed in studio and partly generated at runtime. The main idea is to bind the table columns to set of calculated attributes in order to represent the table data in 'horizontal' form.
The calculated attributes are also generated at runtime. Getter/setters for the attributes are impemented as calculated attribute accessors. For the purpose WebDynpro provides special interface IWDCalculatedAttributeAccessor.
Two context nodes: Original and Inverted
In the context below AnnualIndicators(0..N) contains the original data in 'vertical' form. This node could be in any form (value or model) and is completely determined by the data source (BAPI output, like in my case, or Web Service output, etc.). Node HorizontalTableData(0..N) is designed specially for the table representation and bound to the table control. The node contains 'the same data' as in AnnualIndicators, but inverted in the way where columns of AnnualIndicators becoming rows in HorizontalTableData:

Context structure for table data source
Supply function for node HorizontalTableData is called just one time independently from AnnualIndicators. N of elements is less or equals to N of columns in AnnualIndicators:
public void supplyTableData(IPrivateHorizontalTableView.IHorizontalTableDataNode node, IPrivateHorizontalTableView.ITableDataElement parentElement)
{
//@@begin supplyTableData(IWDNode,IWDNodeElement)
final String[] VERTICAL_ATTRS = { "salary", "profit" };
final String[] VERTICAL_ATTR_LABELS = { "Salary", "Profit" };
for (int i = 0; i < VERTICAL_ATTRS.length; i++) {
final IHorizontalTableDataElement indicatorEl = node.createHorizontalTableDataElement();
indicatorEl.setIndicator(VERTICAL_ATTR_LABELS[i]);
node.addElement(indicatorEl);
}
//@@end
}
Calculated Attribute Accessors
Calculated attribute accessors allows node HorizontalTableData to have an access to the data stored physically in node AnnualIndicators. In other words, elements of HorizontalTableData do not keep any data in attributes, almost all the attributes are calculated and read/write data from/to AnnualIndicators. In fact the accessors implement absolutely transparent data mapping between AnnualIndicators and HorizontalTableData.
Please, see the source code of the acccessors at the end of the blog.
The Java class is universal, not applied to any concrete tables or context nodes. It can be completely reused in other UI components.
Table Design
The table on design time contains just one read-only column Indicators. The first column has fixed position on the left and plays role of a header like in a standard table:

Initial table design
- Table property 'dataSource' is bound to TableData.HorizontalTableData.
- Table property 'scrollableColCount' is 7 in my example. It activates table Column Scroll Bar. If the number of visible scrollable columns is more then 7 user can scroll right.
- Property 'fixedPosition' of the first column is set to 'left'.
- Property 'text' of the TextView is bound to TableData.HorizontalTableData.indicator.
Generation of Table rest
All other table columns are generated dynamically at runtime, but the
generation is performed just one time during the view construction. Each generated column displays data extracted from the corresponding element of node AnnualIndicators.
However, the
number of dynamical columns is pure statical constant and does not depend actually on the number of
'vertical' rows. I mean that we generate some hardcoded maximal static number
of columns the table is ever able to display. At runtime we will just hide unnecessary columns by manipulating Visibility property. If there is no such 'vertical' row in AnnualIndicators the corresponding column will be invisible:

Table behaviour at runtime
Such solution allows to make table construction independent from table data. This means that we can invalidate and reload node AnnualIndicators (invoke BAPI, WebService, etc.) as many times as we want and we do not need to regenerate the table again and again.
public static void wdDoModifyView(IPrivateHorizontalTableView wdThis, IPrivateHorizontalTableView.IContextNode wdContext, com.sap.tc.webdynpro.progmodel.api.IWDView view, boolean firstTime)
{
//@@begin wdDoModifyView
if (firstTime) {
final int MAX_DISPLAYED_COLUMNS = 10;
final IWDTable table = (IWDTable) view.getElement("indicatorsTable");
for (int iDate = 0; iDate < MAX_DISPLAYED_COLUMNS; ++iDate) {
wdThis.generateTableColumn(iDate, table);
}
}
//@@end
}
Generation of Table Column
When we create dynamical column we also create all necessary context attributes required for the column. The context attributes are pure calculated, so we also setup each such attribute with instance of Calculated Attribute Accessor.
Below are explained the attributes we creating for each column with index N:
- colVisibility<N>
- Type: 'ddic:com.sap.ide.webdynpro.uielementdefinitions.Visibility'
- Accessor: HorizontalColumnVisibilityAttrAccessor
- Location: Parent node of HorizontalTableData
- Description: Bound to column's 'visible' property. The accessor returns VISIBLE if N < number of AnnualIndicators elements.
- date<N>
- Type: 'ddic:com.sap.dictionary.date'
- Accessor: HorizontalHeaderAttrAccessor
- Location: Parent node of HorizontalTableData
- Description: Bound to column's header. The accessor gets the value of 'date' attribute of AnnualIndicators element N.
-
cell<N>
- Type: 'ddic:com.sap.dictionary.integer'
- Accessor: HorizontalCellAttrAccessor
- Location: HorizontalTableData
- Description: Bound to column's cell editor. The accessor gets/sets the value of 'salary' attribute of AnnualIndicators element N for 0th HorizontalTableData element. For 1th HorizontalTableData element the accessor gets/sets the value of 'profit' attribute of AnnualIndicators element N.
As you can see the overall number of dynamical attributes is 3*MAX_DISPLAYED_COLUMNS. For the example this is 3*10=30 attributes.
public void generateTableColumn(int iCol, IWDTable table) {
final IWDNode verticalTableData = wdContext.nodeAnnualIndicators();
final IWDNodeInfo tableParentNodeInfo = wdContext.nodeTableData().getNodeInfo();
final IWDNodeInfo tableNodeInfo = tableParentNodeInfo.getChild("HorizontalTableData");
final IWDView view = table.getView();
// Column
final IWDTableColumn col = (IWDTableColumn) view.createElement(IWDTableColumn.class, null);
final IWDAttributeInfo colVisibilityAttr =
tableParentNodeInfo.addAttribute(
"colVisibility" + iCol,
"ddic:com.sap.ide.webdynpro.uielementdefinitions.Visibility");
colVisibilityAttr.setCalculatedAttributeAccessor(
new HorizontalColumnVisibilityAttrAccessor(iCol, verticalTableData));
col.bindVisible(colVisibilityAttr);
table.addGroupedColumn(col);
// Column Header (Date)
final IWDCaption colHeader = (IWDCaption) view.createElement(IWDCaption.class, null);
final IWDAttributeInfo dateAttr =
tableParentNodeInfo.addAttribute("date" + iCol, "ddic:com.sap.dictionary.date");
dateAttr.setCalculatedAttributeAccessor(
new HorizontalHeaderAttrAccessor(iCol, verticalTableData, IAnnualIndicatorsElement.DATE));
colHeader.bindText(dateAttr);
col.setHeader(colHeader);
// Column Cell Editor (Input Field)
final IWDAttributeInfo cellAttr =
tableNodeInfo.addAttribute("cell" + iCol, "ddic:com.sap.dictionary.integer");
cellAttr.setCalculatedAttributeAccessor(
new HorizontalCellAttrAccessor(iCol, verticalTableData, VERTICAL_ATTRS));
final IWDInputField cellEditor =
(IWDInputField) view.createElement(IWDInputField.class, null);
cellEditor.bindValue(cellAttr);
col.setTableCellEditor(cellEditor);
} Conclusions
So finally we build the table with the following features:
- Horizontal table with scrollable columns (which number is not known on design time) and fixed predefined number of rows.
- The unnecessary generated table columns behind the horizontal data range will be hidden (make invisible).
- The original table data (based on BAPI table) is inverted to 'horizontal' representation.
- The mapping between the original data node and the node-'horizontal' representation is implemented with help of dinamical calculated attributes initialized with calculated attribute accessors.
- The mapping between two context nodes works transparently and this solves data synchronization problem.
- The table is completely editable.
- The table is bound to the
context. This means that its design does not depend on table data.
Table data source can be refreshed at runtime many times without table
reconstruction.
- The implemented calculated attribute accessors made generic, reusable, located in separate Java class and can be applied to any 'horizontal' table.
Source Code of Calculated Attribute Accessors
package com.epam.test.ui.table.row;
import com.sap.tc.webdynpro.progmodel.api.IWDAttributeInfo;
import com.sap.tc.webdynpro.progmodel.api.IWDCalculatedAttributeAccessor;
import com.sap.tc.webdynpro.progmodel.api.IWDNode;
import com.sap.tc.webdynpro.progmodel.api.IWDNodeElement;
import com.sap.tc.webdynpro.progmodel.api.WDVisibility;
/**
* Calculated attribute accessors for 'horizontal' tables.
*
* @author Siarhei_Pisarenka
*/
public abstract class HorizontalCalcAttrAccessors {
/** Abstract accessor. Just stores original 'vertical' node as data source and table column index. */
private abstract static class AbstractHorizontalAttrAccessor
implements IWDCalculatedAttributeAccessor {
final int iCol;
final IWDNode verticalNode;
public AbstractHorizontalAttrAccessor(int iCol, IWDNode verticalNode) {
this.iCol = iCol;
this.verticalNode = verticalNode;
}
};
/**
* Abstract horizontal attribute accessor. The accessor shall be setup for an attribute in the 'horizontal'
* node. It allows to get an access to the particular element of the original 'vertical' node and display
* the row data in the corresponding table column. The stored column index of the accessor is interpreted
* at runtime as index of the element if the 'vertical' node.
*
* Sub-classes of the accessor shall provide 'vertical' attribute name.
*/
private abstract static class HorizontalAttrAccessor extends AbstractHorizontalAttrAccessor {
public HorizontalAttrAccessor(int iCol, IWDNode verticalNode) {
super(iCol, verticalNode);
}
public final void set(IWDNodeElement el, IWDAttributeInfo attr, Object value) {
verticalNode.getElementAt(iCol).setAttributeValue(getVerticalAttrName(el), value);
}
public final Object get(IWDNodeElement el, IWDAttributeInfo attr) {
return verticalNode.getElementAt(iCol).getAttributeValue(getVerticalAttrName(el));
}
protected abstract String getVerticalAttrName(IWDNodeElement hEl);
};
/**
* Horizontal attribute accessor for table columns. The accessor additionally keeps 'vertical' node
* attribute name. At runtime the accessor redirects any calls to the 'horizontal' table column attribute
* to the corresponding element of the original 'vertical' node keeping in mind the 'vertical' attribute.
*
* The accessor allows to bind column headers, column visibilities and other attributes.
*/
public static class HorizontalHeaderAttrAccessor extends HorizontalAttrAccessor {
final String verticalAttr;
public HorizontalHeaderAttrAccessor(int iCol, IWDNode verticalNode, String verticalAttr) {
super(iCol, verticalNode);
this.verticalAttr = verticalAttr;
}
protected String getVerticalAttrName(IWDNodeElement hEl) {
return verticalAttr;
}
};
/**
* Horizontal attribute accessor for table cells. The accessor extends HorizontalHeaderAttrAccessor and
* additionally keeps the whole list of 'vertical' node attribute names. Each 'horizontal' row
* corresponds to the attribute name in the list by row index. At runtime the mapping allows to redirect
* each 'horizontal' row coming to the accessor to the corresponding 'vertical' attribute.
*
* The accessor allows to bind cell editors.
*/
public static class HorizontalCellAttrAccessor extends HorizontalAttrAccessor {
final String[] verticalAttrs;
public HorizontalCellAttrAccessor(int iCol, IWDNode verticalNode, String[] verticalAttrs) {
super(iCol, verticalNode);
this.verticalAttrs = verticalAttrs;
}
protected String getVerticalAttrName(IWDNodeElement hEl) {
return verticalAttrs[hEl.index()];
}
};
/**
* Horizontal attribute getter for table column visibilities. Allows to hide unnecessary table columns
* that are behind the original 'vertical' node rows. This means that the column is visible only if such row
* exists in the original 'vertical' node.
*/
public static class HorizontalColumnVisibilityAttrAccessor
extends AbstractHorizontalAttrAccessor {
public HorizontalColumnVisibilityAttrAccessor(int iCol, IWDNode verticalNode) {
super(iCol, verticalNode);
}
public Object get(IWDNodeElement el, IWDAttributeInfo attr) {
return (iCol < verticalNode.size()) ? WDVisibility.VISIBLE : WDVisibility.NONE;
}
public void set(IWDNodeElement arg0, IWDAttributeInfo arg1, Object arg2) {
throw new UnsupportedOperationException();
}
};
}