I am using Eclipse as my IDE, so I am looking forward to also giving the plugin a try. Installation was easy: I pointed Eclipse to
http://dl.google.com/eclipse/plugin/3.4 and the tool did the rest. Took a while to complete though (almost an hour, actually), since my Eclipse had to download quite a few things and I'm too cheap for Internet access beyond basic DSL at home.Anyhoo... so here I am with my shiny new plugin, wondering what to build. Another guestbook? A pet store? Meh
After some soulsearching, I have decided that I rather wanted something a bit more pragmatic -- an app I myself would like to use in real life, but that is not so complex that I cannot get it done in a couple of weekends. One of the few desktop applications I still use (besides web browsers, of course) is KeePassX, a program to store encrypted passwords on your local harddrive. I really love the app, but I cannot easily take it with me (well... I could on a USB key, but I am not only cheap but also tend to loose stuff). Paranoia also forbids me to use one of the many web services out there, since it is not hosted by me and the source is not in my control. Thus, I am going to build a little password-manager application myself.
I figure it is going to take me at least four iterations to get this right. The first one will be building a GWT application with a simple UI that has no server logic behind it (just to learn how layout in GWT works). Step two will be adding a fake servlet backend (not app engine, just in memory). While not exactly App Engine yet, I should have a completely specified client-server API by the end of this process that I can subsequently implement on App Engine (iteration 3). Iteration four will handle deployment, CSS and whatever I may screw up in iterations one and two. I will log my notes of things I run into while I code.
Today, I am going to tackle iteration one. The final result of this iteration can be seen here. First, I create a new eclipse project and call it schluesselmeister (the German translation of Keymaster from the original Ghostbuster movie). I leave all the defaults (use GWT, use App Engine), assuming that those will be fine for me. As package, I choose
com.appenginefan.schluesselmeister. It takes my machine about 30 seconds to create and build the project (maybe I really should update my 4 or 5 year old laptop at some point?), but here I am: two Java packages (com.appenginefan.schluesselmeister.client and com.appenginefan.schluesselmeister.server) plus all the plumbing code for a simple application were created.I start the web application to see if everything is hooked up right. My computer is a little bit in pain, but after 20 seconds or so, everything is up and running. The bootstrapped app asks for a name, and once I enter this, the server pops-up an ajaxy dialog window. So far so good! I stop the dev server (I don't really need to, but I'm freeing up resources on my machine) and get to work. But what to do next?
Whenever I have not really a clue what I am doing, I like to start making simple refactorings to the code -- whether it makes sense or not. I'll just pick anything that I instinctively don't like and try to mess with it. In case of the bootstrap code that the eclipse project created, I choose the fact that it mixes UI creation with the logic of how to connect to the server. If I end up spliting UI code and server logic in two parts, then I would like to make that also clear in my GWT code.
There is a section in the GWT documentation that talks about how to build bigger widgets from composites, so let's give that a try and convert our sample into an independent Widget class. I take all the code from the original sample and move it to a new class, called
OriginalSample (just in case you'd like to check it out in the code). My remaining main class has now basically collapsed to a few lines of setup logic:package com.appenginefan.schluesselmeister.client;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.ui.RootPanel;
/**
* Entry point classes define <code>onModuleLoad()</code>.
*/
public class Schluesselmeister implements EntryPoint {
/**
* This is the entry point method.
*/
public void onModuleLoad() {
RootPanel.get().add(new OriginalSample(
(GreetingServiceAsync) GWT.create(GreetingService.class)));
}
}
That's much more to my taste, because now I can easily try out different things by just copying and modifying the
OriginalSample class (btw: did I mention that you can download all the sample code from this article ?). The following snapshot is a modified version of the UI without any of the server-communication:package com.appenginefan.schluesselmeister.client;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.DialogBox;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.VerticalPanel;
/**
* The UI of the original sample app, just without any listeners
*/
public class LessIsMore extends Composite {
private Button sendButton = new Button();
private TextBox nameField = new TextBox();
private DialogBox dialogBox = new DialogBox();
private VerticalPanel panel = new VerticalPanel();
/**
* Create the popup dialog box
* (not really used, since I took the listeners out)
*/
private void setupDialogBox() {
dialogBox.setText("Remote Procedure Call");
dialogBox.setAnimationEnabled(true);
final Button closeButton = new Button("Close");
// We can set the id of a widget by accessing its Element
closeButton.getElement().setId("closeButton");
final Label textToServerLabel = new Label();
final HTML serverResponseLabel = new HTML();
VerticalPanel dialogVPanel = new VerticalPanel();
dialogVPanel.addStyleName("dialogVPanel");
dialogVPanel.add(new HTML("<b>Sending name to the server:</b>"));
dialogVPanel.add(textToServerLabel);
dialogVPanel.add(new HTML("<br><b>Server replies:</b>"));
dialogVPanel.add(serverResponseLabel);
dialogVPanel.setHorizontalAlignment(VerticalPanel.ALIGN_RIGHT);
dialogVPanel.add(closeButton);
dialogBox.setWidget(dialogVPanel);
}
/**
* Setup the main panel
*/
private void setupMainPanel() {
sendButton.setText("Send");
nameField.setText("GWT User");
panel.add(nameField);
panel.add(sendButton);
// Focus the cursor on the name field when the component loads
nameField.setFocus(true);
nameField.selectAll();
}
public LessIsMore(final GreetingServiceAsync greetingService) {
// Setup sub-components
setupMainPanel();
setupDialogBox();
// All composites must call initWidget() in their constructors.
initWidget(panel);
// Give the overall composite a style name.
setStyleName("example-LessIsMore");
}
}
Next step is to make the design my own. Let's start with the main screen. I kick out any original UI code and start with a simple split screen: the left side will contain a list of categories (like banking, websites, pin-numbers...), while the righthand side has all the keys stored in a particular category. For now, each of these panels is going to be represented by a simple button:
package com.appenginefan.schluesselmeister.client;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HorizontalSplitPanel;
import com.google.gwt.user.client.ui.Widget;
/**
* Layout for the main screen of the key-manager.
*/
public class MainScreen extends Composite {
private Widget setupCategoriesPanel() {
return new Button("Categories");
}
private Widget setupKeysPanel() {
return new Button("Keys");
}
public MainScreen() {
// Setup the list of categories and keys as a split panel
final HorizontalSplitPanel panel = new HorizontalSplitPanel();
panel.setLeftWidget(setupCategoriesPanel());
panel.setRightWidget(setupKeysPanel());
panel.setSplitPosition("20%");
// All composites must call initWidget() in their constructors.
initWidget(panel);
// Give the overall composite a style name.
setStyleName("mainScreen");
}
}
So far so good -- let's take this to the next level. I divide my final UI into three components:
- A simple dialog that lets the user enter a scrambler secret (I intend to encrypt any data before sending it to the server, so that nobody can see my passwords by inspecting the database)
- A form that lets the user create or edit new password entries.
- The main screen, as shown above.
Each of these components sounds a bit more complex than the previous one, so I will start with the scrambler dialog:
package com.appenginefan.schluesselmeister.client;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.DialogBox;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.PasswordTextBox;
import com.google.gwt.user.client.ui.VerticalPanel;
/**
* Layout for the dialog that sets the scrambler.
*/
public class SetScramblerDialog extends DialogBox {
private final PasswordTextBox scrambledText = new PasswordTextBox();
private final Button button = new Button();
private final Label instructions = new Label();
public SetScramblerDialog() {
this.setStyleName("scramblerDialog");
this.setText("Set Scrambler");
this.setAnimationEnabled(true);
this.setModal(true);
button.setText("Apply");
instructions.setText("Enter a secret that scrambles "
+ "all passwords sent to the server");
final VerticalPanel dialogVPanel = new VerticalPanel();
dialogVPanel.add(instructions);
dialogVPanel.add(scrambledText);
dialogVPanel.setHorizontalAlignment(VerticalPanel.ALIGN_RIGHT);
dialogVPanel.add(button);
this.setWidget(dialogVPanel);
button.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
SetScramblerDialog.this.hide();
}
});
}
}
The dialog contains three elements: a password field for the user input, a label with a quick description of what this dialog is good for, and a save-button to apply the changes. I also made the dialog modal, so nobody can make modifications to the password data while the scrambler is not entered.
Here are the things that I noticed when coding this up:
- Using the Widgets is very intuitive. If you have ever written
Swing-based UIs (or anything similar), there is hardly any rampup time at all. Just checking the javadoc is usually enough. - It's good to have Eclipse back! After a while, I found myself not even looking at the Javadoc any more. When I needed a particular method, I could usually guess how it probably would be named and let autocomplete do the rest.
Before I code the second dialog, I have decided to create a quick data object that encapsulates all the data for an entry in the password database. Thanks to modern IDEs, such a class is easy to do and quickly done:
package com.appenginefan.schluesselmeister.client;
/**
* A representation of the data in a single password entry
*/
public class PasswordData {
private String key = "FooMail Account";
private String description = "Password for my FooMail account";
private String url = "http://www.example.com";
private String password = "???";
private String category = "Misc";
// Everything below this point was auto-generated by Eclipse :-)
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
}
The constructor of the dialog accepts a PasswordData variable as parameter, which it will use to prepopulate the fields (and later store input values upon hitting the ok-button, but that's iteration 2). Like before, the IDE makes coding up the dialog a piece of cake:
package com.appenginefan.schluesselmeister.client;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.DialogBox;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.PasswordTextBox;
import com.google.gwt.user.client.ui.TextBox;
/**
* Layout for the dialog used to add or edit a password entry
*/
public class EditPasswordDialog extends DialogBox {
private final Button ok = new Button();
private final Button cancel = new Button();
private final PasswordData data;
private final TextBox key = new TextBox();
private final TextBox description = new TextBox();
private final TextBox url = new TextBox();
private final PasswordTextBox password = new PasswordTextBox();
private final TextBox category = new TextBox();
private final Grid grid = new Grid(6, 2);
private final void addRow(
int rowCount, String text,
TextBox inputField, String defaultValue) {
Label label = new Label(text);
inputField.setText(defaultValue);
grid.setWidget(rowCount, 0, label);
grid.setWidget(rowCount, 1, inputField);
}
public EditPasswordDialog(PasswordData dataToEdit) {
this.setAnimationEnabled(true);
this.setModal(true);
this.data = dataToEdit;
this.setStyleName("editDialog");
ok.setText("Save");
cancel.setText("Cancel");
addRow(0, "Key:", key, data.getKey());
addRow(1, "Category:", category, data.getCategory());
addRow(2, "Description:", description, data.getDescription());
addRow(3, "URL:", url, data.getUrl());
addRow(4, "Password:", password, data.getPassword());
grid.setWidget(5, 0, ok);
grid.setWidget(5, 1, cancel);
this.setWidget(grid);
cancel.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
EditPasswordDialog.this.hide();
}
});
}
}
I am going to spare the dear reader the code for the completed
MainScreen component (remember, you can download all the sample code from this article and take a look yourself). Suffice it to say, that it is the same way of setting things up; just with other interesting components (a Tree and a FlexTable). I also added two buttons that can be used to pop up the two dialogs. The code snippet below shows the ClickHandlers that pop up each dialog. private void initListeners() {
scrambler.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
SetScramblerDialog dialog = new SetScramblerDialog();
dialog.center();
dialog.show();
}
});
add.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
EditPasswordDialog dialog = new EditPasswordDialog(new PasswordData());
dialog.center();
dialog.show();
}
});
}
That's it for today -- easy, wasn't it? By the push of a simple button in Eclipse, I uploaded this version into the cloud, so feel free to take a look. By the way: what you see in your browser is what GWT produces, without any additional style applied in the CSS file. If you have any suggestions on how to make it look pretty (I won't get to that part before iteration 4 or so), don't hesistate to suggest a CSS file by posting to this article. See it as a birthday present -- just like App Engine itself recently, this blog turned one year today :-D
1 comments:
I have enjoyed and learned a lot from your series. I hope you find another new project.
Post a Comment