Tuesday, August 19, 2008

Remembering state of a Grails list page (3)



I'm taking remembering (or maybe I should call it restoring?) the state of a Grails list page a little bit further.

In my Grails project I've added a 'restoreState' method dynamically using the ExpandoMetaClass to all Controller classes. The method is simply added in BootStrap.groovy:


import org.codehaus.groovy.grails.commons.GrailsApplication
import org.codehaus.groovy.grails.commons.metaclass.*

class BootStrap {

def init = { servletContext ->
def application = servletContext.getAttribute(GrailsApplication.APPLICATION_ID)

// add restoreState to controller classes
application.controllerClasses.each { controller ->
controller.metaClass.restoreState << { stateParams ->

// get (or initialize) session state params
def sessionStateParams = delegate.session[delegate.controllerName] = (delegate.session[delegate.controllerName] ?: [:])

// store state params and its values in params and session object
// (1) use params (2) or use session (3) otherwise use default value
stateParams.each { k, v ->
delegate.params[k] = sessionStateParams[k] = (delegate.params[k] ?: (sessionStateParams[k] ?: v))
}
}
}
}

def destroy = {
}
}


The 'restoreState' method can now be invoked from the Controller list action to restore the state of the list page like:


Class AuthorController {

def list = {
restoreState([max: 10, offset: 0, sort: id, order: 'asc'])
[ authorList: Author.list( params ) ]
}
}


Note: after writing this entry and using the technique myself I found out that using the BootStrap class to add this restoreState method dynamically is not really useful.

The problem is that it only adds this method during startup of the container and not during reload. So when you change your controller, you loose the method. Solution would be moving this method to a plugin which also depends on the Controller's core plugin, so also during reload the method would be added.

I ended up creating a BaseController which included this method, and my other Controller classes are extending this BaseController. I further installed the Grails templates and changed the Controller templates so they extend the BaseController by default.

3 comments:

Anonymous said...

This is getting better! Thanks

Btw, i think there a little typo, the call in the example controller should be made to restoreState

Marcel Overdijk said...

Currently using the boostrap approch is that when you add a controller while your app is running, the restoreState method is not added.

To make this happen I think I need to create a plugin in which I can participate in the reloading process of controllers.

rsaddey said...

I do like the BaseController pattern, as it neither hides what's going on and is very easy to understand, nor does it duplicate (runtime) code.

I found another pattern that is not as elegant as the BaseController one, but still is quite easy to understand: Use a global static method (= function), where the delegate is passed in explicitly.

class Util {
static def restoreState(def /*Controller*/ delegate, def /*Map*/ stateParams) {
def sessionStateParams = delegate.session[delegate.controllerName] = (delegate.session[delegate.controllerName] ?: [:])

// store state params and its values in params and session object
// (1) use params (2) or use session (3) otherwise use default value
stateParams.each { k, v ->
delegate.params[k] = sessionStateParams[k] = (delegate.params[k] ?: (sessionStateParams[k] ?: v))
}
}
}

Its invocation now looks similar to:

def list = {
Util.restoreState(this, [max: 10, offset: 0, sort: 'scannedAt', order: 'desc'])
a.s.o.

P.S.: Thanks to blogger.com for its smart formatting capabilities :-(