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...


Donnerstag, 11. Februar 2010

GRAILS IDE

Vor 2 Monaten hab ich mal die SpringSource Tool Suite ausprobiert und war ziemlich enttäuscht: Auf meinem 4Gig-DualCore mit Ubuntu 9.04 lief die Suite nicht akzeptabel: 2 Minuten Bootzeit, ein komplette Kaffeepause Initialisierung und im laufenden Betrieb eine kleine Ewigkeit und ein File zu öffnen - so kann man nicht arbeiten. Aber vor 3 Wochen hab ich dem Release 2.3.0 ne weitere Chance gegeben und war positiv überrascht:

Läuft butterweich!

Installationsanweisungen befolgen, die Groovy/Grails Extensions laden und Spaß haben:
  • Vernünftige (wenn auch nicht perfekte) Groovy Codecompletion
  • Grails Konsole aus Eclipse/STS heraus (CTR + ALT + SHIFT + G !)
  • Multiple Grails Versionen: verschiedene Apps bearbeiten die unterschiedliche Grailsversionen nutzen (sehr praktisch)
Ich bevorzuge den Eclipse XML-Editor zum bearbeiten meiner GSPs. Dazu muss man lediglich *.gsp als XML-Content Type eintragen (Window -> Preferences -> Content-Types: Text -> XML) und unter File Associations für *.gsp den XML-Editor als Default eintragen und schwupps macht auch FE-Entwicklung direkt Spaß

Laut SpringSource JIRA soll es für die Version 2.3.1 auch einen expliziten GSP-Editor geben, wird es sicherlich wert sein dem dan auch mal ne Chance zu geben...

Mittwoch, 14. Oktober 2009

Anmerkungen eines Sprinters (SCRUM)

SCRUM ist ne feine Sache.
Agile SW-Entwicklungs-Paradigmen sind klassischen Vorgehensweisen überlegen und ich bin überzeugter Anhänger von agiler Methodik. Das heisst an sich freu ich mich ja über den Scrum-Hype, aber an dieser Stelle möchte ich mal ein paar Anmerkungen aus meinem perönlichen Scrum-Erfahrungsschatz verewigen:
  • Scrum löst keine Probleme - Menschen/Entwickler lösen Probleme
  • Insbesondere ist Scrum kein Ersatz für kaputte/schlechte/dumme Prozesse im Unternehmen
  • Wenn nicht vernünftig und vollständig kommuniziert wird hilf Scrum auch nicht weiter
  • Wenn Ziele/Requirements nicht klar definiert wird auch Scrum scheitern
  • Schlechte Entwickler bleiben auch in Scrum schlechte Entwickler
  • ScrumMaster ist kein Vollzeit-Job
  • ScrumMaster - Zertifikate sind weniger wert als gesunder Menschenverstand und vernünftiger Umgang im Team miteinander
  • Sklavisches befolgen von Scrum-Regeln aus einem Buch oder einer Scrum-Schulung ist oft kontraproduktiv (siehe Menschenverstand)
  • Scrum ohne Retrospektive und konstanter Verbesserung des Arbeitsablaufs ist kein Scrum, sondern nur ein Feigenblatt für das Management