Part 8: MVP refactoring
We have reached a point in the project where we need to define our application level layout. We will implement our own version of WidgetContainerPresenter in this post to support this.
The reason for this is because I want:
- More direct/flexible control over the container layer
- Support for location and size aware presenters/views
- Support for lazy loading of views
My first attempt at this was to extend WidgetContainerPresenter, but that involved too many workarounds, so our classes will extend BasicPresenter directly instead. We will also modify our BasePresenter to add logic for lazy loading.
But before we go into that, let's discuss the application level layout structure. Currently our AppView only deals with a center widget and we need to add logic to handle widgets in other directions to form the app structure.
Our AppView holds its widgets in a DockLayoutPanel. Remember that we use the Layout-variants for standards mode compliance. Using a DockLayoutPanel means that we must somewhere tell AppView in which direction to add each widget, and which size they should be. DockLayoutPanel, unlike DockPanel, doesn't use tables for layout structure and requires a fixed size for its children.
None of this would be a problem if our AppView knew about its children and their location and size. This seems to be the approach that HUPA has gone for. I'm obviously going for another approach where the AppView is happily clueless about its children view implementations and their properties. My interpretation of MVP is that the presenter is responsible for how the data is organized and the view is simply responsible for actually displaying the data. I believe that the AppPresenter and AppView should follow the same convention as the other presenters and views. So in our case, the AppView should only know that it layout its data using a DockLayoutPanel. What goes where is up to the presenters.
So what we need to do here is to bring in the concept of presenters that are aware of direction and size. I'm not saying that the presenters themselves has to define this, just that they need to be aware of it somehow. Preferrably we define a central point in the app (like AppController) where we construct the top level layout and bind styles to it. We should be able to change the layout with minimum effort, for example moving some widget from the left sidebar to the right sidebar should only require that we change a location flag or inject it in another parent presenter.
But let's work from bottom and up, so we start by having a peek at our BasePresenter which now contains logic for lazy loading. In Part 7, we also added a mechanism here to keep track of our data and if it needs to be updated from server/cache or not. This is related to the new place handling and our presenters can call needsUpdate() in onRevealDisplay() to see if the view needs data update.
public abstract class BasePresenter<D extends BasePresenter.Display> extends WidgetPresenter<D> {
// All presenters use some model id to fetch data.
// We save this here and keep track of changes so we know
// when we need to update data in the presenter
private String mId;
private boolean mNeedsUpdate;
private boolean mFirstReveal = true;
public interface Display extends WidgetDisplay {
// For lazy loading of views
void init();
}
public BasePresenter(D display, EventBus eventBus) {
super(display, eventBus);
}
public void setId(String id) {
if (id == null) {
mId = null;
}
else if (mId == null || !mId.equals(id)) {
mId = id;
mNeedsUpdate = true;
}
}
protected boolean needsUpdate() {
return mNeedsUpdate;
}
protected void dataUpdated() {
mNeedsUpdate = false;
}
public String getId() {
return mId;
}
@Override
public void revealDisplay() {
if (mFirstReveal) {
display.init();
onLazyBind();
mFirstReveal = false;
}
super.revealDisplay();
}
protected abstract void onLazyBind();
@Override
protected void onBind() {
}
@Override
protected void onUnbind() {
}
}
We have added an init() method in the Display interface which our views must implement. Instead of constructing its panels and widgets when created, the views will do it on demand via this init() method. The init() method will be called the first time the view is revealed.
To accomplish this, we override revealDisplay() which is called on the presenter as a result of a place request being fired. We check if this is the first revealDisplay() for this presenter and if it is, we call display.init() and then onLazyBind() which is a new method that our presenters should implement to do any event handler registration related to the view. Then we always call super.revealDisplay() which will call onRevealDisplay() on our presenter and fire off a PresenterRevealedEvent. Now we have support for lazy loading. Even though I had this idea before I read his post, I still want to give David Chandler some credit for his post about this here.
OK. Back to the location/size stuff. Let's move on by defining a class that keeps track of widget location, size and anything else we might need.
public static class WidgetProperties {
public int mLocation;
public int mIndex;
public int mSize;
public WidgetProperties(int loc, int idx, int size) {
mLocation = loc;
mIndex = idx;
mSize = size;
}
}
For now, we only care about location and size, but we could easily extend this later to hold more properties that we need, without changing anything else in the "API".
Then we define a new abstract presenter base class LocationAwarePresenter which looks like this. (It's aware of more than location, but PropertiesAwarePresenter just sounds silly...)
public abstract class LocationAwarePresenter<T extends BaseContainerPresenter.Display> extends
BaseContainerPresenter<BaseContainerPresenter.Display> {
private WidgetProperties mWidgetProperties;
public LocationAwarePresenter(Display display, EventBus eventBus, WidgetPresenter<?>... presenters) {
super(display, eventBus, presenters);
}
public final void setWidgetProperties(WidgetProperties widgetProperties) {
mWidgetProperties = widgetProperties;
}
public final WidgetProperties getWidgetProperties() {
return mWidgetProperties;
}
}
Any presenter whose view should be added to a certain direction or have a certain size should extend this class. I have been thinking back and forth whether to place this logic in the presenter or in the implementing view. I'm not sure this is the final solution, but for now we will keep the logic in the presenters.
LocationAwarePresenter inherits from our new BaseContainerPresenter, which we'll have a look at now. Note that I have stripped the class of stuff that is not relevant for this post to save some space. See the full version in the repository.
public abstract class BaseContainerPresenter<T extends BaseContainerPresenter.Display> extends BasePresenter<T> {
public interface Display extends BasePresenter.Display {
void showWidget(Widget widget, WidgetProperties properties);
void removeWidget(Widget w);
void removeAll();
}
protected List<WidgetPresenter<?>> mPresenters;
protected WidgetPresenter<?> mCurrentPresenter;
public BaseContainerPresenter(T display, EventBus eventBus, WidgetPresenter<?>... presenters) {
super(display, eventBus);
mPresenters = new java.util.ArrayList<WidgetPresenter<?>>();
Collections.addAll(mPresenters, presenters);
}
@Override
protected void onBind() {
onBind(true);
}
protected void onBind(boolean addHandler) {
// Let the presenter decide if we should listen to events
if (addHandler) {
registerHandler(eventBus.addHandler(PresenterRevealedEvent.getType(), new PresenterRevealedHandler() {
public void onPresenterRevealed(PresenterRevealedEvent event) {
if (mPresenters.contains(event.getPresenter())) {
showPresenter((WidgetPresenter<?>) event.getPresenter());
revealDisplay();
}
}
}));
}
for (WidgetPresenter<?> presenter : mPresenters) {
presenter.bind();
}
}
@Override
protected void onUnbind() {
mCurrentPresenter = null;
display.removeAll();
for (WidgetPresenter<?> presenter : mPresenters) {
presenter.unbind();
}
}
/*
* For top level presenters where the children views
* maps to part of the application layout structure.
*/
protected void showAllPresenters() {
for (WidgetPresenter<?> presenter : mPresenters) {
showPresenter(presenter);
}
}
@SuppressWarnings("unchecked")
protected void showPresenter(WidgetPresenter<?> presenter) {
WidgetProperties properties = null;
if (presenter instanceof LocationAwarePresenter) {
properties = ((LocationAwarePresenter) presenter).getWidgetProperties();
}
display.showWidget(presenter.getDisplay().asWidget(), properties);
mCurrentPresenter = presenter;
}
}
First, we have a Display interface that differs slightly from WidgetContainerPresenter. We don't have any addWidget(), since I don't really see any use case for both that and showWidget(). There is no point in adding a widget if it is not currently displayed anyway. We have extended showWidget() to also take WidgetProperties as input. For presenters that don't care about widget properties, this will be null and the view can simply ignore it.
onBind() is slighty modified to let the implementing presenter decide if we want to listen to PresenterRevealedEvents. AppPresenter is different from many other presenters in that it has all its child presenters active simultaneously and constantly. It's view and child views will always be shown since they form the application layout, so we don't really "swap" child presenters in and out as in normal container presenters. Because of that, we don't care about child presenters being revealed in AppPresenter since they are always revealed. There is probably a cleaner solution to this, but this will have to do for now.
And last but not least we have showAllPresenters() and showPresenter(). showPresenter() now checks if the child presenter is an instance of LocationAwarePresenter and fetches its WidgetProperties if it is. The view implementation can then use the WidgetProperties instance to determine how to handle the widget that should be added in showWidget(). showAllPresenters() is related to what I mentioned above with AppPresenter. We want to explicitly show all presenters at once in this case.
BaseContainerPresenter is still very similar to WidgetContainerPresenter, which it should be as well. The main advantage of having our own class is that we have more control in the container layer. This is where most applications will move in different directions, so it might be hard to create a base container class that meets everybodys needs.
Now that we have layed some groundwork, it is time to actually define the application layout structure. There are different ways to do this, and I'm not sure which one makes the most sense. Either we let normal presenters (like LoginPresenter, BannerPresenter, MenuPresenter, etc) be of type LocationAwarePresenter and we add these directly to AppPresenter. DockLayoutPanel can add more than one widget in each direction, as long as we haven't added any center widgets yet. In case you don't know, the center widget requires no location or size since it will fill the remaining space between the directions. This approach has some downsides however. The biggest one is that the widget locations become a bit confusing. If we add two widgets to north, they will stack up vertically and not horizontally like we might want them to. We also need to add all the presenters in a certain order so it becomes quite messy as the app grows. Also, we would need access to the presenters explicitly even though they are injected. This either means creating getters in the Ginjector or create provider methods. Either way it becomes messy when we want to have access to these presenters in AppController to set their WidgetProperties. No, I think we need something more simple, even if this approach appeals to me from a design point of view.
A simpler and more hiearchical approach is to define presenters that maps to certain directions and only acts as containers for what we actually display in the layout. We would define a presenter for each direction (north, south, west, east and center) and create getters for these in the Ginjector which we use in AppController to set direction, size and style. This way we get much more flexibility in how the child views are displayed. The north view adds its children in horizontal directions while the west view adds its children in vertical direction. So we add these to the AppPresenter, and then we add MenuPresenter to WestContainerPresenter and BannerPresenter to NorthContainerPresenter and so on. If we want to change location of a widget, we only change where we inject the presenter. The child presenters could also be location aware if for example we want to support adding children views both horizontally and vertically in a direction. Then we can define horizontal/vertical flags that the direction views check before adding the widgets. I have decided to go for this approach and will hopefully stick with it unless readers have a much better idea or I wake up enlightened some morning.
Let's have a look at our AppView now.
public class AppView extends BaseView implements AppPresenter.Display {
public static final int NORTH = 0;
public static final int SOUTH = 1;
public static final int WEST = 2;
public static final int EAST = 3;
private final DockLayoutPanel mPanel;
public AppView() {
// No lazy loading for AppView
mPanel = new DockLayoutPanel(Unit.PX);
}
@Override
public void init() {
}
@Override
public void removeAll() {
mPanel.clear();
}
@Override
public void removeWidget(Widget w) {
mPanel.remove(w);
}
@Override
public void showWidget(Widget widget, WidgetProperties properties) {
if (properties != null) {
switch (properties.mLocation) {
case NORTH:
mPanel.addNorth(widget, properties.mSize);
break;
case SOUTH:
mPanel.addSouth(widget, properties.mSize);
break;
case WEST:
mPanel.addWest(widget, properties.mSize);
break;
case EAST:
mPanel.addEast(widget, properties.mSize);
break;
default:
mPanel.add(widget);
}
}
else {
mPanel.add(widget);
}
}
@Override
public Widget asWidget() {
return mPanel;
}
}
We have also defined a presenter for each direction, and moved most of our startup code to AppController which now has this method go().
public void go(TeamScapeGinjector injector) {
mInjector = injector;
// Fetch needed instances from injector
final EventBus eventBus = mInjector.getEventBus();
final PlaceManager placeManager = mInjector.getPlaceManager();
final AppPresenter appPresenter = mInjector.getAppPresenter();
// Add AppPresenter's display to the root panel
RootLayoutPanel.get().add(appPresenter.getDisplay().asWidget());
// Setup AppView containers
// TODO: The idea here is that we will define the application layout
// here in one place
// We assign presenters direction and size and will also do css and
// stuff later
mInjector.getNorthPresenter().setWidgetProperties(new WidgetProperties(AppView.NORTH, 0, 50));
mInjector.getSouthPresenter().setWidgetProperties(new WidgetProperties(AppView.SOUTH, 0, 50));
mInjector.getWestPresenter().setWidgetProperties(new WidgetProperties(AppView.WEST, 0, 50));
mInjector.getEastPresenter().setWidgetProperties(new WidgetProperties(AppView.EAST, 0, 50));
appPresenter.revealAll();
// If there are no tokens on history stack,
// launch initial state token provided as parameter
if (History.getToken().length() == 0) {
eventBus.fireEvent(new PlaceRequestEvent(new PlaceRequest(ListTeamsPlace.NAME)));
}
// We have a token, so let's fire off an event and handle it
else {
placeManager.fireCurrentPlace();
}
}
Let's take a look at one of the presenters as well, even though they are skeletons only at this point.
public class NorthContainerPresenter extends LocationAwarePresenter<NorthContainerPresenter.Display> {
public interface Display extends BaseContainerPresenter.Display {
}
@Inject
public NorthContainerPresenter(Display display, EventBus eventBus) {
super(display, eventBus);
}
}
Each presenter also has a corresponding view, like NorthContainerView that for example will layout its children horizontally.
The views we use for our direction presenters are currently just dummy implementations. Once this is posted, I will start implementing lots of "real" presenters and views that will be part of the application. I will not post any article about that, but I'll put a comment about it in the news section when the code has been checked in and deployed.
If you readers have feedback on this, please share! I have struggled quite a lot with this post since there are so many possible solutions. My main goal was to find a solution that fits into the MVP pattern as much as possible and keep the same presenter/view paradigm across all layers. There are some parts of the solution that I'm not 100% happy with, so it is likely that I will do some small modifications. I do however hope that this is the last post that is related to MVP in specific. Now we should be ready to move on and actually build the app instead of jabbering about design patterns
.
I'm also a bit worried that this solution won't work at all with UiBinder, but I'm not yet convinced that UiBinder will bring anything to the table for this app. We will have a post later where we evaluate if UiBinder provides any advantages for us. Perhaps someone that is more familiar with UiBinder could comment on that?
February 20th, 2010 - 12:35
How i can get gwt-presenter-1.1.1-replace-SNAPSHOT.jar?
February 20th, 2010 - 18:22
Thanks, I found the library.
March 24th, 2010 - 04:39
I’m working on an app somewhat similar to yours. Your architecture seems quite reasonable. But I’m also concerned about UIBinder. From what I’ve been able to understand, it can become quite useful. I don’t know about you but in my project I’m the only one with a programming background and moving the layout code to specific files and making it declarative makes it a lot easier to get the other guys to do some hands-on work instead of just conceptual work
Also, it seems to me that it makes much more sense that way.
That said, the fact is that I’m getting much more problems with UIBinder. Specifically with the Maps API. It’s trivial to show a map in the old-school way but I can’t get it to work with UIBinder. The new Layout panels also seem to play their part in this inconvenience of mine…
But then again, I’m no expert in GWT. I know my server side Java fairly well but that’s about it… I’ve never payed much attention to the client side of things…. I’m very curious to see that post on UIBinder…
March 24th, 2010 - 17:24
Hi hjrnunes.
My project is pretty much a one-man hobby project, but the goal is to work as if it was a real project with several developers and designers. I really want to use UiBinder for that reason, but at the same time the slow progress of the application due to all the “refactoring” is getting a bit frustrating. It’s likely I will encounter the problems you describe as well as conflicts with gwt-presenter.
There will be some form of post about UiBinder, but integrating it into TeamScape depends on how much effort I have to put into re-designing stuff.
Have you checked out the PuzzleBazar project? They use MVP + UiBinder. Have a look at http://code.google.com/p/puzzlebazar
April 21st, 2010 - 13:33
how do you see the future of GWT MVP, google pioneered and recommended the use of MVP architecture for GWT development, and there are some implementations of it out there, gwt-presenter/dispatch, mvp4g,gwtp/puzzlebazar …. but these projects are not stable and mature enough, its very nice of the develoeprs to share it with rest of us, but its possible that with each new build of these framework, existing code breaks….. since there is no clear framework, or a standard mvp implementation.
dont you think google should come up with a light-weight framework, and standardize gwt application development ?
August 26th, 2010 - 01:11
@goo-it
Since no one replied over the few months I think I will give my 2cents worth (make it a dollar).
GWT presenter is very invasive . I will stay away from it .Dispatch is good but using the MVP described on GWT site no real need to use it . I do not think google should make it to frame work either . It is better off as a tool kit .
My opinion just use spring as the back end and use Spring mvc (just the serverlets) or my favourite Errai from jboss . I am using Errai bus with spring for stuff spring does including server side DI . Errai takes care of all communications .
September 27th, 2011 - 15:40
Hi, excellent tutorial… I have a litle question about places… I want to use one presenter with its corresponding view for several places…Everything works fine until I press the back button…the history does something messy with the other places, it start to place me in the other places that representer this presenter even if I didnt visit them, and ignore the other request that I did before that…is there any workaround that can be made in order to avoid this??? I really need to use the same presenter in many places…
Thanks in advance.