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 :
HttpSession
sThis 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 Book
s 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
Book
s 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 Book
s 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.