mardi 25 août 2009

Build a Flex CRUD application in 15 minutes with the Grails GraniteDS plugin

A few month ago, I've shown how to build a simple CRUD application with Flex, Grails and the GraniteDS plugin. That was not overly complex but there was still room for improvement in the quantity of code that was needed to make things work.
Grails itself provides a feature called scaffolding that allows to build extremely easily a simple working application skeleton from constraints defined on the domain classes.

Doing the same with Flex is a bit more complex because there are two runtimes involved: the Flash client and the Java server. There are thus three main possible options:

  • Generation and compilation of static Flex code on the server. Its advantages are static typing and full customizability. Its drawbacks are the quantity of code to maintain (even if it's generated, it's still here) and a relative difficulty to do incremental development (once the generated app has been customized, it's harder to update it when the source model changes). This is the option taken by the GFS plugin for example.

  • Dynamic generation of domain metadata on the server used by a Flex framework to build the UI dynamically. This has the big advantage of being completely driven by the server, so changes on the domain classes are immediately visible on the UI without requiring a recompilation of the client, and moreover there is relatively little client code. The corresponding drawback is that the generated application is not typed (client side data would be represented as XML for example) and is more difficult to maintain and customize.

  • Generation of a static Flex domain model and dynamic generation of the Flex UI. This is the approach taken by the GraniteDS plugin, its advantages are a limited amount of client code and a strongly typed client application. The drawbacks are that it's less dynamic than option 2 (changes on the Grails domain class require a recompilation of the Flex application), and less easily customizable than option 1.


  • After this short introduction, let's see how this works with the latest release of gdsflex (0.7.1).

    First we create a new Grails application, once again a book manager (maybe next time I'll find some more original example). Assuming you have grails 1.1.1 installed, type from the command line :

    grails create-app gdsexample2

    cd gdsexample2

    grails install-plugin gdsflex


    As before, the main part is to define the domain model. Let's start this time by a simple Author class.

    grails create-domain-class com.myapp.Author

    Author.groovy

    package com.myapp

    class Author {

    static constraints = {
    name(size:0..50, blank:false)
    birthDate(format:'DD/MM/YYYY')
    picture(nullable:true, widget:'image')
    }

    String uid

    String name

    Date birthDate

    java.sql.Blob picture
    }

    This is a relatively simple domain class, you can notice the constraints that drive Grails scaffolding. So let's add a simple controller :

    grails create-controller com.myapp.Author

    AuthorController.groovy

    package com.myapp

    @org.granite.tide.annotations.TideEnabled
    class AuthorController {

    def index = { redirect(action:list, params:params) }

    def scaffold = Author
    }

    Then run :

    grails generate-flex-app

    grails mxmlc

    grails run-app

    And go to http://localhost:8080/gdsexample2/gdsexample2.swf.

    You should have a basic working Flex application that allows to create and edit authors. So what happened here :

  • The gas3 generator has generated ActionScript 3 classes for each domain class (as that was the case before), this time incuding the existing Grails constraints.

  • A new scaffolding template for controllers has been installed in src/templates/scaffolding that includes the necessary actions for the Flex application.

  • The Flex UI builder classes (org.granite.tide.uibuilder) have been installed in grails-app/views/flex.

  • A main mxml has been generated in grails-app/views/flex/gdsexample2.mxml that includes a menu to access all domain classes.


  • This is a simple example, but we can see how the constraints have been used : the Grails constraints in Author.groovy have been translated to the following block in Author.as :

    public static const meta_constraints:Array = [
    { property: "name",
    blank: "false", size: "0..50"
    },
    { property: "birthDate",
    format: "DD/MM/YYYY"
    },
    { property: "picture",
    widget: "image"
    }
    ]

    Even if you don't want to use the GraniteDS UI builder framework, you could still use these as3 constraints from any custom framework.

    You can check that the blank and size constraints have been mapped to Flex client-side validators, and that the date format has been used in the author table. The 'image' widget is simply a component that appears in edit mode and allows to upload an image from Flex.

    Now that we have seen a basic domain class, let's see how it works with associations.

    grails create-domain-class com.myapp.Book

    grails create-domain-class com.myapp.Chapter

    grails create-controller com.myapp.Book

    Book.groovy

    package com.myapp

    class Book {

    static constraints = {
    title(size:0..100, blank:false)
    category(inList:["Fiction", "Non-fiction", "Biography"])
    author()
    description(size:0..2000, widget:"textArea")
    chapters()
    }

    String uid

    String title

    String category

    Author author

    Set chapters

    String description

    static hasMany = [ chapters:Chapter ]
    static mapping = {
    author fetch:"join"
    chapters cascade:"all-delete-orphan"
    }
    }

    Chapter.groovy

    package com.myapp

    class Chapter {

    static constraints = {
    title(size:0..50, blank:false)
    }

    String uid

    Book book

    String title

    String summary

    static mapping = {
    book fetch:"join"
    }
    }

    BookController.groovy

    package com.myapp

    @org.granite.tide.annotations.TideEnabled
    class BookController {

    def index = { redirect(action:list, params:params) }

    def scaffold = Book
    }

    An important thing is that ManyToOne associations must be marked fetch:"join" or lazy:false. GraniteDS does not support transparent lazy loading for single ended associations, and most likely never will (it would generate unacceptable network and database traffic, one http request and one SQL query for each uninitialized entity). Lazy collections are supported, so OneToMany and ManyToMany associations do not have to be marked lazy:false.

    Let's regenerate the Flex application to add the Book menu to the main mxml.

    grails generate-flex-app

    grails mxmlc


    You can notice that the main mxml has been overwritten. This is quite annoying if you made some changes to the generated file but the previous file is backuped and in general you would have added the entity manually in the mxml. If you have a look to the mxml, only two lines have been added for the new entity :

    <mx:LinkButton label="Books" width="100%" textAlign="left"
    click="mainStack.selectedChild = bookUI" />


    <ui:EntityUI id="bookUI"
    entityClass="{Book}"
    width="100%" height="100%"/>

    The EntityUI is a Flex component and can just be used anywhere in a Flex application (provided the Tide framework initialization has been done correctly).

    We can now create and edit books, and see how many-to-one and one-to-many associations are displayed in the generated application.

    As you eventually tried to add chapters, you will see that it does not work. There are indeed two minor changes to do manually in the AS3 classes to help the UI builder make the link between the associated oneToMany entities: initialize the Chapters collection in the Book.as class, and add a constructor in Chapter.as :

    public function Book():void {
    chapters = new ArrayCollection();
    }


    public function Chapter(book:Book = null):void {
    this.book = book;
    }

    Once these changes are made, just refresh the browser and check that you can add chapters to your books.

    Now we're almost finished, we'll just add a final improvement to this simple app:

    grails html-wrapper

    As the command name indicates, this will generate a html wrapper and allow the application to use Flex deep linking, and to use Grails url mapping on the flex application now located at flexindex.gsp.

    Now you can go to http://localhost:8080/gdsexample2/flexindex.gsp#book/list to access the book list directly.

    The documentation on the plugin page gives more information on what constraints are supported by the builder and how to override the behaviour of the generated application. Anyway the sources of the UI builder are also present in the grails-app/views/flex folder and can be used as a template to build your customized UI.

    It should be easy to add security and data push to the generated application by following the previous blog entry here.

    Hopefully this new feature of the gdsflex Grails plugin is a useful tool to get started quickly with Flex and GraniteDS and to quickly build prototype applications.
    As always, feedback and comments are welcome.

    20 commentaires:

    Unknown a dit…

    Well it looks good but when I try to add a chapter a popup shows with no message. Is there a way to enable debug??

    Thanks

    William Draï a dit…

    You have to install a debug version of the flash player. Did you add the two constructors in Chapter.as and Book.as ?

    Unknown a dit…

    I'm trying to override the UIBuilder for one of my domain entities, namely org.epseelon.confplan.Session. So I created a SessionUIBuilder class in the same package as DefaultUIBuilder, and I annotated it like so: [Name("org.epseelon.confplan.Session.tideUIBuilder")]. Yet, when I execute my application, DefaultUIBuilder seems to be used instead. What did I miss?

    William Draï a dit…

    It should be [Name("org.epseelon.confplan.session.tideUIBuilder")] (lowercase on session).

    Unknown a dit…
    Ce commentaire a été supprimé par l'auteur.
    Unknown a dit…

    Hi,

    I was trying as mentioned in this article and I am getting the below error while doing "grails generate-flex-app"

    Could you help me resolve this issue? Please email me: beeyarkay at yahoo dot com

    Environment set to development
    [mkdir] Created dir: E:\grails_projects\gflex\web-app\plugins\gdsflex-0.7.1
    [copy] Copying 28 files to E:\grails_projects\gflex\web-app\plugins\gdsflex-0.7.1
    [groovyc] Compiling 20 source files to C:\Documents and Settings\ramesh\.grails\1.1.1\projects\gflex\classes
    [groovyc] org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed, Compile error during compilation with javac.
    [groovyc] C:\Documents and Settings\ramesh\.grails\1.1.1\projects\gflex\plugins\gdsflex-0.7.1\src\java\org\granite\grails\integration\GrailsBinaryConverter.java:29: method does not override a method from its superclass
    [groovyc] @Override
    [groovyc] ^
    [groovyc] C:\Documents and Settings\ramesh\.grails\1.1.1\projects\gflex\plugins\gdsflex-0.7.1\src\java\org\granite\grails\integration\GrailsBinaryConverter.java:35: method does not override a method from its superclass
    [groovyc] @Override
    [groovyc] ^
    [groovyc] 2 errors
    [groovyc] 1 error

    William Draï a dit…

    Seems you are using a JDK 5, could you try with a JDK 6. The plugin does not seem to compile on a JDK 5, I'll try to fix this in a next update.

    Unknown a dit…

    Thank you. That fixed it. In the meantime, I commented out the @Override annotations to make it work in java1.5. That was all I did.

    -Ramesh

    Unknown a dit…

    Is possible the integration of webflow for the example of grails?

    Thanks

    Manu Dhanda a dit…

    When I execute the command "grails mxmlc", it throws me the following error:

    Environment set to development
    Error executing script Mxmlc: tried to access class org.granite.web.util.WebComp
    ilerWrapper$_compile_closure1 from class org.granite.web.util.WebCompilerWrapper

    java.lang.IllegalAccessError: tried to access class org.granite.web.util.WebComp
    ilerWrapper$_compile_closure1 from class org.granite.web.util.WebCompilerWrapper

    at org.granite.web.util.WebCompilerWrapper.compile(WebCompilerWrapper.gr
    oovy:26)
    at org.granite.web.util.WebCompilerWrapper$compile.call(Unknown Source)
    at Mxmlc$_run_closure1.doCall(Mxmlc:25)
    at Mxmlc$_run_closure1.doCall(Mxmlc:23)
    at gant.Gant$_dispatch_closure4.doCall(Gant.groovy:324)
    at gant.Gant$_dispatch_closure6.doCall(Gant.groovy:334)
    at gant.Gant$_dispatch_closure6.doCall(Gant.groovy)
    at gant.Gant.withBuildListeners(Gant.groovy:344)
    at gant.Gant.this$2$withBuildListeners(Gant.groovy)
    at gant.Gant$this$2$withBuildListeners.callCurrent(Unknown Source)
    at gant.Gant.dispatch(Gant.groovy:334)
    at gant.Gant.this$2$dispatch(Gant.groovy)
    at gant.Gant.invokeMethod(Gant.groovy)
    at gant.Gant.processTargets(Gant.groovy:495)
    at gant.Gant.processTargets(Gant.groovy:480)

    sihing Krzysiek a dit…

    You should run

    > grails html-wrapper

    before running

    > grails mxmlc

    I hope it helps.

    sihing Krzysiek a dit…
    Ce commentaire a été supprimé par l'auteur.
    sihing Krzysiek a dit…

    When i try to upload image file in Author panel i get:

    Error #2044: Nieobsługiwane IOErrorEvent:. text=Error #2038: Błąd dostępu do pliku.
    at org.granite.tide.uibuilder.editors::ImageEditor/uploadImage()[/Users/kkaczmarek/development/grails/GDSExample/grails-app/views/flex/org/granite/tide/uibuilder/editors/ImageEditor.mxml:38]
    at org.granite.tide.uibuilder.editors::ImageEditor/__upload_click()[/Users/kkaczmarek/development/grails/GDSExample/grails-app/views/flex/org/granite/tide/uibuilder/editors/ImageEditor.mxml:93]

    sihing Krzysiek a dit…

    ok, this is generic bug in osx flash player (!)

    André Filipe Aloise a dit…

    Following the example, when delete an Author pops up a confimation dialog. If you click yes, the Author's List gets empty but the edit button keeps enable and if you click it, it's like you haven't delete the Author at all. The information keeps there to be deleted again. How to fix this? I try doing myself other examples and all of them have this same issue.

    BLackOLo a dit…

    It is possible to deploy it as war file each time i run grails > war i get : Error executing script War: : org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed, D:\Documents\.grails\1.2.1\projects\aiolos\gspcompile\gsp_aiolos_webapp_WEB_INF_classes_org_granite_grails_templatetideDomainClassBase_gsp.groovy: 18: Unknown type: IMPORT at line: 18 column: 1. File: D:\Documents\.grails\1.2.1\projects\aiolos\gspcompile\gsp_aiolos_webapp_WEB_INF_classes_org_granite_grails_templatetideDomainClassBase_gsp.groovy @ line 18, column 1.
    import org.granite.generator.as3.reflect.JavaProperty;
    ^

    plus more rubbish :)

    thx

    kwame a dit…

    Hello everybody,
    Following http://www.grails.org/plugin/gdsflex I've created a sample.
    Here is the error outputed after I have executed "grails mxmlc":

    Welcome to Grails 1.1.1 - http://grails.org/
    Licensed under Apache Standard License 2.0
    Grails home is set to: C:\grails\grails-1.1.1

    Base Directory: G:\grails-projects\mygdsflex3
    Running script C:\Documents and Settings\Administrator\.grails\1.1.1\projects\mygdsflex3\plugins\gdsflex-0.7.2\scripts\Mxmlc.groovy
    Environment set to development
    compiling file mygdsflex3.mxml
    Loading configuration file G:\grails-projects\mygdsflex3\web-app\WEB-INF\flex\flex-config.xml
    error during compilation Line number support for XML tag attributes is not available. It is possible that compile-time MXML error reporting and component debugging may not give correct line numbers. Please make sure that xercesPatch.jar is in the classpath.
    mygdsflex3.mxml compilation ended at: Tue Mar 16 19:57:31 CET 2010.
    Thank you to help me.
    Kwame

    SAS Made Easy a dit…

    wonderful work! It works fine in development mode. however, when I jar it and moves to tomcat, I kept receiving "send failed" message. could you tell me why?

    thanks!

    Anonyme a dit…

    Thanks a lot for this tutorial - the first part worked great thanks although I had to do 'grails html-wrapper' before running 'grails mxmlc' as someone helpfully suggested in the comments.

    However the last part didn't work. After I added the Chapter.as and Book.as constructors nothing changed - I still got the same behaviour as before (blank message box etc).

    Carol William a dit…

    Great Post! you have done a great job. Thanks for sharing it with us. Well done and keep posting Grails Application.