public interface ComponentXML
By allowing you to keep layout information and property settings in an XML format, Component XML enables non-developers to build and maintain portions of your application, either by editing screens within Reify or by directly editing the XML itself.
Unlike the similar GWT "UIBinder" technology, Component XML does not require a compilation step. XML screen definitions can be generated on the fly, modified at runtime, stored in a database, and in all other ways treated as a dynamic resource. See the section "Dynamic Component XML" for details.
To create a Smart GWT component in XML code, you create a tag with the component's class name. You can set that component's properties either as tag attributes:
<Button title="Click me" width="200" />or in nested tags:
<Button> <title>Click me</title> <width>200</width> </Button>
To set a property that is an Array of simple types (like int, or String), repeat tags like
so (for DynamicForm.colWidths
):
<DynamicForm> <numCols>2</numCols> <colWidths>250</colWidths> <colWidths>*</colWidths> </DynamicForm>To set a property that takes an Array of complex objects, use the property name as a container tag, then create further nested tags for the objects in the array, like so (for
ListGrid.fields
):
<ListGrid> <fields> <ListGridField name="accountName" ... /> <ListGridField name="accountType" ... /> </fields> </ListGrid>
This same approach works for creating nested layouts, such as placing a ListGrid in a VLayout:
<VLayout> <members> <ListGrid .. /> </members> <VLayout>
Referring to previously defined components
To refer to another component by ID in XML, use <Canvas withID=/>. For example:
<Canvas ID="myCanvas"/> <Canvas ID="myCanvas2"/> <VLayout> <members> <Canvas withID="myCanvas"/> <Canvas withID="myCanvas2"/> </members> </VLayout>
Save your Component XML as a file called screenName.ui.xml under
webroot/shared/ui/. Placing your .ui.xml file in this directory makes it visible to
the system; the location of this directory can be configured in server.properties
by setting
the project.ui property. screenName can be any valid identifier (no spaces,
dashes or periods - underscores OK).
If you have multiple top-level tags (eg, your code is similar to the example above under "Referring to previousy defined components") use <isomorphicXML> as a top-level container tag - this has no impact on processing and is just an idiom to make your file valid XML, since XML does not allow multiple top-level tags in a document.
Component XML screens are then loaded using the ScreenLoaderServlet. The default SDK comes
with this servlet already registered at
projectBase/sc/screenLoader. If you've modified web.xml
or only included some of the default servlets, you may need to add it now - see the
Installation Instructions
.
To create an application that consists of just the imported mockup, just add a <script src> tag pointing to the ScreenLoader servlet and referring to the screenName you used when you saved your file. For example, add the following to your bootstrap .html file:
<script src="sc/screenLoader?screenName=screenName"></script>If you want to load screens dynamically, or if you want to load more than one screen, use
RPCManager.loadScreen()
. See the section
on "Multiple screens and global IDs"
below.
You can retrieve the components in your loaded screen in order to add event handlers to
them, call APIs on them, place them into layouts you programmatically create, and in general
add dynamic behavior. Retrieve the components via the Canvas.getById()
API
(note, when working with multiple screens, be sure to see the upcoming section about managing
global IDs).
You can then add event handlers normally. For example, say there is a ListGrid with ID "mainGrid" and a DynamicForm with ID "editForm" in the same screen, and you want to populate the form with whatever record is clicked on in the grid:
ListGrid grid = (ListGrid)Canvas.getById("mainGrid"); final DynamicForm form = (DynamicForm)Canvas.getById("editForm"); grid.addRecordClickHandler(new RecordClickHandler() { public void onRecordClick(RecordClickEvent event) { form.editRecord(event.getRecord()); } });
You can also add a loaded screen to an existing layout container. For example, perhaps you've
already written parts of the application via normal coding techniques, and now you want to take
a screen defined in Component XML and place it in a particular Layout you've already created
("existingLayout" below) - just use Layout.addMember()
as usual:
existingLayout.addMember(Canvas.getById("componentId"));Component XML files can also refer to components you have created programmatically, and incorporate them into layouts. For example, if you have created a ListGrid component with ID "theGrid", you could refer to that grid using a
<Canvas withID=""/>
tag, which can be used
anywhere a
Canvas is expected. For example:
<VLayout ... > <members> <Canvas withID="theGrid"/> </members> </VLayout>Note that this approach requires that the referenced component has been created before
loadScreen
is called.
Component XML files can declare Action
s to take in response to events. An
Action
is a declarative method call on this or some other component, with or
without parameters. Being declarative, actions have some advantages over procedural code:
they make your application easier to understand and easier to maintain, and they allow
tools such as Reify to understand and edit your event handling logic.
To take a simple example, this is how you would declare an Action
to display
a record in a DetailViewer
when that record is
clicked in a ListGrid
.
<ListGrid dataSource="Customer" autoID="customerGrid"> ... <recordClick> <Action target="customerDetailGrid" name="viewSelectedData" mapping="viewer"/> </recordClick> </ListGrid>The three elements of this declaration:
target
and name
are both required attributes of any
Action
,
and they must be valid. If target
does not refer to a valid component, or
name
is not the name of a valid method on that component, you will generate a
runtime error. Note, the rules around describing valid actions in the
Declaring Events and Actions section of the ComponentSchema
article apply to Reify only. When building applications through Reify,
only methods marked action="true"
will appear in the list of valid actions for
a given target component. However, any documented method can be called as an
Action
in manually-created Component XML, as can any registered
string method
,
Event handlers can also invoke workflow
processes
, which are a special
kind of multi-step Action
. You specify a workflow process like this (see the
Process documentation
for details of what
goes inside the
<Process>
tag)
<ListGrid dataSource="Customer" autoID="customerGrid"> ... <recordClick> <Process> ... </Process> </recordClick> </ListGrid>
Finally, you are not limited to one Action
per event: you can declare any
number of Action
s and/or Process
es inside an event handler
declaration.
Parameters are defined in an Action
declaration in the mapping
attribute. This attribute is optional; if the target action method does not require
parameters, this attribute can be omitted. If provided, mapping
should be a
comma-separated list of values. Each of these values is either:
this
, which is a reference to the source component
(ie, the component upon which the Action
is being defined)new Date()
Action
s
are declared inside event handler declarations that correspond to Smart GWT event methods.
These methods are passed parameters, and these parameters are available,
via the mapping
, to any contained Action
. Providing the correct
mapping requires that you know the name of the parameter you are interested in, and this
information is present in the documentation.
To take the above example, we want to call viewSelectedData()
on the
DetailViewer
, so looking at the documentation for
DetailViewer.viewSelectedData()
, we can see that it takes a single parameter of
type ListGrid
or TileGrid
, or the ID of a ListGrid
or TileGrid
. This parameter tells the DetailViewer
which
component's selected data to show, so we want to pass in the ListGrid
itself,
the component we are declaring this Action
on.
One way to do this would be to use a mapping of "this"
. As you can see from
the example above, though, there is another way. If we look at the documentation for the
event method wrapping our Action
- ListGrid.recordClick()
- we will see
that it is passed a number of parameters, the first of which is a pointer to the
ListGrid
itself. As the documentation shows, this parameter is called "viewer".
Therefore, we can use a mapping
of "viewer". If we were declaring an
Action
to call a method that requires a Record
parameter, we can
look at the documentation for recordClick()
again and note that it is also
passed the record just clicked, in a parameter called record
. So our mapping
for that Action
would be "record".
A Component XML screen created in Reify or via the
Balsamiq importer
will assign global IDs to all
components
generated from your mockup so that you can retrieve them by ID to add event handlers and
call APIs. However if you build an application out of multiple screens built at different
times, these IDs can collide, which will cause components to overwrite each other as they
each try to use the same ID.
To solve this, the RPCManager.loadScreen()
API will ignore global IDs on loaded
components, assigning them sequential generated IDs instead (which will never collide).
Instead of using global IDs, the callback for loadScreen() will automatically provide
you with the outermost component of a loaded screen, and that outermost component will
provide access to other components by their original IDs via Canvas.getByLocalId()
.
This allows you to add loaded screens to existing layouts, attach event handlers and take other programmatic actions, all without ever establishing global IDs.
A typical application that uses screens stored in Component XML will have several such
screens, or in some cases, hundreds or thousands. RPCManager.cacheScreens()
can be
used to load a set of screen definitions without actually creating any UI components - a
subsequent call to RPCManager.createScreen()
is used to actually create the screen
when needed. These two APIs provide the same global ID management facilities as
loadScreen()
.
As discussed in the Smart GWT Architecture
overview
, screen
definitions are typically very small, and should be loaded entirely up front or in very
large batches. Further, screen definitions have essentially negligible runtime
overhead until the screen is actually created.
Therefore, use the following best practices for screen loading, even if you have very few or only one screen defined in Component XML:
RPCManager.cacheScreens()
RPCManager.createScreen()
.
cacheScreens()
, loading sets of screens that are likely to be used
together.
Components can be dynamically provided on the server side by using the API defined in
ScreenLoaderServlet.addDynamicScreenGenerator()
which allows adding DynamicScreenGenerators to the system for providing the .ui.xml files on
the fly:
ScreenLoaderServlet.addDynamicScreenGenerator(new DynamicScreenGenerator() { public String getScreen(String id) { if (id.equals("testDynamicScreenPrefix3")) { return null; } id=id.replace("testDynamicScreen",""); return ""; } }, "testDynamicScreenPrefix");
Whenever the system needs a screen in future, it will first call the registered
DynamicScreenGenerator's
getScreen(String)
method for providing the given screen; Only if all queried
DynamicScreenGenerators returns null
, will proceed to use the normal system for
obtaining
the screen instances.
NOTE:
In the provided example we register a DynamicScreenGenerator which will be called for each screen the system tries to load, if it starts with "testDynamicScreenPrefix", except the screen with id "testDynamicScreenPrefix3" for which we return null.
While registering a DynamicScreenGenerator is the first choice since is compatible with ScreenLoaderServlet's ability to load several screens in a single HTTPRequest, there are two additional ways to load Component XML screens - you can create a .jsp that uses the JSP tags that come with the SDK:
<%@ taglib uri="http://www.smartclient.com/taglib" prefix="isomorphic" %> <isomorphic:XML> ... Component XML ... </isomorphic:XML>
Or you can use the server-side API com.isomorphic.XML.toJS():
XML.toJS("<isomorphicXML xmlns:xsi=\"nativeType\">" + componentXMLCode + "</isomorphicXML>");However these two approaches will allow to only load one screen at a time. The JSP code above and the programmatic call to XML.toJS() both return a JavaScript code, which is the response that
RPCManager.loadScreen()
expects. The
XML.toJS()
API can be easily combined with
direct use of the server-side
DataSource API
to build
a version of the ScreenLoaderServlet that can retrieve Component XML from a database
or any Java API.
For static Component XML screens (cannot be changed at runtime), you can optionally run the
XML.toJS() process as a build step to save a small amount of runtime overhead in XML to JS
translation. Use RPCManager.loadScreen()
to load the resulting JavaScript by
overriding the RPCRequest.actionURL
to
point to the generated JavaScript file.
Note that the overhead is minor enough that this is not worth doing unless you have a very
large deployment and a very large number of static Component XML files.
XML parsing errors, which happen when XML is not well-formed and would be rejected by any standard XML parser, are reported in the server-side log, and, when possible, also in the client-side log within the "Results" tab of the Developer Console.
If you are loading a screen via the RPCManager.loadScreen()
API, you can see the
response from the server in the RPC tab of the Developer Console - this will show you issues
such as a misplaced ScreenLoaderServlet (HTTP response code will be 404 - Not Found) or
responses that contain server exception details instead of the expected JavaScript response.
You can also use the "Eval XML" section in the "Results" tab of the Developer Console to interactively experiment with Component XML ("Eval XML" button) and as a means of seeing the generated JavaScript ("Show JS" button).
Component XML files support embedding references to messages loaded from ResourceBundles via
the same JSTL-like <fmt> syntax as is used for DataSource .ds.xml files. See
DataSource localization for details
.
If you define a new component class com.mycompany.MyListGrid
which is a subclass
of the
built-in component ListGrid
, and you register your class for reflection
,
you can create it in XML as shown below:
<ListGrid constructor="com.mycompany.MyListGrid" width="500"/>
By using the <ListGrid> tag you advertise that properties should be interpreted
as ListGrid
properties. By specifying constructor
you tell SmartGWT what class to create.
Your custom component (e.g. com.mycompany.MyListGrid
) may have additional
properties which are not present in the standard superclass (e.g. ListGrid
).
You can set such properties in XML as if they were pre-defined properties:
<ListGrid constructor="com.mycompany.MyListGrid" myProperty="false"/>
In this case, the BeanFactory
code
will ultimately call MyListGrid.setMyProperty(false);
in order
to set the property. Since BeanFactory
knows that the property
takes a boolean, it will automatically convert the string value "false" to a
boolean, using the type conversions described below.
Instead of relying on the automatic type conversions, you can force a property to be interpreted as a given type by using the "xsi:type" attribute:
<ListGrid> <constructor>com.mycompany.MyListGrid</constructor> <myProperty xsi:type="xsd:boolean">false</myProperty> </ListGrid>
The same notation works when you want to declare that an entire subobject has a given type.
For example, this would cause the custom property "myListGrid" to have a live
ListGrid
instance as its value. All of the properties
on the <myListGrid> tag
will be correctly interpreted as ListGrid properties and have the correct types.
<Canvas> <myListGrid xsi:type="ListGrid" width="500" height="600"/> </Canvas>
For your reference: "xsi" stands for "XML Schema Instance"; this notation derives from XML Schema standards for explicitly specifying type inline.
Instead of using the constructor
and xsi:type
attributes for
custom components and custom properties, you can create a ComponentSchema
that
describes the custom component. Declaring a component schema allows you to use your
component just like the built-in SmartGWT components, and also allows your component to
be used within Reify
.
The BeanFactory
code
uses a reflection-like mechanism to discern the type which a SmartGWT
property requires, and automatically
converts supplied values to the required type when possible. In cases where
conversion is impossible, an IllegalArgumentException
is
thrown.
Where the setter for a property takes a primitive type (boolean, double, float, int, or long), any "null" value supplied will be converted to 0 (for the numeric types) or false (for boolean). Conversely, if the setter takes the boxed version of the type (Boolean, Double, etc.), any primitive value supplied will be auto-boxed. Note that byte, short and char properties are not currently handled.
Properties which take numeric types will convert other numeric
types, as well as strings, using standard Java APIs (e.g.
Integer.valueOf()
). Boolean "true" will be converted to 1, and false to 0.
If the supplied value cannot be converted to the numeric type, an
IllegalArgumentException
will be thrown.
Properties which take a Date type will convert from strings using
DateUtil.parseInput(String)
.
Properties which take Enum types will convert from strings using
Enum.valueOf()
.
However, any dashes ("-") in the string will be converted to underscores, and
the string will be converted to upper-case if necessary. If the string does not match
one of the Enum
values, an IllegalArgumentException
will be thrown.
Properties which take Array types will convert arrays where the individual values can be converted to the appropriate type. If a single value is supplied, it will be wrapped in an array.