Wednesday, August 1, 2007

Another Sexy Flex Grails Example



Here is another (quick) Flex example which uses Grails on the back-end. It contains all CRUD operations in just one screen. Just follow the steps below to see it for yourself:


  1. Create a new grails application: grails create-app flexongrails

  2. Create a new Book domain class: grails create-domain-class Book
    Open the Book domain class and replace the code with:
    class Book { 
    String isbn
    String title
    String author
    Float price
    String format

    static constraints = {
    isbn(maxLength:20, unique:true)
    title(maxLength:50)
    author(maxLength:50)
    price(min:0F, max:999F, scale:2)
    format(inList:["Hardcover", "Paperback", "e-Book"])
    }
    }

  3. Create a new Book controller: grails create-controller Book
    Open the Book controller class and replace the code with:
    class BookController {

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

    // the delete, save and update actions only accept POST requests
    // def allowedMethods = [delete:'POST', save:'POST', update:'POST']

    def list = {
    response.setHeader("Cache-Control", "no-store")
    def bookList = Book.list(params)
    render(contentType:"text/xml") {
    data {
    for(i in bookList) {
    book {
    id(i.id)
    isbn(i.isbn)
    title(i.title)
    author(i.author)
    price(i.price)
    format(i.format)
    }
    }
    }
    }
    }

    def save = {
    def book
    if(params.id) {
    book = Book.get(params.id)
    }
    else {
    book = new Book()
    }
    book.properties = params
    book.save()
    render ""
    }

    def delete = {
    def book = Book.get(params.id)
    if(book) {
    book.delete()
    }
    render ""
    }

    }


    What you can see here is that the list action return xml data which will be used by Flex. Important is setting the cache control in the response header. The save action will be used both creating as editing.

  4. Create a new Flex project (in Flex builder) and replace the code in the main mxml file with:
    <?xml version="1.0" encoding="utf-8"?>
    <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" creationComplete="listService.send()">

    <mx:HTTPService id="listService" url="http://localhost:8080/flexongrails/book/list" useProxy="false" method="GET"/>
    <mx:HTTPService id="saveService" url="http://localhost:8080/flexongrails/book/save" useProxy="false" method="POST" result="listService.send()">
    <mx:request xmlns="">
    <id>{book_id.text}</id>
    <isbn>{isbn.text}</isbn>
    <title>{title.text}</title>
    <author>{author.text}</author>
    <price>{price.text}</price>
    <format>{format.text}</format>
    </mx:request>
    </mx:HTTPService>
    <mx:HTTPService id="deleteService" url="http://localhost:8080/flexongrails/book/delete" useProxy="false" method="POST" result="listService.send()">
    <mx:request xmlns="">
    <id>{dg.selectedItem.id}</id>
    </mx:request>
    </mx:HTTPService>

    <mx:NumberFormatter id="priceFormatter" precision="2"/>

    <mx:Script>
    <![CDATA[
    [Bindable]
    private var formatArray:Array = ["Hardcover", "Paperback", "e-Book"];

    private function clearForm():void {
    book_id.text = "";
    isbn.text = "";
    title.text = "";
    author.text = "";
    price.text = "";
    format.selectedIndex = 0;
    }

    private function formatPrice(item:Object, column:DataGridColumn):String {
    return priceFormatter.format(item.price);
    }
    ]]>
    </mx:Script>

    <mx:VDividedBox x="0" y="0" height="100%" width="100%" paddingLeft="10" paddingRight="10" paddingTop="10" paddingBottom="10">
    <mx:Panel width="100%" height="300" layout="absolute" title="Create/Update Book">
    <mx:Form x="10" y="10" width="930" height="200">
    <mx:FormItem label="ID">
    <mx:TextInput width="120" id="book_id" text="{dg.selectedItem.id}" enabled="false"/>
    </mx:FormItem>
    <mx:FormItem label="ISBN">
    <mx:TextInput width="220" id="isbn" text="{dg.selectedItem.isbn}" maxChars="20"/>
    </mx:FormItem>
    <mx:FormItem label="Title">
    <mx:TextInput width="320" id="title" text="{dg.selectedItem.title}" maxChars="50"/>
    </mx:FormItem>
    <mx:FormItem label="Author">
    <mx:TextInput width="320" id="author" text="{dg.selectedItem.author}" maxChars="50"/>
    </mx:FormItem>
    <mx:FormItem label="Price">
    <mx:TextInput width="120" id="price" text="{priceFormatter.format(dg.selectedItem.price)}"/>
    </mx:FormItem>
    <mx:FormItem label="Format" width="220">
    <mx:ComboBox id="format" selectedIndex="{formatArray.indexOf(dg.selectedItem.format)}">
    <mx:dataProvider>{formatArray}</mx:dataProvider>
    </mx:ComboBox>
    </mx:FormItem>
    </mx:Form>
    <mx:ControlBar horizontalAlign="right">
    <mx:Button label="New" click="clearForm()"/>
    <mx:Button label="Save" click="saveService.send(); clearForm()"/>
    </mx:ControlBar>
    </mx:Panel>
    <mx:Panel width="100%" height="444" layout="absolute" title="Book List">
    <mx:DataGrid x="0" y="0" width="100%" height="100%" id="dg" dataProvider="{listService.lastResult.data.book}">
    <mx:columns>
    <mx:DataGridColumn width="120" headerText="ID" dataField="id"/>
    <mx:DataGridColumn width="220" headerText="ISBN" dataField="isbn"/>
    <mx:DataGridColumn width="320" headerText="Title" dataField="title"/>
    <mx:DataGridColumn width="320" headerText="Author" dataField="author"/>
    <mx:DataGridColumn width="120" headerText="Price" dataField="price" labelFunction="formatPrice"/>
    <mx:DataGridColumn width="220" headerText="Format" dataField="format"/>
    </mx:columns>
    </mx:DataGrid>
    <mx:ControlBar horizontalAlign="right">
    <mx:Button label="Delete" click="deleteService.send()" enabled="{dg.selectedItem != null}"/>
    </mx:ControlBar>
    </mx:Panel>
    </mx:VDividedBox>

    </mx:Application>

  5. Run the Flex application


You now have a basic CRUD Flex application which uses Grails as back-end. The example is very basic and the Grails Book controller isn't responding back any possible validation errors; they just get absorbed. Also for a couple of row retrieving all the rows at once is no problem, but in real life it will me thousands and thousands of records, so also server side paging and ordering is needed. Well, this could be easily implemented in the Book controller (as it is basic Grails functionality) but most of the work will go in the Flex application I think...

For server-side paging/sorting a custom Flex component would be needed which remembers the current page and just submits the required paging and sorting fields to the serve-side. This is independent to Grails as it could be used by any server-side application. I guess I'm not the first one looking into this so I might find something on the internet.

11 comments:

Anonymous said...

this is brilliant
i have been wanting to blend flex & grails together for some time
(in all the wrong ways), after going through your tutorial - I am most certain Flex & Grails will be a great combination.

Definite will do this in my next project. I think Adobe is taking a little too far with AIR, Flex itself is n'bad if they can reduce the reliance on AS further, better.

ph.charriere said...

it runs very well, but i've a problem : i can't save "linked" objects, ie :

class Book {
static hasMany = [ notes : Note ]
...

i try to add with Flex a value object
Note.text
Note.book_id
and save it with save controller of Book ...

No error, but no Note ...

any idea ?

Anonymous said...

I love Grails and I love Flex. Still, I think we should be able to generate the MXML files also if we want to keep Grails advantage.
Ok, ok... maybe I am running here before crawling... but we should try to go there... and I would love to give you a hand although I am no expert in neither of both technologies.
I sent you a Google Talk invitation. Maybe we could chat there... See ya...

Martin said...

Philippe, you need to save the note with the NoteController; I presume that has a "belongsTo [book:Book] " statement? In which case you save the note, not the book, and put something like "if (params.book_id) { params.book = Book.get(params.book_id } "

I expect you've solved this by now yourself!!

Germán said...

Great work!, a very easy and simple example, I would like to see the same thing with scaffold. I´ll try myself and let you know.

Germán said...

Great work!, a very easy and simple example, I would like to see the same thing with scaffold. I´ll try myself and let you know.

Karthik Blog said...

hi i cannot run the application i get error as


"[RPC Fault faultString="HTTP request error" faultCode="Server.Error.Request" faultDetail="Error: [IOErrorEvent type="ioError" bubbles=false cancelable=false eventPhase=2 text="Error #2032: Stream Error. URL: http://localhost:8080/flexongrails/book/list"]. URL: http://localhost:8080/flexongrails/book/list"]
at mx.rpc::AbstractInvoker/http://www.adobe.com/2006/flex/mx/internal::faultHandler()[C:\autobuild\3.2.0\frameworks\projects\rpc\src\mx\rpc\AbstractInvoker.as:220]
at mx.rpc::Responder/fault()[C:\autobuild\3.2.0\frameworks\projects\rpc\src\mx\rpc\Responder.as:53]
at mx.rpc::AsyncRequest/fault()[C:\autobuild\3.2.0\frameworks\projects\rpc\src\mx\rpc\AsyncRequest.as:103]
at DirectHTTPMessageResponder/errorHandler()[C:\autobuild\3.2.0\frameworks\projects\rpc\src\mx\messaging\channels\DirectHTTPChannel.as:362]
at flash.events::EventDispatcher/dispatchEventFunction()
at flash.events::EventDispatcher/dispatchEvent()
at flash.net::URLLoader/redirectEvent()"

Karthik Blog said...

i have an error while using this code

"[RPC Fault faultString="HTTP request error" faultCode="Server.Error.Request" faultDetail="Error: [IOErrorEvent type="ioError" bubbles=false cancelable=false eventPhase=2 text="Error #2032: Stream Error. URL: http://localhost:8080/flexongrails/book/list"]. URL: http://localhost:8080/flexongrails/book/list"]
at mx.rpc::AbstractInvoker/http://www.adobe.com/2006/flex/mx/internal::faultHandler()[C:\autobuild\3.2.0\frameworks\projects\rpc\src\mx\rpc\AbstractInvoker.as:220]
at mx.rpc::Responder/fault()[C:\autobuild\3.2.0\frameworks\projects\rpc\src\mx\rpc\Responder.as:53]
at mx.rpc::AsyncRequest/fault()[C:\autobuild\3.2.0\frameworks\projects\rpc\src\mx\rpc\AsyncRequest.as:103]
at DirectHTTPMessageResponder/errorHandler()[C:\autobuild\3.2.0\frameworks\projects\rpc\src\mx\messaging\channels\DirectHTTPChannel.as:362]
at flash.events::EventDispatcher/dispatchEventFunction()
at flash.events::EventDispatcher/dispatchEvent()
at flash.net::URLLoader/redirectEvent()"

Sagar Jadhav said...

I am unable to execute this example...it is looking for some http://localhost:8080/crossdomain.xml which is never there. This example does not work

Sagar Jadhav said...

The example works now but I had to install the flex plug-in ...grails install-plugin flex and then the example started working.
Actually I am using the grails 1.1-beta3 version hence may be that was the problem..also I had to upgrade the flash player to 10

Jeff Self said...

I'm trying this example but its only partially working. The form works. I can add data and it shows in my grails app. But the DataGrid is not returning any data.

It just keeps a message saying 'Transferring data from localhost...' but no data is ever displayed. I've gone over it and the code is the same in the Flex app.