UIX Developer's Guide |
![]() Contents |
![]() Previous |
![]() Next |
By now, you should be pretty used to the UIX data binding model, which lets you change attribute values from one render to another. But how do you handle changes in structure? What if you need a section of your page to be hidden or shown? How can you repeat a section over and over, each time with different dynamic data? And how can you integrate Java code into your UIX, to handle the most demanding dynamic pages? This chapter will teach you about the features UIX supports that make all these options easy.
This chapter assumes you have a solid knowledge of the core features of the UIX framework. In particular, it's essential that you understand the Data Binding chapter.
This chapter contains the following sections:
<switcher>
Element<include>
ElementThe very simplest feature for dynamic pages is the
"rendered"
attribute. This attribute is supported on all
UIX Components UINode
classes and each corresponding UIX element.
When set to "false", that element - and all of its children - are
simply ignored. For example, the following UIX will output "UIX is cool.":
<flowLayout>
<contents>
UIX is
<styledText rendered="false" text="not "/>
cool.
</contents>
</flowLayout>
Very simple - and when data bound, this lets you turn on and off small
or large parts of your page with a single variable:
<table data:rendered="hasTablePermission@permissions">
<contents>
...
</contents>
</table>
This is both powerful and simple, so try rendered
before trying some of the more complicated techniques described later in this chapter.
A caveat needs to be mentioned with using rendered
and
the columns of a <table>
. If you're familiar with
that component, you'll remember that each element inside of its <contents>
element defines a column. It's entirely legal to use rendered
on these elements, and it's legal
to databind rendered
- but not to databind it to the
current (per row) DataObject
:
<table>
<contents>
<!-- Legal -->
<styledText rendered="false" ... />
<!-- Also legal -->
<styledText data:rendered="foo@someName" ... />
<!-- NOT legal -->
<styledText data:rendered="perRow" ... />
</contents>
</table>
You'd probably try this if you wanted to change the contents of a
column from one row to the next. Thankfully, there's a simple
workaround - or, actually, two workarounds. A very good solution is
using the <switcher>
element (described in the next
section). But, if you want to use "rendered"
, take all
of the possible elements that should go into that column, and wrap
them inside of a <column>
element:
<table>
<contents>
<!-- Legal -->
<styledText rendered="false" ... />
<!-- Now legal -->
<column>
<contents>
<styledText data:rendered="perRow" ... />
<styledText data:rendered="otherPerRow" ... />
</contents>
</column>
</contents>
</table>
The "rendered"
attribute is great for turning on and
off one part of a page, but it's a pain if you always want to use one
of several options - if you had 10 possibilities, you'd have to
provide ten true/false values from your DataObject
. It'd
be a lot easier to just expose a single value, like the name of the
possibility to choose. This is where the UIX
<switcher>
element - and the UIX Components
SwitcherBean
comes in.
A <switcher>
element has a single attribute:
"childName"
. And it can only contain one type of
element: <case>
. Each <case>
element has a single attribute, "name"
. When it's asked
to display, it finds the child whose name matches the "childName"
attribute and displays that child - and only that child. An example
should make this all pretty clear; the following UIX will display
"Second case":
<switcher childName="second">
<case name="first">
<styledText text="First case"/>
</case>
<case name="second">
<styledText text="Second case"/>
</case>
</switcher>
It's an error to use the same "name"
twice, but it's
perfectly legal to have a "childName"
that doesn't match
any "name"
; the <switcher>
just won't
render anything.
An important point: <switcher>
is a UINode
element. So you can only use it where you could use any other UINode
element - <styledText>
,
<flowLayout>
, etc. You can't add a
<switcher>
element just anywhere in your UIX. For
example, the following UIX is illegal, because the contents of a
<data>
element should be a DataProvider UIX
element, not a UINode.
<dataScope>
<provider>
<data name="...">
<!-- ILLEGAL -->
<switcher ...>
</switcher>
<data>
</provider>
</dataScope>
You may wonder: why don't we have a <default>
element, just like the Java "switch" statement? UIX's data binding
can handle this without adding extra syntax! If you databind a UIX
attribute and also explicitly specify it, then the explicit value is
used as a default:
<switcher childName="defaultName" data:childName="...">
<case name="first">
...
</case>
<case name="second">
...
</case>
<!-- A default choice -->
<case name="defaultName">
...
</case>
</switcher>
If the "childName"
found with data binding can't be
resolved - that is, it ends up as "null", then UIX falls back on the
explicitly specified value: "defaultName".
If you're writing Java code with UIX Components beans, you'll use
the SwitcherBean
class. Instead of <case>
elements, you'll call setNamedChild
. Here's that first
UIX example, only in Java:
SwitcherBean switcher = new SwitcherBean();
switcher.setChildName("second");
switcher.setNamedChild("first",
new StyledTextBean("First case", null));
switcher.setNamedChild("second",
new StyledTextBean("Second case", null));
Now, the obligatory caveat. <switcher>
works
well in nearly all cases, but there are some element types where it
causes problems. For example, we don't support wrapping a
<switcher>
around a set of
<column>
elements in a <table>
;
you have to put the <switcher>
inside of the
<column>
element. This causes problems because the
<table>
looks for its <column>
children, and doesn't see them (because they're covered up by the
<switcher>
). These cases are fairly obscure, but
you should be aware they exist. The "rendered"
attribute
doesn't have this problem.
So far, you can turn and off pieces of your page. But these aren't enough to handle a very common case: repeating a section of your page. For example, say you want the following HTML:
Here, you want to stamp out the same bit of content several times, but each
time with a different piece of data. The UIX "childData"
attribute - and the UIX Components DataObjectListNodeList
class -
let you implement this.
To start, let's write the UIX for a single row:
<tableLayout>
<contents>
<rowLayout>
<contents>
<styledText text="Name:"/>
<textInput name="????" text="????"/>
<cellFormat width="10"/>
<styledText text="Phone number:"/>
<textInput name="????" text="????"/>
<contents>
</rowLayout>
</contents>
</tableLayout>
Now, we'll add in "childData" to get you the needed result. Unlike
most attributes, you don't set "childData"
directly on
the row. Instead, you'll set it on the<contents>
element. Another oddity - it doesn't support being directly set - you
have to use databinding (as of UIX 2.0.3 - we may add a syntax for
setting it directly in future releases). Here's the example,
adding in "childData"
and a <dataScope>
to define where we're getting the data:
<dataScope>
<provider>
<data name="source">
<method class="SampleClass" method="getRows"/>
</data>
</provider>
<contents>
<tableLayout>
<contents data:childData="rows@source">
<rowLayout>
...
</rowLayout>
</contents>
</tableLayout>
</contents>
</dataScope>
We'll show the contents of <rowLayout>
and the
getRows()
method soon enough, but for now note that we've
put the "data:childData"
on the
<tableLayout>'s
contents, not the
<rowLayout>
; we're asking it to repeat the row
layout, not the contents of the row layout. (If it were on the row
layout's <contents>
element, you'd see one very,
very long row, not several identical rows.)
The Java type of "childData"
must be a UIX
DataObjectList
. Remember - these are analogous to an
array of DataObjects
. For every DataObject
in the DataObjectList
, we repeat the contents once. If
there's five DataObjects
in rows@source
,
we'll have five rows. And each time you render, the "current"
DataObject
will be the next DataObject
in
that list. This is exactly the same mechanism that UIX uses for the
<table>
and its "tableData"
attribute.
So, let's write our Java model. We'll use the UIX
introspection layer to turn a simple Java class into
DataObjects
:
import oracle.cabo.ui.data.DataObjectList;
import oracle.cabo.ui.data.bean.BeanArrayDataObjectList;
public class SampleClass
{
// For simplicity's sake, we're using public fields.
// The introspection code can handle "get" methods too.
public class Person
{
public String id;
public String firstName;
public String phoneNumber;
}
static public DataObject getRows(
RenderingContext context, String ns, String name)
{
// Get all of the people in an array
Person[] people = _getPeople(...);
// Use introspection to turn it into a DataObjectList
DataObjectList list =
new BeanArrayDataObjectList(people);
// And return that one item in a DataObject
return new DictionaryData("rows", list);
}
}
Now, we can write our <rowLayout>
element:
...
<tableLayout>
<contents data:childData="rows@source">
<rowLayout>
<contents>
<styledText text="Name:"/>
<!-- text input: bind the "text" attribute
directly to the "firstName" property of our data.
For the "name", concatenate the ID and "Name" -->
<textInput data:text="firstName">
<boundAttribute name="name">
<concat>
name<dataObject select="id"/>
</concat>
</boundAttribute>
</textInput>
<cellFormat width="10"/>
<styledText text="Phone number:"/>
<!-- another text input - pretty much the same as before -->
<textInput data:text="phoneNumber">
<boundAttribute name="name">
<concat>
phone<dataObject select="id"/>
</concat>
</boundAttribute>
</textInput>
<contents>
</rowLayout>
</contents>
<tableLayout>
Let's extract that first <textInput> and break it down:
<textInput data:text="firstName"> <boundAttribute name="name"> <concat> firstName<dataObject select="id"/> </concat> </boundAttribute> </textInput>
The two points to notice about this example.
DataObject
: the data:text="firstName"
accomplishes that.
Finally, there's nothing about "childData"
that
requires you use only one element inside
<contents>
; if you have two elements, each will be
repeated. In general, if you have M elements, and your
DataObjectList
has N entries, UIX will act as
if you have M*N elements. So, for example, if we change our
UIX to:
<tableLayout>
<contents data:childData="rows@source">
<rowLayout>
<styledText text="Name:"/>
<textInput .../>
</rowLayout>
<rowLayout>
<styledText text="Phone number:">
<textInput .../>
</rowLayout>
</contents>
</tableLayout>
then our HTML result will change to:
Instead of stamping out one row with four columns, we're now stamping out two rows each with two columns. And nothing's changed in our Java layer.
If you're developing with Java, you still have all of this
functionality, but with a very different syntax. Instead of setting
an attribute, you'll use the DataObjectListNodeList
class. UINodeLists
are the storage abstraction UIX uses
for the indexed children of a UINode
, and by replacing
the UINodeList
you can greatly alter a node's behavior.
Here's our example, only written using UIX Components and Java.
import oracle.cabo.ui.collection.DataObjectListNodeList;
import oracle.cabo.ui.data.DataBoundValue;
...
TableLayoutBean tlb = new TableLayoutBean();
BoundValue rows = new DataBoundValue(_YOUR_NAMESPACE, _YOUR_NAME, "rows");
tlb.setIndexedNodeList(new DataObjectListNodeList(rows));
RowLayoutBean rowLayout = new RowLayoutBean();
tlb.addIndexedChild(rowLayout);
...
A small point of caution: setting the UINodeList
erases all of the indexed children of a node - so you'll want to call
setIndexedNodeList()
before adding any children.
The techniques you've seen so far can accomplish a lot. But they do eventually run into limits. You can repeat or turn off an element that's in the UIX tree, but you can't add an element that isn't in the tree at all!
Say, for example, you're trying to display and edit the results of
an arbitrary SQL query. There's no way to know how many columns
there'll be until you run the query, nor is there any way to know the
type of each column. In Java, this wouldn't be a big deal - you'd
just create the UINodes
at runtime, and forget about
caching. And it's fine to fall back on that technique for parts of a
UIX application - there's nothing wrong with mixing purely Java-based
pages with UIX pages. But it'd be nice to write as much of your page
as possible in UIX, and only use Java for the pieces that absolutely
need it. The <include>
element (and the Java
IncludeBean
) make this possible.
You may have already seen <include>
used
to include one UIX Controller page inside another:
<include ctrl:node="aPageName"/>
But, instead of asking UIX Controller to handle the include, we can use
standard UIX databinding. "node"
is really just another
attribute - although in this case, its type is actually
UINode
! So, let's write a UIX page:
<dataScope>
<provider>
<data name="source">
<method class="SampleClass" method="getNode"/>
</data>
</provider>
<contents>
<flowLayout>
<contents>
UIX knows
<include data:node="javaNode@source"/>
</contents>
</flowLayout>
</contents>
</dataScope>
and some Java code:
public class SampleClass
{
static public DataObject getNode(
RenderingContext context, String ns, String name)
{
// Create a StyledTextBean
UINode node = new StyledTextBean("includes!", null);
// And return that one node in a DataObject
return new DictionaryData("javaNode", node);
}
}
That's all there is to it. The page will act as if it had been written:
<flowLayout>
<contents>
UIX knows
<styledText text="includes!"/>
</contents>
</flowLayout>
A very, very important note: like <switcher>
,
the <include>
element is a UINode
element. So it can only be added in places where another
UINode
element could go. Contrary to what some
developers expect, <include>
is not a
lexical inclusion facility - it is strictly a UINode
substitution layer.
You can actually use this same technique with an entirely
UIX Components-based page. It's not as silly an idea as it may seem. Instead
of recreating the entire page and all of its UINodes
every time you render, you'll be able to keep most of the page cached
and only recreate the part that varies. There are some complexities
of using IncludeBean
directly - UIX hides these for you -
so you should read its documentation carefully before trying this.
The <include>
element is good at gluing in new
elements into a page. Together with databinding, the
"rendered"
flag, <switcher>
, and
"childData"
, you've got a fairly large toolbox. But we
still haven't presented a way to take a static UIX file and
arbitrarily change it in Java.
Since UIX is XML, one approach is to use any standard XML parsing
API - DOM, JDOM, SAX, XSLT, etc. - to read in UIX, modify the XML,
then pass the modified XML to UIX. This will work, and since you're
getting your modifications in before our parser sees the UIX, you'll
have unlimited freedom to mark up the source XML - or even generate
from some source other than static XML. But if you want to use the
UIX Components Java APIs, the DeltaTree
API is what you need.
DeltaTree
takes a UINode
and lays
differences - "deltas" - on top of it. Because it doesn't modify the
underlying Java tree at all, it's completely thread-safe. The
principle is straightforward: mark certain UINode
elements with a "nodeID" attribute. (This attribute is used to
identify nodes on the server - it doesn't get displayed on the
client.) From Java, we'll find those nodes, wrap them in a proxy,
then set attributes on the proxy. Then we render the proxy instead of
the original tree. For the full details on this API, you'll want to
consult our JavaDoc, but let's walk through a very simple example.
First, a simple UIX page. We'll find a single UINode and update its "styleClass" attribute to change its CSS style.
<page xmlns="http://xmlns.oracle.com/uix/controller">
<javaClass name="DeltaTreeTest"/>
<content>
<stackLayout xmlns="http://xmlns.oracle.com/uix/ui">
<contents>
<styledText text="One"/>
<styledText text="Two"/>
<styledText text="Three" nodeID="three"/>
</contents>
</stackLayout>
</content>
</page>
This is mostly straightforward, but note two things:
<javaclass>
element; this will
override the PageDescription
Java class used to
handle this page. For the purposes of this example, this is a
straightforward way to hook up to DeltaTree
.
"nodeID"
attribute on one of the three
<styledText>
elements. This will let us find
this element from our Java code without trying to guess at the
node hierarchy.
Now, for the Java code:
package oracle.cabo.servlet.demo;
import oracle.cabo.ui.MutableUINode;
import oracle.cabo.ui.UINode;
import oracle.cabo.ui.beans.StyledTextBean;
import oracle.cabo.ui.path.DeltaTree;
import oracle.cabo.ui.path.PathUtils;
import oracle.cabo.ui.path.Path;
import oracle.cabo.servlet.ui.DefaultUINodePageDescription;
public class DeltaTreeTest extends DefaultUINodePageDescription
{
public UINode getRootUINode()
{
// Get the original root UINode
UINode node = super.getRootUINode();
// Wrap it in a DeltaTree
DeltaTree tree = new DeltaTree(node);
// Find the node we want
Path path = PathUtils.findPathWithNodeID(null, node, "three");
// Turn it into a MutableUINode
MutableUINode nodeThree = tree.getMutableUINode(null, path);
// Set the style class (using a static method, instead of casting)
StyledTextBean.setStyleClass(nodeThree, "OraErrorText");
// And render the DeltaTree, instead of the original root
return tree.getRoot();
}
}
Let's walk through this code line by line.
public class DeltaTreeTest extends DefaultUINodePageDescription
This sets up our page description class. Page descriptions are used in UIX to encapsulate both the rendering and event handling parts of a page. We're extending the default class.
public UINode getRootUINode()
This method is responsible for returning the body UINode of the
page. (A second method returns the node responsible for the HTML
<head> section.)
UINode node = super.getRootUINode()
Get the static, unmodified root node of the page. This may or
may not be the <stackLayout>
element - it's
unwise to rely on the structure being precisely preserved.
DeltaTree tree = new DeltaTree(node);
Wrap it in a DeltaTree
. The original node is
completely unmodified - but you should be careful to refer to the
delta tree from this point on. It's also critical that you wrap
the root in a delta tree - it's no good to wrap a child node in a
DeltaTree
, because the parent will still point to the
original child.
Path path = PathUtils.findPathWithNodeID(null, node, "three");
A Path
object is used in UIX Components to locate nodes in
a tree. It stores every step that's used to get from the root
down to the node - every getIndexedChild()
or
getNamedChild()
call. The PathUtils
class has utility methods for finding nodes with several criteria.
That null
that we've passed in is the
RenderingContext
- it's always legal to pass
null
, though you won't be able to get at the values
of databound attributes without a rendering context.
MutableUINode nodeThree = tree.getMutableUINode(null, path);
Use the Path
to get a mutable wrapper for the
target node.
StyledTextBean.setStyleClass(nodeThree, "OraErrorText");
And set the attribute on the target. We're using a static method
on StyledTextBean
. You cannot cast the
mutable node to a StyledTextBean
- it is not an
instance of that class.
return tree.getRoot();
Finally, ask the DeltaTree
for its root - which
will contain all the modified state. Since we haven't modified
the original node object at all, you cannot just return
node
here - you wouldn't get any of the changes.
Not so tough. Of course, you can do a lot more than just set some
attributes. You can do anything you would with UINodes
you created yourself - add nodes, remove nodes, set
UINodeLists
, set up data binding, etc.
If you're a Java developer using the UIX Components beans directly, you can
also use DeltaTree
. Of course, since you've created the
beans in Java, you can just directly modify those beans. But if
you're trying to reuse bean trees created in one request for another
request, DeltaTree
lets you those beans trees in a
thread-safe and memory efficient way.
With the basics of databinding and all the techniques you've seen in this chapter, and the you should now be able to create dynamic pages using uiXML and UIX Components.