When I wrote a topic about using
the Command pattern to organize RPC calls in GWT, I mentioned the MVP pattern for building the architecture of GWT projects. Immediately after the speech of Comrade Ray Rayan with a report on the design of complex applications on GWT, workers around the world began to implement the voiced ideas in the form of libraries and frameworks. The result of these works are the means to apply both some aspects of the MVP approach (
GWT-Presenter ) and its entirety (
Mvp4G ). This is all great, but I personally (I am sure that the rest of the GWT developers) would like to have a standardized (if you can say so) framework / approach for organizing GWT applications using the MVP scheme. And now the team that is responsible for Google for GWT, finally, in version 2.1, along with the other tasty buns, suggested the built-in MVP framework.
In this article I will try to highlight the main points of the embedded GWT MVP framework. As an example, a small application built using this approach will be used.
This note is a free treatise (
GWT MVP Development ) and in no way claims a unique approach. If someone is interested in such a rather big introduction
I will say right away that I will not focus on the design pattern MVP itself. You can get acquainted with its scheme, for example, on (
MVP on the Wiki ). In order to interest% username% I will give a screenshot of the application, which I eventually got.

Yes, I understand that the application does not do anything useful, but on its basis I will try to show how to divide the architecture into separate independent parts (mail, contacts and tasks, by the way, taken just from the ceiling), organize switching and communication between them with using the built-in mechanisms in GWT 2.1. In addition, the M-component of the MVP pattern, i.e. model, because the application for illustrative purposes there is no data binding.
Main components of embedded MVP
The GWT team proposed the following key components for building an application using MVP:
- Activity - in the classical approach is Presenter. Responsible for the logic of the currently open view. It does not contain any GWT widgets or related UI code. But in turn, it has an associated view object. Starts and stops automatically using ActivityManager
- ActivityManager - the built-in object that manages the life cycle of the Activities that are registered in it
- Place - is responsible for the state of the current view. Basically, the view state is transmitted using URLs (for example, open contact with ID = <such> editing) or on another, history tokens. Thanks to the PlaceHistoryHandler object, which “listens” to changes in the browser’s address bar, the desired state of the Place object can be recreated. When recreating or saving the state of a Place object, a PlaceTokenizer object is used, whose methods are called when recreating and saving the state of the described object
- PlaceHistoryMapper, ActivityMapper are mapping classes that are essentially responsible for registering all the Places and Activities of the application. The ActivityMapper also, based on the transferred Place object (which in turn was recreated from the history token), decides which Activity object will be associated with the corresponding URL state
- View - simple Composite-widgets, which may consist of other nested widgets. Contain an instance of a related presenter (activity). They do not contain logic, except logic, necessary for UI-needs, for example, style switching, etc. Execution of all useful logic is delegated to the associated presenter by calling its methods.
Let us now consider in more detail all these components separately using the example of a real code. By the way, the application code is available for
free access . So you can pry into it as you read.
')
Visual component or View
First of all, it should be said that in MVP, it is customary to “exchange” between the presenter and the view only interfaces. Therefore, for each type in the application there is a corresponding interface
package com.gshocklab.mvp.client.mvp.view; import com.google.gwt.user.client.ui.IsWidget; public interface IMailView extends IsWidget { public void setPresenter(IMailPresenter presenter); public interface IMailPresenter { } }
The view interface describes the corresponding presenter interface that will handle the entire payload (sending requests for receiving data, processing events from the event bus, etc.).
The implementation of this interface will be simple and should not cause any special questions.
public class MailView extends Composite implements IMailView { interface MailViewUiBinder extends UiBinder<Widget, MailView> { } private static MailViewUiBinder uiBinder = GWT.create(MailViewUiBinder.class); private IMailPresenter presenter; public MailView() { initWidget(uiBinder.createAndBindUi(this)); } @Override public void setPresenter(IMailPresenter presenter) { this.presenter = presenter; } }
The associated ui.xml file contains just one widget, a Label with simple text. It makes no sense to bring his code, it can be viewed on the project website.
That's all for the view part. We now turn to the more interesting, to the Activity.
Page logic (views) or Activity
In the demo application on the left there is a navigation bar with links. Clicking on these links switches between views and sets the CSS style for the current link. I rendered this action into the abstract parent class AbstractMainActivity, which is a successor of the built-in class AbstractActivity.
package com.gshocklab.mvp.client.mvp.activity; public abstract class AbstractMainActivity extends AbstractActivity { private static Map<String, Element> navLinks = new LinkedHashMap<String, Element>(); static { navLinks.put(AppConstants.MAIL_LINK_ID, DOM.getElementById(AppConstants.MAIL_LINK_ID)); navLinks.put(AppConstants.CONTACTS_LINK_ID, DOM.getElementById(AppConstants.CONTACTS_LINK_ID)); navLinks.put(AppConstants.TASKS_LINK_ID, DOM.getElementById(AppConstants.TASKS_LINK_ID)); } public void applyCurrentLinkStyle(String viewId) { for (String linkId : navLinks.keySet()) { final Element link = navLinks.get(linkId); if (link == null) continue; if (linkId.equals(viewId)) { link.addClassName("b-current"); } else { link.removeClassName("b-current"); } } } }
And the concrete implementation of a specific Activity.
package com.gshocklab.mvp.client.mvp.activity; public class MailActivity extends AbstractMainActivity implements IMailView.IMailPresenter { private ClientFactory clientFactory; public MailActivity(ClientFactory clientFactory) { this.clientFactory = clientFactory; } @Override public void start(AcceptsOneWidget container, EventBus eventBus) { applyCurrentLinkStyle(AppConstants.MAIL_LINK_ID); final IMailView view = clientFactory.getMailView(); view.setPresenter(this); container.setWidget(view.asWidget()); } }
How it works: ActivityManager, when it receives a URL change event from PlaceHistoryManager, creates the necessary Activity instance using the ActivityMapper and starts it with the start () method. In this method, one of the parameters is the container in which the view widget will be substituted. The view we get from ClientFactory, which will be a little lower. We inject the presenter into the resulting instance of the view and display the view as a widget. Yes, another CSS rule is being established for the link that leads to the current view. But this is purely visual design.
ClientFactory is a simple factory that creates the necessary objects. Its interface is described as follows.
public interface ClientFactory { public EventBus getEventBus(); public PlaceController getPlaceController(); public IMailView getMailView(); public IContactsView getContactsView(); public ITasksView getTasksView(); }
Its implementation is not distinguished by “intelligence and wit”.
public class ClientFactoryImpl implements ClientFactory { private final EventBus eventBus = new SimpleEventBus(); private final PlaceController placeController = new PlaceController(eventBus); private final IMailView mailView = new MailView(); private final IContactsView contactsView = new ContactsView(); private final ITasksView tasksView = new TasksView(); @Override public EventBus getEventBus() { return eventBus; } @Override public PlaceController getPlaceController() { return placeController; } @Override public IMailView getMailView() { return mailView; } @Override public IContactsView getContactsView() { return contactsView; } @Override public ITasksView getTasksView() { return tasksView;} }
The instantiation of the ClientFactory object will be performed using Deffered binding according to the rule, which is described in the GWT module description file. But more about that again later in the section where the configuration of the entire MVP enterprise in a single working system will be considered. It is worth noting here that in real projects for tasks that ClientFactory solves it is better to use Google GIN. Advantages of the DI-tool, it makes no sense to describe, they are already clear.
The last of the key elements of the built-in MVP is an object that is responsible for the state of the UI and performs manipulations with history tokens.
Place or hash URLs and their processing
As mentioned above, the Place object is responsible for the current state of the UI. The state is transferred via URL through history tokens. In fact, in this object, you can store parameters that are passed to the hash URL. The status of the URL is encoded / decoded using a Tokenizer object. When working with request parameters that are passed to a hash URL, it is very important to follow the following rule: all parameters that come to us from the URL after processing should be encoded back into the URL in the same way. It is this logic that is implemented in the methods of the Tokenizer class.
By convention, which is accepted by the GWT team and is described in the official manual, the tokenizer class is usually described as an internal static class of the Place object. This simplifies the code for storing query parameters in the Place object's variables. Although it is possible to apply an approach with separate classes for tokenizers.
Not to be unfounded, consider the code class MailPlace
package com.gshocklab.mvp.client.mvp.place; import com.google.gwt.place.shared.Place; import com.google.gwt.place.shared.PlaceTokenizer; import com.google.gwt.place.shared.Prefix; public class MailPlace extends Place { private static final String VIEW_HISTORY_TOKEN = "mail"; public MailPlace() { } @Prefix(value = VIEW_HISTORY_TOKEN) public static class Tokenizer implements PlaceTokenizer<MailPlace> { @Override public MailPlace getPlace(String token) { return new MailPlace(); } @Override public String getToken(MailPlace place) { return ""; } } }
This class is inherited from the built-in Place class. In it, the constant declared is the part of the hash URL that will uniquely identify the state. In this case, it is “mail”. The class Tokenizer is responsible for recreating the state and saving it via history tokens. The binding of a specific hash URL to the tokenizer is performed using the
Prefix annotation.
Processing history is generally an interesting topic and deserves a separate article. Here we confine ourselves to having our own hash URL with each Place object. This URL must end with “:”. After this colon, you can specify additional parameters, for example, you can create URLs like #mail: inbox, #contacts: new, etc. and these tokens will be processed in the getPlace () method. In fact, the first part of the hash URLs is the subsystem identifier (mail, tasks, etc.), everything that follows after the colon can be interpreted as subsystems' actions.
In the demo project, additional tokens (or actions) are not used, so the getToken () method in all tokenizers returns an empty string, and the getPlace () method returns the created Place object.
Determining the desired Activity and registering handlers
When a new URL is received and the Place object is successfully instantiated, the ActivityManager manager uses the ActivityMapper to decide which object to launch. The definition is implemented is simple and trite.
public class DemoActivityMapper implements ActivityMapper { private ClientFactory clientFactory; public DemoActivityMapper(ClientFactory clientFactory) { super(); this.clientFactory = clientFactory; } @Override public Activity getActivity(Place place) { if (place instanceof MailPlace) { return new MailActivity(clientFactory); } else if (place instanceof ContactsPlace) { return new ContactsActivity(clientFactory); } else if (place instanceof TasksPlace) { return new TasksActivity(clientFactory); } return null; } }
Registering hash URL handlers, i.e. Tokenizers are running on the PlaceHistoryMapper interface.
package com.gshocklab.mvp.client.mvp; import com.google.gwt.place.shared.PlaceHistoryMapper; import com.google.gwt.place.shared.WithTokenizers; import com.gshocklab.mvp.client.mvp.place.ContactsPlace; import com.gshocklab.mvp.client.mvp.place.MailPlace; import com.gshocklab.mvp.client.mvp.place.TasksPlace; @WithTokenizers({MailPlace.Tokenizer.class, ContactsPlace.Tokenizer.class, TasksPlace.Tokenizer.class}) public interface DemoPlaceHistoryMapper extends PlaceHistoryMapper { }
All that is needed at this stage is simply to list the tokenizer classes of the application in the @WithTokenizers annotation.
Putting it all together
All the code for initializing and starting the MVP framework is assembled in the onModuleLoad () method of EntryPoint
package com.gshocklab.mvp.client; import com.google.gwt.activity.shared.ActivityManager; import com.google.gwt.activity.shared.ActivityMapper; import com.google.gwt.core.client.EntryPoint; import com.google.gwt.core.client.GWT; import com.google.gwt.event.shared.EventBus; import com.google.gwt.place.shared.PlaceController; import com.google.gwt.place.shared.PlaceHistoryHandler; import com.google.gwt.user.client.History; import com.google.gwt.user.client.ui.RootLayoutPanel; import com.google.gwt.user.client.ui.SimplePanel; import com.gshocklab.mvp.client.layout.AppLayout; import com.gshocklab.mvp.client.mvp.DemoActivityMapper; import com.gshocklab.mvp.client.mvp.DemoPlaceHistoryMapper; import com.gshocklab.mvp.client.mvp.place.MailPlace; public class MvpInActionEntryPoint implements EntryPoint { private SimplePanel containerWidget; private MailPlace defaultPlace = new MailPlace(); @Override public void onModuleLoad() { final AppLayout mainLayout = new AppLayout(); containerWidget = mainLayout.getAppContentHolder(); final ClientFactory clientFactory = GWT.create(ClientFactory.class); EventBus eventBus = clientFactory.getEventBus(); PlaceController placeController = clientFactory.getPlaceController();
I think the explanation code will be superfluous, everything is simple and clear. It should be noted that the call to History.newItem (“mail:”) may be superfluous. MailActivity and so run, because MailPlace is specified as the default Place. Another thing is that at the start we will not see the hash URL #mail: in the address bar of the browser. If the display of the starting hash URL for the project is not critical, then the call to History.newItem () can be removed.
In order for the embedded MVP framework to work, you need to include the corresponding GWT modules in the GWT module description file (gwt.xml file)
<?xml version="1.0" encoding="UTF-8"?> <module rename-to='mvpinaction'> <inherits name='com.google.gwt.user.User' /> <inherits name="com.google.gwt.activity.Activity"/> <inherits name="com.google.gwt.place.Place"/> <entry-point class='com.gshocklab.mvp.client.MvpInActionEntryPoint' /> <replace-with class="com.gshocklab.mvp.client.ClientFactoryImpl"> <when-type-is class="com.gshocklab.mvp.client.ClientFactory" /> </replace-with> <source path='client' /> </module>
It also specifies the deferred binding rule for creating the ClientFactory instance.
Instead of conclusion
That's all. I do not provide the file with the root layout of the AppLayout application, it can be viewed in the source code. In this file there are links, in the href attributes of which are hash URLs to go to the application subsystems. You can also open this or that subsystem simply by typing the correct URL in the address bar of the browser. It will automatically start the process of converting the state from the URL to the corresponding place with the launch of the corresponding activity, which will display the desired form.
I note in addition that the note and demo project did not consider such important moments in life as the use of the event bus (eventBus), the processing of hash URL parameters and much more.
Working
demo-project ,
source code Google Code . Watch out Mercurial!
Waiting for feedback and comments.
PS Sorry for the multi-letter. I hope that this introductory note turned out to be useful to someone (although it doesn’t fully cover all the power of the embedded MVP), I wrote it for good reason and it will serve as a starting point for implementing really cool GWT applications