Skip navigation header
Oracle ADF UIX Developer's Guide Go to Table of Contents
Contents
Go to previous page
Previous
Go to next page
Next

9.A. An ADF UIX Table Example

This section describes an example of an Oracle ADF UIX table. The table features implemented include record navigation, sorting, multiple selection, and detail-disclosure. The UIX XML and Java code used in this example is designed such that it can be copied into your development environment and easily modified to quickly produce functional mockups. Before reading this section you should be comfortable with the information presented in the following chapters, as well as with :

This chapter contains the following sections:

The Table Data

The table data for this demo will be a list of books. Each book will have the following properties: title, description, author, pages, ISBN, publisher and date published. Consequently, here is the bean for a book:

public class Book
{
  public Book(String title, String description, String author,
              String isbn, int pages, Date published, String publisher)
  {
    _title = title;
    _desc = description;
    _author = author;
    _isbn = isbn;
    _pages = pages;
    _published = published;
    _publisher = publisher;
  }

  public String getTitle()
  {
    return _title;
  }

  public String getDescription()
  {
    return _desc;
  }

  public String getAuthor()
  {
    return _author;
  }

  public String getISBN()
  {
    return _isbn;
  }

  public int getPages()
  {
    return _pages;
  }

  public Date getPublishedDate()
  {
    return _published;
  }

  public String getPublisher()
  {
    return _publisher;
  }

  private final String _title, _desc, _author, _isbn, _publisher;
  private final int _pages;
  private final Date _published;
}

Ideally there will be a library of such books; a user will connect to the library and perform a search. The resulting books that match the search criteria will be copied into a book list, and the user will view this list using a Table.

In this demo, however, we do not implement searching. Instead we randomly populate a list with books and display them to a user. These random books are created by the class oracle.cabo.doc.demo.table.BookUtils; however, the actual workings of this class are irrelevant.

The List of Books is stored in an oracle.cabo.doc.demo.table.UserData object. This object usually holds user information such as username and preferences; however, for this demo it only holds one property: the book-list. The UserData object is stored on the HttpSession so that there will be book list per user.

Once this List is databound to a <table> element's tableData attribute, each property of a Book can be displayed in a table column using <column> elements. In the following example, the Published Date property is displayed in a column:

<table name="books"
       tableData="${sessionScope.userData.bookList}">
 <contents>
  <column>
   <columnHeader>
    <sortableHeader text="Published Date"/>
   </columnHeader>
   <contents>
    <!-- standard Java introspection calls the method
         getPublishedDate() to resolve the property
         publishedDate -->
    <text text="${uix.current.publishedDate}"/>
   </contents>
  </column>
  <!-- Other columns -->
  ...
 </contents>
</table>

If you are unsure how the publishedDate key is mapped to the bean's getPublishedDate() method, please see the relevant section in the Databinding chapter.

Since multiple <column> elements are needed to display a Books properties it was necessary to create a column template: <demo:sortableColumn>. In addition to the attributes and children inherited from <column>, the template recognizes the attributes header and key. The header text (eg: "Published Date") will be displayed in the column's header, and the key (eg: "publishedDate") will be used to get the bean's property. Here is the UIX XML code for the template:

<templateDefinition targetNamespace="http://xmlns.oracle.com/uix/demo"
                    localName="sortableColumn">
 <type base="data:column">
  <attribute name="key" javaType="string"/>
  <attribute name="header" javaType="string"/>
  <!-- by default this column will be sortable.
          However, if this attribute is set to 'false',
          then this column will not be sortable -->
  <attribute name="sortable" javaType="boolean"/>
 </type>

 <content>
  <column>
   <attributeMap><rootAttributeMap/></attributeMap>

   <columnHeader>
    <sortableHeader text="${uix.rootAttr.header}"
                    value="${uix.rootAttr.key}">
     ...
    </sortableHeader>
   </columnHeader>
   <contents>
    <text text="${uix.current[uix.rootAttr.key]}"/>
   </contents>
  </column>
 </content>
</templateDefinition>

With the new template our table looks like:

<table name="books"
       tableData="${sessionScope.userData.bookList}">
 <contents>
  <demo:sortableColumn key="title" header="Title"/>
  <demo:sortableColumn key="author" header="Author">
   <columnFormat width="20%"/>
  </demo:sortableColumn>
  <demo:sortableColumn key="pages" header="Pages">
   <columnFormat width="1%" columnDataFormat="numberFormat"/>
  </demo:sortableColumn>
 </contents>
</table>

In the above example we display three columns corresponding to the title, author and pages properties of each Book.

TableState

Now that we have displayed our Books in a table, how do we implement Record Navigation, Sorting and Detail-Disclosure? These features are implemented by the class oracle.cabo.doc.demo.table.TableState. An instance of this class is constructed with a List (eg: the Book List), and a blockSize (the maximum number of records to show on a single page). This class has methods for getting the table data a page at a time, toggling the detail-disclosure state for some rows, and sorting the List by some bean property.

Record Navigation

Record Navigation is implemented by databinding the <table> element's blockSize, value and maxValue to the TableState's blockSize, startIndex and lastIndex. In addition, the <table> element's tableData attribute must be bound to the currentRecordSet property of TableState. This property returns the data in pages of blockSize elements each.

Instead of returning the Book List directly, the UserData class is changed to return the corresponding TableState object, by means of a getBookTableState() method. Now the <table> element looks like:

<table tableData="${sessionScope.userData.bookTableState.currentRecordSet}"
       blockSize="${sessionScope.userData.bookTableState.blockSize}"
       maxValue="${sessionScope.userData.bookTableState.lastIndex}"
       value="${sessionScope.userData.bookTableState.startIndex}">
 ...
</table>

The above table will trigger a UIX Servlet goto event when its navigation controls are clicked. The event will be accompanied with a value parameter which will indicate the index of new record set to display. The following code handles this event by setting the new record set index on the TableState:

UserData uData = _getUserData(..); // from HttpSession
TableState tState = uData.getBookTableState();
String valueParam = event.getParameter(UIConstants.VALUE_PARAM);
int value = Integer.parseInt(valueParam);
tState.setStartIndex(value);

Sorting

The sortable headers in the table are implemented using <sortableHeader> elements. The TableState object has a property sortBy, which is the (Book) bean property the table is currently sorted on (eg: author). It also has a sortOrder property that indicates the direction (whether ascending or descending) of the sort. These two properties are combined using some logic to produce the ascending or descending values for each <sortableHeader> element's sortable attribute. Here is the relevant UIX XML (in <demo:sortableColumn>):

<sortableHeader text="${uix.rootAttr.header}"
                value="${uix.rootAttr.key}">
 <boundAttribute name="sortable">
  <if>
   <!-- is this table currently sorted by the bean property displayed by
        this column? -->
   <comparison type="equals">
    <dataObject select="sortBy">
     <contextProperty select="demo:tableState"/>
    </dataObject>
    <dataObject source="${uix.rootAttr.key}""/>
   </comparison>

   <!-- if so, then we need to return either "ascending" or "descending"
        depending on the sortOrder -->
   <if>
    <dataObject select="sortOrder">
     <contextProperty select="demo:tableState"/>
    </dataObject>
    <fixed>ascending</fixed>
    <fixed>descending</fixed>
   </if>

   <!-- Otherwise, we might be sortable (though not sorted in any
        particular order) as long as the user has not set the sortable
        attribute to false -->
   <if>
    <defaulting>
     <dataObject source="${uix.rootAttr.sortable}"/>
     <!-- by default, we are sortable -->
     <fixed text="true"/>
    </defaulting>
    <fixed>yes</fixed>
    <fixed>no</fixed>
   </if>

  </if>
 </boundAttribute>
</sortableHeader>

The sortable column headers produce sort events when clicked. Each event will be accompanied with a value parameter which will indicate the bean property that that column is displaying. This event is handled by creating a Comparator for that bean property and calling the setSortBy method on TableState:

UserData uData = _getUserData(..); // from HttpSession
TableState tState = uData.getBookTableState();
String valueParam = event.getParameter(UIConstants.VALUE_PARAM);
// create a Comparator that will sort our list of Book beans by the
// required property:
BeanComparator sorter = new BeanComparator(Book.class, valueParam);
tState.setSortBy(valueParam, sorter);

Detail-Disclosure

The TableState object has a property called detailDisclosureList that maintains the detail-disclosure state for the table. This property must be databound to the <table> element's detailDisclosure attribute:

<table tableData="${sessionScope.userData.bookTableState.currentRecordSet}"
       detailDisclosure="${sessionScope.userData.tableState.detailDisclosureList}"
       blockSize="${sessionScope.userData.bookTableState.blockSize}"
       maxValue="${sessionScope.userData.bookTableState.lastIndex}"
       value="${sessionScope.userData.bookTableState.startIndex}">
 ...
</table>

When the hide/show links (in the detail column) are clicked, hide and show UIX Servlet events are triggered. This event is also accompanied by a value parameter, which is the index of the row that was clicked. The event is handled by calling the toggleDisclosed method on the TableState:

UserData uData = _getUserData(..); // from HttpSession
TableState tState = uData.getBookTableState();
String eventName = event.getName();
String valueParam = event.getParameter(UIConstants.VALUE_PARAM);
// This handles detail disclosure.  The event must have a "value"
// parameter which is the index of the row to disclose or
// undisclose. This index is based at zero.  The "value" might also be
// VALUE_SHOW_ALL which signals a show-all or hide-all.
if (UIConstants.VALUE_SHOW_ALL.equals(valueParam))
{
  // if the event is "show" then do a show-all. Otherwise, do a hide-all:
  tState.showAllDetails(UIConstants.SHOW_EVENT.equals(eventName));
}
else
{
  int value = Integer.parseInt(valueParam);
  tState.toggleDisclosed(value);
}

Table Event Handler

In this demo all of the above events (for Record Navigation, Sorting, and Detail Disclosure) are handled by the method books_tableEvent in the class oracle.cabo.doc.demo.table.BookEvents. This method identifies which table produced the event (in this demo there is only one table) by examining the source parameter, and gets the corresponding TableState object and performs the above event handling. The event handler is registered in UIX XML by:

<event name="goto sort show hide" source="books">
 <method class="oracle.cabo.doc.demo.table.BookEvents"
         method="books_tableEvent"/>
</event>

Supporting Multiple Tables

It is easy to support multiple tables on the same page. It is necessary to have a separate TableState object for each. In the event handler (for the table events) the appropriate TableState to use can be determined by examining the source parameter.

In the UIX XML, each <table> would have to have its tableData, detailDisclosure, blockSize, maxValue and value attributes databound to the appropriate properties on the corresponding TableState. For example, the "books" <table> got its TableState by calling the getBookTableState() method on the UserData:

<table name="books"
       tableData="${sessionScope.userData.bookTableState.currentRecordSet}"
       detailDisclosure="${sessionScope.userData.tableState.detailDisclosureList}"
       blockSize="${sessionScope.userData.bookTableState.blockSize}"
       maxValue="${sessionScope.userData.bookTableState.lastIndex}"
       value="${sessionScope.userData.bookTableState.startIndex}">
 ...
</table>

It is rather annoying to have to do this databinding for each and every table. A more efficient implementation would use a template: <demo:table>. This template would inherit all the properties and children of the <table> but would also take a tableState attribute:

<templateDefinition targetNamespace="http://xmlns.oracle.com/uix/demo"
                    localName="table">
 <type base="data:table">
  <attribute name="tableState"
             javaType="oracle.cabo.doc.demo.table.TableState"/>
 </type>

 <content>
    <table tableData="${uix.rootAttr.tableState.currentRecordSet}"
           detailDisclosure="${uix.rootAttr.tableState.detailDisclosureList}"
           blockSize="${uix.rootAttr.tableState.blockSize}"
           maxValue="${uix.rootAttr.tableState.lastIndex}"
           value="${uix.rootAttr.tableState.startIndex}">
     <childList><rootChildList/></childList>
     <attributeMap><rootAttributeMap/></attributeMap>
     <childMap><rootChildMap/></childMap>
    </table>
 </content>
</templateDefinition>

Now developers can create a "books" table simply by writing:

<demo:table name="books"
            tableState="${sessionScope.userData.bookTableState}">
 ...
</demo:table>

Note that there is no need to databind tableData, detailDisclosure, blockSize, maxValue and value individually; the template takes care of that. So if you wanted to create an additional table called "wishList" which got its data from a wishListTableState property on the UserData you could create another <demo:table>:

<demo:table name="wishList"
            tableState="${sessionScope.userData.wishListTableState}">
 ...
</demo:table>

Multiple Selection

Selection is switched on by setting a <tableSelection> child on the <table> element. In this demo, we will support deleting books from the user's book list. This requires a button on the table's control bar; when the button is clicked we will send a delete event to the server:

<demo:table name="books" ... >
 <tableSelection>
  <multipleSelection text="Select and ...">
   <contents>
    <submitButton text="Delete" event="delete"/>
   </contents>
  </multipleSelection>
 </tableSelection>
 ...
</demo:table>

Handling Selection

The event handler for a selection related event can get the currently selected indices by using oracle.cabo.servlet.ui.data.PageEventFlattenedDataSet and oracle.cabo.ui.beans.table.SelectionUtils. Here is the appropriate code fragment for the event handler for the delete event (described above):

// the data that is coming from the client are form elements inside a
// table. therefore, we can use some FlattenedDataSet implementation to
// get at the data:
PageEventFlattenedDataSet clientData =
  new PageEventFlattenedDataSet(event, "books");
// get the indices that were selected (note that these indices are
// relative to the current record set):
int[] selection = SelectionUtils.getSelectedIndices(clientData);

Now that we have the selection we can get the table data from the TableState that we want to mutate (in this case it is the Book table state) and perform the deletion:

UserData uData = _getUserData(..); // from HttpSession
TableState tState = uData.getBookTableState();
List tableData = tState.getTableData();

// this is the offset to add to convert a selection index into an actual
// book index. Note that the -1 is significant since getStartIndex is
// based at 1 and the selection indices are based at zero:
int currentRecordOffset = tState.getStartIndex() - 1;

// BEGIN handle select-all
// ** select-all not yet implemented **
// END handle select-all

// if we loop forward then the books we delete first will affect the
// indices of the books we delete second. So must loop backwards:
for(int i=selection.length-1; i>=0; i--)
{
  // each selected index is relative to the current record set. therefore
  // we need to add an offset to get the index of the actual book:
  tableData.remove(selection[i] + currentRecordOffset);
}

Handling Select All

If the user clicks select-all then he/she might expect all of the records (not just the ones in the current record set) in the table to be deleted. In order to support this we need to detect when the user has done a select-all. This is done by getting the value of the UIConstants.SELET_MODE_KEY from the table's FlattenedDataSet, and seeing if it is "all". The following code fragment (which is an update to the one above) demonstrates handling a select-all followed by delete:

// BEGIN handle select-all
// check to see if user has picked select-all:
Object selectMode = clientData.selectValue(null /*renderingContext*/,
                                           UIConstants.SELECT_MODE_KEY);
if ("all".equals(selectMode))
{
  // we need to handle select-all+delete carefully - we can't just delete
  // everything since the user might have unselected something in the
  // current record set. so first, delete everything upto (but not
  // including) the current record set:
  tableData.subList(0, currentRecordOffset).clear();

  // now the current record set starts at index 0:
  currentRecordOffset = 0;

  // second, delete everything that follows the current record set:
  int sz = tableData.size();
  int firstIndexOnNextPage = tState.getBlockSize();
  // check to make sure that there are records on the next page:
  if (firstIndexOnNextPage < sz)
    tableData.subList(firstIndexOnNextPage, sz).clear();

  // third, continue deleting the selected records in the current record
  // set:
}
// END handle select-all

Mockup Tables

Ideally, table data is created in Java code in response to some query. However, when doing UI Mockups, it is convenient to define the table data in the UIX XML as static inline data. How do we create an interactive table with inline data?

The answer is to create a TableState object using the convenience methods in the class oracle.cabo.doc.demo.table.MockupUtils. Here is a simple UIX XML code fragment:


<dataScope>
 <provider>
  <data name="tableStates">
   <method class="oracle.cabo.doc.demo.table.MockupUtils"
           method="createTableState"/>
  </data>
  <!-- create some static data for the Roster table -->
  <data name="Roster">
   <inline>
    <row subject="Math" students="33" teacher="Jacobs"/>
    <row subject="Physics" students="34" teacher="Kim"/>
    <row subject="Chemistry" students="31" teacher="Dutta"/>
    <row subject="Biology" students="25" teacher="Liang"/>
    <row subject="Phys Ed" students="50" teacher="Goldstein"/>
    <row subject="English" students="29" teacher="Sulaiman"/>
    <row subject="French" students="20" teacher="Simone"/>
    <row subject="Spanish" students="23" teacher="Fernando"/>
    <row subject="German" students="11" teacher="Bosch"/>
   </inline>
  </data>
  ...
 </provider>
 <contents>
  ...
  <demo:table name="rosterTable"
              tableData="${uix.data.Roster.row}"
              tableState="${uix.data.tableStates[0]}">
   <contents>
    <demo:sortableColumn key="subject" header="Subject"/>
    <demo:sortableColumn key="teacher" header="Teacher"/>
    <demo:sortableColumn key="students" header="Students"/>
   </contents>
   ...
  </demo:table>
  ...
 </contents>
</dataScope>

Finally, the event handlers for the mockup tables need to be registered:


<handlers>
 <!-- the examples in this file require an HttpSession to be created
     for each user -->
 <event name="null">
  <method class="oracle.cabo.doc.demo.table.MockupUtils"
          method="createSession"/>
 </event>

 <!-- handle all the table events -->
 <event name="goto sort show hide">
  <method class="oracle.cabo.doc.demo.table.MockupUtils"
          method="handleTableEvent"/>
 </event>
</handlers>

Conclusion

This concludes the Book Table example. If your environment uses HttpSession it should be easy to adapt the classes and code used in this example (with minor modifications) into your application.