Saturday, February 27, 2010

Dynamic Layouts in GWT

After figuring out how to identify the current user, the next task in building my math quiz application was to build a basic layout for the quiz itself.

For starters, all I need is a header that displays the name of the current user, and a content area that displays the math problems. A user answers a problem by filling in a text box and hitting enter, after which the program should refresh the content area and show a new problem. After they've done a certain number of problems, the content area shows an "All done" message.

Implementing the basic layout and associated dynamic logic was fairly simple. First I added a body area to the layout:

<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
  xmlns:g='urn:import:com.google.gwt.user.client.ui'>

  <g:DockLayoutPanel unit="EM">

    <g:north size="4">
      <g:HTML>
        Hello,
        <span ui:field='nameSpan' />
      </g:HTML>
    </g:north>

    <g:center>
      <g:FlowPanel ui:field="problemPanel">
      </g:FlowPanel>
    </g:center>

  </g:DockLayoutPanel>

</ui:UiBinder>

Then I added a method to my EntryPoint implementation, which clears the problemPanel and shows a new problem up to a fixed limit:

public class Uitest implements EntryPoint {

  ...

  private int problemCount = 0;

  public void onModuleLoad() {

    ...
    showProblem();
  }
  
  private void showProblem() {
    
    problemPanel.clear();
    
    long a = Math.round(Math.random() * 10);
    long b = Math.round(Math.random() * 10);

    problemPanel.add(new InlineLabel(Long.toString(a)));
    problemPanel.add(new InlineLabel("+"));
    problemPanel.add(new InlineLabel(Long.toString(b)));
    problemPanel.add(new InlineLabel("="));
    
    final TextBox textBox = new TextBox();
    textBox.setWidth("2em");
    
    textBox.addChangeHandler(new ChangeHandler() {

      @Override
      public void onChange(ChangeEvent event) {
        if (problemCount++ < 5) {
          showProblem();
        } else {
          problemPanel.clear();
          problemPanel.add(new Label("All done!"));
        }
      }
    });
    
    problemPanel.add(textBox);

    textBox.setFocus(true);    
  }
}

I am assuming that it is acceptable for the class to be stateful (keep track of the count of problems for a single user), since this code runs on the client.

I ran the project in Eclipse and it worked as expected...almost. The positioning of the problem was oddly shifted on first viewing the page, and then it seemed to straighten itself out after the first answer. By process of elimination I discovered that the setFocus was causing this. First I verified that this method was not changing the DOM in any way; it was not. After looking through the docs I finally tried deferring the focus command until after all other pending operations were complete:

DeferredCommand.addCommand(new Command() {

      @Override
      public void execute() {
        textBox.setFocus(true);
      }
    });

This solved the display problem. Now I was ready to implement the remote service to record the user's score.