Friday, August 26, 2011

GWT Development with Activities and Places

In GWT 2.1 the activities and places were introduced to allow you to use the browser history management. In the following versions this features where improved. I prepared this example already months ago but due to lack of time I was not able to post it until now. On the end of the article you will find a link with the source code!

What is the use case? I have the following layout:

image

This layout will have two sides, left side with a menu and links and right side where the content will be shown, when I click on the links on the left side, the content on the right side should change. With this layout I would like to use the GWT activities and places to allow the user to use the browser back button.

I would try to construct my tutorial like the article regarding activities and places, which you can find here.

Views

First we would need a view for the main layout. I also used UiBinder to make it more interesting. I call it RootView. I know the name is not really very well select, but should be OK for this example. Into the RootView I will register my activity mappers and also I will handle the right side where the content should be shown. Here is a part of the code which I used in the constructor:

1 public RootView(ClientFactory clientFactory) {
2 initWidget(uiBinder.createAndBindUi(this));
3
4 this.clientFactory = clientFactory;
5
6 // Start ActivityManager for the main widget with our ActivityMapper
7 ActivityMapper leftActivityMapper = new LeftSideActivityMapper(
8 clientFactory);
9 ActivityManager leftActivityManager = new ActivityManager(
10 leftActivityMapper, this.clientFactory.getEventBus());
11 leftActivityManager.setDisplay(leftPanel);
12
13 // right side
14 ActivityMapper rightActivityMapper = new RightActivityMapper(
15 clientFactory);
16 ActivityManager rightActivityManager = new ActivityManager(
17 rightActivityMapper, this.clientFactory.getEventBus());
18 rightActivityManager.setDisplay(rightPanel);
19
20 }
21

The RootView extends from the MainView interfaces:


1 public interface MainView extends IsWidget {
2
3 void setWidgetName(String widgetName);
4
5 void setPresenter(Presenter presenter);
6
7 ClientFactory getClientFactory();
8
9 public interface Presenter {
10
11 void goTo(Place place);
12 }
13
14 }
15

and here is the UiBinder:


1 <!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
2 <ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
3 xmlns:g="urn:import:com.google.gwt.user.client.ui" xmlns:c="urn:import:com.gwt2go.dev.client.ui.widget"
4 xmlns:h="urn:import:com.gwt2go.dev.client.ui.widget">
5 <ui:style>
6 .eastPanel {
7 background-color: #F60;
8 }
9
10 .westPanel {
11 background-color: #EEE;
12 }
13
14 .northPanel {
15 -moz-border-bottom-colors: none;
16 -moz-border-image: none;
17 -moz-border-left-colors: none;
18 -moz-border-right-colors: none;
19 -moz-border-top-colors: none;
20 background-color: #627AAD;
21 border-color: #1D4088 #1D4088 -moz-use-text-color;
22 border-style: solid solid none;
23 border-width: 1px 1px 0;
24 height: 30px;
25 /* margin-left: 180px; */
26 color: #FFFFFF;
27 display: inline-block;
28 font-weight: bold;
29 }
30
31 .title {
32 color: #FFFFFF;
33 font-size: 20pt;
34 font-weight: bold;
35 text-shadow: #ddd 3px 3px 1px;
36 margin: 0;
37 padding: 0 0 0 4px;
38 }
39
40 .subtitle {
41 color: #FFFFFF;
42 font-size: 16pt;
43 margin: 0;
44 padding: 0 0 0 6px;
45 }
46
47 .titleBar {
48 padding: 0 10px;
49 border-bottom: 1px solid #C3C3C3;
50 }
51
52 .pushButton {
53 background-color: #6D86B7;
54 outline: medium none;
55 border-style: solid solid none;
56 border-width: 1px 1px 0;
57 padding: 5px 5px 5px 3px;
58 }
59
60 .menuButton {
61 background-color: #6D86B7;
62 outline: medium none;
63 border-style: solid solid none;
64 border-width: 1px 1px 0;
65 /* padding: 5px 5px 5px 3px; */
66 }
67
68 .rightHorizontalPanel {
69 margin-right: 5px;
70 }
71 </ui:style>
72
73 <g:DockLayoutPanel unit='EM'>
74 <g:north size='3'>
75 <g:FlowPanel styleName="{style.northPanel}">
76 <g:HTMLPanel styleName='{style.titleBar}'>
77 <table cellpadding='0' cellspacing='0' width='100%'>
78 <tr>
79 <td>
80 <table cellpadding='0' cellspacing='0'>
81 <tr>
82 <td style='line-height:0px'>
83 <!-- Logo Image. -->
84 &nbsp;
85 </td>
86 <td>
87 <!-- Title and subtitle. -->
88 <h2 class='{style.subtitle}'>
89 <ui:msg key='mainSubTitle'>testfactory</ui:msg>
90 </h2>
91 </td>
92 </tr>
93 </table>
94 </td>
95 <td align='right' valign='top'>
96 <table cellpadding='0' cellspacing='0'>
97 <tr>
98 <td valign='middle'>
99 <g:HorizontalPanel styleName="{style.rightHorizontalPanel}">
100 <g:MenuBar title="Account" ui:field="menuBar" animationEnabled="true" autoOpen="true">
101 <g:MenuItem text="Home" ui:field="btnHome"/>
102 <g:MenuItem text="Profile" ui:field="btnProfile"/>
103 <g:MenuItem text="Account" ui:field="btnAccount">
104 <g:MenuBar vertical="true">
105 <g:MenuItem text="Settings" ui:field="btnSettings"/>
106 <g:MenuItem text="Logout" ui:field="btnLogout"/>
107 </g:MenuBar>
108 </g:MenuItem>
109 </g:MenuBar>
110 </g:HorizontalPanel>
111 </td>
112 </tr>
113 </table>
114
115 </td>
116 </tr>
117 </table>
118 </g:HTMLPanel>
119 </g:FlowPanel>
120 </g:north>
121 <g:west size='15'>
122 <g:ScrollPanel>
123 <g:SimplePanel ui:field="leftPanel"></g:SimplePanel>
124 </g:ScrollPanel>
125 </g:west>
126 <g:center>
127 <g:ScrollPanel ui:field='rightPanel' />
128 </g:center>
129 </g:DockLayoutPanel>
130
131 </ui:UiBinder>

As you can see from the RootView I have two new activity mapper, LeftSideActivityMapper and the RightActivityMapper. The LeftSideActivityMapper will handles the left side menu and loads the menu and the links on the left side. The RightActivityMapper will handles the right side and controls what should be loaded.


You would also need to create a composite for the left side menu:


1 public class LeftSide extends Composite {
2
3 private static LeftSideUiBinder uiBinder = GWT
4 .create(LeftSideUiBinder.class);
5
6 interface LeftSideUiBinder extends UiBinder<Widget, LeftSide> {
7 }
8
9 @UiField
10 Button button1;
11
12 @UiField
13 Button button2;
14
15 @UiField
16 Button button3;
17
18 @UiField
19 Button btnEditor;
20
21 @UiField(provided=true) CellTree cellTree = new CellTree(
22 new TreeViewModel() {
23 // final AbstractDataProvider<String> dataProvider = new ListDataProvider<String>();
24 // final AbstractSelectionModel<String> selectionModel = new NoSelectionModel<String>();
25 @Override
26 public <T> NodeInfo<?> getNodeInfo(T value) {
27 /*
28 * Create some data in a data provider. Use the parent value as a prefix
29 * for the next level.
30 */
31 ListDataProvider<String> dataProvider = new ListDataProvider<String>();
32 for (int i = 0; i < 2; i++) {
33 dataProvider.getList().add(value + "." + String.valueOf(i));
34 }
35
36 // Return a node info that pairs the data with a cell.
37 return new DefaultNodeInfo<String>(dataProvider, new TextCell());
38 //return new DefaultNodeInfo<String>(dataProvider, new TextCell(), selectionModel, null);
39 }
40 @Override
41 public boolean isLeaf(Object value) {
42 return value.toString().length() > 10;
43 //return true;
44 }
45 }, "Item 1");
46
47 ClientFactory clientFactory;
48
49 public LeftSide(ClientFactory clientFactory) {
50 initWidget(uiBinder.createAndBindUi(this));
51 button1.setText("SortingTable1");
52 button2.setText("SortingTable2");
53 button3.setText("SortingTable_GWT2.3");
54 btnEditor.setText("EditorWS");
55
56 this.clientFactory = clientFactory;
57 this.cellTree.setAnimationEnabled(true);
58 }
59
60 @UiHandler("button1")
61 void onButton1Click(ClickEvent e) {
62 this.clientFactory.getPlaceController().goTo(new RootPlace("table"));
63 }
64
65 @UiHandler("button2")
66 void onButton2Click(ClickEvent e) {
67 this.clientFactory.getPlaceController().goTo(new RootPlace("table2"));
68 }
69
70 @UiHandler("button3")
71 void onButton3Click(ClickEvent e) {
72 this.clientFactory.getPlaceController().goTo(new RootPlace("sortingtable23"));
73 }
74
75 @UiHandler("btnEditor")
76 void onbtnEditorClick(ClickEvent e) {
77 this.clientFactory.getPlaceController().goTo(new RootPlace("editor"));
78 }
79
80 }
81

The interesting part here is the button click events. As you can see from the code I use the RootPlace to pass the name of the activity I want to load.


Activities


The activity which load the left side menu looks now like this:


1 public class LeftSideActivity extends AbstractActivity implements
2 MainView.Presenter {
3
4 // Used to obtain views, eventBus, placeController
5 // Alternatively, could be injected via GIN
6 private ClientFactory clientFactory;
7
8 public LeftSideActivity(LeftSidePlace place, ClientFactory clientFactory) {
9 this.clientFactory = clientFactory;
10 }
11
12 public LeftSideActivity(ClientFactory clientFactory) {
13 this.clientFactory = clientFactory;
14 }
15
16 @Override
17 public void start(AcceptsOneWidget panel, EventBus eventBus) {
18 // MainView rootView = this.clientFactory.getRootView();
19 // rootView.setPresenter(this);
20 // panel.setWidget(rootView);
21
22 LeftSide leftSide = this.clientFactory.getLeftSide();
23 panel.setWidget(leftSide);
24 }
25
26 @Override
27 public void goTo(Place place) {
28 clientFactory.getPlaceController().goTo(place);
29 }
30
31 }
32

Now since the left side activity is always the same for the right side activity I will have a several. I would use some of my previously examples: CellTableActivity, CellTableSortingActivity, CellTableSorting23Activity, EditorActivity, GoodbyeActivity, which you can all find here. It will be too much source code to post here.


Mappers


Mappers are very important now here. The left mapper is not really special check the code bellow, but the mapper which handles the right side is more complicated, let’s check the code:


LeftSideActivityMapper:


1 public class LeftSideActivityMapper implements ActivityMapper {
2
3 private ClientFactory clientFactory;
4
5 public LeftSideActivityMapper(ClientFactory clientFactory) {
6 super();
7 this.clientFactory = clientFactory;
8 }
9
10 @Override
11 public Activity getActivity(Place place) {
12 // The activity you will get here, is the one from the RootView!!!
13
14 return new LeftSideActivity(clientFactory);
15
16 }
17
18 }
19

and the RightSideActivityMapper:


1 public class RightActivityMapper implements ActivityMapper {
2
3 private ClientFactory clientFactory;
4
5 public RightActivityMapper(ClientFactory clientFactory) {
6 super();
7 this.clientFactory = clientFactory;
8 }
9
10 @Override
11 public Activity getActivity(Place place) {
12 // The activity you will get here, is the one from the RootView!!!
13
14 if (place instanceof RootPlace) {
15 String name = ((RootPlace) place).getRootName();
16 if (name.equalsIgnoreCase("table")) {
17 return new CellTableActivity("table", clientFactory);
18 }
19
20 if (name.equalsIgnoreCase("table2")) {
21 return new CellTableSortingActivity(null, clientFactory);
22 }
23
24 if (name.equalsIgnoreCase("sortingtable23")) {
25 return new CellTableSorting23Activity(null, clientFactory);
26 }
27
28 if (name.equalsIgnoreCase("editor")) {
29 return new EditorActivity(null, clientFactory);
30 }
31
32 }
33
34 return new GoodbyeActivity(clientFactory);
35
36 }
37
38 }
39

As you can see from the RightActivityMapper I will have one RootPlace which I will pass from the left side to the right side activity. This root place tells me which activity I have to load on the right side. You can use this also to pass another variable if it’s needed.


Places


The RootPlace looks like this:


1 public class RootPlace extends Place {
2
3 private String rootName;
4
5 public RootPlace(String token) {
6 this.rootName = token;
7 }
8
9 public String getRootName() {
10 return rootName;
11 }
12
13 public static class Tokenizer implements PlaceTokenizer<RootPlace> {
14 @Override
15 public String getToken(RootPlace place) {
16 return place.getRootName();
17 }
18
19 @Override
20 public RootPlace getPlace(String token) {
21 return new RootPlace(token);
22 }
23 }
24 }

Remember that you can make the root place even more complicated and pass more parameters if you wish and it’s needed.


Here the left side place:


1 public class LeftSidePlace extends Place {
2
3 private String rootName;
4
5 public LeftSidePlace(String token) {
6 this.rootName = token;
7 }
8
9 public String getRootName() {
10 return rootName;
11 }
12
13 public static class Tokenizer implements PlaceTokenizer<LeftSidePlace> {
14 @Override
15 public String getToken(LeftSidePlace place) {
16 return place.getRootName();
17 }
18
19 @Override
20 public LeftSidePlace getPlace(String token) {
21 return new LeftSidePlace(token);
22 }
23 }
24 }
25

PlaceHistoryMapper


Do not forget to register your places into the history mapper:


1 @WithTokenizers({ CellTablePlace.Tokenizer.class, HelloPlace.Tokenizer.class,
2 GoodbyePlace.Tokenizer.class, CellTableSortingPlace.Tokenizer.class,
3 RootPlace.Tokenizer.class, RightSidePlace.Tokenizer.class, LeftSidePlace.Tokenizer.class })
4 public interface AppPlaceHistoryMapper extends PlaceHistoryMapper {
5
6 /*
7 * At GWT compile time, GWT generates (see PlaceHistoryMapperGenerator) a
8 * class based on your interface that extends AbstractPlaceHistoryMapper.
9 * PlaceHistoryMapper is the link between your PlaceTokenizers and GWT's
10 * PlaceHistoryHandler that synchronizes the browser URL with each Place.
11 *
12 * For more control of the PlaceHistoryMapper, you can use the @Prefix
13 * annotation on a PlaceTokenizer to change the first part of the URL
14 * associated with the Place. For even more control, you can instead
15 * implement PlaceHistoryMapperWithFactory and provide a TokenizerFactory
16 * that, in turn, provides individual PlaceTokenizers.
17 */
18 }
19

You would definitely need to register the RootPlace and the LeftSidePlace!


Very short about the life cycle. When you put everything together. Basically when you do this first you register into the main view your activity mappers like I did into the my RootView. Every activity mapper loads a activity which load the UI defined in it. Into your RootView place you have to prepare the layout so that you can specify where every activity and the related UI needs to be loaded. Inside the activity mapper you can handles the activity depending the the values in it, define which activity should be loaded, you see this into the RightActivityMapper.


I do understand that this example is more complex so I checked in everything into my Gwt2Go project which you can find here: http://code.google.com/p/gwt2go/


Check out the code there and play around with the working example, I think this is the best way to understand how it works!

7 comments:

  1. Thanks for the article.I am not able to see code at http://code.google.com/p/gwt2go/.

    ReplyDelete
  2. Hi Sandy, I just checked and the source code is there, can you try again?

    ReplyDelete
  3. Just for info, I think better will be if you just check out the project from here: http://code.google.com/p/gwt2go/ using Eclipse and go thru the project and see the examples. The source code for the above article is in several places, so you have to check into the namespaces, activities, mapper, ui and place

    cheers

    ReplyDelete
  4. How to run this project ? (http://code.google.com/p/gwt2go/)

    my eclipse error:

    Starting HTTP on port 8888
    The development shell servlet received a request for 'gwt2go.nocache.js' in module 'gwt2go.gwt.xml'
    [WARN] Resource not found: gwt2go.nocache.js; (could a file be missing from the public path or a tag misconfigured in module gwt2go.gwt.xml ?)
    [ERROR] Unable to find 'Gwt2go/html.gwt.xml' on your classpath; could be a typo, or maybe you forgot to include a classpath entry for source?
    [ERROR] Unable to find 'Gwt2go/html.gwt.xml' on your classpath; could be a typo, or maybe you forgot to include a classpath entry for source?
    The development shell servlet received a request to generate a host page for module 'Gwt2go.html'
    [ERROR] Unable to find 'Gwt2go/html.gwt.xml' on your classpath; could be a typo, or maybe you forgot to include a classpath entry for source?

    ReplyDelete
  5. I am not really sure what the problem could be, did you check for this error in google, looks like something went wrong when you import the project. Maybe I have to prepare a tutorial for this.

    ReplyDelete
  6. I can't find the source code at this location http://code.google.com/p/gwt2go/. Can you please help with that.

    ReplyDelete
  7. I just checked, everything is there, could please share what exactly you was not able to find. You have to look in the /trunk folder, all classes are there.

    ReplyDelete