What's new? | Help | Directory | Sign in
Google
                
Search
for
Updated Jul 16, 2008 by rj...@google.com
Labels: Type-Library
UiBinder  
UI Binder Use Cases

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

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);
  }
}

Comment by dan.baternik, Jul 30, 2008

A declarative ui definitely appears to be step in the right direction. I'm following this very closely :)

Comment by svramu, Aug 15, 2008

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

Comment by svramu, Sep 02 (2 days ago)

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.

Comment by svramu, Sep 02 (2 days ago)

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.


Sign in to add a comment