Monday, June 20, 2011

Navigation Control

One of the main features introduced in Mvp4g-1.3.0/1.3.1 is the navigation feature. This feature allows you to detect when a user leaves a page. The most common use of this feature is when the user is on a page with a form. Before leaving this type of page, you usually want to check if all the data entered by the user is saved. If not, you ask the user to confirm if he wants to leave the page without saving or if he wants to cancel the navigation.

To demonstrate this feature, I'm going to reuse the example developed for the previous tutorial and add a form on page 2. You can download the example here (Project: Mvp4g_Navigation_Control).

Adding a form to our example


This form is pretty simple with only two fields (first and last name) and a save button.

We have the following screen:


Like the rest of the application, we follow the Reverse MVP pattern to develop our form. We then have the following interfaces:
public interface IPage2View extends IsWidget, LazyView {

    public interface IPage2Presenter {
        
        void onSaveClick();

    }
    
    HasValue<String> getFirstName();
    
    HasValue<String> getLastName();
    
    ...
}
When displaying page 2, we update the values displayed on the screen with the local variable value and inversely when clicking on save.
@Presenter( view = Page2View.class )
public class Page2Presenter extends LazyPresenter<IPage2View, NavigationControlEventBus> implements IPage2Presenter {

    private UserBean user;

    ...

    public void onGoToPage2( String origin ) {
        ...
        view.getFirstName().setValue( user.getFirstName() );
        view.getLastName().setValue( user.getLastName() );

        eventBus.setBody( view );
    }

    @Override
    public void onSaveClick() {
        user.setFirstName( view.getFirstName().getValue() );
        user.setLastName( view.getLastName().getValue() );
        view.alert( "User Info saved." );
    }

    ...
}
Now that we have set our form, we need to check that, when the user leaves the page, he doesn't lose any data.

Defining navigation events


The first questions we need to answer are what does it mean to leave a page and when does it occur? In the GWT world, we have a single page application so we can't consider that a user goes to a new page when it loads a new HTML file. Instead the notion of places has been introduced. A place defines where the user is in the application. So leaving a page means going to a new place.

In Mvp4g, you go to a new place by firing an event. Thus the first step to control user navigation is to indicate which event brings the user to a new place.

In order to do this, a new attribute has been added to the @Event annotation, 'navigationEvent'. If set to true, it indicates that when this event is fired, the user is brought to a new place. In our example, we have two place events, goToPlace1 and goToPlace2.
@Events( startView = RootView.class, historyOnStart = true )
public interface NavigationControlEventBus extends EventBusWithLookup{

    ...

    @Event( handlers = Page1Presenter.class, historyConverter = PageHistoryConverter.class, name = "page1", navigationEvent = true )
    void goToPage1( String origin );

    @Event( handlers = Page2Presenter.class, historyConverter = PageHistoryConverter.class, name = "page2", navigationEvent = true )
    void goToPage2( String origin );

}
Whenever one of these events is fired, a control will be performed if a navigation confirmation has been set. This control will also be performed whenever the history token changes.

Creating a navigation confirmation


Now that we have configured our navigation events, we need to create a navigation confirmation to control the navigation. In order to do this, we need to create a class that implements NavigationConfirmationInterface. This interface defines the following method:
void confirm( NavigationEventCommand event );
This method is called whenever a navigation event is fired or if the history token changes. It takes one parameter, a NavigationEventCommand that represents the event. As you may have noticed, this method is asynchronous, which means that it doesn't return a boolean to confirm the event or not. Instead, the method is in charge of confirming the event by calling the fireEvent method of the NavigationEventCommand parameter (if you don't want to confirm the event, you just don't call this method). Why is this method asynchronous? Mainly for two reasons:
  • You may want to call services when confirming your event (to save data before going to a new page for example).
  • You may want to use your own custom confirm popup. Since it is asynchronous, you can easily block any other action until the user answers the question.

To create a NavigationConfirmationInterface, I usually have the presenter implement it directly. This way, I can easily have access to the data and the view to confirm the navigation and I limit the number of classes.

In our example, we want to control that data entered in the form has been saved. In order to do this, we just check if the data displayed in the view matches the data of the local variable. If it doesn't, we display a message to the user using the default browser confirm popup. If data matches or if the user wants to leave the page anyway, we confirm the event by calling the fireEvent method.
@Presenter( view = Page2View.class )
public class Page2Presenter extends LazyPresenter<IPage2View, NavigationControlEventBus> implements IPage2Presenter, NavigationConfirmationInterface {

    ...

    @Override
    public void confirm( NavigationEventCommand event ) {
        boolean sameData = user.getFirstName().equals( view.getFirstName().getValue() ) && user.getLastName().equals( view.getLastName().getValue() );
        if ( sameData || view.confirm( "Data not saved, are you sure you want to leave the page?" ) ) {
            event.fireEvent();
        }
    }
}

Setting a navigation confirmation


Now that we have created our NavigationConfirmationInterface object, we need to tell the application to use it. To do this, we call the eventbus method, setNavigationConfirmation. By default, no confirmation is set so all navigation events will be confirmed and fired. Also, you can only set one NavigationConfirmationInterface at a time for your whole application.

The next question we have is when do we set our NavigationConfirmationInterface? Usually the best time to set it is when it's associated presenter becomes active (ie when this presenter handles the place event). Thus, when handling the place event, we call the setNavigationCommand method.
@Presenter( view = Page2View.class )
public class Page2Presenter extends LazyPresenter<IPage2View, NavigationControlEventBus> implements IPage2Presenter, NavigationConfirmationInterface {
    ...
    public void onGoToPage2( String origin ) {
        //set the presenter to control the navigation when its view is being displayed
        eventBus.setNavigationConfirmation( this );
        ...
    }
    ...
}
Another question you may have now is that if I set a NavigationConfirmationInterface whenever the presenter becomes active, shouldn't I remove it whenever the presenter becomes inactive (ie whenever the user goes to another place)? This is actually done automatically for you. Whenever you confirm an event by calling the fireEvent method of the NavigationEventCommand, this code is executed:
eventBus.setNavigationConfirmation( null );
This code unsets any NavigationConfirmationInterface instance. If for some reason, you don't want this to happen, instead of calling:
event.fireEvent();
you can use:
event.fireEvent(false);

What's next?


We have now setup the Navigation feature. Its first goal is to obviously control users' navigation but it is actually a lot more powerful as you can realize any action you need when the user goes to a new place.

In the next posts, I'm going to continue covering advanced Mvp4g features related to history and navigation by describing the hyperlink token generation feature and the custom place service feature.

2 comments:

  1. I updated the example with Mvp4g-1.4.0, the tutorial didn't change.

    ReplyDelete
  2. I have been having a problem with this and MVP4g's history framework. When using them both, if the user used the back button then cancelled navigation, the history would act as if the user had navigated successfully, and on the next usage of the back button would navigate back to the page prior to the previous page.

    Have you encountered this before, and/or do you have any information on the topic?

    ReplyDelete