Category Archives: mvp4g

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