In this final installment of the series, you'll add an interactive form for adding new albums for an artist. This will use GWT Ajax, JSNI, and XForms controls. You'll look at how GWT can augment XForms. To demonstrate this, you'll look at how you can use GWT to provide localized content for our XForms controls. Finally, you'll take a look at a not-so-well-known feature of GWT, Java™-style sorting, that provides yet another example of how GWT can make a Java developer's job a little easier.
This article uses GWT version 1.4 and the Mozilla XForms plugin 0.8 (see the Resources for download links). The Mozilla XForms plugin works with any Mozilla-based Web browser, such as Firefox and Seamonkey. GWT requires knowledge of Java technology, and Web technologies such as HTML and CSS. This article makes heavy use of JavaScript as well. XForms makes heavy use of the Model-View-Control paradigm, so familiarity with that is helpful. Prior exposure to XForms and GWT is helpful of course, but is not necessary.
You want to create a simple data entry form. It will have two fields, the title of the album and the year it was recorded. You'll have a button that will trigger your remote call. XForms makes it easy to create a UI like using its controls, as shown in Listing 1.
Listing 1. Album entry form using XForms controls
<xhtml:div id="albumForm">
<xforms:input id="title">
<xforms:label>Title:</xforms:label>
</xforms:input>
<xforms:input id="year">
<xforms:label>Year:</xforms:label>
</xforms:input>
<xforms:trigger id="btn">
<xforms:label>Add Album</xforms:label>
<xforms:load resource="javascript:addAlbum()"
ev:event="DOMActivate"/>
</xforms:trigger>
</xhtml:div>
|
It should be no surprise that XForms makes it very easy to create a form. The only thing tricky going on here is that your trigger is calling JavaScript. Normally an XForms trigger calls a submission action defined on the XForms model. You are going to use GWT to make an Ajax request to a remote service, so your trigger is calling JavaScript instead. Let's take a look at the JavaScript for calling the remote service.
Calling a remote service from an XForm
In Part 3 you
defined a service (AlbumService) that included a method for
adding a new album. That's the service you are going to call. Looking at the signature
of that interface, you'll need to create a new Album object
to send to the server. See the class in Listing 2.
Listing 2.
Album class
package org.developerworks.rockstar.client;
import com.google.gwt.user.client.rpc.IsSerializable;
public class Album implements IsSerializable{
private int id;
private int artistId;
private String title;
private int year;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getArtistId() {
return artistId;
}
public void setArtistId(int artistId) {
this.artistId = artistId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getYear() {
return year;
}
public void setYear(int year) {
this.year = year;
}
}
|
One of the things you should notice in this class is that it has the ID of the artist. So when you create a new album, you'll need the ID of the artist. This is passed in as a request parameter to the page. You'll need some way to store this information and make it available to the your script that is going to invoke your remote service. There are a number of ways to do this, but one nice technique you can use with XForms is to store it as part of a model, as shown in Listing 3.
Listing 3. Storing
request parameter in XForms model
<xforms:model id="request">
<xforms:instance id="artist" xmlns="">
<Data>
<ArtistId><%=request.getParameter("artistId")
%></ArtistId>
</Data>
</xforms:instance>
</xforms:model>
|
You could imagine putting other various request parameters as part of the model instance data. Now let's take a look at the addAlbum() you referenced in the XForms trigger. Since you are using GWT, this will just be another method in the AlbumLib, as shown in Listing 4.
Listing 4. The
addAlbum() method
public void addAlbum(){
String title = this.getAlbumTitle();
int year = Integer.parseInt(this.getAlbumYear());
int artistId = Integer.parseInt(this.getArtistId());
Album album = new Album();
album.setArtistId(artistId);
album.setTitle(title);
album.setYear(year);
AlbumServiceAsync albumService = this.getAlbumService();
AsyncCallback callback = new AsyncCallback(){
public void onFailure(Throwable caught) {
removeAlbum();
refreshXformsModel();
}
public void onSuccess(Object result) {
// added optimistically, so nothing to do here
}
};
albumService.addAlbum(album, callback);
this.addAlbumToModel(album);
this.refreshXformsModel();
}
|
The method is fairly straightforward. You create a new album, get a reference to the album service, and then invoke that service's addAlbum() method. There are several other methods are using here, so let's take a look at those too in Listing 5.
Listing 5. Helper methods for
addAlbum()
private native String getAlbumTitle()/*-{
return $doc.getElementById("title").value;
}-*/;
private native String getAlbumYear()/*-{
return $doc.getElementById("year").value;
}-*/;
private native String getArtistId()/*-{
var model = $doc.getElementById("request");
var instance = model.getInstanceDocument("artist");
var dataElement = instance.getElementsByTagName("Data")[0];
return dataElement.getElementsByTagName("ArtistId")[0].firstChild.getTextValue();
}-*/;
private native void removeAlbum()/*-{
var model = $doc.getElementById("albums");
var instance = model.getInstanceDocument("albumData");
var dataElement = instance.getElementsByTagName("Data")[0];
dataElement.removeChild(dataElement.lastChild);
}-*/;
|
All of these are native JavaScript methods created using GWT's JSNI facility. The getAlbumTitle() and getAlbumYear() are
both just using JavaScript DOM methods to get the values from the form elements. The
getArtistId() method uses the XForms JavaScript APIs to
access the artist ID stored in the model shown in Listing 3.
Finally, the removeAlbum() method also uses the XForms
JavaScript APIs to remove the last album from the XForms model. Looking back at Listing 4, you see that removeAlbum() is called when your remote service invocation fails. That's because the addAlbum() method in Listing 4 optimistically adds the new album to the model and refreshes the UI controls that are bound to the model.
Note: The other two methods called by addAlbum(), the
addAlbumToModel() and refreshXformsModel() methods, can be seen in the previous article, as well as with the source code for this article.
Creating the XForms controls with GWT JSNI
In the previous
article, you saw that you can use GWT and JSNI to create XForms controls. You've
defined some new controls for your data entry form, but you can once again make use of
GWT and JSNI to create these new controls. You'll define a new method for this called
createEntryForm(), which is shown in Listing 6.
Listing 6. The
createEntryForm() method
private native void createEntryForm()/*-{
var xfNs = "http://www.w3.org/2002/xforms";
// get the container div
var container = $doc.getElementById("albumForm");
var titleIn = $doc.createElementNS(xfNs, "xforms:input");
titleIn.setAttribute("id","title");
var titleInLabel = $doc.createElementNS(xfNs, "xforms:label");
titleInLabel.appendChild($doc.createTextNode("Year:"));
titleIn.appendChild(titleInLabel);
container.appendChild(titleIn);
var yearIn = $doc.createElementNS(xfNs, "xforms:input");
yearIn.setAttribute("id","year");
var yearInLabel = $doc.createElementNS(xfNs, "xforms:label");
yearInLabel.appendChild($doc.createTextNode("Year:"));
yearIn.appendChild(yearInLabel);
container.appendChild(yearIn);
var trigger = $doc.createElementNS(xfNs, "xforms:trigger");
trigger.setAttribute("id", "btn");
var btnLbl = $doc.createElementNS(xfNs, "xforms:label");
btnLbl.appendChild($doc.createTextNode("Add Album"));
trigger.appendChild(btnLbl);
var loader = $doc.createElementNS(xfNs, "xforms:load");
loader.setAttribute("resource", "javascript:addAlbum()");
loader.setAttributeNS("http://www.w3.org/2001/xml-events", "ev:event", "DOMActivate");
trigger.appendChild(loader);
container.appendChild(trigger);
}-*/;
|
This method is very straightforward. You simply create the various XForms controls
using normal JavaScript DOM APIs, and add this to your DOM tree. In this case, you create the two inputs and the trigger. You have to make sure to use the namespace-aware versions of things like createElement and setAttribute. This is because XForms uses its own namespaces to augment the XHTML schema. Now all you need to do is add a reference to this method in your startup script, and you're ready to start testing the application.
For testing the application, you'll need to once again use Web mode. You can launch the application in hosted mode, and then launch Web mode, as shown in Figure 1.
Figure 1. Launching Web mode
This will bring up the application in Web Mode. You can click on one of the artists now to bring up the albums page, as shown in Figure 2.
Figure 2. The albums page in Web mode
Add the new title and year and click the Add Album button. The interface should update, as shown in Figure 3.
Figure 3. New album added
Now you are using GWT and XForms together to create the UI and create dynamic interactions with back-end services. Let's take a look at some of the other benefits of using these two technologies together, now that you've seen how to combine them.
Using GWT localization with XForms
GWT is designed to be a multi-purpose framework for building Web applications. One of the common problems with Web applications is localization. Google has a presence in many countries, so localization is a problem they should be very familiar with. Therefore, it should come as no surprise that GWT has some clever features designed for localization. XForms is a much more focused technology, so something like localization is not part of its scope. Thus GWT's localization can be very useful for an XForms application.
Localization with GWT: The Constants interface
When it comes to localization, many Java technology developers only use one thing: resource bundles. Those are useful, but limited. GWT uses an implied interface pattern. There is a Java interface corresponding to a properties file. This creates a compile-time contract between localized content and Java code, including GWT code. Let's take a look at using this technique with your application.
Your albums page has five main pieces of content: the title label, the year label, the title input label, the year input label, and the label on the Add button. This defines a Java interface, as shown in Listing 7.
Listing 7. Java interface for albums content
package org.developerworks.rockstar.client;
import com.google.gwt.i18n.client.Constants;
public interface AlbumContent extends Constants{
public String titleLabel();
public String yearLabel();
public String titleInputLabel();
public String yearInputLabel();
public String addButtonLabel();
}
|
Now you can create a properties file corresponding to this interface. Your interface is named AlbumContent, so the file must be called AlbumContent.properties and must be in the same package as the AlbumContent interface. The file is shown in Listing 8.
Listing 8.
AlbumContent.properties
titleLabel=Title:
yearLabel=Year:
titleInputLabel=Title:
yearInputLabel=Year:
addButtonLabel=Add Album
|
The names of the properties in this file must correspond to the methods defined in your interface. Listing 8 shows one version of the properties file. You can then have this translated into as many languages as needed and add them to the same location. At runtime, GWT will determine the locale and you can get a localized implementation of the interface. This can be called from client-side Java methods, and thus also from native JavaScript methods defined using JSNI. Let's take a look at this technique in Listing 9.
Listing 9. Accessing localized content
private AlbumContent getContent(){
return (AlbumContent) GWT.create(AlbumContent.class);
}
private native void createEntryForm()/*-{
// get the localized content
var content = this.@org.developerworks.rockstar.client.AlbumLib::getContent();
// ...
var titleInLabel = $doc.createElementNS(xfNs, "xforms:label");
var titleLabelTxt = content.titleInputLabel();
titleInLabel.appendChild($doc.createTextNode(titleInputTxt));
// ...
var yearInLabel = $doc.createElementNS(xfNs, "xforms:label");
var yearLabelTxt = content.yearInputLabel();
yearInLabel.appendChild($doc.createTextNode(yearLabelTxt));
// ...
var btnLbl = $doc.createElementNS(xfNs, "xforms:label");
var btnLblTxt = content.addButtonLabel();
btnLbl.appendChild($doc.createTextNode("Add Album"));
}-*/;
|
Listing 9 shows a convenience method for accessing localized
content. It then shows a new version of the createEntryForm
you created earlier. This time you're grabbing the localized strings from your localized implementation of the interface. Only the code dealing with the localized content is shown, with the non-relevant code omitted in the listing. The full source code is available for download. This is a simple example of GWT's localization. It can handle more sophisticated localized content, such as content with placeholders.
The rock star contest: More hidden GWT gems
You've spent a lot of time on the albums' page of your application, combining the capabilities of GWT and XForms. Now let's go back to the artist page and add some more functionality to it. Let's allow your record executives to vote for one of the artists. You'll add a votes field to our XML and to your Artist class. You'll also add a vote feature to your ArtistService. Here's what the implementation on the server will look like, as shown in Listing 10.
Listing 10. Implementation of
voteForArtist
public void voteForArtist(int artistId) {
Artist artist = null;
for (Artist a : this.artists){
if (a.getId() == artistId){
artist = a;
break;
}
}
if (artist != null){
int votes = artist.getVotes() + 1;
artist.setVotes(votes);
}
dao.saveArtists(this.artists);
}
|
You simply find the artist, increment its votes, and save the list. This is definitely not the most efficient code, but good enough for your purpose. Next you'll modify your code for your table of artists, as shown in Listing 11.
Listing 11. Adding voter buttons to the artist table
private void populateTable(){
// clear the table
int rowCount = this.artistTable.getRowCount();
for (int i=0;i<rowCount;i++){
this.artistTable.removeRow(i);
}
// create the header
this.artistTable.getRowFormatter().addStyleName(0, "tableHeader");
this.artistTable.setText(0, 0, "Name");
this.artistTable.setText(0, 1, "Genre");
this.artistTable.setText(0,2, "Vote Now!");
// now add artists
for (int i=0;i<artistList.size();i++){
//this.artistTable.setText(i+1, 0, artists[i].getName());
final Artist artist = (Artist) artistList.get(i);
String html = "<a
href=\"Albums.jsp?artistId="+artist.getId()+"\">"+artist.getName()+"</a>";
this.artistTable.setHTML(i+1, 0, html);
this.artistTable.setText(i+1, 1, artist.getGenre());
final Button btn = new Button("Vote for:" + artist.getName());
ClickListener cl = new ClickListener(){
public void onClick(Widget sender) {
voteForArtist(artist);
}
};
btn.addClickListener(cl);
this.artistTable.setWidget(i+1, 2, btn);
}
this.artistTable.setBorderWidth(4);
}
|
Notice that you added a Vote button next to each artist in your table. You're calling a voteForArtist() method, which is shown in Listing 12.
Listing 12. The
voteForArtist() method
private void voteForArtist(final Artist artist){
ArtistServiceAsync artistService = this.getArtistService();
AsyncCallback callback = new AsyncCallback(){
public void onFailure(Throwable caught) {
artist.setVotes(artist.getVotes() - 1);
}
public void onSuccess(Object result) {
// Nothing to do here
}
};
artistService.voteForArtist(artist.getId(), callback);
artist.setVotes(artist.getVotes() + 1);
}
|
Now you just need a way to show the results of the voting. You'll create a grid for this. You'll use a grid instead of a FlexTable since you know how many artists there are when you create the grid. You'll create a button for showing or hiding the leaderboard, as shown in Listing 13.
Listing 13. Button for showing or hiding the leaderboard
final Panel lbPanel = new VerticalPanel();
final Button leaderButton = new Button("Show Leaderboard");
public void onModuleLoad() {
// add the outer panel, then add to it
RootPanel.get().add(outerPanel);
outerPanel.add(artistTable);
outerPanel.add(formPanel);
outerPanel.add(leaderButton);
outerPanel.add(lbPanel);
// ... More code omitted for brevity
ClickListener lbListener = new ClickListener(){
public void onClick(Widget sender) {
if (leaderButton.getText().equals("Show Leaderboard")){
showLeaderBoard();
leaderButton.setText("Hide Leaderboard");
} else {
lbPanel.clear();
leaderButton.setText("Show Leaderboard");
}
}
};
leaderButton.addClickListener(lbListener);
// ...
}
|
You'll go back and forth between showing and hiding the leaderboard, and changing the label on the button to match the state. Now you just need to draw the leaderboard, as shown in Listing 14.
Listing 14. Show leaderboard
private void showLeaderBoard(){
int size = this.artistTable.getRowCount() ;
Grid leaderBoard = new Grid(size,2);
leaderBoard.getRowFormatter().addStyleName(0, "tableHeader");
leaderBoard.setText(0, 0, "Aritst");
leaderBoard.setText(0, 1, "Votes");
// sort the artists
List sorted = sortArtists();
for (int i=0;i<size-1;i++){
Artist artist = (Artist) sorted.get(i);
leaderBoard.setText(i+1, 0, artist.getName());
leaderBoard.setText(i+1,1, ""+artist.getVotes());
}
lbPanel.add(leaderBoard);
}
|
Everything has been straighforward and the GWT has been "vanilla" so far, and you
might be wondering why you're bothering to show this here. Here's where things get interesting. Notice you have a mysterious call to a sortArtists() method. You see, your list of artists is not inherently sorted, and of course you want it sorted for the leaderboard. So should you go back to the server to sort the list? Obviously it would be better to do it on the client. That means writing a sort in JavaScript, right? Let's take a look at how to do this GWT style, as shown in Listing 15.
Listing 15. Sorting the artists
private List sortArtists(){
List sorted = new ArrayList(artistList);
Collections.sort(sorted, new ArtistComparator());
return sorted;
}
static class ArtistComparator implements Comparator{
public int compare(Object arg0, Object arg1) {
Artist a0 = (Artist) arg0;
Artist a1 = (Artist) arg1;
return a1.getVotes() - a0.getVotes();
}
}
|
You've sorted the list the way we do it in Java. You created a Comparator and used the
JDK's Collections.sort() method. The JDK documentation indicates that this method gives you a modified mergesort with guaranteed N*log(N) performance. Would you want to write a mergesort in JavaScript? Well, you don't have to with GWT. Now let's test your application, as shown in Figure 4.
Figure 4. Hidden leaderboard
Just click on the Show Leaderboard button to bring up the leaderboard, as shown in Figure 5.
Figure 5. Show leaderboard
Looks like Dojo Darling could use some more votes. Click on its button, then Hide/Show the leaderboard to refresh it, as shown in Figure 6.
Figure 6. Updated leaderboard
In this final installment in the GWT and XForms series, you saw how to add interactive forms to your application. These forms can use GWT to create XForms controls that can then invoke GWT Ajax services. The response from these services can be handled through GWT, and can in turn use JSNI to alter your XForms model data and update your XForms UI controls. This tight integration between GWT and XForms allows XForms to take advantage of key features of GWT. You also saw that you can localize your XForms using GWT's localization facilities. Finally, you saw one of the lesser-known, but powerful, features of GWT: Java-style sorting. Features like this, as well features like localization and GWT's new image bundling, make it so beneficial to use GWT not only for new projects, but also to enhance other projects and technologies.
| Description | Name | Size | Download method |
|---|---|---|---|
| Part 4 sample code | rockstar4_src.zip | 16KB | HTTP |
Information about download methods
Learn
-
For a great introduction to XForms read the three-part series Introduction to XForms (Chris Herbroth, developerWorks, September 2006).
-
Learn more about using JavaScript and XForms together in XForms tip: Call JavaScript from an XForms form (Nicholas Chase, developerWorks, January 2007).
-
See how JavaScript can improve the functionality of XForms in the article Use JavaScript to make your XForms more robust (Michael Galpin, developerWorks, July 2007).
-
Learn how XForms can work with Ajax to create auto-suggest functionality in the article Use XForms and Ajax to create an autosuggest form field (Michael Galpin, developerWorks, July 2007).
-
Read one of the first in-depth tutorials on GWT in the article Ajax for Java
developers: Exploring the Google Web Toolkit (Philip McCarthy, developerWorks, June 2006).
-
Learn more about building Web applications with GWT in the four-part developerWorks tutorial series Build an Ajax application using Google Web Toolkit, Apache Derby, and Eclipse (Noel Rappin, developerWorks, December 2006).
-
Get familiar with GWT's Ajax capabilities in the two-part tutorial series Build an Ajax-enabled application using the Google Web Toolkit and Apache Geronimo (Michael Galpin, developerWorks, May 2007).
-
Check out IBM developerWorks' Ajax Resource Center
-
Visit the XForms home at W3C.
-
developerWorks technical events and bcasts: Stay current with developerWorks technical events and bcasts.
-
Podcasts: Tune in and catch up with IBM technical experts.
Get products and technologies
-
The best place for GWT information is straight from the source, at the official Google Web Toolkit site.
-
Get the XForms extension for Mozilla, Firefox, or Seamonkey.
Discuss
Comments (Undergoing maintenance)





