| Oracle ADF UIX Developer's Guide | Contents |
Previous |
Next |
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 :
HttpSessionsThis chapter contains the following sections:
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.
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 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);
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);
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);
}
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>
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>
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>
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);
}
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
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>
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.
Copyright © 2001, 2004, Oracle.
All rights reserved.