UIX Developer's Guide |
Contents |
Previous |
Next |
The table
is perhaps both the most powerful and most complicated user interface component available in UIX Components. Any web application that has to display or update tabular data will likely need to use at least one table
. This chapter examines all the different ways to configure a table
component.
This chapter contains the following sections:
Many developers presented with the array of options available in the Table assume that it is meant to solve any problem; this is not the case! The Table is provided specifically to present (and possibly update) data which is naturally described in a two dimensional format. Here are some rules of thumb that indicate a situation where using the Table makes sense:
If these rules do not apply, a Table is probably not the appropriate widget to use. Some of the following heuristics imply appropriate use of tables, although not all apply in every situation:
On the other hand, there are some motives which do NOT justify the use of a Table:
TableLayoutBean
or other layout features.
If your design still warrants the use of a Table, it's time to start learning how to create one!
When designing your Table's appearance, the core concept to keep in mind is that data items being compared exist as rows, and the facets or actions to take on those items exist as columns. Since we've established that the items should be similar in nature, the number of facets to them should also be consistent. Thus, the columns in a Table should be the same for every row. However, the number of data items to display, and consequently the number of rows, is likely to vary. For instance, a Table used to represent a shopping cart in a web application might have no items at the outset, but multiple items in it after a round of shopping.
Each of the intersections of a data item row and a column is referred to as a cell. Thus, the table becomes a two dimensional display of many cells, with one row of cells for each data item and with any given column of cells representing the same facet or action compared across many data items.
Tables will also need labels to keep the data straight in the user's mind. For these, we use column and row headers: column headers across the top label which facet or action that column contains -- like the price of a shopping cart item; row headers down the left can be used before each data item to impose a visible ordering strategy. Sometimes it is also useful to present a summary of the data, and in these cases we use a column footer at the bottom of the table. For the sake of consistency, we also refer to the column header and footer items as having cells.
Other formatting options can be used in a Table for advanced displays and additional information, but we will introduce them throughout this chapter. For now, let's create some tables.
Before we go any further, let's see our first UIX example. Not surprisingly, this is the most basic table.
<table xmlns="http://xmlns.oracle.com/uix/ui"/>
That example is dull by design, and in fact it won't display a thing. In it, we've only declared that we want to show a table element in the UIX Components namespace. We haven't actually configured the table yet, which is why it looks blank. In order to display something, we need to add some data items and columns, which is done through "stamps".
What is a stamp? A stamp is just a UINode
--
any UINode
-- that is rendered more than once on a
given page. It is called a "stamp" because, like the stamp someone uses to
mark a paper document "Confidential", our stamp always produces the same mark
whenever it is used. A stamp can be a button, a text field, or any other
widget that needs to be repeated. In the Table, there is one stamp per column,
and this stamp is repeated, or rendered, down all the cells of that table
column. To add a column stamp to a table, simply give the table an indexed
child. Each indexed child becomes a new column stamp, with the first indexed
child becoming the left-most column stamp, and subsequent children being added
to the right of the last stamp.
Why do we use the stamp concept to render data in a table column? Recall that tables should be used to compare similar data items. Since each column represents a facet of a data item, it makes sense that the appearance of the cells in a given column would be similar. Making stamps similar in each cell -- but not exactly the same -- is explained later in this chapter.
Let's add our first column stamp to our table example:
<table ... >
<contents>
<!-- the first column stamp, a text node -->
<text text="SampleText"/>
</contents>
</table>
Unfortunately, even though we now have a stamp in our table there still isn't anything interesting displayed. In order to explain why, we need to introduce the cardinal rule of the table:
DataObject
According to this rule, if we don't have any DataObjects the stamps won't get rendered, so let's add some data.
The DataObject
s we need are supplied by the first
attribute we will examine, tableData
, which
represents the rows in the table. As previously mentioned, the number of rows
in a table can vary across its life (for instance, as the user adds or removes
items from a shopping cart). As is the case with all UIX Components concepts
that involve variable length lists, this tableData
attribute will have a DataObjectList
as its
type. Recall that a DataObjectList
does only two
things: it reports its length and it allows access to each DataObject in it by
index. For the tableData
, the length of the DataObjectList
determines the number of rows displayed,
and each DataObject
in the list provides data for
its corresponding row.
Let's put some sample data in our existing example. Since this is just a demo, we will use uiXML's inline data capability. Most real applications would expose their data via more complex, dynamic sources, however.
<dataScope xmlns="http://xmlns.oracle.com/uix/ui"
xmlns:data="http://xmlns.oracle.com/uix/ui">
<provider>
<!-- all the data used by our table demo -->
<data name="demoTableData">
<inline>
<!-- all the row DataObjects used by our table for tableData -->
<demoRowData/>
<demoRowData/>
<demoRowData/>
</inline>
</data>
</provider>
<contents>
<table data:tableData="demoRowData@demoTableData">
<contents>
<!-- the first column stamp, a text node -->
<text text="SampleText"/>
</contents>
</table>
</contents>
</dataScope>
We've introduced a few things into our demo code now:
<dataScope>
element, which will provide the
table with a data source. The table itself is moved into the contents of the
<dataScope>
, and the data is placed in the <provider>
section.
DataObjectList
with a size of three
and with each element in turn representing one of the three index-based DataObject
s
in the DataObjectList
.
data:tableData
-- which supplies the
table with the DataObjectList
it needs to represent its rows. Specifically, the
"demoRowData" DataObjectList
we just placed in the provider section is selected to be
the row data source.
After running this example, you will see that the table finally displays
three cells in a vertical column, each containing some identical sample
text. This is because we have a single column stamp -- the text element -- and
three DataObject
s in our tableData
list; according to the table rule, our text
stamp will be rendered once for every DataObject
in the list. Thus, we see the text three times.
Let's see what happens when we change things a bit. First, we will add a
fourth data object to our row list. This will cause four rows to be rendered
in our table, again due to the cardinal table rule indicating that each stamp
is rendered once for every DataObject
.
<dataScope ... >
<provider>
<!-- all the data used by our table demo -->
<data name="demoTableData">
<inline>
<!-- all the row DataObjects used by our table for tableData -->
<demoRowData/>
<demoRowData/>
<demoRowData/>
<demoRowData/>
</inline>
</data>
</provider>
<contents>
...
</contents>
</dataScope>
Next, we'll alter the table in the other dimension by adding a second column stamp, this time a button. According to the table rule, both the existing text stamp AND the new button will each be rendered four times.
<dataScope ... >
<provider>
...
</provider>
<contents>
<table data:tableData="demoRowData@demoTableData">
<contents>
<!-- the first column stamp, a text node -->
<text text="SampleText"/>
<!-- the second column stamp, a button -->
<button text="Push Me" destination="http://www.example.org"/>
</contents>
</table>
</contents>
</dataScope>
These two concepts can be extended to put any number of rows in a table, and to vary the type and number of columns. Try it and see!
This is all well and good for tables that show the same data in every row, but practically speaking that isn't likely to occur in any real tables. In order to create useful examples, we will need databinding.
Recall that databinding is used throughout UIX Components to change the
contents of a page on a per-render basis. Here we take databinding one step
further by using it to change table contents on a per-row basis. Since the
UINode
stamped down a column doesn't change for
each row, we use databinding to vary what actually gets stamped in any given
cell. This makes the stamps serve a more useful purpose; the type of content
they stamp in each cell of a column is similar -- for example, each cell in
the column will be a text field -- but the content itself is different -- for
example, the text inside each text field will be row by row.
This works by using UIX Components's concept of a "current" DataObject
. As with any UIX Components databinding,
attributes of a node can be bound to the current DataObject
instead of a specifically named DataObject
. This causes the generated result to change
based on whatever DataObject
happens to be current
at the time of the query. The table makes this concept possible by changing
the current DataObject
when it renders each of the
table rows. Thus, if a column stamp attribute is bound to the same key on the
current DataObject
, the actual value of that
attribute will change in every table row!
Now the rationale for using DataObject
s to
determine the number of rows to render makes more sense, because each element
in the table's tableData DataObjectList
is used as
the "current" DataObject
for its corresponding
row. Let's illustrate by augmenting our existing example:
<dataScope ... >
<provider>
<!-- all the data used by our table demo -->
<data name="demoTableData">
<inline>
<!-- all the row DataObjects used by our table for tableData -->
<demoRowData someText="First row"/>
<demoRowData someText="Second row"/>
<demoRowData someText="Third row"/>
<demoRowData someText="Fourth row"/>
</inline>
</data>
</provider>
<contents>
<table data:tableData="demoRowData@demoTableData">
<contents>
<!-- the first column stamp, a text node -->
<text data:text="someText"/>
<!-- the second column stamp, a button -->
<button data:text="someText" destination="http://www.example.org"/>
</contents>
</table>
</contents>
</dataScope>
Finally, we have a table with contents that are different in each row. Here's what we changed:
DataObject
s, a text String which will be returned when
the DataObject
is queried with the key
"someText". We made the result different in each row.
DataObject
with
the key "someText". This query goes to the current DataObject
, rather than a named DataObject
, because we used the attribute value
"someText" instead of "someText@aNamedDataObject". Because of this, the text
we stamp on each row comes from the table data.
For the sake of clarity, let's tweak the example a little so that the text and button stamps use separate data:
<dataScope ... >
<provider>
<!-- all the data used by our table demo -->
<data name="demoTableData">
<inline>
<!-- all the row DataObjects used by our table for tableData -->
<demoRowData firstColumnText="First row" secondColumnText="Button #1"/>
<demoRowData firstColumnText="Second row" secondColumnText="Button #2"/>
<demoRowData firstColumnText="Third row" secondColumnText="Button #3"/>
<demoRowData firstColumnText="Fourth row" secondColumnText="Button #4"/>
</inline>
</data>
</provider>
<contents>
<table data:tableData="demoRowData@demoTableData">
<contents>
<!-- the first column stamp, a text node -->
<text data:text="firstColumnText"/>
<!-- the second column stamp, a button -->
<button data:text="secondColumnText" destination="http://www.example.org"/>
</contents>
</table>
</contents>
</dataScope>
To do this, we gave each of our row DataObject
s
two text values -- "firstColumnText" and "secondColumnText" -- instead of just
one. Then, we bound the text column stamp to "firstColumnText" and the button
to "secondColumnText". As a result, the two columns now have different text.
Even though our examples here are simple, the implications are very
powerful. Keep in mind that any UINode
can be used
as a column stamp, and any attribute of that stamp can be bound to the current
DataObject
. This provides a tremendous amount of
flexibility in controlling the contents of the table cells.
You might be asking yourself a few questions as this point. Why go to all the trouble to use stamps and separate data? Why not just let developers specify the contents of each and every cell, content and data together?
There is a good reason, which is to separate the model (or data) from the view (or appearance) of the table. This allows developers to specify the appearance of the table by changing the column stamps, and later plug in any data source to supply those stamps with concrete data. If we instead specified exactly what went into each cell, we would not be able to use the same set of UIX Components nodes for every page view. We would not be able to change the number of rows in a table from render to render, or the data in each of the cells, either. Specifying the structure of the table once and reusing that structure for every page render is one of the core features of the entire UIX Components framework.
The tableData
attribute is of type DataObjectList
and so can be set in a couple of
ways. For static data that never changes you can inline it into the uiXML code,
as in the following example:
<table ...>
<tableData>
<demoRowData firstColumnText="First row" secondColumnText="Button #1"/>
<demoRowData firstColumnText="Second row" secondColumnText="Button #2"/>
<demoRowData firstColumnText="Third row" secondColumnText="Button #3"/>
<demoRowData firstColumnText="Fourth row" secondColumnText="Button #4"/>
</tableData>
...
</table>
The uiXML parser knows that tableData
is of type
DataObjectList
and will parse the children of the
tableData
element into a DataObjectList
. The row key, in this case demoRowData
, can be anything; as long as all four rows
have the same row key, they will be grouped into the same DataObjectList
. Incidentally, there is no data binding
here; the tableData
attribute is set to be the
newly created DataObjectList
.
One of the problems with the above approach is that the table data cannot be shared between tables. Sharing can be achieved using data binding and the following form (that has been presented before):
<dataScope ... >
<provider>
<data name="demoTableData">
<inline>
<demoRowData firstColumnText="First row" secondColumnText="Button #1"/>
<demoRowData firstColumnText="Second row" secondColumnText="Button #2"/>
<demoRowData firstColumnText="Third row" secondColumnText="Button #3"/>
<demoRowData firstColumnText="Fourth row" secondColumnText="Button #4"/>
</inline>
</data>
</provider>
<contents>
<table name="table1" data:tableData="demoRowData@demoTableData">
...
</table>
<table name="table2" data:tableData="demoRowData@demoTableData">
...
</table>
</contents>
</dataScope>
In the above example, the inline
data provider
creates a DataObject
with a single key, demoRowData
, that is bound to a DataObjectList
. The outer DataObject
is unnecessary; in the following section, we
will create the table data in Java and avoid creating the outer DataObject
.
Data binding allows us to build DataObjectList
s in
Java and provide them as data for our tables. Here is a quick example:
<dataScope ... >
<provider>
<data name="demoTableData">
<method class="test.MyTable" method="getTableData" />
</data>
</provider>
<contents>
<table data:tableData="@demoTableData">
...
</table>
</contents>
</dataScope>
Note the change (above) in the tableData
binding. This is because we don't wrap our DataObjectList
inside a DataObject
. The Java code that creates the table data is presented below. (Both the uiXML and Java aspects of data binding is explained in
detail in Data Binding.)
package test;
public class MyTable
{
public static DataObject getTableData(RenderingContext context,
String namespace,
String name)
{
DataObject[] data = new DataObject[4];
data[0] = new MyDataObject("First Row", "Button #1");
data[1] = new MyDataObject("Second Row", "Button #2");
data[2] = new MyDataObject("Third Row", "Button #3");
data[3] = new MyDataObject("Fourth Row", "Button #4");
// convert the array into a DataObjectList
return new ArrayDataSet(data);
}
private static final class MyDataObject implements DataObject
{
public MyDataObject(String column1, String column2)
{
_col1 = column1;
_col2 = column2;
}
public Object selectValue(RenderingContext context, Object key)
{
if ("firstColumnText".equals(key))
return _col1;
else if ("secondColumnText".equals(key))
return _col2;
return null;
}
private final String _col1, _col2;
}
}
In the above example, we create our own implementation of
DataObject
which recognizes two keys. We then create an array of
instances of our data, and make a DataObjectList
out of them
using the oracle.cabo.ui.data.ArrayDataSet
class. Note that
ArrayDataSet
implements both DataObject
and
DataObjectList
, and so can be used to return table data from
within a DataProvider
.
All this time we have been creating static data. Now that we are running Java code we can create dynamic data; in the following example, we create table data with a directory listing:
package test;
public class MyTable
{
public static DataObject getDirectoryData(RenderingContext context,
String namespace,
String name)
{
// Make sure this directory exists on your file system
return new DirDataObjectList(new File("/home/user/"));
}
private static final class DirDataObjectList
implements DataObjectList, DataObject
{
public DirDataObjectList(File dir)
{
_files = dir.listFiles();
}
public int getLength()
{
return _files.length;
}
public DataObject getItem(int index)
{
// in a more prudent implementation, we would be caching these
// DataObjects, rather than creating new ones each time.
return new FileDataObject(_files[index]);
}
public Object selectValue(RenderingContext context, Object key)
{
// we don't support any properties on this DataObject, since this is
// primarily a list of DataObjects.
return null;
}
private final File[] _files;
}
private static final class FileDataObject implements DataObject
{
public FileDataObject(File file)
{
_file = file;
}
/**
* This DataObject recognizes two keys: name which gives the
* file name, and length which gives the file length.
*/
public Object selectValue(RenderingContext context, Object key)
{
if ("name".equals(key))
return _file.getName();
else if ("length".equals(key))
return new Long(_file.length());
return null;
}
private final File _file;
}
}
And the corresponding UIX code would look like:
<dataScope ... >
<provider>
<data name="demoTableData">
<method class="test.MyTable" method="getDirectoryData" />
</data>
</provider>
<contents>
<table data:tableData="@demoTableData">
<contents>
<text data:text="name"/>
<text data:text="length"/>
</contents>
...
</table>
</contents>
</dataScope>
Another class that might help creating table data in Java is oracle.cabo.ui.data.ListDataObjectList
which converts a
Java Vector
(and soon a JDK1.2 List
) into a DataObjectList
.
There are other classes that help to process table data on the server side;
these will be discussed in a later section.
At this point, we have dealt only with the data portion of the table, not with the peripheral sections of the table that lie around the data. The first of these we will examine is the column header, which provides the labels for each of our columns. We will then examine the row header, which provides the labels for each row. There are actually two ways to create column headers, the first of which we will deal with now, and the second of which will be described in a later section concerning column encapsulation.
Like columns themselves, column headers are stamped, but in this case they
are stamped across the top of the table, rather than down the cells in a
column. To register a stamp node to serve as the column header, we must set it
as the named child columnHeaderStamp
. The
following example illustrates this:
<dataScope ... >
<provider>
...
</provider>
<contents>
<form name="testForm">
<contents>
<table ... >
<contents>
...
</contents>
<!-- add a column header stamp node -->
<columnHeaderStamp>
<text text="Column Header"/>
</columnHeaderStamp>
</table>
</contents>
</form>
</contents>
</dataScope>
Notice that there is now a special section above the data columns which
is differentiated in appearance from the other cells. The column header stamp
is stamped across the top of the table, once per data column. Of course,
we would prefer that the column header stamps contain labels appropriate to
the content of the columns, instead of just showing the same text in every
cell. For this we will need to use a new attribute on the table to databind
the column header, the columnHeaderData
:
<dataScope ... >
<provider>
<!-- all the data used by our table demo -->
<data name="demoTableData">
<inline>
<!-- all the row DataObjects used by our table for tableData -->
<demoRowData ... />
...
<!-- DataObjectList to provide information to the column header stamps -->
<demoColumnHeaderData headerText="First Header"/>
<demoColumnHeaderData headerText="Second Header"/>
</inline>
</data>
</provider>
<contents>
<form name="testForm">
<contents>
<table data:columnHeaderData="demoColumnHeaderData@demoTableData"
... >
<contents>
...
</contents>
<!-- add a column header stamp node -->
<columnHeaderStamp>
<text data:text="headerText"/>
</columnHeaderStamp>
</table>
</contents>
</form>
</contents>
</dataScope>
Now, each header has its own unique text. To accomplish this, we did three things:
DataObjectList
, whose size is equal to the number of
columns and whose DataObject
s each contain the text to display in a column
header, stored under the "headerText" key.
columnHeaderData
comes from the databound
attribute whose value is the "demoColumnHeaderData" DataObjectList
under the
named DataObject
"demoTableData". This allows the table to use our new
DataObjectList
to supply data to the header stamps.
DataObject
. When the column header
stamps are rendered, the individual DataObject
s in the column header data are
each in turn made the current DataObject
when the stamp is rendered above
each header. This causes the text to change from column header to column header.
This state of affairs will be adequate for many tables. However, some
tables need to allow the users to sort the data items by some criterion, and
also need to indicate when the rows are currently sorted. For this purpose,
UIX Components supplies a specific column header stamp that meets this need,
the SortableHeaderBean
. This bean is described in
a later section.
In some tables, you will want to label each individual data row with an
accompanying label. To achieve this, the table bean offers the rowHeaderStamp
. This stamp is similar to the column
header stamp. Consider the following example which illustrates
the use of a row header stamp:
<dataScope ... >
<provider>
<!-- all the data used by our table demo -->
<data name="demoTableData">
<inline>
...
<!-- DataObjectList to provide information to the row header stamps -->
<demoRowHeaderData headerText="1"/>
<demoRowHeaderData headerText="2"/>
<demoRowHeaderData headerText="3"/>
</inline>
</data>
</provider>
<contents>
<form ... >
<contents>
<table ...
data:rowHeaderData="demoRowHeaderData@demoTableData" >
<contents>
...
</contents>
<!-- row header stamp node -->
<rowHeaderStamp>
<text data:text="headerText"/>
</rowHeaderStamp>
</table>
</contents>
</form>
</contents>
</dataScope>
The mechanism for adding a row header is the same, only with slightly
different child and attribute names: rowHeaderStamp
named child and rowHeaderData
table attribute. Note, though, that we
haven't provided four DataObject
s of row header
data in our example. This is to illustrate that even without data, the header
stamp will render for a row, although its result may be less than desired.
By design, many tables have stamps that render controls that can be edited by the user. The idea is that the user will enter data into the cells of the table which will then be submitted back to the server for processing.
For example, suppose we want to render an editable form input element in
every cell of a table column, so that users can enter or change data in those
cells. Normally, this can be achieved in a page outside of a table via
standard form submissions. Any form input widget has an associated name
attribute, which is sent to the server along with
the value of the input element when the form is submitted. This is how the
server is informed of the values on a page.
As mentioned before, any widget, even an input element, can be used as a
column stamp in a table. But if the widget is stamped multiple times down the
cells of a column, how is the unique value in each cell sent to the server?
Wouldn't this cause each input element to be rendered multiple times with the
same name
attribute, which would not be the desired HTML?
The answer is that the table fixes this through a process called "name transformation." In essence, the table treats the name
attribute on any of its stamps as special, and
alters it before rendering the stamp into a given cell. For instance, if a
column stamp was an input control with the name "foo", the table would
alter the actual value of the name rendered into each cell to take the
form:
tableName:foo:rowIndex
where "tableName" would be replaced by the value of the table's name
attribute and the "rowIndex" would be replaced with
an integer indicating which row that stamp is being rendered into. This means
that an input element named "foo", rendered in a table named "myTestTable" in
the third row would get the name:
myTestTable:foo:3
In the following example, we've altered our table to put textInput
elements in the first column instead of text
nodes. If you view the HTML source for this page, you will see that the table
has transformed the names of the generated text fields in each row as
described above.
<dataScope ... >
<provider>
...
</provider>
<contents>
<form name="testForm">
<contents>
<table name="myTestTable"
... >
<contents>
<textInput data:text="firstColumnText" name="foo"/>
<button data:text="secondColumnText" destination="http://www.example.org"/>
</contents>
</table>
</contents>
</form>
</contents>
</dataScope>
When the form containing this table is submitted, either as the
result of a navigation bar link or any other form-triggering action,
the server will now receive four name/value pairs representing the
values in the textInput
column:
Form Control Name | Initial Value |
---|---|
myTestTable:foo:0 | First row |
myTestTable:foo:1 | Second row |
myTestTable:foo:2 | Third row |
myTestTable:foo:3 | Fourth row |
UIX also provides utility classes for retrieving these values on the server
after they have been submitted. The class oracle.cabo.data.ServletRequestDataSet
may be used to
retrieve the values from a ServletRequest
. And the
class oracle.cabo.servlet.ui.data.PageEventFlattenedDataSet
may be used with a UIX Controller PageEvent
. Each
of these classes implements a DataObjectList
of all
the input elements in a given table. The length of this list is the number of
rows in the table. Each DataObject
in this list
corresponds to a row in the table. Use the name
of
an input element, as the key (with each DataObject
) to get the value of that element (on the
respective row). The following example implements an event handler that pulls
out all the table input element values and concatenates them.
public static EventResult doSubmitEvent(BajaContext bc, Page page,
PageEvent event)
{
// create a new FlattenedDataSet for the table "myTestTable"
DataSet tableInputs = new PageEventFlattenedDataSet(event,
"myTestTable");
StringBuffer s = new StringBuffer(40);
// this would be the number of rows in the table
int sz = tableInputs.getLength();
for(int i=0; i<sz ;i++)
{
// get the DataObject representing all the input elements on the current
// table row.
DataObject row = tableInputs.getItem(i);
// get the value of the input element named "foo". we can safely use
// null for the RenderingContext here:
Object value = row.selectValue(null, "foo");
s.append(value);
}
EventResult result = new EventResult(page);
result.setProperty("case", "submit");
// store the concatenation of the values of all the "foo" elements on the
// EventResult
result.setProperty("result", s);
return result;
}
This automatic name transformation is sometimes undesirable; for example, if
you wanted to render a radio button group vertically down a column, then you
would have to ensure that each radio element had the same name (so that they
belong in the same group and are mutually exclusive). Currently, the only way
to do this is to turn off the automatic name transformation and handle name
transformations privately by data binding each name
attribute. Automatic name transformation is turned
off by setting the nameTransformed
attribute of
the table
to false
(or
Boolean.FALSE
in Java).
<table ...
nameTransformed="false">
...
</table>
If you would still like to use PageEventFlattenedDataSet
(or ServletRequestDataSet
) to get at your data on the server,
then you should use oracle.cabo.ui.data.FlattenedDataSet
to transform all
the table input elements. To get the transformation of input element "foo" on
row 5 of table "myTestTable" use:
String newName = FlattenedDataSet.getFlattenedName("myTestTable", 5, "foo");
To get the transformation of your special radio group column with name "radio" use:
String newName = FlattenedDataSet.getFlattenedName("myTestTable", "radio");
One final step is needed to make everything work correctly. The proxied
attribute of the table
must be set to true
(this attribute is explained in a later section). Now you can use PageEventFlattenedDataSet
in the usual way to get at the
table input element values. To get at the value of your special radio group
column use:
DataSet tableInputs = new PageEventFlattenedDataSet(event,
"myTestTable");
Object radioValue = tableInputs.selectValue(null, "radio");
While we have seen how to create a table with a few rows in it, it is often the case that the data sets in applications will often have large numbers of items. Clearly, a table listing the employees in a large corporation cannot show all records at the same time. For this purpose, we use record navigation to break such a table into more manageable pieces.
With only a bit of additional data, the table bean can render a navigation area which indicates that the current data rows are part of a larger whole. Each row in the table is assigned an index number, and the user is told which row numbers are in view, and which are not currently visible. To render this navigation area, you should supply the table with a few additional attributes:
value
: the first row number currently visible
on the screen. If not given, it defaults to 1.
minValue
: the index of the first row in the entire
data set, not just the rows on screen. If it is not specified, the minValue
is assumed to be "1".
maxValue
: the index of the last row in the entire
data set, not just the rows on screen. If this is not specified, the
appearance of the navigation bar will change to reflect that the total
number of rows is not known.
blockSize
: the number of rows to show per data
set, which defaults to 25. The table automatically handles the case where the
edge sets of rows may contain fewer rows than other sets of rows. For
instance, if we navigate through a set of 403 records 25 at a time, the last
set will only contain 3 records (even though the blockSize
is 25).
It is not necessary to supply all four attributes to the table if they are not known. Providing even one of them will cause the table to render a navigation area, and it will default those attributes without explicit values. As an example, here is our demo table with some additional navigation properties which indicate that there is more data than that currently onscreen.
<dataScope ... >
<provider>
...
</provider>
<contents>
<table value="5"
maxValue="50"
blockSize="4"
... >
<contents>
...
</contents>
</table>
</contents>
</dataScope>
In our example, we tell the table that we are viewing rows starting at row 5 (out of a total of 50) and we want to view at most 4 rows at a time. Although we don't give a minimum value, it is assumed to be "1".
It is important to note that the navigational bar is purely cosmetic; it does
not control how many rows are actually rendered, or which row is rendered
first. Regardless of the values of the value
and
blockSize
attributes, the table will always render
as many rows as there are DataObject
s in the tableData
attribute.
When the navigational links are used, the table generates a UIX Controller
event named UIConstants.GOTO_EVENT
, or goto
in
uiXML (see UIX Controller for
details about events). This event has three parameters which are described in
the table below:
Event Parameter | UIConstant | Description |
---|---|---|
source | SOURCE_PARAM |
This parameter identifies the table that generated the event. The value is the "name" attribute of the table. |
value | VALUE_PARAM |
This parameter identifies the set of rows that the user is currently viewing. It is set to be the index of the first row in the set. |
size | SIZE_PARAM |
This is the size of the set of rows that is currently being viewed. This
is usually the blockSize of the table, except in
the case when an edge set of rows is being viewed. |
On the server, we need to create a new DataObjectList
that contains only the table rows that
are requested. This means that we need to start with the row identified by the
value parameter and only have as many rows as the size
parameter. This is done with the help of the class oracle.cabo.ui.data.PagedDataObjectList
.
public class TableDemo {
public static EventResult doGotoEvent(BajaContext bc, Page page,
PageEvent event)
{
// if this is a "goto" event, then we need to get the "value" parameter to
// figure out what our start index is. If this is not a "goto" event, then
// we want to start at index "1"
String valueParam = ((event!=null) &&
UIConstants.GOTO_EVENT.equals(event.getName()))
? event.getParameter(UIConstants.VALUE_PARAM)
: "1";
// the "value" parameter starts at "1"; however, our data is zero based,
// so adjust the offset
int value = Integer.parseInt(valueParam)-1;
DataObjectList tableData = new PagedDataObjectList(_TABLE_DATA,
_BLOCK_SIZE.intValue(),
value); //start index
// in a more efficient implementation, we would not use DictionaryData;
// instead, we would implement our own DataObject
DictionaryData data = new DictionaryData();
// we need to add one here, since our data is zero based, but the table
// start index must start at 1
data.put("value", new Integer(value+1));
data.put("size", _BLOCK_SIZE);
data.put("maxValue", new Integer(_TABLE_DATA.getLength()));
data.put("current", tableData);
EventResult result = new EventResult(page);
result.setProperty("tableData", data);
return result;
}
// we want to render at most 30 rows on a single page
private static final Integer _BLOCK_SIZE = new Integer(30);
private static final DataObjectList _TABLE_DATA = _sCreateTableData();
/**
* this method creates all the static table data.
*/
private static DataObjectList _sCreateTableData()
{
int sz = 94;
Object[] data = new Object[sz];
for(int i=1; i<=sz; i++)
{
data[i-1] = "Test Data "+i;
}
return new ArrayDataSet(data, "firstColumnText");
}
}
In the above example, we first start by getting the value parameter;
this is the index of the first row in the current view of the table. We need
to make an offset adjustment since the DataObjectList
index is zero based, but the value
parameter starts at one.
A PagedDataObjectList
is created with the block
size that we want (30) and the current start index. Finally, we put this DataObjectList
into a DataObject
. The DataObject
implements the keys value, size, maxValue and
current which must be bound respectively to the value
, blockSize
, maxValue
and tableData
attributes of the table. This is done by the following uiXML:
<table data:tableData="current@tableData@ctrl:eventResult"
data:value="value@tableData@ctrl:eventResult"
minValue="1"
data:maxValue="maxValue@tableData@ctrl:eventResult"
data:blockSize="size@tableData@ctrl:eventResult"
name="table1"
... >
...
</table>
The above example uses several degrees of indirection in the data binding. See Data Binding for more details about the data binding.
In the above examples, all the navigation links point back to the current
page. In some applications, it might be necessary to direct navigation
requests to some other URL. This can be done by setting a destination
attribute on the table. This destination
must be a well-formed URL of a server that
can accept the event parameters we listed. If a destination is provided, the
table will render the record navigation links in the page so that they send
URL parameters with the necessary record navigation event, to the destination
specified. An example is given below:
<table destination="http://www.example.org/eventHandler"
... >
...
</table>
Another way to send table events to the server is to specify that the table uses HTML forms for its communication. Although the end result of using forms is actually identical to using a destination URL -- namely, the server will still receive a set of key and value pairs indicating the user's action -- using forms has an additional benefit. If forms are used to submit table actions like record navigation, all other values of the form will be sent to the server along with the table navigation parameters.
As an example, suppose that a table control and a textInput
both exist on the same page, and inside the
same HTML form. When the the user clicks on the table navigation bar link to
move to a new set of rows, it might also be desirable to send the value of the
textInput
to the server, so that its contents might
be preserved and redisplayed when the new page is generated for the user. To
achieve this, simply set the table's formSubmitted
attribute to be true
. Doing so will cause the table
to submit the form in which it resides via Javascript -- along with all the
other values in that form -- when the user clicks on a navigation area
link.
But how, then, does the table send the server the four necessary parameters indicating which record navigation event the user chose? It achieves this by rendering four special, hidden fields into the HTML form in which the table also resides. Before the form is submitted by the table, Javascript is used to change the values of these hidden fields to indicate which action the user took. Here's an example of using form submission:
<dataScope ... >
<provider>
...
</provider>
<contents>
<form name="testForm">
<contents>
<table formSubmitted="true"
... >
<contents>
...
</contents>
</table>
</contents>
</form>
</contents>
</dataScope>
Form submission is also desirable when form input elements are used as column stamps of a table
There is another special case for record navigation: tables which for some reason or other have no rows to currently display. One example is a table that shows search results for a given term, but which has no data rows because the term searched for contains no matches. For such tables, it is helpful to the user to display a message indicating the reason why no data appears. This text message can be set with the "alternateText" attribute, as shown below:
<dataScope ... >
<provider>
<!-- all the data used by our table demo -->
<data name="demoTableData">
<inline>
<!-- no rows in this example! -->
...
</inline>
</data>
</provider>
<contents>
<table data:tableData="demoRowData@demoTableData"
...
alternateText="(No search results were found)">
...
</table>
</contents>
</dataScope>
The alternate text message will only be displayed if there is no "tableData" set on the table, or if the tableData happens to return a row count of zero.
Column headers are made sortable by using a SortableHeaderBean
as the columnHeaderStamp
. However, the sortability of a given
column will not make itself apparent until we supply some additional
information.
In order for the SortableHeaderBean
to render in
a way that indicates that it is sortable, we have to give it a value for the
sortable
attribute. There are three possible values
to give this attribute:
uiXML | UIConstant | Description |
---|---|---|
yes
| SORTABLE_YES
| Indicates that this column is sortable, but is not currently sorted. The column header will then render in such a way as to indicate that the user should select it should he or she wish to sort the data items based on the values of that column. |
ascending
| SORTABLE_ASCENDING
| Indicates that the column header should render as sortable, and display a notification to the user that the table is already sorted on this column. It also indicates that the sorted column values increase when read down the column. |
descending
| SORTABLE_DESCENDING
| Indicates that the column header should render as sortable, and notify the user that the table is already sorted on this column. The renderer will signify that the sorted column values decrease when read down the column. |
We can try out this capability in our demo by making one of our columns sortable and the other sortable (ascending). Notice how databinding the "sortable" attribute of our new SortableHeaderBean to these values changes the appearance of the column header stamp.
<dataScope ... >
<provider>
<data name="demoTableData">
<inline>
<!-- all the row DataObjects used by our table for tableData -->
<demoRowData ... />
...
<!-- DataObjectList to provide information to the column header stamps -->
<demoColumnHeaderData ... sortValue="yes"/>
<demoColumnHeaderData ... sortValue="ascending"/>
</inline>
</data>
</provider>
<contents>
<form name="testForm">
<contents>
<table ... >
<contents>
...
</contents>
<!-- add a sortable column header stamp node -->
<columnHeaderStamp>
<sortableHeader data:text="headerText"
data:sortable="sortValue"/>
</columnHeaderStamp>
</table>
</contents>
</form>
</contents>
</dataScope>
When sortable headers are clicked, the table generates a UIX Controller event
with the name UIConstants.SORT_EVENT
(or "sort" in
uiXML). This event is accompanied by three parameters which are listed in the
table below:
Event Parameter | UIConstant | Description |
---|---|---|
source | SOURCE_PARAM |
This identifies the table generating this event. This would be the name of the respective table.
|
value | VALUE_PARAM |
This will be whatever is provided as the value
attribute of the SortableHeaderBean for that
column header cell. The default value would be the zero-based index of the
column.
|
state | STATE_PARAM |
Indicates whether or not the column header (identified by the value
parameter) is already sorted. If it is already sorted, then this parameter
would be either the UIConstant SORTABLE_ASCENDING
or SORTABLE_DESCENDING depending on which way the
column is sorted. If the column is not already sorted, nothing will be sent as
the state value. |
In the following sections, an example on how to handle sorting on the server is presented. This example is more complicated than it has to be, simply because it is designed to work in a general environment.
The first step is to provide a value
attribute for
the SortableHeaderBean
. We have decided to use the
same key that is used for the column data, as the value
of each column header. This makes sense because on
the server we can get at the value parameter and use it as the key with
our table row DataObject
s to get at the column
data which must be sorted. The following uiXML illustrates this part (it has
three sortable columns: name, age and blood):
<table ... >
<columnHeaderData>
<col text="Name" sort="yes" value="name"/>
<col text="Age" sort="yes" value="age"/>
<col text="Blood Group" sort="yes" value="blood"/>
<col text="Phone"/>
</columnHeaderData>
<columnHeaderStamp>
<sortableHeader data:text="text"
data:value="value" ... >
</sortableHeader>
</columnHeaderStamp>
<contents>
<text data:text="name"/>
<text data:text="age"/>
<text data:text="blood"/>
<text data:text="phone"/>
</contents>
</table>
The next step is to handle the sort event on the server. The following
code pulls out the value and state parameters and puts them on
the EventResult
:
public static EventResult doSortEvent(BajaContext bc, Page page,
PageEvent event)
{
EventResult result = new EventResult(page);
result.setProperty(UIConstants.VALUE_PARAM,
event.getParameter(UIConstants.VALUE_PARAM));
// if we are already sorting in ascending order, then we want to sort in
// descending order. Otherwise, sort in ascending order
Object state = event.getParameter(UIConstants.STATE_PARAM);
result.setProperty(UIConstants.STATE_PARAM,
UIConstants.SORTABLE_ASCENDING.equals(state)
? UIConstants.SORTABLE_DESCENDING
: UIConstants.SORTABLE_ASCENDING);
return result;
}
Now we have to create a DataObject
that would
determine the sortable
attribute for each column
header. The following DataObject
must be called
with the current column header's value
attribute
as the key. It then checks to see if this is the column header that has
been sorted, by comparing key to the value on the EventResult
(this was set in the event handler
above). If it is indeed the right column header, then we return the
state (either ascending or descending); otherwise, we return null.
private static final DataObject _SORT_COLUMN_HEADER = new DataObject() {
public Object selectValue(RenderingContext rc, Object key)
{
BajaContext bc = BajaRenderingContext.getBajaContext(rc);
EventResult er = EventResult.getEventResult(bc);
if (er!=null)
{
// check to see if it is this column that has been sorted. We assume
// that "key" is the "value" attribute of this column header.
if (key.equals(er.getProperty(UIConstants.VALUE_PARAM)))
return er.getProperty(UIConstants.STATE_PARAM);
}
return null;
}
};
The next step would be to databind the sortable
attribute of the SortableHeaderBean
to use the
above DataObject
. We have done this databinding in
an unusual way; we first want to use the above DataObject
to get a value, and failing that we want to
get the value from the appropriate DataObject
for
this column in columnHeaderData
. The following uiXML
code achieves this objective:
<table ... >
<columnHeaderData>
<col text="Name" sort="yes" value="name"/>
<col text="Age" sort="yes" value="age"/>
<col text="Blood Group" sort="yes" value="blood"/>
<col text="Phone"/>
</columnHeaderData>
<columnHeaderStamp>
<sortableHeader data:text="text"
data:value="value">
<boundAttribute name="sortable">
<defaulting>
<!-- first try to get a value from the sortColumnHeader
dataObject. This dataObject is passed whatever the
value key is bound to (in the current columnHeaderData) -->
<dataObject data:select="value"
source="sortColumnHeader"/>
<dataObject data:source="(value)@sortColumnHeader"/>
<!-- if the above boundValue returns null, then try to get
a value from the columnHeaderData -->
<dataObject select="sort"/>
</defaulting>
</boundAttribute>
</sortableHeader>
</columnHeaderStamp>
...
</table>
The boundAttribute
element is another way of
databinding the sortable
attribute of sortableHeader
. The defaulting
boundValue tries to get a value from its
first child, and failing that, returns the value from the second child. In the
above case, the first child tries to get a value from the
sortColumnHeader data provider (let's assume that this data provider is
bound to the _SORT_COLUMN_HEADER
we created
above), and the second child uses the sort key to get the default
sortable state from a DataObject
in columnHeaderData
.
What have we achieved so far? The sortable headers are not only clickable, but each time they are clicked, they toggle between the display sorted in ascending order and the display sorted in descending order. However, the columns are still not sorted; the next section will implement the actual sorting.
public static DataObject getSortedTableData(RenderingContext rc,
String namespace, String name)
{
DataObject[] data;
BajaContext bc = BajaRenderingContext.getBajaContext(rc);
EventResult er = EventResult.getEventResult(bc);
if (er!=null)
{
// we need to clone because we are going to mutate the array:
data = (DataObject[]) _TABLE_DATA.clone();
Object state = er.getProperty(UIConstants.STATE_PARAM);
Object key = er.getProperty(UIConstants.VALUE_PARAM);
Comparator comp = new DataObjectComparator(
rc,
key, // this is the key to sort the DataObjects on
UIConstants.SORTABLE_ASCENDING.equals(state));
Arrays.sort(data, comp);
}
else
data = _TABLE_DATA;
return new ArrayDataSet(data);
}
The above data provider returns a DataObjectList
that is sorted according to the state and value that was set on
the EventResult
by the event handler doSortEvent(...)
. _TABLE_DATA
is the unsorted data. The actual sorting is
done by the java.util.Arrays
class. The other
interesting class is DataObjectComparator
:
private static final class DataObjectComparator implements Comparator
{
public DataObjectComparator(RenderingContext context,
Object key, boolean ascending)
{
_context = context;
_key = key;
_ascending = ascending;
}
public int compare(Object o1, Object o2)
{
DataObject dob1 = (DataObject) o1;
DataObject dob2 = (DataObject) o2;
Comparable val1 = (Comparable) dob1.selectValue(_context, _key);
Object val2 = dob2.selectValue(_context, _key);
int comp = val1.compareTo(val2);
// if we are not sorting in ascending order, then negate the comparison:
return _ascending ? comp : -comp;
}
private final RenderingContext _context;
private final Object _key;
private final boolean _ascending;
}
The above class is charged with comparing two DataObject
s, and uses _key
to get values from both. It assumes the values are Comparable
and does the comparison (negating the result
if we are sorting in descending order).
The ability to select certain rows of a table for further processing is a very
useful feature for many applications. The Table
supports selection via the tableSelection
child. Two selection beans are offered with UIX: SingleSelectionBean
and MultipleSelectionBean
. The former is used in tables
where only a single row may be selected; the latter is used when multiple rows
may be selected. Before we discuss these beans it must be noted that the Table
must be used inside a form in order for
these beans to work correctly. This is because these beans use form elements
to indicate their selection to the server.
Single selection in a table is achieved by using the SingleSelectionBean
as the tableSelection
child. Here is a simple example:
<table name="table1" ... >
<tableSelection>
<singleSelection/>
</tableSelection>
...
</table>
Notice that initially none of the rows are marked as being selected. An
initial selection may be set by using the selectedIndex
attribute of singleSelection
to the (zero based) index of the row
that must be selected:
<singleSelection selectedIndex="1" />
The next step is to let the user know what he/she can do with his/her
selection. This can be done by using the text
attribute of singleSelection
. It is also necessary
to add submitButton
s to the layout, so that the
user may click on them when he/she wants to begin processing; the buttons may
be added as indexed children to the selection bean, as in the following
example:
<tableSelection>
<singleSelection text="Select record and ..."
selectedIndex="1" >
<contents>
<submitButton text="Copy" ctrl:event="copy" />
<submitButton text="Delete" ctrl:event="delete" />
</contents>
</singleSelection>
</tableSelection>
How do we handle selection on the server? You may have noticed that the above
submitButton
s both trigger events on the
server. Here is the event handler (the point to note here is the use of oracle.cabo.ui.beans.table.SelectionUtils
to get at the
selected index):
public static EventResult doSelectionEvent(BajaContext bc, Page page,
PageEvent event)
{
DataObject tableRows = new PageEventFlattenedDataSet(event, "table1");
int index = SelectionUtils.getSelectedIndex(tableRows);
String name = "Nothing Selected";
// make sure that something was selected:
if (index>=0)
{
DataObject row = _TABLE_DATA.getItem(index);
name = row.selectValue(null, "name").toString();
}
EventResult result = new EventResult(page);
result.setProperty("action", event.getName());
result.setProperty("name", name);
return result;
}
Multiple selection in a table is achieved by using the MultipleSelectionBean
as the tableSelection
child. Similar to singleSelection
, multipleSelection
also supports the text
attribute and indexed children. Here is an example:
<table ... >
<tableSelection>
<multipleSelection text="Select record and ...">
<contents>
<submitButton text="Copy" ctrl:event="copy"/>
<submitButton text="Delete" ctrl:event="delete" />
</contents>
</multipleSelection>
</tableSelection>
...
</table>
It is possible to set initial selections by data binding the selected
and selection
attributes of multipleSelection
. Each time the
MultipleSelectionBean
is stamped down a column, it
will evaluate its selected
attribute. During this
time, the corresponding row in the selection
DataObjectList
will be the current DataObject
:
<multipleSelection data:selected="selectedKey" ...>
<selection>
<!-- create a dataObjectList, each dataObject has a selectedKey
whose value is either true or false -->
<row selectedKey="true"/> <!-- select the first row -->
<row selectedKey="false"/>
<row selectedKey="true"/> <!-- select the third row -->
...
</selection>
...
</multipleSelection>
In the above example, we set the value of the selection
attribute inline, but this could easily have been
databound (as we have seen before).
Multiple selection is handled on the server using the SelectionUtils.getSelectedIndices(...)
method. This
method returns an array, each element of which is an index of a row that was
selected. The following is the event handler code:
public static EventResult doMultiSelectEvent(BajaContext bc, Page page,
PageEvent event)
{
PageEventFlattenedDataSet tableRows =
new PageEventFlattenedDataSet(event, "table1");
int[] indices = SelectionUtils.getSelectedIndices(tableRows);
DataObjectList resultTableData = new SelectedList(_TABLE_DATA, indices);
EventResult result = new EventResult(page);
result.setProperty("action", event.getName());
result.setProperty("tableData", resultTableData);
return result;
}
The SelectedList
class is a useful utility class;
it takes a DataObjectList
and an array of selected
indices and implements a DataObjectList
that only
contains the DataObject
s that were selected:
private static final class SelectedList implements DataObjectList
{
public SelectedList(DataObjectList data, int[] selectedIndices)
{
_data = data;
_indices = selectedIndices;
}
// returns the number of selected rows
public int getLength()
{
return _indices.length;
}
// gets the selected row at the given index.
public DataObject getItem(int index)
{
return _data.getItem(_indices[index]);
}
private final DataObjectList _data;
private final int[] _indices;
}
multipleSelection
has a "select all/none" feature.
Typically, this means select (or deselect) the table rows that are visible in
the current record set. However, in some applications, this operation must be
performed on all the rows of a record set (not just the ones that are
currently visible). In order to facilitate this, multipleSelection
adds a form parameter named
"selectMode". The value of this parameter is empty, if the user has not
clicked on select-all/none. If the user clicks on select-all, the value of
this parameter would be "all". If select-none was clicked on, the value would
be "none". The event handling code on the server can use this parameter to
determine if the user clicked on select-all/none.
Both singleSelection
and multipleSelection
support a disabled
attribute. When the selection bean is stamped
down a column, the disabled
attribute will be
evaluated and if this returns Boolean.TRUE
, then
the selection feature for that row will be disabled (The DataObject
for the current table row will be the current
DataObject
):
<dataScope>
<provider>
<data name="tableData">
<inline>
<row name="Person 1" age="11" disabledKey="true"/>
<row name="Person 2" age="12" disabledKey="false"/>
<row name="Person 3" age="13" disabledKey="false"/>
<row name="Person 4" age="14" disabledKey="true"/>
</inline>
</data>
</provider>
<contents>
<form name="form1">
<contents>
<table data:tableData="row@tableData" ... >
<tableSelection>
<multipleSelection ...
data:disabled="disabledKey">
</multipleSelection>
</tableSelection>
...
</table>
</contents>
</form>
</contents>
</dataScope>
In some applications, a table row only displays a summary of properties, for a
given element. In these cases it would be nice to have a way of clicking on a
button and opening up a view with all the details pertinent to that row. The
detail-disclosure feature of the TableBean
provides this functionality.
Detail-disclosure is turned on by setting the detail
named child of the table
. This child is just another stamp; except, it is
rendered only when the user has asked for the details of a row. Therefore,
this child may show all the specifics of the current row. Here is an example:
<table ... >
<detail>
<!-- this is the detailed stamp -->
<labeledFieldLayout>
<contents>
Name
<styledText data:text="name" styleClass="OraDataText"/>
Age
<styledText data:text="age" styleClass="OraDataText"/>
Blood Group
<styledText data:text="blood" styleClass="OraDataText"/>
Phone
<styledText data:text="phone" styleClass="OraDataText"/>
</contents>
</labeledFieldLayout>
</detail>
<columnHeaderData>
<col text="Name"/>
<col text="Age" />
</columnHeaderData>
<contents>
<!-- these are the regular column stamps -->
<text data:text="name"/>
<text data:text="age"/>
</contents>
</table>
In the above example, the regular table displays only the Name and
Age properties of each row. When the user asks for details, the detail
stamp is rendered, providing additional
information such as Blood Group and Phone (Notice that the detail
child is rendered with the current table row
DataObject
as the current DataObject
; so the data binding works the same way as
for a column stamp).
In the above example, however, none of the detail sections were disclosed. So
it was not possible to see a detail
stamp
rendered. The disclosure state of each arrow is controlled by the detailDisclosure
attribute of the table
. This must be a DataObjectList
with a DataObject
for each row of the table. Each DataObject
is queried with the key UIConstants.DISCLOSED_KEY
(or disclosed
in uiXML). If the result of this query is Boolean.TRUE
then the detailed information for that row
will be disclosed. The following is an example:
<table ... >
...
<detailDisclosure>
<row disclosed="false"/>
<row disclosed="false"/>
<row disclosed="true"/> <!-- disclose the third row -->
<row disclosed="false"/>
<row disclosed="false"/>
<row disclosed="true"/> <!-- disclose the sixth row -->
</detailDisclosure>
</table>
In the previous examples the detail-disclosure arrows don't actually do
anything; the examples are purely cosmetic. Let's see how we can make an
interactive demo. When an arrow is clicked a UIX Controller event is
generated: either UIConstants.SHOW_EVENT
or HIDE_EVENT
("show" or "hide" in uiXML) depending on
whether the details must be shown or hidden (respectively). The event has the
following two parameters:
Parameter | UIConstant | Description |
---|---|---|
source | SOURCE_PARAM
| Identifies the table that generated this event. This would be the table's
name attribute.
|
value | VALUE_PARAM
| Identifies the row that must be disclosed (or undisclosed). This is a zero based index. |
The value parameter makes it easy to know which row must be disclosed/undisclosed. However, we don't have enough information to know what rows have already been disclosed. If we want to preserve the current disclosed state of the table, then we need to add some more state to the client-side page. In this example we will add a hidden form parameter named "disclosed" to the detail section (notice that now the table must be used in form submitted mode):
<table formSubmitted="true" ... >
...
<detail>
<labeledFieldLayout>
<contents>
...
<formValue name="disclosed" value="1"/>
</contents>
</labeledFieldLayout>
</detail>
</table>
In the following event handler, we get the index of the row that the user chose and decide whether we want to disclose (or undisclose) depending on the event name:
public static EventResult doHideShowEvent(BajaContext bc, Page page,
PageEvent event)
{
PageEventFlattenedDataSet tableRows =
new PageEventFlattenedDataSet(event, "table1");
// this is the row that must be (un)disclosed:
int row = Integer.parseInt(event.getParameter(UIConstants.VALUE_PARAM));
// decide whether we want to disclose or undisclose depending on the name
// of the event
boolean disclose = UIConstants.SHOW_EVENT.equals(event.getName());
DataObjectList detailData = new DetailData(tableRows, row, disclose);
EventResult result = new EventResult(page);
result.setProperty("detailData", detailData);
return result;
}
In the above code we create a PageEventFlattenedDataSet
which contains the current
disclosure state of the table. We use it to create a detailData DataObjectList
. This list encompasses the
previous disclosure state of the tree and new state. This is then pushed onto
the EventResult
so that it may be accessed from
the uiXML code. The class DetailData
is presented
below:
private static final class DetailData implements DataObjectList
{
/**
* @param pageEvent contains the current disclosure state of the table
* @param index the index of the row that must have its disclosure state
* changed
* @param disclosure the new disclosure state for the row
*/
public DetailData(DataObjectList pageEvent, int index, boolean disclose)
{
_pageEvent = pageEvent;
// initially, none of the table rows will be disclosed, so there will be
// no pageEvent data and this length would be zero:
_length = pageEvent.getLength();
_index = index;
_disclose = disclose;
}
public int getLength()
{
// make sure that the length we return is sufficiently large enough that
// we reach the index we want to change
return (_index >= _length) ? _index+1 : _length;
}
public DataObject getItem(int index)
{
boolean disclose;
if (index==_index)
{
// this is the index that we want to change.
disclose = _disclose;
}
else if (index < _length)
{
// this index can safely be pulled from the pageEvent
DataObject row = _pageEvent.getItem(index);
// if there was a "disclosed" form element on this row then we
// consider the row disclosed:
disclose = (row.selectValue(null, "disclosed") != null);
}
else
disclose = false;
return disclose ? _DISCLOSE_TRUE : _DISCLOSE_FALSE;
}
private final DataObjectList _pageEvent;
private final int _index, _length;
private final boolean _disclose;
private static final DataObject _DISCLOSE_TRUE = new DataObject() {
public Object selectValue(RenderingContext rc, Object key)
{
return Boolean.TRUE;
}
};
private static final DataObject _DISCLOSE_FALSE = new DataObject() {
public Object selectValue(RenderingContext rc, Object key)
{
return Boolean.FALSE;
}
};
}
Basically, what this class does, is use the table's current disclosure
state to determine which rows have been disclosed and return the appropriate
DataObject
for true (if disclosed) or false (if
undisclosed). If the index is the index we want to change, then the new
disclosure state is returned. The only tricky thing here is to remember that
PageEventFlattenedDataSet
only has data in it, if
there were form input elements in the table. In the initial state, nothing is
disclosed, so there will not be any input elements rendered, and the FlattenedDataSet
would return zero as its
length. Therefore, we need to handle this special case carefully.
There are some cases where it is appropriate to summarize the data in a
table in a special section located at the bottom of the data area. This is
known as the column footer, and it, too is a named child of the
table. However, unlike the column header stamp and row header stamp, the
column footer is not a stamp. It is rendered only once at the bottom of the
table instead of being stamped across every column. This is by design; most
column footers summarize table-wide statistics or actions rather than
per-column actions. In fact, there are two special UINode
s that are intended to be used only as column
footers: addTableRow
and
totalRow
. Let's examine them both.
AddTableRow
BeanFirst up is addTableRow
. This column footer
node is intended for use in tables that need to allow the user to add
additional data rows at runtime. You can customize the number of rows to add,
the text it displays, and alter its URL destination. All will default if not
specified, however. Here is an example of one in use:
<table ... >
...
<!-- column footer node -->
<columnFooter>
<addTableRow rows="5"/>
</columnFooter>
</table>
Although this example prompts the user to add 5 rows, without that rows
attribute one new row will be requested when the
bean is activated. Also, this is another table customization which requires
that events be sent to the server. Rows are not added on the fly; the page
must be regenerated with the additional rows included, which means that the
server must be notified and update its data source. Here are the (now
surprisingly familiar) parameters which are sent to the server on a UIConstants.ADD_ROWS_EVENT
(or "addRows" in uiXML)
event:
Parameter | UIConstant | Description |
---|---|---|
source | SOURCE_PARAM
| Once again, the source is the name of the table. |
size | SIZE_PARAM
| The number of rows requested to be added. |
TotalRow
BeanSimilar to the previous bean is the totalRow
. Instead of adding rows, this bean is used to
show the total for numerical columns in a table. Note that, at this time, the
bean itself does not calculate totals; rather, it will display any totals
calculated by the server or by Javascript in its properly formatted
controls.
The totalRow
serves two purposes: it renders a
button in the column footer allowing the user to update the total and it
allows controls to be rendered under each column to hold the total value for
that column. The button is rendered automatically, with text that can be
configured using the bean's text
attribute. Activating the button will once again send an event to the server;
this time, the event is UIConstants.UPDATE_EVENT
(or "update" in uiXML).
Parameter | UIConstant | Description |
---|---|---|
source | SOURCE_PARAM
| The source is the name of the table. |
Unlike addTableRow
, totalRow
allows indexed children, which it uses as the
holders of the total values for each column. The first indexed child of the
totalRow
will be placed under the
rightmost column of the table, with subsequent indexed children being
placed to the left of the previous ones. Thus, the indexed children will
appear under each column from right to left as they are added, as shown in the
following example:
<table ... >
...
<!-- column footer node -->
<columnFooter>
<totalRow>
<contents>
<textInput name="total" text="42" columns="5"/>
</contents>
</totalRow>
</columnFooter>
</table>
Again, at this time it is up to the table creator to actually perform the updates to any total indexed children nodes when the update button is activated.
Sometimes it is necessary to have both a totalRow
and an addTableRow
in a table. This can be done by
adding the totalRow
as an indexed child of the
addTableRow
, as in the following example:
<table ... >
...
<columnFooter>
<addTableRow>
<contents>
<!-- addTableRow beans may have at most a single child,
and this child can only be a totalRow bean -->
<totalRow>
<contents>
<textInput name="total" text="400" />
</contents>
</totalRow>
</contents>
</addTableRow>
</columnFooter>
</table>
Up until now, we've been concerned only with the actual content of the table cells and a few actions that communicate with the server. Sometimes it is necessary to change the appearance or format of our table in order to clarify the structure of the data, or just to make it easier to read. There are many ways to customize a table format, perhaps a dizzying array of combinations. Fortunately, the mechanism for achieving these different customizations is consistent.
The easiest way to change the appearance of a table is to change its width,
which can be done just by setting the width
attribute on the table. Although the width can be specified either by pixels
or percentages of the width of the parent element, percentages are commonly
preferred. Note, though, that no matter what value is specified as the desired
table width, if it is not large enough to accommodate the content it will be
overridden at display time to take up the minimum amount of necessary
space.
Let's move to the alignment of the content cells. According to the user
interface designs, different types of content should be aligned differently to
make them easier for users to understand. This can be achieved using a new set
of DataObject
s used for formatting. In this case,
since alignment should be uniform down the cells of a given column, we will
use the table's columnFormat
attribute. Like many
other data-driven table attributes, this one is also a DataObjectList
. In this case, each DataObject
in the list corresponds to one of the
columns, from left to right in the table layout. Specifying formats inside
any of these DataObject
s causes the corresponding
column to change its appearance accordingly. Let's take a look at a first
example:
<dataScope ... >
<provider>
<!-- all the data used by our table demo -->
<data name="demoTableData">
<inline>
...
<!-- DataObjectList to provide column formatting -->
<demoColumnFormats/>
<demoColumnFormats columnDataFormat="numberFormat"/>
<demoColumnFormats columnDataFormat="iconButtonFormat"/>
</inline>
</data>
</provider>
<contents>
<form ... >
<contents>
<table ...
width="100%"
data:columnFormats="demoColumnFormats@demoTableData">
<contents>
<!-- the first column stamp, a text node -->
<textInput data:text="firstColumnText" name="foo"/>
<!-- a second column stamp, a static text node -->
<text text="42"/>
<!-- the third column stamp, a button -->
<button data:text="secondColumnText" destination="http://www.example.org"/>
</contents>
...
</table>
</contents>
</form>
</contents>
</dataScope>
We changed a few things in this example, so let's explore them in turn. We
set the table to take "100%" width. Then, we added another column stamp to the
table -- a text node -- to give us more to work with. Next, we created a new
DataObjectList
like before, but we called this one
"demoColumnFormats". The DataObject
s in this list
will provide formatting to each of the three columns, in turn. Inside these
DataObject
s, we put some formatting data, which we
will describe in depth soon. Finally, we set a new attribute on the table
itself, columnFormats
, which points to our new
DataObjectList
.
Let's look more closely at the the DataObject
s
used to format our table. We've already indicated that they supply the
formatting information, but how does this work? Unlike other DataObject
s that store values under arbitrary keys
chosen by the implementor, formatting objects must store their values under
known, publicized keys. When rendering, the table will ask each of these
column format DataObject
s for the values stored
under each of these public keys. If the DataObject
returns a value for that key, it is used to change the column format; if not,
the table defaults that particular format.
In our example, the format DataObject
s have
stored a value under the "columnDataFormat" String, one of the well known
keys. This key determines the alignment of the content in the column based on
what type of content is claimed to be inside it. The first DataObject
does not return any value for that key, which
causes it to default its alignment to that of plain text content, or to the
left. The second DataObject
provides the value
"numberFormat" when queried. Since the specifications dictate that numbers
should be right-aligned, the data cells in this table column are aligned
accordingly. Finally, the third column returns "iconButtonFormat" for its data
type, which currently causes its content to be center-aligned, the proper
alignments for columns of icons or buttons. This should be clear when you
view the previous example.
There are four keys that column format DataObject
s can return values for. Let's see an
example that illustrates them all:
<dataScope ... >
<provider>
<!-- all the data used by our table demo -->
<data name="demoTableData">
<inline>
...
<!-- DataObjectList to provide column formatting -->
<demoColumnFormats cellNoWrapFormat="true"/>
<demoColumnFormats columnDataFormat="numberFormat"
displayGrid="false"/>
<demoColumnFormats columnDataFormat="iconButtonFormat"
width="100%"/>
</inline>
</data>
</provider>
<contents>
...
</contents>
</dataScope>
Using this example as a guide, we can summarize all of the possibilities
for formatting on a column format DataObject
:
Key UIConstant |
Description |
---|---|
columnDataFormatCOLUMN_DATA_FORMAT_KEY
|
This key is used to specify the type
of content being displayed in the table column because, unfortunately, the
table is not smart enough to figure this out for itself. Different data
formats cause changes in alignment for the data cells:
|
cellNoWrapFormatCELL_NO_WRAP_FORMAT_KEY
| Sometimes it is preferable to not allow
the contents of a cell to wrap to multiple lines. If this is desired for a given
data column, simply make its column format DataObject return "true" (Boolean.TRUE ) when queried
with this key. This is not the default behavior because disallowing wrapping
can cause tables to become too wide, and is thus discouraged.
|
widthWIDTH_KEY
| Used to indicate what percentage of extra width in the table this
particular column should take. If any table takes up more space than its
content needs, it will apportion that space among its columns. However, some
columns (like buttons) don't need any more space than the minimum they are
given. Other columns, like those containing text, should take extra space if
it allows them to reduce the number of lines needed to display their
content. Column format DataObject s can take
percentage values, like "50%" or "100%" to indicate how much of the extra
space that column should receive. The total among all the columns should add
to 100%.
|
displayGridDISPLAY_GRID_KEY
| By default, a grid line is shown to the left of every column. In some cases, you may want to use vertical grid lines more sparingly to emphasize the relationship between some columns and de-emphasize it between others. If you wish to turn off a vertical grid line located before, or to the left, of a given data column, just return "false" when that column is queried with the this key. |
It is important to note that all of these format values are hints, not hard and fast rules. Some rendering looks or platforms may not support all formatting values, so don't be surprised if a given rule doesn't work in some agents. Formatting should never be considered critical to an application, as it is not depdendable.
Just as columns can control where vertical grid lines appear, it is also possible to customize where horizontal grid lines appear for any given row. This is done using a "rowFormats" attribute on the table, as shown in the following example:
<dataScope ... >
<provider>
<!-- all the data used by our table demo -->
<data name="demoTableData">
<inline>
...
<!-- DataObjectList to provide row formatting information -->
<demoRowFormats/>
<demoRowFormats displayGrid="false"/>
</inline>
</data>
</provider>
<contents>
<form ... >
<contents>
<table ...
data:rowFormats="demoRowFormats@demoTableData">
...
</table>
</contents>
</form>
</contents>
</dataScope>
In this example, we've added a row format DataObject
which requests that no horizontal grid be
shown above the second row. As you can see, it works, although it is rare that
this functionality should ever be used.
Also available for rare cases are columnHeaderFormats
and rowHeaderFormats
, which are also DataObjectList
s that can be set as attributes on the
table itself. The only key that these list DataObject
s respond to is the "cellNoWrapFormat" key,
previously mentioned. Each DataObject
corresponds
to a column or row header and controls its ability to wrap its content. Again,
the default is to allow wrapping, as not doing so can cause the table to grow
too wide for the user's taste. These formatting attributes should be used
sparingly.
The final type of formatting we will examine here is banding. Banding
refers to the ability to alternate colors of cells or rows in a table so that
the content of those cells or columns is grouped visually. There are two ways
to control banding. The first, easiest, and most common is to specify
table-wide banding using a tableFormat
DataObject
. This single formatting object applies to the entire table,
which is why it is a DataObject
instead of a DataObjectList
.
By default tables have no banding, but if a tableFormat DataObject
is set on the table, it will be
queried with the key "tableBanding" (UIConstants.TABLE_BANDING_KEY
). Should the DataObject
return the well-known value "columnBanding"
(UIConstants.COLUMN_BANDING
), alternating columns
of the table will have different background colors, as demonstrated in this
example:
<dataScope ... >
<provider>
<data name="demoTableData">
<inline>
...
<!-- DataObject for table-wide formatting (i.e. banding) -->
<demoTableFormat tableBanding="columnBanding"/>
</inline>
</data>
</provider>
<contents>
<form ... >
<contents>
<table ...
data:tableFormat="demoTableFormat@demoTableData">
...
</table>
</contents>
</form>
</contents>
</dataScope>
Similarly, returning the value "rowBanding" (UIConstants.ROW_BANDING
) will cause rows of the table to
alternate their background colors, as the next example shows. You can also
return an integer when queried with the "bandingInterval" (UIConstants.BANDING_INTERVAL_KEY
) key to control the
number of rows in each alternating band.
<dataScope xmlns="http://xmlns.oracle.com/uix/ui"
xmlns:data="http://xmlns.oracle.com/uix/ui">
<provider>
<data name="demoTableData">
<inline>
...
<!-- DataObject for table-wide formatting (i.e. banding) -->
<demoTableFormat tableBanding="rowBanding"/>
</inline>
</data>
</provider>
<contents>
...
</contents>
</dataScope>
Column banding, however, can also be achieved in arbitrary patterns. This
allows individual columns to take the light banding column without using a
regular alternation pattern. To achieve this affect, simply have one or more
of the column format DataObject
s (described above)
return either "light" (BANDING_SHADE_LIGHT
) or
"dark" (BANDING_SHADE_DARK
) when queried with the
"bandingShade" (UIconstants.BANDING_SHADE_KEY
)
key. In the next example, only the third column will get a light band
color:
<dataScope ... >
<provider>
<data name="demoTableData">
<inline>
...
<!-- DataObjectList to provide column formatting -->
<demoColumnFormats cellNoWrapFormat="true"/>
<demoColumnFormats columnDataFormat="numberFormat"
displayGrid="false"/>
<demoColumnFormats columnDataFormat="iconButtonFormat"
width="100%"
bandingShade="light"/>
<!-- DataObjectList to provide row formatting information -->
<demoRowFormats/>
<demoRowFormats displayGrid="false"/>
<!-- DataObject for table-wide formatting (i.e. banding) -->
<demoTableFormat tableBanding="rowBanding"/>
</inline>
</data>
</provider>
<contents>
...
</contents>
</dataScope>
Note that if banding is explicitly set for any column format object, the table-wide formatting will be completely ignored. It is not permissible to mix table-wide banding with explicit column banding, as this would produce odd coloration patterns.
TableStyle
classoracle.cabo.ui.beans.table.TableStyle
provides a
convenient way for Java programmers to set most of these formatting
options. For example, to create a format for a column with numbers and no grid
use:
DataObject columnFormat = new TableStyle(TableStyle.HIDE_GRID_MASK |
TableStyle.NUMBER_FORMAT_MASK);
In the previous section we've discussed setting column stamps, setting columnHeaderStamp
s, binding columnHeaderData
and setting columnFormats
. Each of these properties involved setting
some global attribute on the table. It would be nice if we had a way to set
all the properties (pertinent to a single column) in a single location. The
ColumnBean
helps you do just that. Here is an
example of using it:
<table ... >
<contents>
...
<column>
<columnFormat columnDataFormat="iconButtonFormat"
bandingShade="light" width="50%" displayGrid="false"/>
<columnHeader>Third Column</columnHeader>
<contents>
<!-- This is the column stamp -->
<button data:text="secondColumnText"/>
</contents>
</column>
</contents>
</table>
The first thing to note is that the column
element
has been set as a column stamp in the table. It supports a named child columnHeader
(which is similar to the columnHeaderStamp
in the table
) which is used as the stamp for this column's
header. A columnHeaderData
attribute is supported
in case it is necessary to data bind the stamp. Both columnFormat
and columnHeaderFormat
are supported to allow the column to
be formatted.
It is also worth noting that the column
element
works in harmony with the other attributes of the table (as the above example
demonstrates). However, any attribute set on the column
will overwrite the corresponding attribute on the
table
.
By using JavaScript, certain Table
operations can
be performed on the client-side. All of these operations require the TableProxy
JavaScript object. In order to use this
object, the table's proxied
attribute must be set
to Boolean.TRUE
:
<table proxied="true">
...
</table>
The following is an example of using the TableProxy
to read table elements. In it we sum up all
the values in a column and update the total. The following is the uiXML code:
<dataScope>
<provider>
<data name="tableData">
<inline>
<row name="Person 1" cost="11" />
<row name="Person 2" cost="12" />
<row name="Person 3" cost="13" />
<row name="Person 4" cost="14" />
...
</inline>
</data>
</provider>
<contents>
<form name="form1">
<contents>
<table proxied="true" data:tableData="row@tableData"
name="table1" ... >
<contents>
<text data:text="name"/>
<textInput name="cost" data:text="cost"/>
</contents>
<columnFooter>
<totalRow destination="javascript:updateTotal();">
<contents>
<textInput name="total" text="50"/>
</contents>
</totalRow>
</columnFooter>
</table>
</contents>
</form>
</contents>
</dataScope>
When the update button is clicked, the updateTotal()
JavaScript method is called. This method
creates a new TableProxy
using the table name
, gets the number of rows in the table by calling
the getLength()
method, gets the form element in
the column (for each row), sums the values and writes the total to the
appropriate text field. Notice the use of the getFormElement(...)
method; this method takes an element
name
and the row index, and returns that element.
function updateTotal()
{
var proxy = new TableProxy('table1');
var rowCount = proxy.getLength();
var total = 0;
for(var i=0; i<rowCount; i++)
{
var currTextField = proxy.getFormElement('cost', i);
// the minus zero here is necessary to convert the string value
// to a number
total += currTextField.value - 0;
}
document.form1.total.value = total;
}
When a table is used in single selection mode, it is possible to use the TableProxy
's getSelectedRow()
method to get the index of the
currently selected row. Here is an example:
function hire()
{
var proxy = new TableProxy('table1');
var row = proxy.getSelectedRow();
// check to make sure that something was selected
if (row < 0)
{
alert('You have not chosen anyone!');
}
else
{
var name = proxy.getFormElement('theName', row).value;
alert('You have chosen to hire '+name);
}
}
The above function is called by the following uiXML:
<table name="table1" ...>
<tableSelection>
<singleSelection ...>
<contents>
<button text="Hire" destination="javascript:hire()"/>
</contents>
</singleSelection>
</tableSelection>
<contents>
<text data:text="name"/>
<text data:text="cost"/>
<formValue name="theName" data:value="name"/>
</contents>
</table>
Similarly, use the getSelectedRows()
method on the
TableProxy
to get the selected row indices in
multiple selection mode. This method returns an array, each element of which
is an index of a selected row. Here is some sample JavaScript code:
function recruit()
{
var proxy = new TableProxy('table1');
var rows = proxy.getSelectedRows();
var length = rows.length;
// make sure that something was selected
if (length > 0)
{
var list = "";
// loop through each selected row and concatenate the name
for(var i=0; i < length; i++)
{
// get the next selected row index
var row = rows[i];
// get the selected row (from the index) and pull out the name
var name = proxy.getFormElement('theName', row).value;
list += '\n'+name;
}
alert("You have chosen to recruit "+list);
}
else
{
alert("You have not chosen anyone to recruit!");
}
}
This method would be called from the following uiXML:
<table ... >
<tableSelection>
<multipleSelection text="Select ...">
<contents>
<button text="Recruit" destination="javascript:recruit()"/>
</contents>
</multipleSelection>
</tableSelection>
...
</table>
This chapter talked about the TableBean
and how to use it to display tabular data and implement record navigation, sorting, selection, detail-disclosure and totaling. For more information, see the JavaDoc for the TableBean
or consult the uiXML Element Reference for the <table>
element.