Sunday, 14 September 2008

Command Objects

We have a bunch of horrible code in our codebase to do with binding form fields to objects. To be fair it dates from a time when we were a lot newer to Grails and Grails and its documentation was a lot less mature. The other day I had to add a date picker to one of our forms and write the binding code for it. The g:datePicker tag creates some select boxes and a hidden field. Unlike other implementations I've used in the past it doesn't use Javascript to compose the select box values into the hidden field on form submit. Instead the hidden field has the value "struct" which I guess is a hint to GrailsDataBinder. To cut a long story short the code I ended up writing is pretty nasty. Especially considering that GrailsDataBinder is perfectly capable of handling this kind of binding without any code being written.

Instead of all this tedious messing about with params Grails controllers can, as most of you probably know, bind form submissions to domain objects or command objects. Binding to domain objects works by doing = params or using the bindData method. So much most of us are used to (even if you wouldn't think so from looking at some of our code).

To have your controller action use a command object you simply specify it as an argument to the action closure. e.g.
    def save = { SaveCommand command ->
if (command.hasErrors()) {
render(view: 'index', model: [command: command])
// do some stuff
The binding is done for you. As you may guess from the command.hasErrors() above, command objects can have constraints exactly as domain objects do. They can even have services and other Spring application context artefacts wired in automatically just like controllers and services can.

A form that contained...
    <input type="text" name="name" value="${command?.name}" />
<g:datePicker name="birthday" value="${command?.birthday}" />
<g:select name="" from="${Sandwich.list()}" value="${command?.sandwich?.id" optionKey="id" />
Would bind to a command like this...
    class MyCommand {
String name // simple property
Date birthday // bound from multiple fields submitted by date picker
Sandwich sandwich // bound domain object

Unit testing is easy because you can simply construct the command object and pass it in to the controller action. You don't have to worry about setting a bunch of esoteric key/value pairs on params to get your controller's hairy binding logic to work. In an integration test, if you really want to, you can set params and test that the command binding works as you expect.

The only non trivial thing in unit tests (in integration tests this is not a problem) is testing command error handling. In unit tests commands won't have an errors property or hasErrors method like they do in a running app or integration test. I've come up with the following which I've raised as an enhancement request for the experimental testing plugin:
    def createCommand(Class clazz) {
def command = clazz.newInstance()
def commandErrors = new BeanPropertyBindingResult(command, GrailsClassUtils.getLogicalPropertyName(, ''))
command.metaClass.getErrors = {-> commandErrors }
command.metaClass.hasErrors = {-> commandErrors.hasErrors() }

void testCommandWithBindingErrors() {
def command = createCommand(MyCommand)
command.errors.reject('birthday', 'nullable')
// assert the form is re-rendered with the command in the model, etc.

Command objects can also be used to handle multipart upload forms...
    <input type="file" name="avatar" />
Maps to...
    class MyCommand {
byte[] avatar
If you need access to the uploaded file's metadata (content type, original filename, etc.) instead of declaring the property as type byte[] use MultipartFile. Either way the binding works seamlessly and allows you to apply constraints to the uploaded file.

The weak area in binding at the moment seems to be one-to-many domain class relationships (which is why I didn't jump into refactoring the code I mentioned at the start of this post). That does require you to write hairy binding code and is probably worth a post in and of itself.

No comments: