Category Archives: GWT

Widgets in GWT Cell Tables

I have recently had the need to embed widgets into a GWT cell table.  I have read about the flyweight pattern, and specifically why cell tables are designed to not have widgets in them, but… when you have a requirement to complete you just need to do it.

I went through two iterations of this design and I am pretty happy with the final design.  I took some ideas from GWT Cell Widgets like EditTextCell as well as some experience from the first not-so-great implementation i created.  If you are new to Cells, it is important to realize that the Cell is instantiated one time, but called (with different data) for each row that is rendered.  This is why it is necessary to maintain a map of this cells data.

Here are some design ideas for this “WidgetCell”

  • Widget creation is expensive, so a widget for each row is only created one time and then stored in a cache for reuse.  This helps with the speed of the table rendering.
  • Widgets are never attached to the DOM.  They exist to be manipulated in memory and then have their HTML extracted and pushed into the DOM.
  • Events from the Widgets, therefore, are not handled.  If you need them you will need to change this implementation a little and pass them through the cell (onBrowserEvent method).  Cell events ARE handled (or can be in subclasses), which was enough for my purposes.
  • Finally, the widgets that I am using need to load data from the backend, so this “WidgetCell” populates the cells asynchronously.  This helps speed up initial table rendering as well.

One important thing to note on that last bullet: Browsers have a limit on the number of HTTP requests that can be active at a given time.  Additional requests (from my experience, i didn’t look into it deeply) are getting queued and block the UI from any other HTTP requests until they are cleared from the queue.  This means anything, like navigation, that requires an HTTP request will be queued and block the user interface.  The best solution I have found for this is to pass all related HTTP requests, like the ones for this widget, through a common RPC handler that performs queuing.  This can limit that data providers allowable HTTP requests, leaves additional HTTP connections available, and allows for cancelling uncalled or in-process requests if navigation changes prior to the table completely populating.

This WidgetCell is an abstract class.  It is meant to be extended, with the implementation defining the widget and handling the widget’s data.  The data can be stored in a map with an entry per row.  The abstract methods all pass through either the CellContext, the Cell context’s key, or the Cell index, depending on that is needed for that method.

Important things to remember:

  1.  As I mentioned earlier, this is a method for displaying widgets into Cells.  The widget lifecycle is NOT respected.  There is no attaching and detaching, and events are not handled from the widget.
  2. Widgets that change themselves over their lifetime will NOT have those changes reflected in the cell unless you take special care to manually reflect those changes in the DOM.  An example is a progress bar.  It will be rendered to the DOM, but any updates need to be re-rendered to the DOM manually because the widget is not attached.

Here is the code.  I am sure there will be questions that i can handle in the comments.  Additionally, I need to thank Colin Alworth at Sencha for comments and pointing out pitfalls that helped direct this final design, and David Maddison’s The GWT rendering process post for more insight on the widget lifecycle.

Here is a file with the code: Textfile with WidgetCell Code


import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import com.google.gwt.cell.client.AbstractCell;
import com.google.gwt.cell.client.EditTextCell;
import com.google.gwt.cell.client.ValueUpdater;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.safehtml.client.SafeHtmlTemplates;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.ui.RequiresResize;
import com.google.gwt.user.client.ui.Widget;

/**
 * A Cell for adding Widgets into CellTables. A single instance of a cell is used for a column, with the render method
 * being called for each new cell in a new row. data changes per row must come in through the render method (via the
 * setValue method in super)
 * 
 * The approached used here:
 * <ol>
 * <li>Keep a cache of widgets so only a finite number are made, and they are reused to minimize expensive widget
 * creation (this is based on the method GWT's {@link EditTextCell} uses to maintain data.)</li>
 * <li>The widgets are never attached to the DOM, they are only used for manipulating them and extracting their HTML</li>
 * <li>Events from the widgets, therefore, are not handled. Only events on the cells themselves. However, if needed, the
 * cell events can be passed along to the widgets in the reuse-map and handled. (the context contains the key for the
 * reuse-map)</li>
 * <li>For ajax use, only a wrapper (placeholder) is laid down in the DOM. This placeholder is replaced when the widget
 * HTML when it is ready.</li>
 * </ol>
 * 
 * @author mpickell
 * @version 1.0
 * @param <W>
 *            the widget that is to be used in this cell
 * @param <D>
 *            the type of data used for this widget. This is passed in through the render method for each cell created.
 * 
 * @see <b>For more information on this implementation:</b>
 *      http://www.sencha.com/forum/showthread.php?196112-Widget-Rendered-Grid/page2
 * 
 *      TODO: rename to AbstractClickableWidgetCell?
 * 
 */
public abstract class ClickableWidgetCell<W extends Widget, D> extends AbstractCell<D> implements RequiresResize {

    /**
     * Widget generator for use by this cell renderer. This used as a factory that is used when a new widget is needed.
     * 
     * @param <C>
     *            widget type returned
     */
    protected interface ClickableWidgetCellWidgetFactory<C extends Widget> {
        C createWidget();
    }

    private static int uniqueKeyForWidgetID = 0;

    /**
     * Widget's displaying the table may enlarge it. If this is non-null, it will be resized when all widgets display.
     */
    private RequiresResize containerToResizeAfterAllWidgetsDisplay;

    /**
     * map of widgets used in this column. Once created, the widget is cached so it doesn't need to be created again.
     * The key here is the context.getKey() object, which uniquely IDs a row. (reuse-map is based on design of GWT's
     * {@link EditTextCell})
     */
    private Map<Object, W> reusableWidgetsMap = new HashMap<Object, W>();
    private Map<Object, String> domPlaceholderIds = new HashMap<Object, String>();

    /**
     * This cell can be used as a non-clickable cell as well. this boolean will block the click and keydown events when
     * set to true via the setter.
     */
    private boolean blockClickableEvents = false;

    /**
     * The HTML templates used to render the cell placeholder.
     */
    interface ClickableWidgetCellPlaceholderTemplate extends SafeHtmlTemplates {
        /**
         * The template for the widget placeholder for this Cell, which includes styles and a value.
         * 
         * @param id
         *            the id for this placeholder template
         * @param value
         *            the safe value. Since the value type is {@link SafeHtml}, it will not be escaped before including
         *            it in the template. Alternatively, you could make the value type String, in which case the value
         *            would be escaped.
         * @return a {@link SafeHtml} instance of a simple container. The container position is set to relative so that
         *         any value placed inside can use absolute positioning.
         */
        @SafeHtmlTemplates.Template("<div style='position:relative;' id='{0}'>{1}</div>")
        SafeHtml cell(String id, SafeHtml value);
    }

    /**
     * Create a singleton instance of the templates used to render the cell.
     */
    private static ClickableWidgetCellPlaceholderTemplate placeholderTemplate = GWT
            .create(ClickableWidgetCellPlaceholderTemplate.class);

    /**
     * Construct a new ClickableWidgetCell
     */
    public ClickableWidgetCell() {
        super("click", "keydown");
    }

    /**
     * @return a factory that creates the default widget type, with any necessary setup, that is used by this Dto.
     */
    protected abstract ClickableWidgetCellWidgetFactory<W> getWidgetFactory();

    /**
     * Trigger any asynchronous calls that are needed to get data, if needed, and then update the widget. When update is
     * complete, execute command to push widget HTML to DOM.
     * 
     * @param widget
     *            the widget that needs to be updated
     * @param onDataReadyCallback
     *            the command to trigger when the widget is updated. This command displays the widget in the page
     */
    protected abstract void updateWidget(final W widget, final Context cellContext, final D data,
            final Command onDataReadyCallback);

    /**
     * After the widget is displayed, handle any clean up (like resize to fill cell maybe). This is a default impl.
     * 
     * @param widget
     *            the widget that was displayed in the DOM
     * @param cellContext
     *            this widget's context
     * @param data
     *            the data used when displaying this widget.
     */
    protected void finalizeWidgetAfterDisplayInGrid(final W widget, final Context cellContext, final D data) {
    }

    /**
     * When resize is triggered on this cell (passed down from the grid) the cell implementation can handle it. This is
     * the default impl.
     * 
     * @param widget
     *            a widget from the reuse queue
     * @param cellContextKey
     *            this widget's context
     * @param placeholderId
     *            the placeholder ID for this widget in the DOM, so the HTML can be updated.
     */
    protected void handleWidgetOnWindowResize(W widget, Object cellContextKey, String placeholderId) {
    }

    /**
     * Default implementation.
     * 
     * This method returns HTML to be placed in the page while waiting for the widget to be displayed. If the widget
     * requires service calls, this will be displayed until the service calls return and the onDataReadyCallback command
     * is executed.
     * 
     * @return a SafeHtml object containing whatever HTML should be displayed while the main widget is being updated.
     *         This will be completely removed and placed by the widget HTML when the updateWidget method calls the
     *         onDataReadyCallback.
     */
    protected SafeHtml getWidgetUpdatingHtml() {
        return new SafeHtml() {
            private static final long serialVersionUID = 1L;

            @Override
            public String asString() {
                return "<i>Loading...</i>";
            }
        };
    }

    @Override
    public void onBrowserEvent(Context context, Element parent, D widgetDataDto, NativeEvent event,
            ValueUpdater<D> valueUpdater) {
        super.onBrowserEvent(context, parent, widgetDataDto, event, valueUpdater); // Handles user press actual
                                                                                   // ENTER key
        if ("click".equals(event.getType())) {
            onEnterKeyDown(context, parent, widgetDataDto, event, valueUpdater);
        }
    }

    @Override
    protected void onEnterKeyDown(Context context, Element parent, D widgetDataDto, NativeEvent event,
            ValueUpdater<D> valueUpdater) {
        if (!blockClickableEvents && valueUpdater != null) {
            valueUpdater.update(widgetDataDto);
        }
    }

    @Override
    public void render(final Context context, final D widgetDataDto, final SafeHtmlBuilder sb) {
        /*
         * Widgets are reused so that their (expensive) creation is not performed each time. After creation, they are
         * attached and detached as needed and re-added to the page.
         */
        Object rowKey = context.getKey();

        /*
         * Either get the existing widget from the cache or create it.
         */
        W wfc = reusableWidgetsMap.get(rowKey);
        if (wfc == null) {
            // Create once and store.
            wfc = this.getWidgetFactory().createWidget();
            reusableWidgetsMap.put(rowKey, wfc);
        }
        final W wfcAsFinal = wfc;

        /*
         * Setup a unique key for the placeholder in this row
         */
        if (!domPlaceholderIds.containsKey(context.getKey())) {
            setupPlaceholderId(context);
        }

        /*
         * Allow the Dto to update the widget, possibly after asynchronous calls to service. Once updated, the command
         * is (i.e., has to be) executed to finalize the displaying of the cell.
         * 
         * This update is triggered as a GWT "thread" using the deferred command so that the render method can complete
         * quickly. The update and display of the widget can be deferred safely because everything needed for it is
         * packaged in the updateWidget method
         */
        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
            @Override
            public void execute() {
                updateWidget(wfcAsFinal, context, widgetDataDto, new Command() {
                    @Override
                    public void execute() {
                        ClickableWidgetCell.this.displayWidget(context, widgetDataDto);
                    }
                });
            }
        });

        /*
         * Put the placeholder in the table so we have a location to go to later. Put the "busy" html in there.
         */
        sb.append(placeholderTemplate.cell(domPlaceholderIds.get(context.getKey()), getWidgetUpdatingHtml()));
    }

    private void displayWidget(final Context cellContext, final D widgetDataDto) {
        Element e = getElementsBySelectorJSNI("#" + domPlaceholderIds.get(cellContext.getKey()));
        if (e != null && reusableWidgetsMap.containsKey(cellContext.getKey())
                && reusableWidgetsMap.get(cellContext.getKey()).getElement() != null) {
            e.setInnerHTML(reusableWidgetsMap.get(cellContext.getKey()).getElement().getString());

            /*
             * The widget is now displayed. Allow the implementation to perform any adjustments to it. The
             * implementation can use the domPlaceholderIds map (via getter) to replace the HTML in the page since the
             * element in the widget is never attached to the page.
             * 
             * Changes to the widget will be reflected in the widget map, placeholder ID can be used (via getter, as a
             * string so it cannot be changed) to push any updated HTML to the page.
             */
            Scheduler.get().scheduleDeferred(new ScheduledCommand() {
                @Override
                public void execute() {
                    ClickableWidgetCell.this.finalizeWidgetAfterDisplayInGrid(reusableWidgetsMap.get(cellContext.getKey()),
                            cellContext, widgetDataDto);
                }
            });

            /*
             * This is a widget that might be expanding the table. Call on the resize container to resize.
             */
            if (containerToResizeAfterAllWidgetsDisplay != null) {
                containerToResizeAfterAllWidgetsDisplay.onResize();
            }
        }

    }

    /**
     * Query for elements using selectors
     * 
     * @param selectors
     *            a string containing one or more CSS selectors separated by commas
     * @return a JS array of elements.
     * 
     * @see https://developer.mozilla.org/en-US/docs/DOM/Document.querySelector
     * @see http://www.w3.org/TR/selectors-api/
     */
    protected native Element getElementsBySelectorJSNI(String selector) /*-{
		return $doc.querySelector(selector);
    }-*/;

    /**
     * Create a unique ID for a placeholder that will be placed in the cell on render.
     * 
     * @param context
     */
    private void setupPlaceholderId(Context context) {
        if (uniqueKeyForWidgetID >= Integer.MAX_VALUE) {
            // housekeeping.
            uniqueKeyForWidgetID = 0;
        } else {
            uniqueKeyForWidgetID++;
        }
        domPlaceholderIds.put(context.getKey(), "WidgetCellID_" + uniqueKeyForWidgetID);
    }

    /**
     * If true, this will block the clickable events and this cell will simply render the widget without accepting
     * events.
     * 
     * @param blockClickableEvents
     */
    public void setBlockClickableEvents(boolean blockClickableEvents) {
        this.blockClickableEvents = blockClickableEvents;
    }

    @Override
    public void onResize() {
        /*
         * on resize of the page, update the widget to the new cell size if necessary.
         */
        if (reusableWidgetsMap != null && !reusableWidgetsMap.isEmpty()) {
            for (Entry<Object, W> entry : reusableWidgetsMap.entrySet()) {
                ClickableWidgetCell.this.handleWidgetOnWindowResize(entry.getValue(), entry.getKey(),
                        domPlaceholderIds.get(entry.getKey()));
            }
        }
    }

    /**
     * Get the ID for the widget placeholder in the page. the innerHtml of this element is the widget HTML.
     * 
     * @param cellContextKey
     * @return
     */
    protected String getDomPlaceholderId(Object cellContextKey) {
        return domPlaceholderIds.get(cellContextKey);
    }

    /**
     * Set the container of this grid so it can be resized when all widgets display.
     * 
     * @param containerToResizeAfterAllWidgetsDisplay
     *            the container that implements RequiresResize & IsWidget
     */
    public void setContainerToResizeAfterAllWidgetsDisplay(RequiresResize containerToResizeAfterAllWidgetsDisplay) {
        this.containerToResizeAfterAllWidgetsDisplay = containerToResizeAfterAllWidgetsDisplay;
    }

}

Share

GWT & mvp4g: Displaying history on the client side.

I’m currently working on a task to display and persist history using GWT and mvp4g.  There are many ways of doing this, but here is how i’m going it.  Maybe this will help you with an additional approach, or maybe you can help me do it better.

Implementation Attempts

1st Pass: GWT’s History Event

First I attempted to use GWT’s History ValueChange event to capture changes happening in history.  I run into trouble here right away.  This event only seemed to capture history changes that occurred when I called History.newItem(...) myself; mvp4g was not triggering it.  I search around on the internet and didn’t find any explanation of what this event was supposed to be doing, or how mvp4g handled it, but apparently mvp4g was not triggering this event for its history changes.  This method was out.

UPDATE: For some reason it eluded me in my searches until after my final design, but you could create your own PlaceService, which is using the ValueChange event in order to re-create views from history.  There are a lot of options here, which i am going to take a look at because it will most likely change the design I am currently using.  See a write-up here.

2nd Pass: hashchange Event

I am lucky to be supporting only most recent browsers, so the next method I attempted was to use the hashchange event to capture every change made in the browser.  This worked very well and caught everything.

In the end, i did not use it because it is detached from the mvp4g History Converters.  It is cleaner to keep all of the history implementation in one place so each converter can define its own behavior, and that behavior is maintained along with those converters.  I would have had to catch a change and then query into one of the converters to get additional information on resolving descriptions, etc.   That is messy enough, but then there is the issue of the converters not being programmatically accessible (I couldn’t figure out how to get them from mvp4g… i would love to know if someone can tell me), and they do not have an eventBus instance.

Anyway, I did not end up using this approach in my design but here is that code if it can help you out.  At the very least, it was good to get to know this hashchange event.  GWT does not have any built in hooks to this event, so I created a new EventHandler to encapsulate it and make it injectable into wherever it is needed.

Here’s the class I created:

import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.event.shared.EventHandler;
import com.google.gwt.event.shared.GwtEvent;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.event.shared.HasHandlers;
import com.google.inject.Singleton;
import com.mvp4g.client.event.BaseEventHandler;

/**
 * EventHandler for hash change events. This is an eventhandler so that it is a singleton that can be injected where
 * needed, and can access the eventbus if events are sent that way.
 */
@Singleton
@com.mvp4g.client.annotation.EventHandler
public class HashChangeEventEmitter extends BaseEventHandler implements HasHandlers {

 // handlers
 private HandlerManager handlerManager;

 protected HashChangeEventEmitter() {
 handlerManager = new HandlerManager(this);
 }

  @Override
 public void setEventBus(EventBus eventBus) {
 super.setEventBus(eventBus);

     Scheduler.get().scheduleDeferred(new ScheduledCommand() {
         @Override
         public void execute() {
             registerHashChangedEventListenerInDOM();
         }
     });
 }

 private void fireHashChangedEventFromDOM(String oldUrl, String newUrl) {
     fireHashChangedEvent(new HashChangedEvent(oldUrl, newUrl));
 }

 /**********************************************************
 * Event registration and emitting
 **********************************************************/

 private void fireHashChangedEvent(HashChangedEvent event) {
     if (handlerManager.getHandlerCount(HashChangedEvent.TYPE) > 0) {
         handlerManager.fireEvent(event);
     }
 }

 /** {@inheritDoc} */
 public HandlerRegistration addHashChangedEventHandler(HashChangedEventHandler handler) {
     return handlerManager.addHandler(HashChangedEvent.TYPE, handler);
 }

 @Override
 public void fireEvent(GwtEvent event) {
     handlerManager.fireEvent(event);
 }

 /**********************************************************
 * Event definition
 **********************************************************/
 public static interface HashChangedEventHandler extends EventHandler {
     void onHashChanged(HashChangedEvent event);
 }

 public static class HashChangedEvent extends GwtEvent {

 public static Type TYPE = new Type();

 private final String oldUrlHash;
 private final String oldUrlRaw;
 private final String newUrlHash;
 private final String newUrlRaw;

 public HashChangedEvent(String oldUrlRaw, String newUrlRaw) {
     super();
     this.oldUrlHash = extractHash(oldUrlRaw);
     this.newUrlHash = extractHash(newUrlRaw);

     this.oldUrlRaw = oldUrlRaw;
     this.newUrlRaw = newUrlRaw;
 }

 private String extractHash(String url) {
     return url.contains("#") ? url.substring(url.indexOf('#')) : "";
 }

 @Override
 public Type getAssociatedType() {
     return TYPE;
 }

 @Override
 protected void dispatch(HashChangedEventHandler handler) {
     handler.onHashChanged(this);
 }

 /** @return just the hash from the old URL */
 public String getOldUrlHash() {
     return oldUrlHash;
 }

 /** @return the entire old URL */
 public String getOldUrlRaw() {
     return oldUrlRaw;
 }

 /** @return just the hash from the new URL */
 public String getNewUrlHash() {
     return newUrlHash;
 }

 /** @return the entire new URL */
 public String getNewUrlRaw() {
     return newUrlRaw;
 }

 }

 private final native void registerHashChangedEventListenerInDOM()
/*-{
 var hashChangedEventEmitter = this;

   $wnd
    .addEventListener(
        "hashchange",
        function(event) {
       hashChangedEventEmitter.@<yourPathToEmitter>.HashChangeEventEmitter::fireHashChangedEventFromDOM(Ljava/lang/String;Ljava/lang/String;)(event.oldURL, event.newURL);
       });
 }-*/;

}

The event is a standard GwtEvent, and the class can be injected wherever it is needed like this:

    @Inject
    protected HashChangeEventEmitter hashChangeEventEmitter;

Final Solution: Instrumenting the History Converters

mvp4g handles history in GWT through History Converters, which uses the GWT History framework.  These allow you to attach history entries directly to EventBus calls.  Anytime you trigger a certain event, you can specify how to capture (and recreate) the resulting action in history.

The design I’m currently using creates an abstract history converter that contains a common method called at the end of every history converter implementation’s event bus call (the one creating the token to save for this history entry).  This method, in turn, calls a couple of abstract methods implemented in the history converters that resolve any information needed to display what this history entry is and the history token itself.  This information is then passed to the presenter of the history display view.  This instruments each history converter and requires all new history converters implement these methods so that the history display is kept updated.

The presenter for the history view is injected the same way the HashChangeEventEmitter was injected in the 2nd pass above.  This presenter then handles pushing the history information to the backend as well as updating the view.

Re-evaluation and design forthcoming

It eluded me in my original google searches when I was working on my original designs, but I am going to revisit this and look at implementing it in a cleaner way using a mvp4g Custom PlaceService.  This is the place that has a collection of all of the history converters and is a central point that can be used to implement exactly what I’m trying to do without needing to clutter up the history converters. It is exactly what I was look for, and knew was there somewhere, but missed…

I will update this post after that…

Share