Monday, October 4, 2010

Grails + SmartGWT = ++good

Here is a rehash of this fine posting on hooking up Grails domain objects to SmartGWT widgets via their (ie SmartGWT's) DataSource object (as opposed to GWT's RPC mechanism).

I'm rehashing it here for my own edification; it took me a while to read between the lines wrt/missing import statements & the implicit directory structure being used in the original posting. As I only toy with these technologies occasionally, I tend to forget the details...

I'm working with:
  • WinXP
  • A zip of Grails 1.3.5
  • A zip of GWT 2.0.4
  • A zip of SmartGWT 2.2
Caveat: I needed to disable AVG's Resident Shield to get Grails' grails run-app command to work in my environment. I'll need to recreate that scenario to tell you the exact (obscure) error I was getting.

Prep you environment by either running these commands from a command shell you're going to use or modify your System Control panel's Advanced/Environment Variables/User variables panel:
  • SET GRAILS_HOME=somewhere\grails-1.3.5
  • SET GWT_HOME=somewhere else\gwt-2.0.4
(I'll have to post a little batchfile I found on the net that switches these settings on the fly; e.g. gr8[.cmd] 1.0.4 will flip your current shell to Grails v1.0.4. I use this for Grails & GWT now)

Create a new Grails project via the usual means i.e.
  • Open a command shell
  • cd to your favourite project directory
  • grails create-app projectName
  • {a myriad of files are created}
  • cd projectName
Create a new directory under projectName\lib i.e.
  • cd .\lib
  • md gwt
Put smartgwt.jar into this new directory i.e.
  • copy path_to_smartgwt_zip_exploded\smartgwt.jar .\gwt\
And copy it into the lib directory too:
  • copy .\gwt\smartgwt.jar .
Install the GWT plugin for Grails i.e.
  • cd ..\
  • grails install-plugin gwt
  • {a myriad of files are created}
Create a domain object type e.g. net.myorg.domain.thing
  • grails create-domain-class net.myorg.domain.thing
  • {a myriad of files are created, incl. projectName\grails-app\domain\net\myorg\domain\Thing.groovy}
Edit the domain class

  • package net.myorg.domain

  • class Thing {

  • String name
  • String title
  • String description
  • Boolean isPublic = Boolean.TRUE

  • static constraints = {
  • name(unique: true, blank: false)
  • title(blank: false)
  • description(nullable: true)
  • }
  • }
Create a controller (aka servlet) for the domain object(s) e.g. net.myorg.controller.thing
  • grails create-controller net.myorg.controller.thing
  • {a myriad of files are created, incl. projectName\grails-app\controller\net\myorg\domain\ThingController.groovy}
Edit the controller


  • package net.myorg.controller

  • import groovy.xml.MarkupBuilder
  • import com.smartgwt.client.data.RestDataSource

  • class ThingController {
  • def thingService

  • def list = {
  • log.info "ThingController.list( ${params} )"
  • def things = thingService.findThings();
  • def xml = new MarkupBuilder(response.writer)
  • xml.response() {
  • status(0)
  • data {
  • things.each { theThing ->
  • flushThing xml, theThing
  • }
  • }
  • }
  • }

  • def save = {
  • log.info "ThingController.add( ${params} )"
  • def theThing = thingService.save(params)
  • def xml = new MarkupBuilder(response.writer)
  • xml.response() {
  • status(0)
  • data {
  • flushThing xml, theThing
  • }
  • }
  • }

  • def remove = {
  • log.info "ThingController.remove( ${params} )"
  • thingService.remove(params)
  • def xml = new MarkupBuilder(response.writer)
  • xml.response() {
  • data {
  • status(0)
  • record {
  • id(params.id)
  • }
  • }
  • }
  • }

  • private def flushThing = { xml, thing ->
  • xml.record(
  • id: thing.id,
  • name: thing.name,
  • title: thing.title,
  • description: thing.description,
  • isPublic: thing.isPublic
  • )
  • }
  • }
Create a service for the controller object e.g.net.myorg.service.thing
  • grails create-service net.myorg.service.thing
  • {a myriad of files are created, incl. projectName\grails-app\services\net\myorg\service\ThingService.groovy}
Edit the service

  • package net.myorg.service


  • import net.myorg.domain.Thing

  • class ThingService {

  • static transactional = true

  • Thing findThing(Long id) {
  • Thing.get(id)
  • }

  • def Thing[] findThings() {
  • Thing.list()
  • }

  • def Thing save(Map parameters) {
  • log.info "save( ${parameters} )"
  • def theThing =

  • Thing
  • .get(parameters.id)
  • if (!theThing) {

  • theThing
  • = new Thing()
  • }
  • theThing.properties = parameters;
  • theThing.isPublic =
  • "true".equals(parameters.isPublic) ? true : false
  • theThing.save()
  • }

  • def remove(Map parameters) {
  • log.info "remove( ${parameters} )"
  • Thing.get(parameters.id)?.delete()
  • }
  • }


Compile the Groovy bits
  • grails compile
Create a GWT module
  • grails create-gwt-module net.myorg.app
  • {2 files are produced: [a] projectName\src\gwt\net\myorg\App.gwt.xml and [b] projectName\src\gwt\net\myorg\client\App.java}
Edit the GWT module file (ie App.gwt.xml)
  • Add the line: <inherits name="com.smartgwt.SmartGwt"/>
  • between: <inherits name="com.google.gwt.user.User"/>
  • and: <entry-point class="net.myorg.client.App"/>
Edit the GWT module's Java source file

  • package net.myorg.client;

  • import com.google.gwt.core.client.EntryPoint;
  • import net.myorg.client.ds.ThingDs;

  • import net.myorg.client.view.ThingView;

  • public class app implements EntryPoint {

  • public void onModuleLoad() {
  • ThingDs thingDs = ThingDs.getInstance();
  • ThingView thingView= new ThingView(thingDs);
  • thingView.draw();
  • }
  • }

Create a Java DataSource file ie net.myorg.client.ds.ThingDs.java
  • touch src\gwt\net\myorg\client\ds\ThingDs.java

  • package net.myorg.client.ds;

  • import com.smartgwt.client.data.RestDataSource;
  • import com.smartgwt.client.data.fields.DataSourceIntegerField;
  • import com.smartgwt.client.data.fields.DataSourceTextField;
  • import com.smartgwt.client.widgets.form.fields.TextItem;
  • import com.smartgwt.client.widgets.form.fields.TextAreaItem;
  • import com.smartgwt.client.data.fields.DataSourceBooleanField;
  • import com.smartgwt.client.data.OperationBinding;
  • import com.smartgwt.client.types.DSOperationType;
  • import com.smartgwt.client.types.DSProtocol;

  • public class ThingDs
  • extends RestDataSource
  • {
  • private static ThingDs instance;

  • public static ThingDs getInstance()
  • {
  • if (instance == null)
  • {
  • instance = new ThingDs();
  • }
  • return instance;
  • }

  • private ThingDs()
  • {
  • //set id + general stuff
  • setID("thingDs");
  • setClientOnly(false);

  • //setup fields
  • DataSourceIntegerField idField = new DataSourceIntegerField("id", "ID");
  • idField.setCanEdit(false);
  • idField.setPrimaryKey(true);

  • DataSourceTextField nameField = new DataSourceTextField("name", "Name", 250, true);
  • TextItem nameItem = new TextItem();
  • nameItem.setWidth("100%");
  • nameField.setEditorType(nameItem);

  • DataSourceTextField titleField = new DataSourceTextField("title", "Title", 250, true);
  • TextItem titleItem = new TextItem();
  • titleItem.setWidth("100%");
  • titleField.setEditorType(titleItem);

  • DataSourceTextField descField = new DataSourceTextField("description", "Description", 500, false);
  • TextAreaItem areaItem = new TextAreaItem();
  • areaItem.setLength(500);
  • areaItem.setWidth("100%");
  • descField.setEditorType(areaItem);

  • DataSourceBooleanField isPublicField = new DataSourceBooleanField("isPublic", "Public", 0, false);

  • setFields(idField, nameField, titleField, descField, isPublicField);

  • //setup operations
  • //1. fetch
  • OperationBinding fetch = new OperationBinding(DSOperationType.FETCH, "/projectName/thing/list");
  • fetch.setDataProtocol(DSProtocol.POSTPARAMS);

  • //2. update
  • OperationBinding update = new OperationBinding(DSOperationType.UPDATE, "/projectName/thing/save");
  • update.setDataProtocol(DSProtocol.POSTPARAMS);

  • //3. add
  • OperationBinding add = new OperationBinding(DSOperationType.ADD, "/projectName/thing/save");
  • add.setDataProtocol(DSProtocol.POSTPARAMS);

  • //4. remove
  • OperationBinding remove = new OperationBinding(DSOperationType.REMOVE, "/projectName/thing/remove");
  • remove.setDataProtocol(DSProtocol.POSTPARAMS);
  • setOperationBindings(fetch, update, add, remove);
  • }
  • }

Create a Java View file ie net.myorg.client.view.ThingView.java
  • touch src\gwt\net\myorg\client\view\ThingView.java


  • package net.myorg.client.view;

  • import com.smartgwt.client.widgets.layout.Layout;
  • import com.smartgwt.client.widgets.layout.HLayout;
  • import com.smartgwt.client.widgets.layout.VLayout;
  • import com.smartgwt.client.widgets.events.ClickHandler;
  • import com.smartgwt.client.widgets.grid.events.RecordClickHandler;
  • import com.smartgwt.client.widgets.grid.events.RecordClickEvent;
  • import com.smartgwt.client.widgets.events.ClickEvent;
  • import com.smartgwt.client.widgets.grid.events.RecordDoubleClickHandler;
  • import com.smartgwt.client.widgets.grid.events.RecordDoubleClickEvent;
  • import com.smartgwt.client.widgets.grid.ListGrid;
  • import com.smartgwt.client.widgets.viewer.DetailViewer;
  • import com.smartgwt.client.widgets.Window;
  • import com.smartgwt.client.widgets.form.DynamicForm;
  • import com.smartgwt.client.widgets.IButton;
  • import com.smartgwt.client.widgets.HeaderControl;
  • import com.smartgwt.client.types.HeaderControls;
  • import com.smartgwt.client.types.Alignment;
  • import net.myorg.client.ds.ThingDs;

  • public class ThingView
  • extends HLayout
  • implements ClickHandler
  • {
  • private final ThingDs thingDs;
  • private final ListGrid table = new ListGrid();
  • private final DetailViewer detail = new DetailViewer();
  • private final Window formWindow = new Window();
  • private final DynamicForm form = new DynamicForm();
  • private final IButton addButton = new IButton();
  • private final IButton saveButton = new IButton();
  • private final IButton removeButton = new IButton();
  • private final IButton cancelButton = new IButton();

  • public ThingView(ThingDs thingDs)
  • {
  • this.thingDs = thingDs;
  • initUi();
  • }

  • private void initUi()
  • {
  • //init myself
  • initMySelf();
  • //init listgrid
  • initGrid();
  • //a detail viewer
  • Layout detailsComp = initDetailViewer();
  • //form for editing
  • initEditForm();
  • //button layout
  • addMember(table);
  • addMember(detailsComp);
  • }

  • private void initMySelf()
  • {
  • setWidth100();
  • setHeight100();
  • setMembersMargin(5);
  • setMargin(5);
  • setPadding(10);
  • }

  • private void initGrid()
  • {
  • table.setDataSource(thingDs);
  • table.setDataPageSize(20);
  • table.setAutoFetchData(true);
  • table.setAlign(Alignment.CENTER);
  • table.setWidth("70%");
  • table.setShowAllColumns(false);
  • table.setWrapCells(true);
  • table.setEmptyCellValue("---");
  • table.addRecordClickHandler(new RecordClickHandler()
  • {
  • public void onRecordClick(RecordClickEvent recordClickEvent)
  • {
  • detail.viewSelectedData(table);
  • }
  • });
  • table.addRecordDoubleClickHandler(new RecordDoubleClickHandler()
  • {
  • public void onRecordDoubleClick(RecordDoubleClickEvent recordDoubleClickEvent)
  • {
  • form.editSelectedData(table);
  • formWindow.show();
  • }
  • });
  • }

  • private Layout initDetailViewer()
  • {
  • detail.setGroupTitle("Thing Details");
  • detail.setDataSource(thingDs);
  • detail.setShowEmptyMessage(true);
  • //add button
  • addButton.setTitle("ADD");
  • addButton.setTooltip("Create a new Thing");
  • addButton.addClickHandler(new ClickHandler()
  • {
  • public void onClick(ClickEvent clickEvent)
  • {
  • form.editNewRecord();
  • formWindow.show();
  • }
  • });
  • removeButton.setTitle("REMOVE");
  • removeButton.setTooltip("Delete this Thing");
  • removeButton.addClickHandler(new ClickHandler()
  • {
  • public void onClick(ClickEvent clickEvent)
  • {
  • table.removeSelectedData();
  • table.fetchData();
  • }
  • });
  • HLayout buttonPane = new HLayout();
  • buttonPane.setAlign(Alignment.CENTER);
  • buttonPane.addMember(addButton);
  • buttonPane.addMember(removeButton);
  • VLayout layout = new VLayout(10);
  • layout.addMember(detail);
  • layout.addMember(buttonPane);
  • return layout;
  • }

  • private void initEditForm() {
  • //the form
  • form.setIsGroup(false);
  • form.setDataSource(thingDs);
  • form.setCellPadding(5);
  • form.setWidth("100%");
  • saveButton.setTitle("SAVE");
  • saveButton.setTooltip("Save this Thing instance");
  • saveButton.addClickHandler(new ClickHandler()
  • {
  • public void onClick(ClickEvent clickEvent)
  • {
  • form.saveData();
  • formWindow.hide();
  • }
  • });
  • cancelButton.setTitle("CANCEL");
  • cancelButton.setTooltip("Cancel");
  • cancelButton.addClickHandler(this);
  • HLayout buttons = new HLayout(10);
  • buttons.setAlign(Alignment.CENTER);
  • buttons.addMember(cancelButton);
  • buttons.addMember(saveButton);
  • VLayout dialog = new VLayout(10);
  • dialog.setPadding(10);
  • dialog.addMember(form);
  • dialog.addMember(buttons);
  • //form dialog
  • formWindow.setShowShadow(true);
  • formWindow.setShowTitle(false);
  • formWindow.setIsModal(true);
  • formWindow.setPadding(20);
  • formWindow.setWidth(500);
  • formWindow.setHeight(350);
  • formWindow.setShowMinimizeButton(false);
  • formWindow.setShowCloseButton(true);
  • formWindow.setShowModalMask(true);
  • formWindow.centerInPage();
  • HeaderControl closeControl = new HeaderControl(HeaderControl.CLOSE, this);
  • formWindow.setHeaderControls(HeaderControls.HEADER_LABEL, closeControl);
  • formWindow.addItem(dialog);
  • }

  • public void onClick(ClickEvent clickEvent)
  • {
  • formWindow.hide();
  • }
  • }
Create a (GSP) page to host the module
  • grails create-gwt-page smart.gsp net.myorg.app
Edit the hosting page
  • Add the line: <script>var isomorphicDir = "gwt/net.myorg.App/sc/"(sorry, closing script tag is not showing on blog)
  • just after the TITLE HTML tag
Move the hosting page
  • move web-app\smart.gsp grails-app\views\
Edit the main layout page
  • remove the reference to main.css
Compile the module
  • grails compile-gwt-modules
Run the server
  • grails run-app
View the page
  • Browse to http://localhost:8080/projectName/smart.gsp
  • Start editing Things