Thursday, November 11, 2010

Think in GWT MVP Framework (Activities and Places)

GWT 2.1 has introduced new MVP framework with Activities and Places. They also have provided a HelloWorld for this MVP Framework, but it may very difficult to learn for new GWT developers. In this post, I want to expand this HelloWorld for going more details (step by step) on developing an application in new MVP framework. You can apply these steps for large scale application development on GWT.

Step 1: Design layout for your app
Every app often begins with an idea + layout design. For example, we have an idea and a layout design as the following:

The layout has 3 areas: top area (may be used for app menu/toolbar), left area (may be used for app masters), right area (may be used for app main content/display).
It's very ugly layout design like Shrek but simple and easy to quick start in right way -:)
In this example, we have the details as below:
Top area: has 2 buttons:
  +Hello World!: click on it >> change text of the button on left area to "Goodbye World!" and change content of right area to "Hello World!"
  + Hello Friend!: click on it >> change text of the button on left area to "Goodbye Friend!" and change content of right area to "Hello Friend !"
Left area: has 1 button, click on it >> change content of right area to "Thanks for your visiting. Goodbye!" and clear left area
Right area: display content depended on clicking on which buttons in top/left area

Step 2: Create a project to develop the app
You can use any tools provided by Google or others to manage the app. I used:
-Eclipse 3.6 Helios (http://www.eclipse.org/downloads)
-Google Plugin for GWT 2.1 and App Engine 1.3.8 (http://dl.google.com/eclipse/plugin/3.6)
-GWT Designer (http://dl.google.com/eclipse/inst/d2gwt/latest/3.6)

For example, we create a project name "hellomvp" with package name "com.hellomvp".
You can download this example at https://code.google.com/p/lvhung.

Step 3: Create OneWidgetLayoutPanel 
We can use SimplePanel for this example, but I want to introduce a layout panel that we can use to add other LayoutPanels like DockLayoutPanel etc. It must implement AcceptsOneWidget for nesting. This class should be in com.hellomvp.client.ui package. See:
public class OneWidgetLayoutPanel extends LayoutPanel implements AcceptsOneWidget {
    private IsWidget widget = null;
   
    @Override
    public void setWidget(IsWidget w) {
        if( widget != null) super.remove(widget);
        widget = w;
        if(w != null) super.add(w);
    }
}
Step 4: Create PresenterNavigator interface
This interface will be used to navigate to places in views. It should be in com.hellomvp.client.ui package. See:
package com.hellomvp.client.ui;


import com.google.gwt.place.shared.Place;


public interface PresenterNavigator {
    void goTo(Place place);
}
Step 5: Create Places
A Place is tied with a particular state of the UI. An Activity will be called via URL by a corresponding Place. The app has 2 activities: hello and goodbye, so it needs 2 Places. They should be in com.hellomvp.client.place package. See:
public class HelloPlace extends Place {
    private String helloName;
   
    public HelloPlace(String token)
    {
        this.helloName = token;
    }

    public String getHelloName()
    {
        return helloName;
    }

    public static class Tokenizer implements PlaceTokenizer<HelloPlace>
    {
        @Override
        public String getToken(HelloPlace place)
        {
            return place.getHelloName();
        }

        @Override
        public HelloPlace getPlace(String token)
        {
            return new HelloPlace(token);
        }
    }
}
============================================
public class GoodbyePlace extends Place {

}
Because we intend to not save URL state and handle URL history when user say Goodbye, so we will drop PlaceTokenizer in GoodbyePlace.

Step 6: Create PlaceHistoryMapper
This class will declares all available Places to manage URL history. It uses the annotation @WithTokenizers to list each of your tokenizer classes. We don't want to save GoodbyePlace URL state, so it won't be here. It should be in com.hellomvp.client.mapper package. See:
@WithTokenizers({ HelloPlace.Tokenizer.class })
public interface AppPlaceHistoryMapper extends PlaceHistoryMapper {

}
Step 7: Create views
We will create views for areas in the layout. Firstly, we have EntryPoint class with UiBinder using DockLayoutPanel. In which, we use OneWidgetLayoutPanel for nesting Places on left and right area. See:
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder
  xmlns:ui='urn:ui:com.google.gwt.uibinder'
  xmlns:g='urn:import:com.google.gwt.user.client.ui'
  xmlns:h='urn:import:com.hellomvp.client.ui' >
  <g:DockLayoutPanel unit='EM'>
    <g:north size='5'>
      <h:TopPanel ui:field='topPanel' />
    </g:north>   
    <g:center>
          <h:OneWidgetLayoutPanel ui:field='leftPanel' >
          </h:OneWidgetLayoutPanel>                 
    </g:center>
    <g:east size='50'>
        <h:OneWidgetLayoutPanel ui:field='rightPanel' >
        </h:OneWidgetLayoutPanel>
    </g:east>   
  </g:DockLayoutPanel>
</ui:UiBinder>
An UI state in each area should have a view. These views should be in com.hellomvp.client.ui package. We will have:
-TopPanel (.java+.ui.xml) on top area
-HelloLeftPanel (.java+.ui.xml) for hello state on left area
-HelloRightPanel (.java+.ui.xml) for hello state on right area
-GoodbyeRightPanel (.java+.ui.xml) for goodbye state on right area

Step 8: Create ClientFactory
ClientFactory is very useful to get needed objects (such as EventBus) throughout your application. We also can change app UIs depended on some properties (like browser user agent). See:
public interface ClientFactory {
    EventBus getEventBus();
    PlaceController getPlaceController();
    HelloRightPanel getHelloRightPanel();
    HelloLeftPanel getHelloLeftPanel();
    GoodbyeRightPanel getGoodbyeRightPanel();
}
Step 9: Modify module .gwt.xml file
Now it's time to modify module .gwt.xml file (HelloMVP.gwt.xml). We add lines as the following:
<!-- Other module inherits                                     -->
<inherits name="com.google.gwt.activity.Activity"/>
<inherits name="com.google.gwt.place.Place"/>

<!-- Use ClientFactoryImpl by default -->
<replace-with class="com.hellomvp.client.ClientFactoryImpl">
     <when-type-is class="com.hellomvp.client.ClientFactory"/>
</replace-with>
We can change ClientFactoryImpl for GWT deferred binding here.

Step 10: Create activities for each area
An Activity is analogous to a presenter in MVP terminology. Activities are started and stopped by an ActivityManager associated with a container Widget. ActivityManager can alert when user leave an activity. These Activities should be in com.hellomvp.client.activity package.

Every activity in every area should be defined. We have:
-LeftPanelHelloActivity for hello activity on left area
-LeftPanelGoodbyeActivity for goodbye activity on left area
-RightPanelHelloActivity for hello activity on right area
-RightPanelGoodbyeActivity for goodbye activity on right area

See LeftPanelHelloActivity and RightPanelHelloActivity:
public class LeftPanelHelloActivity extends AbstractActivity implements PresenterNavigator {
    private ClientFactory clientFactory;
    private String name; //name that will be appended to "Goodbye "
   
    public LeftPanelHelloActivity(HelloPlace place, ClientFactory clientFactory) {
        this.name = place.getHelloName();
        this.clientFactory = clientFactory;
    }

    @Override
    public void start(AcceptsOneWidget panel, EventBus eventBus) {
        HelloLeftPanel helloLeftPanel = clientFactory.getHelloLeftPanel();
        helloLeftPanel.setName(name);
        helloLeftPanel.setNavigator(this);
        panel.setWidget(helloLeftPanel.asWidget());
    }

    @Override
    public void goTo(Place place) {
        clientFactory.getPlaceController().goTo(place);
    }
}
============================================
public class RightPanelHelloActivity extends AbstractActivity {
    private ClientFactory clientFactory;
    private String name; //name that will be appended to "Hello "
   
    public RightPanelHelloActivity(HelloPlace place, ClientFactory clientFactory) {
        this.name = place.getHelloName();
        this.clientFactory = clientFactory;
    }
   
    /**
     * Invoked by the ActivityManager to start a new Activity
     */
    @Override
    public void start(AcceptsOneWidget panel, EventBus eventBus) {
        HelloRightPanel helloRightPanel = clientFactory.getHelloRightPanel();
        helloRightPanel.setName(name);
        panel.setWidget(helloRightPanel.asWidget());
    }
   
    /**
     * Ask user before stopping this activity
     */
    @Override
    public String mayStop() {
        return "Please hold on. This activity is stopping.";
    }
}
Step 11: Create activity mappers
Yeah, we have all Places and Activities. Now let map them together. GWT provides ActivityMapper interface for mapping each Place to its corresponding Activity. Every area should have an ActivityMapper. These ActivityMappers should be in com.hellomvp.client.mapper package.


We have 2 classes: LeftPanelActivityMapper and RightPanelActivityMapper. See:
public class RightPanelActivityMapper implements ActivityMapper {
    private ClientFactory clientFactory;
   
    public RightPanelActivityMapper(ClientFactory clientFactory) {
        super();
        this.clientFactory = clientFactory;
    }

    @Override
    public Activity getActivity(Place place) {
        if (place instanceof HelloPlace)
            return new RightPanelHelloActivity((HelloPlace) place, clientFactory);
        else if (place instanceof GoodbyePlace)
            return new RightPanelGoodbyeActivity(clientFactory);
        return null;
    }
}
=================================================
public class LeftPanelActivityMapper implements ActivityMapper {
    private ClientFactory clientFactory;
   
    public LeftPanelActivityMapper(ClientFactory clientFactory) {
        super();
        this.clientFactory = clientFactory;
    }

    @Override
    public Activity getActivity(Place place) {
        if (place instanceof HelloPlace)
            return new LeftPanelHelloActivity((HelloPlace) place, clientFactory);
        else if (place instanceof GoodbyePlace)
            return new LeftPanelGoodbyeActivity();
        return null;
    }
}
Step 12: Put them all together
Finally, we will put them all together. We can use EntryPoint class. See:
public class HelloMVP implements EntryPoint
{
    interface Binder extends UiBinder<DockLayoutPanel, HelloMVP> { }
  
    private static final Binder binder = GWT.create(Binder.class);
    private Place defaultPlace = new HelloPlace("World!");
  
    @UiField TopPanel topPanel;
    @UiField OneWidgetLayoutPanel leftPanel;
    @UiField OneWidgetLayoutPanel rightPanel;

    /**
     * This is the entry point method.
     */
    public void onModuleLoad() {
        // Create app layout
        DockLayoutPanel outer = binder.createAndBindUi(this);
      
        // Create ClientFactory using deferred binding so we can replace with different
        // impls in gwt.xml
        ClientFactory clientFactory = GWT.create(ClientFactory.class);
        EventBus eventBus = clientFactory.getEventBus();
        PlaceController placeController = clientFactory.getPlaceController();
      
        // Start ActivityManager for each area with its ActivityMapper
        ActivityMapper leftPanelActivityMapper = new LeftPanelActivityMapper(clientFactory);
        ActivityManager leftPanelActivityManager = new ActivityManager(leftPanelActivityMapper, eventBus);
        leftPanelActivityManager.setDisplay(leftPanel);
      
        ActivityMapper rightPanelActivityMapper = new RightPanelActivityMapper(clientFactory);
        ActivityManager rightPanelActivityManager = new ActivityManager(rightPanelActivityMapper, eventBus);
        rightPanelActivityManager.setDisplay(rightPanel);
      
        // Start PlaceHistoryHandler with our PlaceHistoryMapper
        AppPlaceHistoryMapper historyMapper= GWT.create(AppPlaceHistoryMapper.class);
        PlaceHistoryHandler historyHandler = new PlaceHistoryHandler(historyMapper);
        historyHandler.register(placeController, eventBus, defaultPlace);
      
        // Set ClientFactory to main toolbar
        topPanel.setClientFactory(clientFactory);
      
        // Add app layout to RootPanel
        //RootPanel.get().add(outer);
        RootLayoutPanel.get().add(outer);
      
        // Goes to place represented on URL or default place
        historyHandler.handleCurrentHistory();
    }
}