Combining Grails MVC and Webflow
From begug
In this article I want to share my experience in integrating small Spring Webflows in a standard CRUD controller and how to avoid common pitfalls.
Contents |
The past
In the previous project I worked on, we used JSF and Spring Webflow. We did some nice things with Webflow, it was just a pity that we had to write tons of XML.
JSF, on the other hand, is a perfect example of how to make simple things complex (just take a look at the JSF request-response life cycle!). It also requires you to duplicate most of your domain objects in JSF managed beans and write a bunch of XML to wire everything together.
A controller action in a Grails application is typically just a few lines long and you don't need any configuration at all. Furthermore, in a typical web application you need very few flows, if any.
When flows become necessary
When your developing a long (potentially complex) wizard, you'll have to create a full blown webflow. But there are also situations where a full blown webflow is overkill while the standard CRUD actions in your controller are insufficient. For instance when you want to:
- edit lists of objects (although this is partly solved in Grails 1.1)
- leave an edit page for doing a search, and then return to the edit page
I will use the second scenario to demonstrate the problems I ran into and how I solved them...
Iteration #1
Assume that, when editing a book instance, you want to look up the authors that you want to add to the book. You could store the book instance in the session object, but to avoid the problems related to session management and back navigation, it is better to write a small flow.
Your edit action just redirects to the flow:
def edit = {
redirect(action: 'editAndSave', id: params.id)
}
and from the flow (in the same controller) you redirect back to the standard show action:
def editAndSaveFlow = {
start {
action {
flow.book = Book.get(params.id)
}
on('success').to('edit')
}
edit{...}
authorSearch {...}
save{...}
show {...}
}
There are two problems with this:
- The states in the flow collide with standard controller actions (edit, show etc.), and this won't be clear from the exceptions you'll get!
- Flow names have to be unique within the application, so you cannot define a second editAndSaveFlow in another controller.
Iteration #2
We change the names of the flow and the flow states, fill in the flow actions and on completion redirect to the standard show action:
def bookEditFlow = {
start {
action {
flow.book = (params.id) ? Book.get(params.id) : new Book()
}
on('success').to('flowEdit')
}
flowEdit {
render(view: '/book/edit')
on('search') {
//remember the changes that the user made before hitting the search button!
flow.book.properties = params
}.to('authorSearch')
on('removeAuthor') {
flow.book.properties = params
flow.book.removeFromAuthors(Author.get(params.authorId))
}.to('flowEdit')
on('save').to('flowSave')
on('delete').to('flowDelete')
}
authorSearch {
render(view: '/author/search')
on('select') {
flow.book.addToAuthors(Author.get(params.id))
}.to('flowEdit')
on('cancel').to('flowEdit')
}
flowSave {
action {
flow.book.properties = params
if (!flow.book.hasErrors() && flow.book.save()) {
return success()
} else {
return error()
}
}
on('success').to('flowShow')
on('error').to('flowEdit')
}
flowShow {
redirect(controller: 'book', action: 'show', params: [id: flow.book.id])
}
flowDelete {
redirect(controller: 'book', action: 'delete', params: [id: flow.book.id])
}
}
The view doesn't differ much from the standard view without the flow. In edit.gsp we have to use g:submitButton in stead of g:actionSubmit :
<ul id="authors">
<g:each var="a" in="${book?.authors}">
<li>
<g:link controller="author" action="show" id="${a.id}">${a?.encodeAsHTML()}</g:link>
<g:submitButton class="delete" onclick="\$('authorId').value= '${a.id}'" name="removeAuthor" value="Remove"/>
</li>
</g:each>
</ul>
<g:submitButton class="search" name="search" value="Search Author"/>
Note the onclick handler that fills in the hidden authorId that is used in the removeAuthor transition.
Also note that both create and update are combined in one save action and that the create.gsp is not used, but this is the also the subject of Make your GSP DRY
Note for Grails pre 1.1
redirect(controller: 'book', action: 'show', params: [id: flow.book.id])
does not work in versions of Grails prior to 1.1. There is however a workaround:
redirect(url: '/book/show/${flowScope.book.id}')
Because of a bug, the $ sign is not url-encoded when using this form of redirect, and Spring Webflow will interpret the expression. Note that you have to use the full 'flowScope' in stead of the Grails abbreviation 'flow'.
The finishing touch
The standard save action stores a message in the flash scope to notify a successful save. The flow also has a flash scope, but we cannot use it because it is different from the Grails flash scope. Fortunately there is a simple workaround using the RequestContextHolder:
import org.springframework.web.context.request.RequestContextHolder as RCH
...
flowSave {
action {
flow.book.properties = params
if (!flow.book.hasErrors() && flow.book.save()) {
RCH.currentRequestAttributes().flashScope.message = "Book ${flow.book} saved"
return success()
...
Using subflows
It would make sense to put the search part of the flow in a subflow so that it can be reused in other flows:
def bookEditFlow = {
...
authorSearch {
subflow(authorSearchFlow)
on('selected') {
if (conversation.author) {
flow.book.addToAuthors(conversation.author)
}
}.to('flowEdit')
on('canceled').to('flowEdit')
}
...
def authorSearchFlow = {
startSearch {
render(view: '/author/search')
on('select') {
conversation.author = Author.get(params.id)
}.to('selected')
on('cancel').to('canceled')
}
canceled()
selected()
}
Unfortunately the authorSearchFlow is sort of inlined into the calling flow, which makes it impossible to effectively reuse flow definitions. Several people (including me) tried to put flow definitions in static members of a controller, but this doesn't work either. Furthermore the webflow DSL doesn't (yet) support input/output from subflows. Hopefully this will be straightened out soon, after the Grails developers and webflow developers became colleagues ;)
Conclusion
Although the Grails' webflow DSL hasn't completely matured, it can elegantly complement the standard controller actions when needed, without loads of XML configuration. You only have to avoid some common pitfalls.
The example code can be downloaded here
I look forward to your comments!
Ivo Houbrechts 01:18, 14 February 2009 (UTC)