software development

A pattern for working with GWT screen elements

I was working on a project recently using the Google Web Toolkit (GWT) in conjunction with the GWT wrapper (GWT-EXT) for the fabulous ExtJS Javascript library. The program’s design ran up against a road-block, however, when I realized that my application’s architecture was simply not well-suited for the type of work I was doing. The application essentially:

  1. Draws some elements on the screen
  2. Fetches data
  3. Possibly populate some of the existing elements with some of the data
  4. Possibly create new elements based on said data and possibly populate some of the new elements with some of the data

Seems simple, right? It would be except for a snag. See, since I am doing a lot of AJAX wizardry dependent upon the results of my data, some of the magic relies on DOM calculation. The problem is that I need to guarantee that my screen elements are 1) attached to the DOM and 2) visible on the screen. Of course, this presents another problem. I don’t want my elements to be visible until they are ready to be shown, but many interesting Javascript widgets that ship with ExtJS do calculations based on offsetHeight, offsetWidth, etc. — values that can only be computed if the element is visible and attached to the DOM.

My solution was simple, I would just draw the elements off-screen until they were ready to be displayed. But how would I know when the elements were finished “opening?” Enter the interface ScreenElement.java. ScreenElement is a Java interface that any widget which retrieves data and has its appearance modified based on that data should implement:

package com.lostcreations.vmm.client;

import com.google.gwt.user.client.ui.Panel;
import com.gwtext.client.core.ExtElement;

/**
 * An interface for any element drawn on the screen.
 * 
 * @author akutz
 * 
 */
public interface ScreenElement
{
    /**
     * Begin the data retrieval operation.
     */
    public void beginFetch();

    /**
     * Close this screen element. This method should call the following methods
     * in this order.
     * 
     * <ol>
     * <li>hide()
     * </li><li>offScreen()</li>
     * </ol>
     */
    public void close();
    
    /**
     * Does client-side computation after the data has been flushed into the
     * element.
     */
    public void compute();

    /**
     * Draws this screen element.
     */
    public void draw();

    /**
     * Ends the data retrieval operation indicating that no errors occurred.
     */
    public void endFetch();

    /**
     * Ends the data retrieval operation with the option to specify that error
     * occurred.
     * 
     * @param withErrors True to indicate errors; otherwise false.
     */
    public void endFetch(Boolean withErrors);

    /**
     * Flushes the data into the screen element.
     */
    public void flush();

    /**
     * Sets this element's visibility to hidden and its display mode to none.
     */
    public void hide();

    /**
     * Returns true if their is still data being retrieved from the server;
     * otherwise false.
     * 
     * @return True if their is still data being retrieved from the server;
     *         otherwise false.
     */
    public boolean isFetching();

    /**
     * Returns true if this screen element is currently open.
     * 
     * @return True if the screen element is open; otherwise false.
     */
    public boolean isOpen();

    /**
     * Moves the screen element off-screen and then sets its visibility to true.
     */
    public void offScreen();

    /**
     * Is called when the fetch operation is completed. This method should call
     * the following methods in this order:
     * 
     * <ol>
     * <li>flush()</li>
     * <li>compute()</li>
     * <li>hide()</li>
     * <li>onScreen()</li>
     * <li>show()</li>
     * </ol>
     */
    public void onEndFetch();

    /**
     * Is fired when the open operation is completed.
     */
    public void onOpen();

    /**
     * Moves the screen element on-screen and then sets its visibility to false.
     */
    public void onScreen();

    /**
     * Open this screen element. This method should call the following methods
     * in this order:
     * <ol>
     * <li>offScreen()</li>
     * <li>show()</li>
     * <li>draw()</li>
     * <li>beginFetch()</li>
     * </ol>
     * 
     * @param parent The panel this screen element will be added to once opened.
     */
    public void open(Panel parent);

    /**
     * Sets this element's visibility to visible and its display mode to
     * 'block'.
     */
    public void show();

    /**
     * Sets this element's visibility to visible and its display mode to a valid
     * CSS value.
     * 
     * @param displayMode A valid CSS value for display mode.
     */
    public void show(String displayMode);
}

This interface guarantees that an element is ready to be shown to the client. The pattern is as follows:

  1. The element’s open(Panel parent) method is invoked where parent is the panel the element should be added to once it has finished opening.
  2. The element is created and immediately moved off-screen so that the user does not see the element.
  3. The element’s show() method is invoked to make the element visible.
  4. The element’s draw() method is invoked to draw the element’s child controls. Since the element is visible, any control that needs to do DOM calculations will be able to.
  5. The element invokes the beginFetch() method. A timer is then immediately started which checks every 50 milliseconds to see if the element has completed its fetch operation by calling the isFetching() method.
  6. Once the data retrieval has completed, the endFetch() method will be invoked. This causes the timer mentioned in the last step to cancel itself and then invoke the onEndFetch() method.
  7. The onEndFetch() method then invokes the flush() method where the data that was retrieved is acted upon. This could mean additional child controls are added to the element or simply that existing child controls have data populated.
  8. The compute() method is invoked next. This is where any DOM-related computations should take place that are dependent upon the flushed data (such as progress bars). Since the element is still visible off-screen and the data is now in place, the DOM can be updated accordingly and successfully.
  9. The element is then hidden with the hide() method in order to prepare it to be moved onto the screen.
  10. The element is moved on screen by invoking the onScreen() method, but this does not make the element visible.
  11. Next, the element is shown to the user with the show() method.
  12. Finally the onOpen() method is called setting the element’s opened state to True for any timers waiting on the element to be opened.

That’s it! It’s pretty straight-forward, and it guarantees that the elements are drawn correctly before they are shown to the user. Not only that, but it is a great way to think about how you are working with these elements. It really structures things (at least for me). The end of this post includes two classes that implement this interface (a base, abstract class and a final class) to give you an idea of how this works in the real world.

package com.lostcreations.vmm.client;

import com.allen_sauer.gwt.log.client.Log;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Panel;
import com.gwtext.client.core.ExtElement;
import com.gwtext.client.core.Function;
import com.gwtext.client.core.FxConfig;

/**
 * The abstract base class for views.
 * 
 * @author akutz
 * 
 */
public abstract class ViewImpl implements View, ScreenElement
{
    /**
     * The view's main content container.
     */
    private DivVerticalPanel viewContainer;

    /**
     * True if this view is open; otherwise false.
     */
    private boolean isOpen;

    private boolean isFetching;

    Panel parent;

    protected ViewImpl()
    {
        Log.trace("creating new " + getClassName());
        this.viewContainer = new DivVerticalPanel();
        this.viewContainer.addStyleName("view");
        hide();
        Log.trace("created new " + getClassName());
    }

    protected String getClassName()
    {
        return this.getClass().getName();
    }

    public void compute()
    {
    }

    public void close()
    {
        if (!this.isOpen) return;
        hide();
        offScreen();
    }

    public DivVerticalPanel getViewContainer()
    {
        return this.viewContainer;
    }

    public void offScreen()
    {
        ScreenElementUtil.offScreen(this.viewContainer.getElement());
    }

    public void onScreen()
    {
        ScreenElementUtil.onScreen(this.viewContainer.getElement());
    }

    public boolean isOpen()
    {
        return this.isOpen;
    }

    public void open(Panel parent)
    {
        if (this.isOpen) return;

        this.parent = parent;

        offScreen();
        parent.add(this.viewContainer);
        show();
        draw();
        beginFetch();

        // Start a timer that checks ever 50 milliseconds to see if this
        // element is finished fetching data.
        Timer t = new Timer()
        {
            @Override
            public void run()
            {
                if (!isFetching())
                {
                    Log.trace(getClassName() + " finished fetching");
                    cancel();
                    onEndFetch();
                }
                Log.trace(getClassName() + " still fetching");
            }
        };

        t.scheduleRepeating(50);
    }

    /**
     * Begin any data retrieval operations. Classes that override this method
     * must call super.beginFetch() before doing any other operations.
     */
    public void beginFetch()
    {
        this.isFetching = true;
        Log.trace(getClassName() + " begin fetch");
    }

    public void endFetch()
    {
        endFetch(false);
    }
    
    public void endFetch(Boolean withErrors)
    {
        this.isFetching = false;
        Log.trace(getClassName() + " end fetch");
    }

    public final boolean isFetching()
    {
        return this.isFetching;
    }

    public final ExtElement getExtEl()
    {
        return ScreenElementUtil.getExtEl(this.viewContainer.getElement());
    }

    public final void hide()
    {
        ScreenElementUtil.hide(this.viewContainer);
    }

    public void fadeIn()
    {
        ScreenElementUtil.appear(this.viewContainer);
    }

    public void fadeOut()
    {
        ScreenElementUtil.fade(this.viewContainer);
    }

    public void show(String displayMode)
    {
        ScreenElementUtil.show(this.viewContainer);
    }

    public final void show()
    {
        ScreenElementUtil.show(this.viewContainer);
    }

    public void onOpen()
    {
        this.isOpen = true;
    }

    public final void onEndFetch()
    {
        Log.trace(getClassName() + " onEndFetch started");

        flush();
        compute();
        hide();
        onScreen();

        FxConfig fcIn = new FxConfig();
        fcIn.setCallback(new Function()
        {
            public void execute()
            {
                onOpen();
            }
        });

        new ExtElement(this.viewContainer.getElement()).fadeIn(fcIn);

        Log.trace(getClassName() + " onEndFetch completed");
    }
}
package com.lostcreations.vmm.client;

import com.google.gwt.user.client.Timer;

/**
 * This application's main view.
 * 
 * @author akutz
 * 
 */
public class MainView extends ViewImpl
{
    final AccountPanel accountPanel = new AccountPanel();

    /**
     * Initializes a new MainView object.
     */
    public MainView()
    {
        super();
        getViewContainer().getElement().setId("mainView");
    }

    public void draw()
    {
        accountPanel.open(getViewContainer());
    }

    public void flush()
    {

    }

    @Override
    public void beginFetch()
    {
        super.beginFetch();
        
        // Do not end the fetch until the account panel has been completed
        // its open operation.
        Timer t = new Timer()
        {
            @Override
            public void run()
            {
                if (accountPanel.isOpen())
                {
                    cancel();
                    endFetch();
                }
            }
        };
        
        t.scheduleRepeating(50);
    }
}
Advertisements

2 thoughts on “A pattern for working with GWT screen elements

  1. In view implement, can not find what ScreenElementUtil refer to.

    Since it is not in the import statement, perhaps it is another class in the same package?

    Appreciate it if you can help. Really would like to know how the offscreen drawing task is accomplished.

    Thanks in advance!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s