When I looked at MVP related articles, demos and so many
implementations, I keep ask myself following questions.
(1) How to make Place definition simple yet flexible?
(2) How to elegantly decouple presenters and decouple a presenter and
it's container and it's children presenters?
(3) What is the responsibility of Place Service (or Activity Manager
in GWT2.1)? and what does the API look like?
Since I struggled so long to try to find the answers, I'd like to
share what I got so far (I've written a Place Service implementation).
My own humble opinion regarding these questions are:
(1).
Most of time, place can be just a string but some times it need to
carry parameters or even extend from other places. Therefore, a Place
should be an interface instead of concrete class so enum can be used
to define simple static places.
public enum Pages implements PagePlace, GenericPlace {
HOME, HELP, SECURITY_PERMISSION, SECURITY_ROLE // ...
@override
public String getHistoryToken() {
return name().toLowerCase();
}
}
There are places which carry history tokens but some are not, for
example, header and footer of application are places but does not
require history token.
public enum CommonComponents implements ComponentPlace {
ROOT, HEADER, FOOTER, TOPMENU, SUBMENU, HELPCENTER, MAIN
}
For the places need to carry a parameter, it'll need to be defined as
a class instead of enum.
public class IdentifiablePlaceExample extends IdentifiablePlace
implements PagePlace, GenericPlace {
// Runtime place instance will use this constructor to create
place with identifier
public IdentifiablePlaceExample(String identifier) {
this.identifier = identifier;
}
// Return history token for Browser - #!(token):(identifier) will
be displayed in browser url
@Override
public String getHistoryToken() {
return "history-token-you-want";
}
}
(2).
In my opinion, a presenter should NOT attach the corresponding view to
DOM or a container presenter itself.
public interface Presenter {
public Viewable bind(Place place);
public void unbind(Place place);
}
public interface Container extends Presenter {
public void showChild(Place place, Viewable childView);
public void closeChild(Place place, Viewable childView);
}
In my Place Service implementation, Place Service knows the dependency
between presenters but not presenter themselves. Therefore, when a
view been required, Place Service will call the Presenter.bind(Place
place) function to ask the corresponding Presenter to provide a View
instance.
After that, Place Service will find out which Container (a special
type of Presenter) is responsible for displaying that type of Place
and call Container.showChild(Place place, Viewable view) function to
let the Container handle displaying the view in the container view.
E.g. display a particular tab page (The View returned by the
Presenter) in a tab container (The Container presenter).
This way, in my main presenter which handles multiple children pages
will no longer need to know any of other presenters but a generic
Place type.
public class MainPresenter implements Container {
@Override
public void closeChild(Place place, Viewable childView) {
view.getPageContainer().remove(childView.asWidget());
}
@Override
public void showChild(Place place, Widget childView) {
if (place instanceof PagePlace) {
view.getPageContainer().add(childView.asWidget());
}
}
}
(3).
Besides controller simple presenters, a Place Service should provide
automatic (invisible to Presenters) bind/unbind support to complex
dependency graph. E.g.
Root (C)
- Header (P)
- PageContainer (C)
- Login (P)
- Main (C)
- Page1 (P)
- Page2 (P)
- Page3 (P)
- ...
- Footer (P)
To build this kind of complex graph, I've created a Place Binder to
simplify configuration and used a GIN module to centralize them.
// setToRoot is a special function to let place service know when to
stop scanning.
bind(root).serve(Places.ROOT).setToRoot().contains(Places.Header,
Places.PageContainer, Places.Footer);
bind(header).serve(Places.HEADER);
bind(pageContainer).serve(Places.PAGE_CONTAINER).contains(Places.Login,
Places.Main);
// setToDefault make the current presenter default entry of
application (If history token is not specified by user)
bind(login).serve(Places.LOGIN).setToDefault();
// contains function can take a Place class type instead of specific
instance, this way any Place extends PagePlace will be handled by
MainPresenter.
bind(main).serve(Places.MAIN).contains(PagePlace.class);
bind(page1).serve(Pages.PAGE1);
bind(page2).serve(Pages.PAGE2);
bind(page3).serve(Pages.PAGE3);
bind(footer).serve(Places.FOOTER);
After configuring the graph, we can navigate to any HistoryPlace
through Place Service API or browser URL now.
e.g.
"placeService.go();" in the entry point will lead PlaceService
actually call following functions automatically to display all
required views on screen.
LoginPresenter.bind(Places.LOGIN);
PageContainer.bind(Places.PAGE_CONTAINER); // Place Service found out
LOGIN page depend on PAGE_CONTAINER and PAGE_CONTAINER is not bound
yet.
PageContainer.showChild(Places.LOGIN, loginView); // Place Service
tell PAGE_CONTAINER to display login view.
RootContainer.bind(Places.ROOT); // Bind the root view
rootPanelService.get().add(rootView); // Attach the root view
RootContainer.showChild(Places.PAGE_CONTAINER, pageContainerView);
HeaderPresenter.bind(Places.HEADER); // Place Service detect all the
dependency and bind all required views automatically.
RootContainer.showChild(Places.HEADER, headerView);
HeaderPresenter.bind(Places.FOOTER);
RootContainer.showChild(Places.FOOTER, headerView);
When user successfully login, loginPresenter calls
"placeService.go(Pages.PAGE1)" Place Service will detect shared
ancestor of both branches and tell all containers in the branch
current view belongs to close the child.
PageContainer.closeChild(Places.LOGIN, loginView);
LoginPresenter.unbind(Places.LOGIN); // By default, unbind() function
will be called when a view closed;
Page1Presenter.bind(Pages.PAGE1);
MainContainer.bind(Places.MAIN);
MainContainer.showChild(Pages.PAGE1, page1View);
PageContainer.showChild(Places.MAIN, mainView);
Cheers,
Jiang Zhu
--
You received this message because you are subscribed to the Google Groups
"Google Web Toolkit" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to
[email protected].
For more options, visit this group at
http://groups.google.com/group/google-web-toolkit?hl=en.