Dienstag, 29. Juni 2010

GRAILS & SMART GWT

Unfortunately there is not much information on running GWT or even SmartGWT Frontends with Grails, so here we go:

Basically you can follow the steps described in Peter Ledbrooks great blog:

Introducing SmartGWT to Grails

(Don't forget to place the SmartGWT jars under lib/gwt and let you IDE know where to find them).

That will get you up and running with a grails app and a SmartGWT frontend. I will try to show you how to get you data to and from the SmartGWT based on this little domain class and a corresponding service:


/**
* The domain.
*/
class Project {
  String name
  String title
  String description
  Boolean isPublic = Boolean.TRUE

  static constraints = {
    name(unique: true, blank: false)
    title(blank: false)
    description(nullable: true)
  }
}

/**
* A little service.
*/
class ProjectService {
  static transactional = true

  Project findProject(Long id) {
    Project.get(id)
  }

  def Project[] findProjects() {
    Project.list()
  }

  def Project save(Map<String, String&gt; parameters) {
    log.info "save( ${parameters} )"
    def theProject = Project.get(parameters.id)
    if (!theProject) {
      theProject = new Project()
    }
    theProject.properties = parameters;
    theProject.isPublic = 
       "true".equals(parameters.isPublic) ? true : false
    theProject.save()
  }

  def remove(Map<String, String&gt; parameters) {
    log.info "remove( ${parameters} )"
    Project.get(parameters.id)?.delete()
  }
}

Really nothing interesting here - but instead of creating GWT-RPC service interfaces, writing DTOs and calling this little service directly via async callbacks SmartGWT offers the concept of a Datasource. We'll use a RestDatasource to act as a "smart client" of this service, therefore we expose the service in a REST-ful manner. Here's my 5-minute Project controller (which may still be improved):

class ProjectController {
  def projectService

  def list = {
    log.info "ProjectController.list( ${params} )"
    def projects = projectService.findProjects();
    def xml = new MarkupBuilder(response.writer)
    xml.response() {
      status(0)
      data {
        projects.each { theProject -&gt;
          flushProject xml, theProject
        }
      }
    }
  }

  def save = {
    log.info "ProjectController.add( ${params} )"
    def theProject = projectService.save(params)
    def xml = new MarkupBuilder(response.writer)
    xml.response() {
      status(0)
      data {
        flushProject xml, theProject
      }
    }
  }

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

  private def flushProject = { xml, project -&gt;
    xml.record(
        id: project.id,
        name: project.name,
        title: project.title,
        description: project.description,
        isPublic: project.isPublic
    )
  }
}


Again not much surprising stuff here: The controller simply calls the service methods and writes out the returned values as XML using the Groovy MarkupBuilder, so let's finally get to the interesting (SmartGWT-) issues.

After you created a GWT module and a GWT page as well as an entry point (remember the GWT plugin) define a (Singleton-) Datasource for the service as follows:

public class ProjectDs extends RestDataSource {
  private static ProjectDs instance;

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

  private ProjectDs() {
    //set id + general stuff
    setID("projectDs");
    setClientOnly(false);
    //setup fields
    DataSourceIntegerField idField =
        new DataSourceIntegerField("id", "ID");
    idField.setCanEdit(false);
    idField.setPrimaryKey(true);
    DataSourceTextField nameField =
        new DataSourceTextField("name", "Name", 50, true);
    TextItem nameItem = new TextItem();
    nameItem.setWidth("100%");
    nameField.setEditorType(nameItem);
    DataSourceTextField titleField =
        new DataSourceTextField("title", "Title", 50, 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, "/requesta/project/list");
    fetch.setDataProtocol(DSProtocol.POSTPARAMS);
    //2. update
    OperationBinding update =
        new OperationBinding(DSOperationType.UPDATE, "/requesta/project/save");
    update.setDataProtocol(DSProtocol.POSTPARAMS);
    //3. add
    OperationBinding add =
        new OperationBinding(DSOperationType.ADD, "/requesta/project/save");
    add.setDataProtocol(DSProtocol.POSTPARAMS);
    //4. remove
    OperationBinding remove =
        new OperationBinding(DSOperationType.REMOVE, "/requesta/project/remove");
    remove.setDataProtocol(DSProtocol.POSTPARAMS);
    setOperationBindings(fetch, update, add, remove);
  }
}

  • We extend the RestDatasource and create the fields to be shown in our views. (Note the call to idField.setPrimaryKey(true). If you forget to define the PK on the Datasource operations like 'remove' will fail to reflect correctly on the UI). 
  • Furthermore we tell the DS in what kind of UI components the field values should be rendered - that can be omitted or handled elsewhere. 
  • Last but not least we bind the good old CRUD-Operations to our simple Groovy controller/service
All that's missing now is a UI bound to this datasource to show and manipulate our data. This is where SmartGWT becomes really elegant: Simply create a ListGrid and set it's Datasource. The same simplicity applies to components like DetailViewer and DynamicForm so I wrote a little ProjectView that actually implements a complete 'list/show/add/update/remove' - type of UI (including a modal edit screen) in one class and it does not feel like spaghetti code :)

public class ProjectView extends HLayout implements ClickHandler {
  private final ProjectDs projectDs;
  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 ProjectView(ProjectDs projectDs) {
    this.projectDs = projectDs;
    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(projectDs);
    table.setDataPageSize(20);
    table.setAutoFetchData(true);
    table.setAlign(Alignment.CENTER);
    table.setWidth("70%");
    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("Project Details");
    detail.setDataSource(projectDs);
    detail.setShowEmptyMessage(true);
    //add button
    addButton.setTitle("ADD");
    addButton.setTooltip("Create a new Project");
    addButton.addClickHandler(new ClickHandler() {
      public void onClick(ClickEvent clickEvent) {
        form.editNewRecord();
        formWindow.show();
      }
    });
    removeButton.setTitle("REMOVE");
    removeButton.setTooltip("Delete this Project");
    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(projectDs);
    form.setCellPadding(5);
    form.setWidth("100%");
    saveButton.setTitle("SAVE");
    saveButton.setTooltip("Save this Project 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();
  }
}

A few things to notice

  • The way we query our Datasource - we don't, we just let the ListGrid do the dirty work :)
  • The way we construct the form - we don't, it just gets our datasource and creates itself :) (Same applies to the DetailViewer)
  • The way the components interact - again interaction is not "direct" but uses the Datasource to pass around records ( "form.editSelectedData(table);" )
  • The rest is just layouting an eye-candy

Here's the entry point


public class App implements EntryPoint {
  /**
   * This is the entry point method.
   */
  public void onModuleLoad() {
    ProjectDs projectDs = ProjectDs.getInstance();
    ProjectView projectView = new ProjectView(projectDs);
    projectView.draw();
  }
}

And all that's left do do is
  1. grails run-app (to start the service)
  2. grails compile-gwt-modules (may take a while)
  3. grails run-gwt-client

Hope this little post sheds some light on how one could use SmartGWT with Grails - any comments appreciated...


15 Kommentare:

  1. hey, post was really helpful.It helped me a lot as i didn't find any example that dealt with client server interaction in the SmartGWT showcase examples. I just have one question..Are the styles customizable for the SmartGWT widgtes? Can i apply my css styles and change the look and feel of the widgets?

    AntwortenLöschen
  2. Excellent post Josip!

    Regarding the styles, yes, they are highly customizable with simple modifications to skin_styles.css. For more advanced theme customizations you can alter load_skins.js.

    There are several themes provided with SmartGWT - Enterprise (the one used here), Enterprise Blue, and Graphite. You can check them out by using the theme changer drop down at the top right of the showcase demo : http://www.smartclient.com/smartgwt/showcase/#main

    Here's another example of an extremely lightweight theme that uses minimal media and takes advantage of CSS3 as well :

    http://www.smartclient.com/smartgwt/playpen/#featured_tree_grid

    AntwortenLöschen
  3. If you use in host mode there is a problem (JS alert error).
    http://localhost:8080/myapp/app.gsp?gwt.codesvr=127.0.0.1:9997

    Error: Cannot change the configuration property 'headerControls'...

    But if you access http://localhost:8080/myapp/app.gsp. It's works

    Why?

    AntwortenLöschen
  4. The full stack

    00:15:38,530 [ERROR] Unable to load module entry point class org.example.client.App (see associated exception for details)
    java.lang.IllegalStateException: Cannot change configuration property 'headerControls' to [Ljava.lang.Object;@4cd904 after the component has been created. at com.smartgwt.client.widgets.BaseWidget.error(BaseWidget.java:540) at com.smartgwt.client.widgets.BaseWidget.error(BaseWidget.java:528) at com.smartgwt.client.widgets.BaseWidget.setAttribute(BaseWidget.java:764) at com.smartgwt.client.widgets.Window.setHeaderControls(Window.java:1391) at org.example.client.ProjectView.initEditForm(ProjectView.java:150) at org.example.client.ProjectView.initUi(ProjectView.java:45) at org.example.client.ProjectView.(ProjectView.java:34) at org.example.client.App.onModuleLoad(App.java:12) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at com.google.gwt.dev.shell.ModuleSpace.onLoad(ModuleSpace.java:369) at com.google.gwt.dev.shell.OophmSessionHandler.loadModule(OophmSessionHandler.java:185) at com.google.gwt.dev.shell.BrowserChannelServer.processConnection(BrowserChannelServer.java:380) at com.google.gwt.dev.shell.BrowserChannelServer.run(BrowserChannelServer.java:222) at java.lang.Thread.run(Thread.java:619)

    AntwortenLöschen
  5. @Sanjiv: Thanks :)

    @Anonym: Strange error - haven't seen it yet!

    Could it be that you are missing a Grails controller for the "myapp/app.gsp"? I left out the controller in the post as it is empty and does nothing but provide an emtpy 'def index= {}' closure. - Just a guess, but if the page works calling it directly (with the .gsp) you could try using such an empty controller...

    AntwortenLöschen
  6. I'm currenlty working to offer more information about SMartGWT/GRails integration.

    See:
    * www.grails.org/plugin/smartgwt
    * code.google.com/p/grails-gwt-smart/

    I'm really intested to integration of your tutorial. Because, my tuto are little bit limited.

    follow me on twitter:jvmvik

    AntwortenLöschen
  7. Thank you this was a real help.

    AntwortenLöschen
  8. Thanks for this great tutorial. In host mode I get the same error message 'Error: Cannot change the configuration property 'headerControls'...'. Any insight into the cause of this?
    Peter

    AntwortenLöschen
  9. ok, got it: In the function initEditForm() make sure that
    formWindow.setHeaderControls(HeaderControls.HEADER_LABEL, closeControl);
    first thing before any of the other formWindow.set(..) methods are called.

    AntwortenLöschen
  10. Great tutorial. I'm able to added projects but when it tries to retrieves the projects to populate the ListGrid it get a 404 when trying to access /projectName/project/list. If you can shed some light it would be much appreciated.

    Thanks!!!

    AntwortenLöschen
  11. what a great post you made, thank you Josip,I have some felling about Datasource and binding of smartgwt and how to get all these done together with grails now, with your great tutorial, I prevent wasting another 1 week trying to find something from the chaos

    AntwortenLöschen
  12. gr8 post Josip...
    Just wanted to know... at what place would I put the "(Singleton-) Datasource for the service" .. in
    src/java or src/gwt under 'client' package or some where else ..?

    AntwortenLöschen
  13. Hey Josip,
    gr8 tutorial, we'd much appreciate if you can also share the source code of the example.

    AntwortenLöschen
  14. +1 Anonym,

    hi Josip... I believe this is perfect fonxr someone who has the basic idea of how grails and smartGWT work. but a novice like me would really appreciate a packaging structure screen shot or some class placment guidance....

    thanx

    AntwortenLöschen
  15. How to handle relationships between domain classes to save data when using SmartGWT and grails?

    AntwortenLöschen