Interface ComponentXML


public interface ComponentXML

Component XML

Component XML is an XML format for declaring Smart GWT components and screen definitions. Available with Smart GWT Pro and above, Component XML is the same format used by Reify to save screens.

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.

Basic Usage

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>
  

Loading screens stored in Component XML

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.

Event Handlers & Scripting loaded components

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.

Declarative Actions

Component XML files can declare Actions 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 is the global ID of the component on which the action will be called
  • name is the name of the method to call
  • mapping is an optional definition of the parameters to pass to the method. See the separate section on parameters below
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 Actions and/or Processes inside an event handler declaration.

Parameters and Actions

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:

  • A variable name
  • The special variable this, which is a reference to the source component (ie, the component upon which the Action is being defined)
  • A literal, like 'foo' or 17. Note, string literals must be enclosed in quotes, or they will be interpreted as variable names
  • A valid Javascript expression, like new Date()
Of these, the most interesting and most commonly-used are the first two. Actions 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".

Component XML and global IDs

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.

Loading multiple screens

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:

  • at application startup, load all screens using RPCManager.cacheScreens()
  • create screens lazily (when they are about to be shown to the end user) using RPCManager.createScreen().
  • for applications with very very large numbers of screens where loading all screen definitions up front creates a very large download, consider multiple calls to cacheScreens(), loading sets of screens that are likely to be used together.

Dynamic Component XML

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 "<VLayout ID=\""+id+"\" border=\"1px solid blue\"/>";
       }
   }, "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:

  • If this API is used, DynamicScreenGenerator will be called for every screen that the framework needs. Instead of this, the API contains alternative methods which will allow adding DynamicScreenGenerators only for a given string prefix or a regular expression.

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.

Troubleshooting

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).

Localization / Internationalization

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.

Custom Components

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.

Custom Properties

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.

Component Schema

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.

Type Conversions

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.