Tuesday, April 5, 2011

Layout with Mvp4g

The MVP pattern associated with an event bus is definitely the best way to build a fast and robust GWT application but the learning curve and the boiler plate code are important. That's why Mvp4g has been created. It is really easy to learn since it is based on a simple idea: whenever a presenter doesn't know how to do something, it fires an event to ask the rest of the application to do it . It is also simple to use since all you need is a few annotations.

In this tutorial, I will explain how to set your application layout (menu, header, footer) and display/switch screens following the MVP/event bus pattern thanks to Mvp4g. In the second part, I will introduce a technique to write even less code.

This tutorial assumes that you're familiar with GWT and have some notion of MVP (if not, I suggest you to go over the official GWT documentation). You should also read this page to learn how to set up a Mvp4g project.

Part 1: Your First Application


The application developed in this tutorial is pretty simple:


It's made of an header, a footer, a menu and a body. The body can display 2 different elements: page 1 and page 2. For this tutorial, each view will be a simple label, except for the menu that will contain 2 links to page1 and page2. Thus we have the following layout:

You can download the sample code for part 1 here (Project: Mvp4g_Layout_Part1).

Setting up Presenters/Views

First we need to define a presenter/view pair for each element: HeaderPresenter/HeaderView, FooterPresenter/FooterView, MenuPresenter/MenuView, Page1Presenter/Page1View and Page2Presenter/Page2View . We also need to define a presenter/view pair which will contain other presenters' views: RootPresenter/RootView. For our example, this pair should be able to contain an header, a footer, a menu and a body.

In the MVP pattern, the presenter has a reference to the view. For our example, we'll use the Reverse MVP (or View Delegate) approach which means the view also has a reference to the presenter. This approach lets you define presenter with better view abstraction since all DOM event handlers are added by the view. Thus it's easier to cover them with Junit tests and reuse them with different view implementations (you may have another view's implementation for mobile devices for example). You can read this great article for more information on the different MVP approaches.

To reduce the coupling between the presenter and its view, dependency injection will be used. Thus the first step to build our example is to define interfaces for each presenter/view:
public interface IMenuView extends IsWidget {

    public interface IMenuPresenter {
       
    }

}
We'll add methods to these interfaces later on.

Now that we have our interfaces, we need to implement them. We can divide our presenters in to  two categories: the ones with views that will be displayed once the application starts and the ones that won't. In the first category, we find the HeaderPresenter, FooterPresenter, MenuPresenter and RootPresenter. Since we want these presenters to be built right away, they will extend the Mvp4g BasePresenter class.
In the second category, we have Page1Presenter and Page2Presenter. As a best practice, these presenters (and their views) should be built only when they're going to be displayed for the first time. This way, your application will be faster to start. Mvp4g provides an easy way to build a presenter (and its view) when it handles its first event. All you have to do is have your presenter extend Mvp4g LazyPresenter class.

This is what the implementation looks like:
@Presenter( view = MenuView.class )
public class MenuPresenter extends BasePresenter<IMenuView, LayoutEventBus> implements IMenuPresenter {
   ...
}
public class MenuView extends ReverseCompositeView<IMenuPresenter> implements IMenuView {
   ...
}
Finally we need to set the view/presenter implementation used for injection. Thanks to @Presenter, we set the view implementation to inject to the presenter. By having the view implement the ReverseViewInterface, we tell Mvp4g to inject the presenter to the view.

We need the same code for each presenter/view except for Page1Presenter and Page2Presenter that will extend LazyPresenter.

Defining layout events

Now that we have our presenters/views, we need to define how to display them. We saw before views could be displayed at four different locations (header, menu, body, footer). In Mvp4g, each presenter will need to ask the rest of the application to display its views at a specific location thanks to an event. Thus for each location, we'll have an event (that we can categorize as a layout event).

If you're not familiar with how to create events with Mvp4g, you can look at this page. To sum up, you define your event bus with an interface. For each event, you define a method and annotate it with @Event to set your handlers. Each handler has to define the handling method; this method is named on + event's method name and has the same parameters as the event's method name. An implementation of this event bus is created by Mvp4g and injected in to each presenter. To fire an event, you just have to call the event's method.

These are our layout events:
public interface LayoutExampleEventBus extends EventBus {

    /*
     * Layout events
     */
    @Event( handlers = RootPresenter.class )
    void setHeader( IsWidget header );

    @Event( handlers = RootPresenter.class )
    void setMenu( IsWidget menu );

    @Event( handlers = RootPresenter.class )
    void setBody( IsWidget body );

    @Event( handlers = RootPresenter.class )
    void setFooter( IsWidget footer );

}
We can notice events accept an IWidget not a Widget. This is to have a better abstraction of the view. Also if you use the Widget class (or any class based on GWT.create), you'll most likely need to use GWTTestCase for your Junit tests. This is something you need to avoid because regular Junit test are much faster.

The header, footer and menu need to automatically be displayed when the application starts. In order to do this, we configure an event to be fired when the application starts. This event will be handled by their presenters and in response to this event, they will fire a layout event to tell the application to display their views.

@Presenter( view = MenuView.class )
public class MenuPresenter extends BasePresenter<IMenuView, LayoutExampleEventBus> implements IMenuPresenter {

    public void onStart() {
        eventBus.setMenu( view );
    }


}
We annotate the start event with @Start to tell Mvp4g to fire this event when the application starts.

public interface LayoutExampleEventBus extends EventBus {

    @Start
    @Event( handlers = { HeaderPresenter.class, MenuPresenter.class, FooterPresenter.class, RootPresenter.class } )
    void start();

    ...

}
Now that our layout events are fired when the application starts, they need to be handled. Before, we defined a RootPresenter/RootView to display other presenters' view, thus we'll use it to treat these events. For each event, the RootPresenter will just ask its view to display the received view at a particular location.

The RootView then needs to provide a method for each available position:

public interface IRootView extends IsWidget {

   ...
    
    void setHeader( IsWidget header );

    void setMenu( IsWidget menu );

    void setBody( IsWidget body );

    void setFooter( IsWidget footer );

}
public class RootView extends ReverseCompositeView<IRootPresenter> implements IRootView {
        ...
    @UiField
    SimplePanel header, menu, body, footer;

    @Override
    public void setHeader( IsWidget header ) {
        this.header.setWidget( header );
    }

    @Override
    public void setMenu( IsWidget menu ) {
        this.menu.setWidget( menu );
    }

    @Override
    public void setBody( IsWidget body ) {
        this.body.setWidget( body );
    }

    @Override
    public void setFooter( IsWidget footer ) {
        this.footer.setWidget( footer );
    }

}
and when handling a layout event, the RootPresenter just needs to call the associated RootView's method.
@Presenter( view = RootView.class )
public class RootPresenter extends BasePresenter<IRootView, LayoutExampleEventBus> implements IRootPresenter {

    public void onSetHeader( IsWidget header ) {
        view.setHeader( header );
    }

    public void onSetMenu( IsWidget menu ) {
        view.setMenu( menu );
    }

    public void onSetBody( IsWidget body ) {
        view.setBody( body );
    }

    public void onSetFooter( IsWidget footer ) {
        view.setFooter( footer );
    }
}
Now that each static element is displayed inside the RootView, we need one final step: display the RootView inside our application's html body. To do this, we set the RootPresenter as the start presenter:
@Events( startPresenter = RootPresenter.class )
public interface LayoutExampleEventBus extends EventBus {
...
}
then in the GWT entry point, we start our Mvp4g application, retrieve the start view (which is the view of the start presenter) and add it to the RootPanel.
public class LayoutExampleEntryPoint implements EntryPoint {

   public void onModuleLoad() {
        Mvp4gModule module = GWT.create( Mvp4gModule.class );
        module.createAndStartModule();
        RootPanel.get().add( (Widget)module.getStartView() );
    }
}
We now have our static elements displayed. Thanks to the MVP/event bus pattern, we can see that we have a good seperation of concern. Only the RootView is aware of the actual different location. For example, you can decide to change the menu's location (like moving it from the top to the right of the screen) without impacting the rest of the application by modifing only the RootView. Also the RootView is not aware of the actual view displayed at a specific location, so you can then easily change your menu by modifying just one line of code.

Displaying dynamic views

In our application, we have 2 views that are displayed dynamically inside the body location, Page1View and Page2View. The main difference with static views is that instead of having them displayed when the application starts, they will get displayed when another presenter asks for it. With Mvp4g, this presenter will do this thanks to an event that we can categorize as a place event. This place event will have 2 goals:
  • tell the dynamic view's presenter to display its view. This presenter will execute this action by firing a layout event with its view.
  • send any information needed to display the view. In our example, we'll pass a string that will be displayed in the view.
For each dynamic view, we'll need an event.
@Events( startView = RootView.class )
public interface LayoutExampleEventBus extends EventBus {

    ...

    @Event( handlers = Page1Presenter.class )
    void goToPage1( String name );

    @Event( handlers = Page2Presenter.class )
    void goToPage2( String name );

}
This is how the dynamic view's presenter will handle the event.
@Presenter(view = Page1View.class)
public class Page1Presenter extends LazyPresenter<IPage1View, LayoutExampleEventBus> implements IPage1Presenter {

    public void onGoToPage1(String name){
        view.setName( name );
        eventBus.setBody( view );
    }
    
}
Now that we have defined these events, we can use them anywhere in the application. In our example, we need to fire them when you click on one of the menu links. As we said before, we use the Reverse MVP pattern to define our presenter/view. With this pattern, the view is in charge of managing the click handler (and this is easily done thanks to UiBinder) but it can't fire a Mvp4g event directly, it has to go through the presenter. In our example, the presenter provides two methods to fire these events.
public interface IMenuView extends IsWidget {

    public interface IMenuPresenter {
        void goToPage1();
        
        void goToPage2();        
    }

}
@Presenter( view = MenuView.class )
public class MenuPresenter extends BasePresenter<IMenuView, LayoutExampleEventBus> implements IMenuPresenter {

...

    @Override
    public void goToPage1() {
        eventBus.goToPage1( "You clicked the menu" );
    }

    @Override
    public void goToPage2() {
        eventBus.goToPage2( "You clicked the menu." );
    }

}
Our view (built with a uibinder) can then easily call these methods.
public class MenuView extends ReverseCompositeView<IMenuPresenter> implements IMenuView {

    ...    
    
    @UiHandler( "page1" )
    public void onClickPage1(ClickEvent e){
        presenter.goToPage1();
    }
    
    @UiHandler( "page2" )
    public void onClickPage2(ClickEvent e){
        presenter.goToPage2();
    }

}
The last step is to display a dynamic view when the application starts. There are different ways to do this. I chose to have the RootPresenter take care of this when handling the start event.

@Presenter( view = RootView.class )
public class RootPresenter extends BasePresenter<IRootView, LayoutExampleEventBus> implements IRootPresenter {

    ...

    public void onStart() {
        eventBus.goToPage1( "The application started." );
    }

}
This way, you can easily modify the view to display at start by just modifying the event fired by the RootPresenter.

Once again, we can notice the light coupling between the presenter that fires the place event and the presenter that handles it. You can easily switch the view to display thanks to one line of code.

Part 2: Reducing the boilerplate code thanks to GIN

The header, menu, and footer are static elements that won't change. The setHeader, setFooter and setMenu will then only be used once. Since each event requires some lines of code, it is always better if we can prevent them. Fortunately, Mvp4g offers a GIN based technique to remove these events.

You can download the sample code for part 2 here (Project: Mvp4g_Layout_Part2).

The idea is, instead of passing the static views to the RootView via layout events, we will inject them to the RootView thanks to GIN.
In order to do this, we first need to modify the RootView to inject the static views. We add GIN injection and remove all the setter methods.
<!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">
    <g:HTMLPanel styleName="root">
        <g:Widget ui:field="header" />
        <g:Widget ui:field="menu" />
        <g:SimplePanel ui:field="body" styleName="body" />
        <g:Widget ui:field="footer" />
    </g:HTMLPanel>
</ui:UiBinder>
public class RootView extends ReverseCompositeView<IRootPresenter> implements IRootView {

    private static RootViewUiBinder uiBinder = GWT.create( RootViewUiBinder.class );

    @UiField( provided = true )
    Widget header, menu, footer;

    @UiField
    SimplePanel body;

    interface RootViewUiBinder extends UiBinder<Widget, RootView> {
    }

    @Inject
    public RootView( HeaderView header, MenuView menu, FooterView footer ) {
        this.header = header;
        this.menu = menu;
        this.footer = footer;
        initWidget( uiBinder.createAndBindUi( this ) );
    }

    @Override
    public void setBody( IsWidget body ) {
        this.body.setWidget( body );
    }
    
}
In the UiBinder, we can define the header, menu, footer as simple widgets since there is no need for it to know the implementation.

We then have to mark HeaderView, MenuView and FooterView as singleton so that the same instance is injected to the presenter and the RootView.

@Singleton
public class MenuView extends ReverseCompositeView<IMenuPresenter> implements IMenuView {
...
}
Finally we have to remove the setHeader, setMenu and setFooter events (and the methods to handle them in RootPresenter) since they are now useless.

Header, menu and footer don't have to handle the start event anymore since they don't need to execute any action when the application starts. However we need to tell Mvp4g to bind these presenters at start for two reasons:
  • Mvp4g builds a presenter right before it has to handle its first event by calling the presenter's bind method. Any presenter with a view to be displayed must then be built. In our sample code, it's not an issue because our bind methods are empty but for example, you will have an error if you extend a lazy presenter. Thanks to a feature introduced by 1.4.0, you can specify presenters that only need to be binded for an event, without handling it,
  • If a presenter doesn't handle any event or isn't binded for any event or isn't the start presenter, then Mvp4g considers it as useless and ignores it to optimize the application.
A presenter with a view injected in to another view should always at least be binded by an event before being displayed.

Thus we'll change our event bus so that header, footer and menu presenters don't handle the event but are just binded by it:
public interface LayoutExampleEventBus extends EventBus {

    @Start
    @Event( bind = { MenuPresenter.class, FooterPresenter.class, HeaderPresenter.class }, handlers = RootPresenter.class )
    void start();
        ...
}
Thus, thanks to this simple technique, you can reduce the layout events to a minimum. You can reuse this technique anytime you have a static nested view.

What's next?


I hope this tutorial helped you have a better understanding of how you can set your layout thanks to events and have a light coupling between each element. There are still many points to cover to build a complex GWT application. In the next tutorial, I will cover how easy it is to add browser history support.

9 comments:

  1. I updated the tutorial with 1.4.0 features. The main differences are:
    -you now define a startPresenter instead of a startView,
    -the bind feature is used to reduce boilerplate code (see end of part 2).

    ReplyDelete
    Replies
    1. Man, links with project are not working. I am realy need to end this tutorial. Pls post new links. I cant see you UIBinders, and i have error with "setName" method. Pls help, find that project man.

      Delete
  2. Great work. Amazing

    ReplyDelete
  3. A great tutorial. Thanx!

    ReplyDelete
  4. Thanks for the tuto, very helpful!

    ReplyDelete
  5. I cannot see ReverseCompositeView class in version 1.4.0 of Mvp4G. Is it a ReverseViewInterface that is supposed to be used instead?

    ReplyDelete
  6. Hello, and thanks for the tutorial, but I have some trouble after part 2 with ui designer inside of eclipse, when I try to display the RootView.ui.xml I get this message :

    Internal Error
    encountered unexpected internal error.

    This could be caused by a bug or by a misconfiguration issue, conflict, partial update, etc.

    java.lang.AssertionError: This UIObject's element is not set; you may be missing a call to either Composite.initWidget() or UIObject.setElement()

    java.lang.AssertionError: This UIObject's element is not set; you may be missing a call to either Composite.initWidget() or UIObject.setElement()
    at com.google.gwt.user.client.ui.UIObject.getElement(UIObject.java:527)
    at com.google.gwt.user.client.ui.HTMLPanel.addAndReplaceElement(HTMLPanel.java:199)
    at com.xxx.webapp.client.view.RootView_RootViewUiBinderImpl_designTime1351869332328.createAndBindUi(RootView_RootViewUiBinderImpl_designTime1351869332328.java:65)
    at com.xxx.webapp.client.view.RootView_RootViewUiBinderImpl_designTime1351869332328.createAndBindUi(RootView_RootViewUiBinderImpl_designTime1351869332328.java:1)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    ...

    However it still works fine with the others ui.xml files.
    I suppose this is because of gin injection, i found a solution that seems to work for the one who asked for it, but it doesn't work in my project... (create a constructor with no args and set objects manually, but with this example the simplePanel is out of the constructor)

    I'm still a beginner with gwt so I may have missed something...

    ReplyDelete