Thing are quiet around here for the holidays, so I want to do another post on our Gwittir framework for GWT. In the last post I demonstrated some basic data binding and made a little Flickr browser. I am going to reuse a lot of the same technologies here and show you a little iPhone RSS reader. Yes, I know the GWT team did their own, but this is a nice example to show some of the differences, so you can compare and contrast. I also have a running example up.
I am using ROME on the server side again, this time with the OPML plugin
Since the last example was a single “screen” we didn’t have to deal with lifecycle issue for data binding. This is where the Gwittir MVC framework features come it. Each BoundWidget in the Gwittir API supports an Action. The typical use for this are implementations of the BindingAction. Lets look at the LIstView class, which is the first page on the screen. Some stuff is omitted for clarity.
public class ListView extends AbstractBoundWidget {
private static final Field[] FIELDS = { new Field("title", "Title", "end") };
private BoundTable entries;
private Label pageTitle = new Label("Gwittir Reader");
private Label settings = new Label("settings");
public ListView() {
BoundWidgetTypeFactory factory = new BoundWidgetTypeFactory(false);
factory.add(String.class, BoundWidgetTypeFactory.LABEL_PROVIDER);
entries = new BoundTable(
BoundTable.NO_SELECT_COL_MASK +
BoundTable.NO_SELECT_CELL_MASK,
factory, FIELDS);
this.pageTitle.setStyleName("title");
this.settings.setStyleName("settings");
VerticalPanel panel = new VerticalPanel();
Grid grid = new Grid(1, 3);
grid.setWidget(0, 0, this.settings);
grid.setWidget(0, 1, this.pageTitle);
panel.add(grid);
panel.add(entries);
panel.setCellHorizontalAlignment(
entries, HasHorizontalAlignment.ALIGN_CENTER);
panel.setWidth("100%");
super.initWidget(panel);
}
public BoundTable getEntries() {
return entries;
}
public void setEntries(BoundTable entries) {
this.entries = entries;
}
}
Don’t worry so much about the BoundTable constructor there. I will come back to that. What you should see, however, is that the table of entries is a property of the ListView widget. What we are actually getting from the service is an instance of the Feed class for an OPML file. (I have used a snipped of my Google Reader OPML for the defaults). To bind this, I create a ListAction class that has a three stage lifecycle: set() is called as the display is being prepared; bind() is called after the display has been attached, and unbind() is called as the display is unattached. These are all managed by the AbstractBoundWidget class, so you don’t have to worry about it. But you do need to establish the bindings. I have omitted the execute() method below for clarity. I will come back to than in just a moment.
public class ListAction implements BindingAction {
Binding binding;
public void bind(BoundWidget widget) {
this.binding.bind();
}
public void set(BoundWidget widget) {
ListView view = (ListView) widget;
this.binding = new Binding( view, "entries.value", (Bindable) view.getModel(), "entries");
this.binding.setLeft();
}
public void unbind(BoundWidget widget) {
this.binding.unbind();
}
}
Notice the binding here to “entries.value” That dot notation means bind to the sub-property, or the “value” property of the BoundTable.
Now, You have a bunch of screens for your app and binding actions. The next step is to wire them up into an applicaiton. Gwittir includes screen/screen area manager called the FlowController. To use it, you first establish a FlowContext with all the sub elements of the screen tagged with a String identifier. You can then provide an Action implementation to be set on them. Here I am using single instances of the screens because they are, in effect, stateless. You can also provide factories in the form of the BoundWidgetProvider and ActionProvider and get new instances created as the flow reaches that target.
In your code, you then invoke FlowController.call() with a widget, the target name and an Object that will be set as the “model” property on the desgination screen. The flow controller will crawl up the DOM tree to find the first FlowContext with a widget mapped to that name. This allows you to nest multiple FlowContexts in your app without naming collisions. You can see how this is set up in the EntryPoint implemetation below.
public class ReaderEntryPoint implements EntryPoint {
public static final FeedServiceAsync SERVICE = (FeedServiceAsync) GWT
.create(FeedService.class);
static {
((ServiceDefTarget) SERVICE).setServiceEntryPoint(
GWT.getModuleBaseURL() + "FeedService");
}
public void onModuleLoad() {
String opml = Cookies.getCookie("opml");
if(opml == null) {
opml = GWT.getModuleBaseURL() + "opml.xml";
}
final SlideTransitionSimplePanel panel = new SlideTransitionSimplePanel();
RootPanel.get().add(panel);
panel.setWidth("100%");
FlowController.setHistoryManager(new SimpleSessionHistoryManager());
FlowContext ctx = new FlowContext();
ctx.add("LIST", new ListView(), new ListAction());
ctx.add("VIEW", new FeedView(), new FeedAction());
ctx.add("OPML", new OPMLEnter(), new OPMLAction());
FlowController.setFlowContext((HasWidget) panel, ctx);
SERVICE.getFeed(
opml,
new AsyncCallback() {
public void onFailure(Throwable thrown) {
thrown.printStackTrace();
}
public void onSuccess(Object result) {
Feed f = (Feed) result;
FlowController.call(panel, "LIST", result);
}
});
}
}
First I am establising the OPML to use by getting it from a Cookie or using the default. Next I create a SlideTransitionSimplePanel, a class from the Gwittir .fx.ui package that does an animation as the contained widget changes. Next I create an instance of each of my screens, and their respective Action classes, and add them to the flow context. Finally, I call .setFlowContext() telling the flow controller the panel is the container I want it to manage, and the FlowContext for that panel. Finally, I make the first service call to get the Feed containing the OPML entries, and call the FlowController to the LIST screen.
I mentioned earlier about the BoundWidgetProvier. Here you can begin to see another reason to use this class: Constructors. If you have a large application, as some that we have built, the sheer time it takes to contruct all the objects at the start of your app can make it feel slow. Using the BoundWidgetProvider and ActionProvider classes can delay construction of objects until they are needed.
You can also see the SimpleSessionHistoryManager class here. The FlowController has its own history system that I won’t go into in detail here. However, for basic stuff, simply adding a SSHM class will make the Back/Forward buttons work in your app automagically. It can’t do bookmarks and deep links, because they have to do with service state, but it is a great shortcut for simple history support without dealing with the GWT History system.
Stepping back to the LIstAction, there was one method I left out: .execute(). For the ListView class, I do two things. First, I add a propertyChangeListener to the “entries” table to first the action when you select something (this is an omitted part of the ListView constructor):
final ListView instance = this;
this.entries.addPropertyChangeListener(
"selected",
new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent arg0) {
getAction().execute(instance);
}
});
Then I create the execute method on the action. This will make the next trip to the service and call the flow controller to the FeedView class.
public void execute(final BoundWidget widget) {
Entry entry = (Entry) ((ListView) widget).getEntries().getSelected().get(0);
if( entry.getAlternate() != null ){
ReaderEntryPoint.SERVICE.getFeed( entry.getAlternate(), new AsyncCallback(){
public void onFailure(Throwable arg0) {
Winow.alert("FAIL!");
}
public void onSuccess(Object arg0) {
FlowController.call( (Widget) widget, "VIEW", arg0);
}
});
}
}
I get the selected Entry from the table, I fetch the “alternate”, or RSS/Atom feed link property, then I get the feed from that URL and pass it as the Model to the VIEW target on the flow controller. Hopefully you can now see how the two main screens are wired up, so I want to talk a bit about the BoundTable class.
BoundTable is our end-all-be-all data grid class. It has a rediculous number of options on it, so as you can see, the constructor uses a set of integer bit masks to toggle them. The three things you need to get immediately are Field, BoundWidgetTypeFactory and some basic options. For a more complex example I will turn to the FeedView class.
public class FeedView extends AbstractBoundWidget {
private static final Field[] FIELDS = {
new Field("title", "Title"), new Field("author", "Author"),
new Field("pubDate", "Date", "end")
};
private BoundTable entries;
private Feed feed;
private Label feedTitle = new Label();
public FeedView() {
BoundWidgetTypeFactory factory = new BoundWidgetTypeFactory(false);
factory.add(Date.class, BoundWidgetTypeFactory.LABEL_PROVIDER);
factory.add(String.class, BoundWidgetTypeFactory.LABEL_PROVIDER);
factory.add(
Entry.class,
new BoundWidgetProvider() {
public BoundWidget get() {
return new EntryView();
}
});
entries = new BoundTable(
BoundTable.INSERT_WIDGET_MASK + BoundTable.NO_SELECT_COL_MASK
+ BoundTable.NO_SELECT_CELL_MASK, factory, FIELDS);
//... it goes on.
So the set of fields here contain 2 or three arguments. The property name is the first, the second is the label to use on the thead — these don’t matter since I have not given the tables the show header bit mask. The final field I give one more argument to. This is a CSS style name to apply. I use the “end” CSS to put the arrow behind the last cell. You can pass in all the same things you would use for a Binding class to these, including validators and converters as from the previous post. We don’t need those here, though.
Next is the BoundWidgetTypeFactory. This is a lookup of BoundWidgetProviders to the class they should be used to render. The “false” in the constructor says “Don’t use the defaults.” The default config makes editable views of all the fields. Here I juse use the shortcut LABEL_PROVIDER to tell it to use a Label to render Strings and Dates. Finally, I create a BoundWidgetProvider for my Entry class. Since the table is rendering Entry object, this seems odd. I am doing this because I am using the “INSERT_WIDGET_MASK”. This does the Google Reader style insertion below the selected table row. To use this, you need a View component for the class the table contains. This will then be used as the inserted view below the selected row. I am also telling the BoundTable not to toggle CSS properties for selected cells and columns. Going into all the CSS options here on the table would take too much time, and this post is already long, so I will leave it at that.
Once again, I hope you have found this useful. Full source is available here. Just to give you a little perspective here, this took me a total of about 3 hours to put together, and that includes futzing with the CSS, which still isn’t 100% iPhoney.


