UIX Developer's Guide |
![]() Contents |
![]() Previous |
![]() Next |
This chapter tells how to create pages that generate dynamic content for each user, even though the node tree itself remains unchanged, by using uiXML's data binding attributes. You will learn how UIX supports JavaBeans and Maps
for grouping data, and how UIX iterates through data with arrays and Lists
. You'll also learn how to build inline data directly into your uiXML for prototyping. Next, Java developers can learn about the DataObject
, BoundValue
, DataProvider
, and DataObjectList
interfaces that support this functionality, and how you can use these Java APIs to provide more customized implementations of data binding.
Finally, you'll learn the details about the UIX support for JavaBeans.
This chapter contains the following sections:
The next several sections will introduce UIX data binding from the perspective of a uiXML developer. If you're only developing UIX with the Java API, you might skip ahead to Data binding from the Java API.
Let's start with the simplest possible example, the very first example from the last chapter:
<text xmlns="http://xmlns.oracle.com/uix/ui"
text="hello, world"/>
Now, let's add support for data binding "hello, world" to something more interesting. In particular, let's give ourselves a very simple JavaBean that can display the current date and time:
package yourpackage;
import java.util.Date;
public class CurrentDateBean
{
public CurrentDateBean() { }
public String getTime()
{
return (new Date()).toString();
}
}
Now, we want to change our page so that it uses
getTime()
. We'll need to do three things:
First, we'll data bind "text":
<text xmlns="http://xmlns.oracle.com/uix/ui"
xmlns:data="http://xmlns.oracle.com/uix/ui"
data:text="time@currentDate"/>
We've made three small changes to the example:
If you tried running this example, you'd see absolutely nothing. That's because we haven't given "currentDate" to the page, so the databinding failed, and the "text" is left to null. We do this by adding <dataScope> to the page:
<dataScope xmlns="http://xmlns.oracle.com/uix/ui"
xmlns:data="http://xmlns.oracle.com/uix/ui">
<provider>
<data name="currentDate">
<method class="yourpackage.DataDemo" method="getCurrentDate"/>
</data>
</provider>
<contents>
<text data:text="time@currentDate"/>
</contents>
</dataScope>
This looks like a much greater change. Let's walk through what's happened:
getCurrentDate()
method in the
yourpackage.DataDemo
class. There's a number of elements
that can go inside <data>, but we'll stick to <method>
here.
If you try typing in this page and running it, you'll get an error when UIX can't find the "yourpackage.DataDemo" class. We still need to write that class:
package yourpackage;
import oracle.cabo.ui.RenderingContext;
public class DataDemo
{
static public Object getCurrentDate(
RenderingContext context, String namespace, String name)
{
return new CurrentDateBean();
}
}
This is another simple class. Java data providers you write that
are referenced by the uiXML <method>
element all
must have the same signature you see here. We don't need any of these
three parameters in this example - we just need to return our
JavaBean. You can, if you wish, move this method into the
CurrentDateBean
class, but we generally recommend against
that coding style. Your JavaBeans should be designed to be reusable
in any architecture, and as free as possible of dependencies on UIX,
Servlets, or any other technology.
Before we continue, let's consider two questions:
DataDemo.getCurrentDate()
be called?
DataDemo.getCurrentDate()
will be called once each
time the page renders. Even if CurrentDateBean
had multiple properties, and we used all of them, the bean will
get loaded once. But getCurrentDate()
will get
called every time the page renders. If you have to display the
page 100 times, this method will be called 100 times.
CurrentDateBean.getTime()
be called?
CurrentDateBean.getTime()
will be called at least once.
UIX can't guarantee that it will ask for each property only once; it
may be necessary to ask repeatedly. Since getTime
always
creates a new Date
object, if you use "time" repeatedly
on a page, you may see different times in the page. If this is a
problem, you could create the Date
in the
CurrentDateBean
constructor and reuse the same object on
each call to getTime()
.
JavaBeans are an elegant way to encapsulate data, but it can
be tedious writing a new JavaBean class for every bit of state
you want to hand to UIX, and tedious to add new "get" methods
every time a new property arises. And sometimes, it's impossible
to know the full list of property names in advance - you'll
only discover the names at run-time. UIX supports the Java
Map
interface for all these scenarios.
Here's a data provider that uses a map instead of a JavaBean:
package yourpackage;
import oracle.cabo.ui.RenderingContext;
import java.util.HashMap;
public class DataDemo
{
static public Object getURLAndText(
RenderingContext context, String namespace, String name)
{
HashMap map = new HashMap();
map.put("text", "Oracle Corporation");
map.put("url", "http://www.oracle.com");
return map;
}
}
And, here's the UIX that consumes it:
<dataScope xmlns="http://xmlns.oracle.com/uix/ui"
xmlns:data="http://xmlns.oracle.com/uix/ui">
<provider>
<data name="link">
<method class="yourpackage.DataDemo" method="getURLAndText"/>
</data>
</provider>
<contents>
<text data:text="text@link"
data:destination="url@link"/>
</contents>
</dataScope>
It's very simple to develop this way, but not quite as elegant as using JavaBeans. It's a matter of personal taste which to use at which times, and most developers will want to use both techniques.
Let's extend our first example a bit further. Instead of simply
displaying the time, let's show "The time is: " followed by the time.
We could just add another <text>
element containing
the static text, but we'll take a different approach. When you use
"data:", you can get a value off of a JavaBean, but you can't manipulate
that value. The <boundAttribute>
element lets
you go further. You can perform some very complicated expressions;
we'll use a simple concatentation rule:
<dataScope xmlns="http://xmlns.oracle.com/uix/ui"
xmlns:data="http://xmlns.oracle.com/uix/ui">
<provider>
<data name="currentDate">
<method class="yourpackage.DataDemo" method="getCurrentDate"/>
</data>
</provider>
<contents>
<text>
<boundAttribute name="text">
<concat>
The time is:
<dataObject select="time" source="currentDate"/>
</concat>
</boundAttribute>
</text>
</contents>
</dataScope>
The only part of our example that's changed is the
<text>
element. We're no longer databinding "text"
using "data:text"; instead, we're using
<boundAttribute>
. It contains a
<concat>
element that joins together the "time"
from our bean (grabbed with a <dataObject>
element)
with a static string.
There are dozens of elements that can be used inside
<boundAttribute>
, and we're not going to go into
all of them here. You can look at these elements using Code Insight
in the uiXML editor in JDeveloper, or by browsing the UIX Element
Reference. These elements are called BoundValue
elements
after the Java interface to which they correspond. Some of the other
useful BoundValue
elements include:
Many of these will be described in detail in later chapters, but see the Element Reference for the full list.
By now, you can set up pages with dynamic data. However, the
interfaces you've seen only support unordered bags of properties.
It's a very inconvenient interface if you need to display tabular
data, or any other data that looks like a Java array. UIX supports
Lists
and Java arrays for iteration. It also supports a
UIX-specific API, DataObjectList
, as you'll see later.
Now, how to get the data back out of these arrays and
Lists
and into your pages? For this, you'll need
something UIX calls the "current DataObject
."
Any data framework has to support iterating through data. For Java,
it's a "for" loop. For a database, it's a cursor. For UIX
Components, it's the "current DataObject
." To
get data out of the current DataObject
, you omit the
source altogether:
<styledText>
<boundAttribute name="text">
<dataObject select="textKey"/>
</boundAttribute>
</styledText>
<!-- OR -->
<!-- Note that there's no "@" sign here -->
<styledText data:text="textKey"/>
You can explicitly set the current DataObject
using
the "currentData" attribute of a <dataScope>:
<dataScope data:currentData="currentDate">
<!-- Now we have a current data object -->
...
</dataScope>
... but this is not the usual way it's used. More commonly, you
use a component that will automatically step through your
List
or array for you and automatically set teh current
DataObject
. The UIX <table> is one such component.
We're not going to discuss <table> in a lot of detail here - it
has enough features to merit its own chapter
- but we'll describe it just enough to understand how the current
DataObject
works.
Let's start by building up a List
for our table.
Here, we'll use an ArrayList
of Maps
. We
could just as easily use an array of Maps
, or an array of
JavaBeans
, etc.
package yourpackage;
//...
import java.util.List;
import java.util.ArrayList;
public class DataDemo
{
// ...
static public Object getTableData(
RenderingContext context, String namespace, String name)
{
List list = new ArrayList();
for (int i = 0; i < 5; i++)
{
HashMap row = new HashMap();
row.put("text", "Row " + i);
row.put("text2", "Column 2, row " + i);
list.add(row);
}
return list;
}
}
Now, we'll put this data into a <table>
. Each
column of a <table>
is represented by a single
child inside <contents>
. If you have three such
children, the table will have three columns.
The trick lies in getting each child to render differently in each
row. And this happens with the "current DataObject
".
When the table is rendering its first row, the current
DataObject
will point at the first Map
in
our List
. When the second row is rendering, the current
DataObject
will point at the second Map
in
our List
. And so forth.
So, let's add two columns to our table, first a normal text column, then a column in red. (In this example, we're using the built-in "OraErrorText" CSS style - one of the built-in UIX styles.):
<dataScope>
<provider>
<data name="someTableData">
<method class="yourpackage.DataDemo" method="getTableData"/>
</data>
</provider>
<contents>
<table data:tableData=".@someTableData">
<contents>
<styledText data:text="text"/>
<styledText styleClass="OraErrorText" data:text="text2"/>
</contents>
</table>
</contents>
</dataScope>
And done. Not much code, but there's a lot fine points.
First, the <table> element gets its data, using a data bound
"tableData" attribute. The oddest thing here is that period (".") in
our data binding expression. If you remember how we wrote
getTableData()
, we returned the ArrayList
.
For the table's data, we don't want a property of the
ArrayList
- we want the ArrayList
itself.
This is a magic syntax that accomplishes that.
Next, we've added two children. The first is a
<styledText>
that gets its text from the "text"
key. Note the absence of any at-sign ("@") here - that means "get
'text' from the current DataObject". The second is another
<styledText>, with its text bound to a different key. We can
add columns, remove columns, and reorder columns directly in the UI,
without changing our data model at all.
This is a trivial example: we haven't added column or row headers,
or any of roughly one zillion other things the table can do. And
we've only used ordinary, uneditable text. But all of these features
work with the same technique. Any time you need something to render
differently from one row to the next in a table, there must be some
piece of information in the "current DataObject
" that
describes that difference.
For example, say you want a link in each row, but it goes to a
different location. Simple! Use a <link>
element,
databind the "destination" attribute to the current
DataObject
, and update your data to include a destination
in each row. In UIX, everything can be data bound - so every row can
be different.
This section describes how you can nest keys and data objects to produce more complex data bindings. First of all, use the following form to produce a nested key data binding:
data:attributeName="keyN@...@key2@key1@name"
In this case, when the value of this attribute needs to be computed, key1
is used with the DataObject
referenced by name
to produce another DataObject
. key2
is used
with this second DataObject
to produce a third
DataObject
. This process continues until keyN
is used to produce the final value of this
attribute. Note that all the DataObject
values
must themselves be DataObject
s; only the value of
the last key, keyN
, need not be a DataObject
(it must be whatever type is required by the
attribute). The following is an example of nested key binding:
<dataScope>
<provider>
<data name="families">
<inline>
<Smith members="4">
<address number="2255" street="37th Ave" city="San Francisco"
state="CA" zip="94116" />
</Smith>
<Jones members="3">
<address number="500" street="Oracle Parkway" city="Redwood Shores"
state="CA" zip="94065" />
</Jones>
</inline>
</data>
</provider>
<contents>
<messageTextInput prompt="The Smiths live in"
data:text="city@address@Smith@families" /> <!--San Francisco-->
<messageTextInput prompt="The Jones's live in"
data:text="city@address@Jones@families" /> <!--Redwood Shores-->
</contents>
</dataScope>
In the above example, the first messageTextInput
produces The Smiths live in San Francisco, while the second produces
The Jones's live in Redwood Shores.
Parentheses may be used to produce nested data object bindings. Consider the following example:
data:attributeName="(key1@name1)@name2"
In the above example key1
is used with (the DataObject
referenced by) name1
to produce the key that will be used with name2
to produce the final value of this attribute. This
is an example of the key itself being data bound. The data object part can
also be data bound; the following two examples are equivalent:-
<!-- These two are equivalent -->
data:attributeName="key2@(key1@name)"
data:attributeName="key2@key1@name"
The following is a parenthesis example using the families
data structure created in the previous example;
it produces The Jones family has 3 members.
<dataScope>
<provider>
<data name="families">
... as above ...
</data>
<data name="selection">
<inline current="Jones" />
</data>
</provider>
<contents>
The<link data:text="current@selection"/> family
has<link data:text="members@(current@selection)@families" />
members.
</contents>
</dataScope>
Parentheses may be nested as in the following example:
data:attributeName="((key1@name1)@name2)@name3"
In the above example, key1
is used with name1
to produce the key that will be used with name2
to produce the key for name3
that would compute the final value.
Text before and after the @ symbol is optional. If the key is left out,
then the DataObject
itself is returned, as in the
following example:
<!-- The following attribute is bound to the data object itself -->
data:attributeName="@name"
And if the data-object-name part is left out, the key will be used with the current data object (this is the default case when no @ symbol is provided), as in the following examples:
<!-- These keys are used with the current data object -->
data:attributeName="key@"
data:attributeName="key"
If no @ symbol is provided an implicit one is assumed (as in the second binding in the above example). The following example illustrates this further:
<!-- These are different bindings -->
data:attributeName="key@name"
data:attributeName="(key@name)"
The two bindings in the above example are different; the latter binding has an
implicit @ and could have been written as data:attributeName="(key@name)@"
. In the latter case,
the value returned by applying key
to the data
object name
is used as a key into the current data
object.
There are certain restrictions on keys and data object names; for example, you can't have the symbol @ as part of a key's text. A future version of UIX will allow you to escape such characters and permit their usage in UIX.
The DataObject
Java interface is the generic data source
for UIX Components and uiXML pages. When you hand JavaBeans or Maps
to UIX, we convert those objects into DataObject
s with a
lightweight adapter class. We'll discuss that adapter later;
here, we'll talk about the Java API for
DataObject
itself, and why you might care.
DataObject
is a very simple interface, with only a single
method:
public interface oracle.cabo.ui.data.DataObject
{
/**
* Select a single value out of the DataObject.
* @param context the current RenderingContext
* @param select a select key
* @return the selected value
*/
public Object selectValue(RenderingContext context, Object select);
}
It's such a small interface that it places almost no requirements on the developer or the structure of the data. This makes it easy to adapt any form of data, like:
UIX includes built-in bindings to BC4J (see Business Components for Java Integration) and built-in bindings to JavaBeans and Maps, which we've already shown.
When developers first see the "select
" parameter, most
naturally think of Hashtable
keys, but a
DataObject
doesn't need to work like that at all. The
"select
" parameter could be:
Integer
index into an arrayMost of the time you're developing with UIX, there's no reason to
code against the DataObject
API. But there are a few
good reasons to consider using DataObject
instead of Maps
and JavaBeans.
DataObjects
are the actual abstraction UIX uses for data.
So, when you're using our Java API to create our beans, you'll have
to use DataObjects
, since that's what the methods take.
(You can use BeanAdapterUtils
to easily convert
JavaBeans and Maps into DataObjects
, as we'll see later.)
In addition, our built-in support for JavaBeans doesn't quite come
for free. There are performance costs. First, we need to create
adapter objects that implement DataObject
. These are
fairly lightweight, but object creation is always worth avoiding if
possible. Second, introspection - dynamically discovering and calling
methods at runtime - is inherently slow. We do a good job at caching
the most expensive part of this process - finding the
Method
objects - but calling into the Method
objects is still slower than a pre-compiled method call in a
hand-coded DataObject
.
Still, remember the well-known axiom of programming: "Premature
optimization is the root of all evil." Just because hand-coded
DataObjects
are faster doesn't mean you should spend time
writing DataObjects
. When used in moderation, there's no
reason why using JavaBeans should seriously impact performance. And
for JavaBeans that are inherently slow - because they need to access
the database, for example - the small additional overhead of the
adapter is largely irrelevant. But if you perform profiling and
discover that the overhead of adapting one of your beans is
significantly affecting performance, you can attack the dual problems
of object creation and introspection overhead. Later, you'll see
some easier ways to tackle the performance problem.
Finally - and most usefully - DataObjects
receive
a RenderingContext
. A RenderingContext
has a lot of useful values. Here, let's use the Locale
to properly format our date string:
package yourpackage;
import oracle.cabo.ui.RenderingContext;
import oracle.cabo.ui.data.DataObject;
import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;
public class CurrentDateObject implements DataObject
{
public Object selectValue(RenderingContext context, Object select)
{
if (!"time".equals(select))
return null;
Locale locale = context.getLocaleContext().getLocale();
DateFormat formatter = DateFormat.getDateTimeInstance(
DateFormat.MEDIUM, DateFormat.MEDIUM, locale);
return formatter.format(new Date());
}
}
If you use this class instead of CurrentDateBean
,
the date will automatically be formatted correctly for the current
Locale
.
It's possible to define both data and lists of data right in your UIX. Often, this is useful because it lets you dummy up data before your page is attached to a real data source.
First, let's introduce a new DataProvider
element: <inline>
.
<dataScope xmlns="http://xmlns.oracle.com/uix/ui"
xmlns:demo="http://www.example.org/">
<provider>
<data name="demo:linkData">
<inline>
.... data object is in here ...
</inline>
</data>
</provider>
<contents>
...
</contents>
</dataScope>
It behaves much like the <method>
element we saw
before, but doesn't call out to Java code. You'll define the data
right inside your UIX.
Any attributes of the <inline>
define key/value
pairs in the data it creates. For instance,
<inline url="http://example" text="Inline text">
</inline>
will define data with two values, one at key "url", the other at key
"text". Simple enough.
But you can also add data and lists of data as subvalues, using child elements. The name of the element is the key where that child data or list of data will be stored. If the same element name appears more than once, the elements together define a list of data. If it appears once, it actually defines both a single bag of data and a single-element list of data! An example should help clarify:
<inline foo="Inline text">
<stuff random="Stuff text"/>
<row text="First row"/>
<row text="Second row"/>
</inline>
This example gives you data that contains three things:
String
"Inline Text".
<dataScope>
<provider>
<data name="inlineData">
<inline>
<tableKey text="Row 0" text2="Column 2, Row 0"/>
<tableKey text"Row 1" text2="Column 2, Row 1"/>
... etc ...
<tableKey text="Row 4" text2="Column 2, Row 4"/>
</inline>
</data>
</provider>
<contents>
<table data:tableData="tableKey@inlineData">
<contents>
<styledText data:text="text"/>
<styledText styleClass="OraErrorText" data:text="text2"/>
</contents>
</table>
</contents>
</dataScope>
The next several sections will discuss many details specific to the Java API. Even if you're developing entirely from uiXML, this may be of interest if you want to know how your uiXML works or you want to extend the framework.
The BoundValue
interface is the key to dynamic
pages in UIX Components. It's simply a single method - a
RenderingContext
goes in, and any Java
object comes out:
public interface oracle.cabo.ui.data.BoundValue
{
public Object getValue(RenderingContext context);
}
What makes this simple interface useful is that it can be used in
place of any attribute value in any UIX Components bean. If the value of an
attribute is set to a BoundValue, then getting the value of that
attribute won't return the BoundValue. Instead, UIX Components will call
through BoundValue.getValue()
, and return the result
instead. For example:
// Remember the code to set the text of a StyledTextBean...
styledTextBean.setText("Some text");
// ... is really a cover for:
styledTextBean.setAttributeValue(UIConstants.TEXT_ATTR,
"Some text");
// So you can also set a BoundValue:
BoundValue boundValue = ...;
styledTextBean.setAttributeValue(UIConstants.TEXT_ATTR,
boundValue);
Then, at display time, if we ask the StyledTextBean for its text:
text = styledTextBean.getAttributeValue(renderingContext,
UIConstants.TEXT_ATTR)
// .. is equivalent to writing:
BoundValue boundValue = ...;
text = boundValue.getValue(renderingContext);
Keep in mind: you can use setAttributeValue()
with a BoundValue
and data bind any attribute.
It doesn't matter if that bean has convenience methods for data binding
that attribute, or if anyone else has ever thought of data binding that
attribute before, or what type of Java Object
that attribute
requires. Every attribute can be data bound.
Many of our beans do offer convenience methods for binding their
attributes. In fact, StyledTextBean
's "text" attribute
is one of these. So, we can write:
BoundValue boundValue = ...;
styledTextBean.setTextBinding(boundValue);
But this is only a convenience. Any attribute can be bound on any bean, whether or not we've added a convenience method.
While we provide many implementations of BoundValue
, let's start with a custom
implementation. The following code will always return the current
date:
public class CurrentDate implements BoundValue
{
public Object getValue(RenderingContext context)
{
return new Date();
}
}
Now, let's use this with a DateFieldBean
:
DateFieldBean dateField = new DateFieldBean();
dateField.setValueBinding(new CurrentDate());
The date field this produces will always be initialized with the current date. This might seem pretty similar to:
DateFieldBean dateField = new DateFieldBean();
dateField.setValue(new Date());
But there's a big difference. In this second example, the date
gets set once. That's fine if you're using the bean once, then
throwing it away. But UIX Components beans can be reused, over and over
again. With the BoundValue
you used in the first
example, UIX Components will ask for the date every time it has to render the
page - so the date will still be correct, days or months after the
DateFieldBean
was created!
Next, we'll show how to write your own DataObject
class.
This class turns a Java ResourceBundle
into a
DataObject
. This class is so useful that it's part of
UIX Components, but walking through it shows the basic ideas in
implementing a DataObject
. This example also shows one
way to handle exceptions in DataObjects
(see the Handling Errors chapter for other
approaches.)
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import oracle.cabo.ui.data.DataObject;
public class BundleDataObject implements DataObject
{
public BundleDataObject(ResourceBundle bundle)
{
_bundle = bundle;
}
public Object selectValue(RenderingContext context, Object select)
{
try
{
return _bundle.getObject(select.toString());
}
catch (MissingResourceException e)
{
context.getErrorLog().logError(e);
return null;
}
}
private ResourceBundle _bundle;
}
Things to note:
ResourceBundle
, we only
allocate one object. If we'd created a Map
and pulled all the data out of the ResourceBundle
,
we would have had to create many, many more objects. Likewise,
we'll only get the keys actually used at runtime.
DataObjects
should avoid throwing exceptions.
Here, we detect any MissingResourceExceptions
, and
log them on the standard UIX ErrorLog
before returning
a safe value - null.
You should feel very comfortable writing your own implementations
of DataObject
. Custom implementations can help keep your
application efficient and performant.
Once you've created DataObjects
, you have to make
them available to your UIX Components page by attaching them to the
RenderingContext
interface. (As a reminder, this
interface gives UIX Components beans and uiXML pages the context they
need to render.) We're interested here in one particular method of
RenderingContext
:
public DataObject getDataObject(String namespaceURI,
String localName)
This method gives a UIX Components bean or BoundValue
access to
any number of DataObjects
. As usual in UIX, we've
partitioned these DataObjects
by two separate strings.
You can pick one or more namespaces based on the URL of your web site.
Then, pick any local names without conflicts with other code.
For a very little while, we'll leave aside the question of how
DataObjects
get into a
RenderingContext
, and show how you get data out of a
DataObject
and into your UINodes
or UIX.
This is where the DataBoundValue
class comes in. In UIX
Components, the DataBoundValue
class lets you get at the
DataObjects
on the RenderingContext
.
LinkBean bean = new LinkBean();
DataBoundValue destinationBinding =
new DataBoundValue(_EXAMPLE_NAMESPACE,
"dataObjName",
"url");
bean.setAttributeValue(UIConstants.DESTINATION_ATTR,
destinationBinding);
// ...
static private final String _EXAMPLE_NAMESPACE =
"http://www.example.org";
This is equivalent to executing the following Java code while rendering:
// ...
DataObject dataObject = context.getDataObject("http://www.example.org",
"dataObjName");
Object destination = dataObject.selectValue(context, "url");
// ...
Because DataBoundValue
is so useful, our beans have
convenience methods that will create the DataBoundValue
for you:
LinkBean bean = new LinkBean();
bean.setDestinationBinding(_EXAMPLE_NAMESPACE
"dataObjName",
"url");
By now, you can write your own DataObjects
, you can get
them from a RenderingContext
, and you can bind them to
attributes. What's missing from the picture is a way to get the
DataObjects
into the RenderingContext
in the
first place. This happens with (you guessed it, another interface)
the DataProvider
interface. This is another small
interface:
public interface DataProvider
{
public DataObject getDataObject(
RenderingContext context,
String namespace,
String name);
// ... methods omitted
}
There's some methods we've left out, but getDataObject
is the important one: it's the method RenderingContext
will call to get at DataObjects
. And since it gets
passed the namespace and name, each DataProvider
can
serve up many different DataObjects
. Only one thing's
left: a way to attach a DataProvider
to a
RenderingContext
- for that, we only need to introduce
one more method:
public interface RenderingContext
{
// ...
public void addDataProvider(DataProvider provider);
// ...
}
You can call this method as many times as you want to hook your
DataProviders
up to a RenderingContext
. (In
practice, you'd be better off putting all your
DataProviders
inside a TableDataProvider
,
then putting that inside a CachingDataProvider
). And
we're there! Now, let's recap before we go to some examples of the
whole stack in action. See Figure 4-1.
Figure 4-1: Relationships among DataObjects, DataProviders, RenderingContexts, and UINodes
DataProvider
is a collection of DataObjects
.DataObject
is a collection of arbitrary data.RenderingContext
gets DataObjects
from DataProviders
.DataBoundValue
object (or a UIX dataObject
element) connects everything to a UINode attribute.
Let's create a very simple page. It will have only one link, and
databind its contents to a single DataObject
. And
we'll display it all in a JSP. First, we'll define some constants:
public class DataDemo
{
// ...
private static final String _DEMO_NAMESPACE = "http://www.example.org/";
private static final String _DATA_OBJECT_NAME = "TextData";
private static final Object _TEXT_KEY = "textKey";
private static final Object _DESTINATION_KEY = "urlKey";
}
Now, let's write the function that creates the UIX Components nodes:
public class DataDemo
{
static public UINode getPage()
{
// An ultra-simple example - we'll create a single link:
LinkBean link = new LinkBean();
link.setTextBinding(
new DataBoundValue(_DEMO_NAMESPACE,
_DATA_OBJECT_NAME,
_TEXT_KEY)
);
// Or, we can use the built-in DataBoundValue convenience methods
link.setDestinationBinding(_DEMO_NAMESPACE,
_DATA_OBJECT_NAME,
_DESTINATION_KEY);
// And put the link inside of a "BodyBean" (see below)
BodyBean body = new BodyBean();
body.addIndexedChild(link);
return body;
}
// ...
}
This is a lot like the code we wrote earlier. We create a link
bean, and bind two attributes to a data object. The only new piece is
the BodyBean
. This isn't part of data binding - it's the
UIX Components bean that should always be used to create the HTML
<body>
tag.
And now, we'll write the function that sets up the DataProvider.
Note: This is still a primitive example - the data is hardcoded, and we're
using the rudimentary DictionaryData
class to store it.
But we could pass in the JSP PageContext
- you could use
that context to get whatever state you need here. What's more, the
RenderingContext
is passed in to both the
DataProvider
and the DataObject
when they
are asked for data. And, since you can get all the usual Servlet
objects from a RenderingContext - ServletRequest
,
HttpSession
, etc., there's no reason to load all the data
up front. Instead, you can wait until the moment the data is actually
requested.
public class DataDemo
{
// ...
static public DataProvider getDataProvider()
{
DataObject data = _getData();
// And put it in a DataProvider
TableDataProvider provider = new TableDataProvider();
provider.put(_DEMO_NAMESPACE, _DATA_OBJECT_NAME, data);
return provider;
}
static private DataObject _getData()
{
// Build up a DataObject
DictionaryData data = new DictionaryData();
data.put(_TEXT_KEY, "Shameless promotion!");
data.put(_DESTINATION_KEY, "http://www.oracle.com");
return data;
}
// ...
}
Note the TableDataProvider
we've used here. This
class handles partitioning up DataObjects
or other
DataProviders
by namespace and name.
Now, at last, let's write the JSP that renders this:
<%@ page contentType="text/html" %>
<%@ page import='oracle.cabo.ui.ServletRenderingContext'%>
<%@ page import='oracle.cabo.ui.beans.StyleSheetBean'%>
<%@ page import='oracle.cabo.ui.data.DataProvider'%>
<%@ page import='yourpackage.DataDemo'%>
<html>
<head>
<%
// Create a rendering context
ServletRenderingContext rContext =
new ServletRenderingContext(pageContext);
// Include the stylesheet that UIX Components needs
StyleSheetBean.sharedInstance().render(rContext);
%>
</head>
<%
// Get the data provider, and attach it to the
// rendering context
DataProvider provider = DataDemo.getDataProvider();
rContext.addDataProvider(provider);
DataDemo.getPage().render(rContext);
%>
</html>
If you run this example, you'll see the words "Shameless promotion!" linked to the Oracle web site.
For this example, we've also introduced another bean -
StyleSheetBean
. This bean automatically adds the
stylesheet that all UIX Components pages need. Later chapters discuss
the power and customizability this bean gives you with a single line
of code. For now, know that it belongs in every
<head>
HTML.
That's all you need to do. Admittedly, this is overkill for
rendering one little link. But with a generic
DataProvider
that can talk to a database or provide up
localized text, or any other source of localized data.
By now, you can use BoundValue
and
DataObject
to set up pages with dynamic data. However,
the DataObject
interface just supports a bag of
properties. It's a very inconvenient interface if you need to display
tabular data, or any other data that looks like a Java array. This is
where the DataObjectList
interface comes in.
The last new interface introduced in this chapter is a simple one:
public interface DataObjectList
{
public int getLength();
public DataObject getItem(int index);
}
A DataObjectList
is at heart a DataObject
array. (By not actually using an array, we can keep
DataObjectLists
immutable - there's no methods to "set"
anything - and we let you lazily create DataObjects
or
reuse DataObject
instances.)
The ArrayDataSet
class is a conveniently simple implementation of DataObjectList
. Like
DictionaryData
, it's simple to use, but forces you to create all the DataObjects
ahead of time:
DataObject[] array = new DataObject[10];
for (int i = 0; i < 10; i++)
{
array[i] = ...;
}
ArrayDataSet list = new ArrayDataSet(array);
So remember: you should expect to write your own implementations of DataObjectList
.
Now, how to get the data back out of a DataObjectList
and into your pages? For this, you'll need the "current
DataObject
."
Any data framework has to support iterating through data. For Java,
it's a "for" loop. For a database, it's a cursor. For UIX Components, it's
the "current DataObject
."
You'll remember the RenderingContext.getDataObject
method
introduced earlier in this chapter. There's another method
for getting a DataObject
that we hadn't mentioned before
now:
public interface RenderingContext
{
// ..
/**
* Get a DataObject by namespace and name.
*/
public DataObject getDataObject(String namespaceURI,
String localName)
/**
* Get the "current" DataObject.
*/
public DataObject getCurrentDataObject();
// ..
}
This new method, getCurrentDataObject()
, returns this
"current DataObject
" - the UIX Components equivalent of a
database cursor, and what you'll use to bind to a
DataObjectList
.
To get data out of the "current DataObject
" in Java, you
can use a special one-argument constructor of
DataBoundValue
, or a one-argument convenience method in
our beans:
StyledTextBean textBean = new StyledTextBean();
textBean.setAttributeValue(UIConstants.TEXT_ATTR,
new DataBoundValue(TEXT_KEY));
// ... OR, EASIER ...
textBean.setTextBinding(TEXT_KEY);
To put this all together, let's start building up a
DataObjectList
for a TableBean
:
public class DataDemo
{
// ...
static private DataObjectList _getDataForTable()
{
DataObject[] rows = new DataObject[5];
for (int i = 0; i < rows.length; i++)
{
DictionaryData row = new DictionaryData();
row.put(_TEXT_KEY, "Row " + i);
row.put(_TEXT_2_KEY, "Column 2, row " + i);
rows[i] = row;
}
return new ArrayDataSet(rows);
}
// ...
private static final Object _TEXT_2_KEY = "text2Key";
}
Again, for the sake of keeping the example simple, we've used hardcoded
data, but your application can do much more. Now, we have to get this
DataObjectList
into our table. Now, we could call:
TableBean table = new TableBean();
table.setTableData(_getDataForTable());
but data set that way wouldn't be dynamic, so we couldn't reuse the
TableBean
from one request to the next. To make the data
dynamic, we'll add it into the same DataObject
we were
using in our old demo. We'll also need an new key, and we'll change
our code to add the TableBean
to our page:
public class DataDemo
{
// ...
static public UINode getPage()
{
// ... old code ...
// And now, let's create the table
TableBean table = new TableBean();
table.setTableDataBinding(_DEMO_NAMESPACE,
_DATA_OBJECT_NAME,
_TABLE_DATA_KEY);
body.addIndexedChild(table);
return body;
}
static private DataObject _getData()
{
// Build up a DataObject
DictionaryData data = new DictionaryData();
// ... old code ...
// And now, add the DataObjectList
data.put(_TABLE_DATA_KEY, _getDataForTable());
return data;
}
// ...
static private final Object _TABLE_DATA_KEY = "tableKey";
}
As before, the DataObject
will be added to the
RenderingContext
using a DataProvider
. That
code doesn't have to change at all. Also, note that we've used
the same old code for binding the table data - "tableData" is simply
another attribute, and no different from any other element.
There are a lot of objects here. Let's review what's been created:
DataProvider
that serves up one DataObject
.
DataObject
contains two pieces of text and one
DataObjectList
.
DataObjectList
contains five more
DataObjects
.
DataObjects
has two pieces of text.
We haven't yet seen the "current DataObject
" in action
yet, but we'll need to use it once we have added columns to the table.
Each column of a TableBean
is represented by an indexed
child. If you call addIndexedChild()
three times, the
table will have three columns. Each of these columns renders by using
its child element as a stamp once for each row.
The trick lies in getting each element to render differently in
each row. And this happens with the "current
DataObject
". When the table is rendering its first row,
RenderingContext.getCurrentDataObject()
will return the
first DataObject
in the "tableData"
DataObjectList
. When the second row is rendering,
getCurrentDataObject()
returns the second
DataObject
. And so forth.
So, let's add two columns to our table, first a normal text column,
then a column in red. (In this example, we're using the built-in
"OraErrorText" CSS style - one of the styles supplied by the
StyleSheetBean
.)
public class DataDemo
{
// ...
static public UINode getPage()
{
// ... old code ...
// And now, let's create the table
TableBean table = new TableBean();
table.setTableDataBinding(...);
// Create the first column
StyledTextBean firstColumn = new StyledTextBean();
// Use _TEXT_KEY for the data of the first column
firstColumn.setTextBinding(_TEXT_KEY);
// Add it to the table
table.addIndexedChild(firstColumn);
// Create the second column, using _TEXT_2_KEY
StyledTextBean secondColumn = new StyledTextBean();
secondColumn.setStyleClass("OraErrorText");
secondColumn.setTextBinding(_TEXT_2_KEY);
table.addIndexedChild(secondColumn);
// ...
}
// ...
}
And done. Let's talk about how this renders. The table does a lot of the work, putting in borders and setting backgrounds for you. Then, the first cell renders.
For row 0, column 0, the table uses the first
StyledTextBean
. And
RenderingContext.getCurrentDataObject()
will return the
first DataObject
in the DataObjectList
: this
has "Row 0" and "Column 2, row 0" as entries. The
StyledTextBean
asks for its text, which is a
DataBoundValue
. The "current DataObject
" is
retrieved and asked for _TEXT_KEY
- it returns "Row 0",
which renders in that cell.
Then, on to row 0, column 1. The same "current
DataObject
" is still in effect - but the other
StyledTextBean
is rendering. It asks for
_TEXT_2_KEY
, and so it renders "Column 2, row 0".
Now, row 1. We're back to the first StyledTextBean
,
but now we've moved to the next DataObject
in the
DataObjectList
. So,
RenderingContext.getCurrentDataObject()
will return a
different DataObject
. So this row gets filled in with
"Row 1" in the first column, and "Column 2, row 1" in the second
column.
So things continue throughout the entire table. This is a trivial
example: we haven't added column or row headers, or any of roughly one
zillion other things the table can do. And we've only used ordinary,
uneditable text. But all of these features work with the same
technique. Any time you need something to render differently from one
row to the next in a table, there must be some piece of information in
the "current DataObject
" that describes that difference.
For example, say you want a link in each row, but it goes to a
different location. Simple! Use a LinkBean
databind the "destination"
attribute to the current DataObject
, and update your
DataObjectList
to include a destination in each row. In
UIX Components, everything can be data bound - so every row can be
different.
For most developers, the mechanisms by which JavaBeans, Maps,
Lists, and arrays turn into DataObjects
or
DataObjectLists
can remain magic. However, some will
find it useful to know more of the details. In particular, there are
some APIs - especially on our Java Beans - that explicitly take
DataObjects
or DataObjectLists
. Second, you
may want to customize the adapters to add additional functionality or
improve performance. In either case, you'll want to use
oracle.cabo.ui.data.bean.BeanAdapterUtils
.
To convert an arbitrary object - be it a bean, a Map
,
or even a DataObject
- into a DataObject
,
use one of the BeanAdapterUtils.getAdapter()
methods;
most often, you'll want to use:
public static DataObject getAdapter(RenderingContext context, java.lang.Object instance)
To convert an array or List
into a DataObjectList
,
use:
public static DataObjectList getAdapterList(RenderingContext context, java.lang.Object listInstance)
Before trying to improve the performance, make sure that you actually have a problem. The adapter layer is lightweight and performant enough that most applications will not need to bother with optimizing this portion of your code. Until you've run a
First, you can avoid creating the adapter objects each time they are needed. Instead of letting UIX automatically create the adapters, create them yourselves and cache the result. For example:
public class DataDemo
{
static public Object getLinkBean(
RenderingContext context,
String namespace,
String name)
{
return _sAdapter;
}
static private LinkDataBean _sInstance =
new LinkDataBean("http://www.oracle.com",
"Shameless bean promotion!");
// Create a static adapter:
static private DataObject _sAdapter;
static
{
try
{
_sAdapter = BeanAdapterUtils.getAdapter(_sInstance);
}
catch (InstantiationException ie) { }
catch (IllegalAccessException iae) { }
}
}
This is only helpful if the bean is "long-lived". If the bean is only used in one request, then caching the adapter may not be of much help, but if it's a session- or application-level bean, this can be a small but very simple performance win.
Second, you can attack the performance problems inherent in
introspection by writing your own DataObject
adapter.
Or, even better - let us write the adapter for you! UIX includes a
Java-based tool called BuildBeanDOAdapter
that takes
compiled JavaBeans and automatically writes DataObject
adapters. Just type on the command line:
java oracle.cabo.ui.tools.BuildBeanDOAdapter yourpackage.LinkDataBean
We won't show you all the code this produces, but here's the
interesting piece: the selectValue()
code produced for
our example LinkDataBean
:
public Object selectValue(RenderingContext context, Object select)
{
LinkDataBean instance = _instance;
if (instance == null)
return null;
try
{
if ("url".equals(select))
return instance.getUrl();
if ("text".equals(select))
return instance.getText();
}
catch (Exception e)
{
context.getErrorLog().logError(e);
}
return null;
}
This is a lot faster than introspection. In a recent
benchmark (Java 1.3.0 on Windows 2000), it was twenty to thirty times
faster. However, for very, very large JavaBeans with hundreds of
properties, these calls to String.equals()
become a
serious problem and can actually make this "optimization" hurt
performance! For these cases, the adapter-building tool supports a
"-fast" command-line option. The resulting class uses some trickery
with String hashcodes to greatly increase the speed of
selectValue()
to once again be faster than basic
introspection. Whichever way you build your adapter class, these
adapters are also much smarter than introspection about converting
boolean
and int
return values into
Boolean
and Integer
objects.
Once you've created these adapter classes, you could change your data binding code to explicitly create these adapters:
public class DataDemo
{
static public Object getLinkBean(
RenderingContext context,
String namespace,
String name)
{
LinkDataBean bean = ...;
return new LinkDataBeanDataObject(bean);
}
}
...but it'll be tedious (and error-prone) to make sure you've
caught all the cases. Adapters can instead be directly registered
with BeanAdapterUtils
with their static
registerAdapter()
call. If you add this call in your
initialization code, the rest of your code can continue returning your
bean instance as it always did:
public class DataDemo
{
static public Object getLinkBean(
RenderingContext context,
String namespace,
String name)
{
// Under the covers, UIX will use LinkDataBeanDataObject to
// adapt this bean, but we don't to know that here!
return new LinkDataBean(...);
}
// Register the adapter once
static
{
LinkDataBeanDataObject.registerAdapter();
}
}
UIX don't require your data to be fully comply with all aspects of the JavaBeans specification, though we happily accept classes that are. We'll automatically handle all of the following situations:
LinkDataBean
could be written as:
public class LinkDataBean
{
public LinkDataBean(String url, String text)
{
this.url = url;
this.text = text;
}
public String url;
public String text;
}
This isn't generally considered good design for your own JavaBeans,
but it does mean that we can support legacy classes like
java.awt.Point
without any special adapters.
// The interface is public...
public interface LinkData
{
public String getText();
public String getUrl();
}
// ...but the bean is private
class LinkDataBean implements LinkData
{
...
}