Wednesday, July 20, 2011

Using GWT MVP with multiple views on the page

The Issue:
When developing a GWT app with the MVP approach it is common to make use of Activities and Places as described in the developer guide
Under this approach an ActivityManager makes use of an ActivityMapper which you define in order to respond to PlaceChangeEvents by running the relevant Activity. The Activity is passed a reference to an AcceptsOneWidget panel which is typically a container for the entire application. The Activity is responsible for constructing the application view and setting it into this container.
The potential pitfall of this approach is that every time a new activity is run we clear out the entire application view and reconstruct everything from scratch. If your application is composed of multiple distinct regions, for example a Menu and a Content area, and the view for each region can change independently of the other, then you will be forcing the browser to do extra unnecessary work to rebuild the DOM with the Menu region, even though only the Content region has changed.
A Possible Solution:
The solution is presented in two stages
1) Examine a simple approach that will allow for multiple regions on a page to be managed and changed independently of one another.
2) Explore a way to dynamically remove regions that are not required in a given context (e.g. removing the menu region when on the login screen)
We will use this simple example application layout
Login ScreenMain Screen

We will first define our application layout giving each region it's own AcceptsOneWidget panel.
appContainer = new DockLayoutPanel(Unit.PX);
mainContent = new ScrollPanel();
leftMenu = new ScrollPanel();
banner = new SimplePanel();

appContainer.addNorth(banner, 50);
appContainer.addWest(leftMenu, 300);
appContainer.add(mainContent);
For each region that we want to be able to update the view independently of the other regions (the menu and the content region) we will define a new ActivityMapper and ActivityManager
ActivityMapper contentActivityMapper = new ContentActivityMapper(clientFactory);
ActivityManager contentActivityManager = new ActivityManager(contentActivityMapper, eventBus);       
contentActivityManager.setDisplay(mainContent);
               
ActivityMapper menuActivityMapper = new MenuActivityMapper(clientFactory);
ActivityManager menuActivityManager = new ActivityManager(menuActivityMapper, eventBus);
menuActivityManager.setDisplay(leftMenu);
Note that a different panel is set into the display of each ActivityManager.
Because both managers share an EventBus, both ActivityMappers will be called whenever a PlaceChangeEvent is fired. Since each manager should only run a new activity in response to a subset of the possible Places in your app, we will make some changes to the mapper so that it only responds to the Places that affect the given region. By having the mapper hold a reference to its currently executing Activity, it can just return this same activity whenever a Place change it is not interested in gets fired. This will have no affect as the currently executing Activity will not be run again.
public class MenuActivityMapper implements ActivityMapper {

    private Activity currentActivity;
   
    @Override
    public Activity getActivity(Place place) {
        if (place instanceof MainMenuPlace)
        {
            return new MainMenuActivity();
        } else if (place instanceof SubMenuPlace) {
            return new SubMenuActivity();
        } else {
            //Some other content Place that we aren't interested in
            return currentActivity;
        } 
}  
That completes the first stage. Now say we want to allow the menu region to be hidden when the user is at the login screen and just use the content region to display the login view.
Create a new panel HideableScrollPanel which extends ScrollPanel. Set the menu to use the new panel instead of ScrollPanel. HideableScrollPanel will take as input the parent DockLayoutPanel container and the expected size of the panel when it is displayed. Override the setWidget method so that it will minimize the panel if null is set.
public void setWidget(IsWidget activityWidget) {
        Widget widget = Widget.asWidgetOrNull(activityWidget);                       
        if (widget == null)
        {                   
            parent.setWidgetSize(this, 0);
        } else {
            parent.setWidgetSize(this, size);
        }
        super.setWidget(widget);
    }
Now you can define a HideActivity which when run simply sets the AcceptOneWidget panel it is passed a value of null. If the AcceptOneWidget panel is an instance of your HideableScrollPanel then the panel will be removed from the screen. When a LoginPlace change is received the MenuActivityMapper should return a HideActivity.