For the past couple of months, I've been developing a GWT application using a mix of plain ol' GWT and GXT widgets. When I first started developing it, I didn't know how to best organize my code and separate the logic. The solution I came up with was to adopt some sort of MVC framework. Since I was already using GXT, I opted for GXT's lightweight MVC implementation.
As mentioned in Testing GWT Applications, GXT's MVC doesn't have much documentation. The best reference documentation seems to be Christian's Getting started with Ext-GWT: The Mail reference application.
Page Transitioning with Dispatcher
After working with GXT MVC for a couple months, I'm still not sure I fully understand how navigation and event dispatching works. The biggest point of confusion for me is how to best use GXT's Dispatcher class.
The problem with Dispatcher is it has a two methods that seem to do the same thing.
forwardEvent
(4 variations)
dispatch
(3 variations)
In addition to these methods in Dispatcher, there's two fireEvent
methods in GXT's View class. According to my calculations, that means there's 9 different options for transitioning from one view to the next. Which one is best to use?
From what I've learned, I think it's best to use fireEvent
in Views and forwardEvent
in Controllers and other widgets. IMO, dispatcher
should never be used except in your HistoryListener's implementation onHistoryChanged
method.
The important thing to realize about this method is it should only work if the View's Controller is registered for the event.
protected void fireEvent(AppEvent event) {
Controller c = controller;
while (c != null) {
if (c.canHandle(event)) {
c.handleEvent(event);
}
c = c.parent;
}
}
However, fireEvent
seems to work even when the View's Controller isn't registered for that event. This is because onHistoryChanged
gets called in the EntryPoint. For experienced GXT MVC users, does this navigation handling mesh with your findings?
The most important thing for navigation to work successfully is enabling History support. The next section talks about how to do this effectively.
Enabling History Support
To help explain things better, I created a simple GWT MVC Example application and used Maven to create an archetype with it. You can create a project from the archetype using the following command:
mvn archetype:create -DarchetypeGroupId=org.appfuse.archetypes \
-DarchetypeArtifactId=gwt-mvc -DarchetypeVersion=1.0-SNAPSHOT \
-DgroupId=com.mycompany.app -DartifactId=myproject \
-DremoteRepositories=http://oss.sonatype.org/content/repositories/appfuse-snapshots
To enable history support in this application, I implemented HistoryListener in my EntryPoint (Application.java) and added the following logic to initialize:
// If the application starts with no history token, redirect to 'home' state
String initToken = History.getToken();
if (initToken.length() == 0) {
History.newItem(HistoryTokens.HOME);
}
// Add history listener
History.addHistoryListener(this);
// Now that we've setup our listener, fire the initial history state.
History.fireCurrentHistoryState();
In this example, HistoryTokens is a class that contains all the URLs of the "views" in the application.
public class HistoryTokens {
public static final String HOME = "home";
public static final String CALENDAR = "calendar";
public static final String NOTES = "notes";
public static final String SEARCH = "search";
}
In order to make URLs like http://localhost:8080/#calendar go to the calendar view, the following logic exists in the onHistoryChanged
method.
Dispatcher dispatcher = Dispatcher.get();
if (historyToken != null) {
if (historyToken.equals(HistoryTokens.HOME)) {
dispatcher.dispatch(AppEvents.GoHome);
} else if (historyToken.equals(HistoryTokens.CALENDAR)) {
dispatcher.dispatch(AppEvents.Calendar);
} else if (historyToken.equals(HistoryTokens.NOTES)) {
dispatcher.dispatch(AppEvents.Notes);
} else if (historyToken.equals(HistoryTokens.SEARCH)) {
dispatcher.dispatch(AppEvents.Search);
} else {
GWT.log("HistoryToken '" + historyToken + "' not found!", null);
}
}
Controllers are registered in the EntryPoint as follows:
final Dispatcher dispatcher = Dispatcher.get();
dispatcher.addController(new CalendarController());
dispatcher.addController(new HomeController());
dispatcher.addController(new NotesController());
dispatcher.addController(new SearchController());
Controllers respond to events they're registered for. This is done in their constructor:
public CalendarController() {
registerEventTypes(AppEvents.Calendar);
}
In order for navigation to work, you have to create links with history tokens1. For example, here's a link from the HomeView
class:
Hyperlink notesLink = new Hyperlink("Notes", HistoryTokens.NOTES);
notesLink.addClickListener(new ClickListener() {
public void onClick(Widget widget) {
Dispatcher.get().fireEvent(AppEvents.Notes);
}
});
You'll notice in this example, I'm using Dispatcher's fireEvent
method. If I wanted to pass some data with your event, you'll need to use forwardEvent
. Here's an example from CalendarView
:
Button submit = new Button("Submit");
submit.addSelectionListener(new SelectionListener<ButtonEvent>() {
public void componentSelected(ButtonEvent ce) {
AppEvent<Date> event =
new AppEvent<Date>(AppEvents.GoHome, date.getValue(), HistoryTokens.HOME);
Dispatcher.forwardEvent(event);
}
});
In this example, you could also use Dispatcher.dispatcher()
, but I believe this will cause the transition to happen twice because the onHistoryChanged
method gets called too. This doesn't matter for the most part, except when you start to use DispatcherListeners.
Hopefully this article has helped you understand how GXT's MVC framework works. I'm interested in learning how other GWT MVC frameworks work. If you've used one, I'd love to hear about your experience.
Friday Fun Test
Here's a test for those interested in digging into the GXT MVC example. There's a bug in this application that prevents something from happening. I'll buy a drink for the person that finds the bug and I'll buy two drinks for the person that comes up with a solution.
1. If you use the default constructor on Hyperlink and use setText()
, make sure to call setTargetHistoryToken()
too. If you don't, a blank history token will be used and # causes the browser to scroll to the top before a page transition happens.