|
|
GWT UI Binder Use Cases
Ray Ryan
This document provides various use cases for the use of the UiBinder, a proposed service to generate Widget and DOM structures from XML markup.
Background
There are problems with the declarative ui template service as it was originally proposed
- A template-based UI must be istantiated via GWT.create(), causing an implementation detail to be visible as public api
- Within a template, only widgets with a zero arg constructor can be used
- ClientBundles cannot be used
- Template xml files are found by magical name matching conventions, and applying more than one xml template to a class is impossible
In addressing these issues, we have talked about encouraging a proxy style of use (basically, use Composite to wrap whatever widget gets GWT.create()'d), but dislike the extra object creation implied. We also hope for a system that can choose to use innerHTML, cloning, or DOM assembly as makes sense per browser type. These shortcomings could be addressed by a combination of developer discipline (yuck) and perhaps the builder pattern, but we still found ourselves faced with the likelihood of hurried developers wrapping an unneeded, generated object.
Emily hit upon the idea of the Configurator (here rechristened UiBinder). It’s like a factory, but responsible for filling in the fields of a Widget (or other object) that someone else instantiates, rather than instantiating one itself. This seems to offer all the benefits of a builder, with no concerns of extra object creation, and as a nice side effect avoids a lot of boilerplate. This document illustrates its application in various use cases.
/**
* Interface implemented by classes that generate DOM and widget
* structures from ui.xml template files. Put the {@link Template}
* annotation on a UiBinder class declaration to point the code
* generator at the right ui.xml file.
*/
interface UiBinder<T> {
/**
* Create a new UI and inject it into the given owner.
* Elements marked in the template by g:field will be
* assigned to like named fields or setter methods
* in owner.
*/
bindUi(T owner);
}/**
* Extends UiBinder to provide separate calls to create the root
* of the UI, and to bind the UI. This allows fancy implementations
* to support deferred instantiation.
*
* <p>Calling the inherited bindUi(owner) method is the same
* as calling bindRest(createRoot(), owner);
*/
interface ForkedUiBinder<R, O> extends UiBinder<O> {
/**
* Return the root element defined in the template,
* possibly empty. The type R must match the root
* element in the ui.xml template file.
*/
R createRoot();
/**
* Given a ui root from a previous call to createRoot(),
* install it into owner. Elements marked in the
* template by g:field will be assigned to like named
* fields or setter methods in owner.(If creation of
* root's contents were deferred, they will be
* instantiated now.)
*/
bindRest(R root, O owner);
}Hello World
Make a simple generated UI, with a named element, and without widgets.
<!-- HelloWorld.ui.xml --> <span xmlns:g='import:com.google.gwt.user.client.ui'> Hello, <span g:field="nameSpan"/>. </span>
public class HelloWorld extends UIObject { // Could extend Widget instead
@Template("HelloWorld.ui.xml")
interface MyUiBinder extends UiBinder<HelloWorld> {}
private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class);
SpanElement nameSpan;
public HelloWorld(String name) {
// call setElement(), set nameSpan
uiBinder.bindUi(this);
nameSpan.setInnerText(name);
}
}
// Use:
SpanElement helloWorld = new HelloWorld("World").getElement();Hello Composite World
Make a simple widget-based UI
<!-- HelloWidgetWorld.ui.xml --> <g:HTMLPanel xmlns:g='import:com.google.gwt.user.client.ui'> Hello, <g:ListBox g:field="listBox"/>. </g:HTMLPanel>
public class HelloWidgetWorld extends Composite {
@Template("HelloWidgetWorld.ui.xml")
interface MyUiBinder extends UiBinder<HelloWidgetWorld> {}
private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class);
ListBox listBox;
public HelloWidgetWorld(String... names) {
// call initWidget(), set listBox
uiBinder.bind(this);
for (String name : names) { listBox.addItem(name); }
}
}
// Use:
HelloWidgetWorld helloWorld =
new HelloWidgetWorld("able", "baker", "charlie");Hello Deferred World
Note that this example uses the same ui.xml file as the first. Templates need have no knowledge of what kind of binder will use them.
<!-- HelloWorld.ui.xml --> <span xmlns:g='import:com.google.gwt.user.client.ui'> Hello, <span g:field='nameSpan'/>. </span>
public class Deferral extends Widget {
@Template("HelloWorld.ui.xml")
interface MyUiBinder extends ForkedUiBinder<SpanElement, Topical> {}
private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class);
SpanElement nameSpan;
public Deferral(String name) {
setElement(uiBinder.createRoot());
}
@Override protected onAttach() {
// set nameSpan
uiBinder.bindRest((SpanElement)getElement(), this);
nameSpan.setInnerText(name);
super.onAttach();
}
}
// Use:
Deferral helloDeferral = new Deferral("World");Putting a label on a checkbox (referring to generated ids within a template)
You want to make your personal variant on the single most common widget, a checkbox with a nice, accessible HTML label element tied to it:
<!-- LabeledCheckBox.ui.xml --> <span xmlns:g='import:com.google.gwt.user.client.ui'> <input type="checkbox" g:field="myCheckBox"> <label g:for="myCheckBox" g:field="myLabel"/> </span>
public class LabeledCheckBox extends Widget {
@Template("LabeledCheckBox.ui.xml")
interface MyUiBinder extends UiBinder<LabeledCheckbox> {}
private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class);
InputElement myCheckBox;
LabelElement myLabel;
public LabeledCheckBox() { uiBinder.bind(this); }
public void setValue(boolean b) { myCheckBox.setChecked(b); }
public boolean getValue() { return myCheckBox.isChecked(); }
public void setName(String name) { myLabel.setInnerText(name); }
public String getName() { return myLabel.getInnerText(); }
}The proposal here is that a g: prefix on any attribute other than id fills it with the id generated for a corresponding g:field.
There are type matching issues here. The g:field of a DOM element is a string id, while that for a UIObject is typed. So, this should fail with a type mismatch:
<some:WidgetOfSomeKind g:field="theWidget"> <label gwt:for="theWidget" />
Using ClientBundles with a UiBinder
<!-- LogoNamePanel.ui.xml -->
<gwt:HTMLPanel
xmlns:g="import:com.google.gwt.user.client.ui"
xmlns:res="with:com.my.app.widgets.logoname.Resources">
<img res:apply="logoImage">
<div res:class="style.mainBlock">
<div res:apply="style.userPictureSprite">
Well hello there
<span res:class="style.nameSpan" g:field="userNameField"/>
</div>
</div>
</gwt:HTMLPanel>public class LogoNamePanel extends Composite {
@Template("LogoNamePanel.ui.xml")
interface MyUiBinder extend UiBinder<LogoNamePanel> {}
private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class);
SpanElement nameSpan;
public LogoNamePanel() {
uiBinder.bind(this);
}
public void setUserName(String userName) {
nameSpan.setInnerText(userName);
}
}
public interface Resources extends ClientBundle {
@Resource("Style.css")
Style style();
@Resource("Logo.jpg")
ImageResource widgetyImage();
public interface Style extends CssResource {
String mainBlock();
String nameSpan();
Sprite userPictureSprite();
}
}The with: uri type marks an object whose methods can be called to fill in attribute values. If no public api is provided to set the "with" argument (as in this example), it must be instantiable by GWT.create().
public interface Applicator<T> {
void apply(T t);
}
<div res:-apply="style.widgetyLogoSprite"/>Note also that there is no requirement that a with: class implement the ClientBundle interface.
Share ClientBundle instances
Extends Resourceful (from the example above) to allow its bundle to be passed in.
public class Resourceful extends Composite {
@Template("Resourceful.ui.xml")
interface MyUiBinder extends UiBinder<Resourceful> {
MyUiBinder with(Resources resources);
}
private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class);
SpanElement widgetyText;
public Resourceful(Resources resources) {
uiBinder.with(resources).bind(this);
}
}If this were using a ForkedUiBinder, it would look like this:
public class Resourceful extends Composite {
@Template("Resourceful.ui.xml")
interface MyUiBinder extends ForkedUiBinder<HTMLPanel, Resourceful> {
MyUiBinder with(Resources resources);
}
private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class);
SpanElement widgetyText;
public Resourceful(Resources resources) {
initWidget(uiBinder.createRoot());
uiBinder.with(resources).bindRest(getWidget(), this);
}
}The trick here is to define a with() method on MyUiBinder, corresponding to any with: url defined in your template that you wish to make public.
But wait, I hear you say. We have a single static instance of MyUiBinder, shared by every call to the Resourceful() constructor. Is it really safe to give it an instance method like with(), and load it up with state that may linger if bindRest() is never called?
Not to worry--the binder instance returned by with() does not need to be the same instance that received the with() call. When made necessary like this, we can return a disposable UiBinder instance to accumulate whatever per-instance state is needed. Slick, eh?
And you've probably noticed that you can use this technique to make any constructor argument available to the template...
Using a widget that requires constructor args
You have an existing widget that needs constructor arguments.
public CricketScores(String... teamNames) {...} You use it in a template.
<!-- UserDashboard.ui.xml --> <gwt:HTMLPanel xmlns:gwt='import:com.google.gwt.user.client.ui' xmlns:my='import:com.my.app.widgets' > <my:WeatherReport g:field="weather"/> <my:Stocks g:field="stocks"/> <my:CricketScores g:field="scores" /> </gwt:HTMLPanel>
public class UserDashboard extends Composite {
@Template("UserDashboard.ui.xml")
interface MyUiBinder extends UiBinder<UserDashboard> {}
private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class);
public UserDashboard() {
uiBinder.bind(this);
}
}An error results:
UserDashboard.ui.xml:7:2 [ERROR] Cannot instantiate CricketScores without a zero args constructor. You must define a non-private CricketScores createCricketScores() method on UserDashboard.
So you define one:
public class UserDashboard extends Composite {
@Template("UserDashboard.ui.xml")
interface MyUiBinder extends UiBinder<UserDashboard> {}
String[] teamNames;
public UserDashboard(String... teamNames) {
this.teamNames = teamNames;
uiBinder.bind(this);
}
CricketScores createCricketScores() {
return new CricketSores(teamNames);
}
}or perhaps
public class UserDashboard extends Composite {
@Template("UserDashboard.ui.xml")
interface MyUiBinder extends UiBinder<UserDashboard> {}
CricketScores scores;
public UserDashboard(CricketScores scores) {
this.scores = scores;
uiBinder.bind(this);
}
CricketScores createCricketScores() {
return scores;
}
}Apply different xml templates to the same widget
You're an MVC developer. You have a nice view interface, and a templated Widget that implements it. How might you use several different xml templates for the same view?
public class FooPickerController {
public interface Display {
HasText titleField();
SourcesChangeEvents pickerSelect();
}
public void setDisplay(FooPickerDisplay display) { ... }
}
public class FooPickerDisplay extends Composite
implements FooPickerController.Display {
@Template("RedFooPicker.ui.xml")
interface RedBinder extends UiBinder<FooPickerDisplay> {}
private static RedBinder redBinder = GWT.create(MyUiBinder.class);
@Template("BlueFooPicker.ui.xml")
interface BlueBinder extends UiBinder<FooPickerDisplay> {}
private static BlueBinder blueBinder = GWT.create(MyUiBinder.class);
HasText titleField();
SourcesChangeEvents pickerSelect();
protected FooPickerDisplay(UiBinder<FooPickerDisplay> binder) {
uiBinder.bind(this);
}
public static FooPickerDisplay createRedPicker() {
return new FooPickerDisplay(redBinder);
}
public static FooPickerDisplay createBluePicker() {
return new FooPickerDisplay(blueBinder);
}
}
Sign in to add a comment

A declarative ui definitely appears to be step in the right direction. I'm following this very closely :)
I don't know how to link to a comment in another wiki page... Please see my comment/request in http://code.google.com/p/google-web-toolkit-incubator/wiki/CssResource
Thanks to the showcase example, and http://groups.google.com/group/Google-Web-Toolkit/browse_thread/thread/860aefef0daa2539/8839d9da2f7b28a7 (also http://www.zenika.com/blog/wp-content/uploads/2007/08/tutorial-binding-en.pdf), I managed to have my own generator to bind the HTML to GWT in compile time.
The use case I'm trying is to provide different GUI for same functionality, and with generators I can map the right GUI as one of the compiler permutation. I peeked and used the Locale like JS usage for my GWT module. All neat and clean! The issue I see is that it pastes the whole HTML as it is in the generated html files even in obfuscated mode. Makes sense as any optimization on HTML can be invasive. So I'm looking if the UiBinder will optimize it much more.
Will UiBindier? allow me to create more than one GUI for the same screen (a collection of widgets with logic)? BTW, I use the HTML only for layout (so a designer can supply it). All the components are embedded into it with the logic. Any day I'll be happy to ditch my homemade generator (however sweet it is) for GWT supported one.
oh FooPickerDisplay? says it all! (and that static call ensures only one thing is really instantiated) Sorry should have read more carefully. And the whole model is not alien for me, and this is neat. I thought my model is convoluted with inner interfaces etc, but looks like it is necessary.