(Quick Reference)

8.1 Controllers - Reference Documentation

Authors: Andres Almiray

Version: 1.2.0

8.1 Controllers

Controllers are the entry point for your application's logic. Each controller has access to their model and view instances from their respective MVC group.

Controller actions are usually defined using a closure property form, like the following one

class MyController {
    def someAction = { evt = null ->
        // do some stuff
    }
}

It is also possible to define actions as methods, however the closure property form is preferred (but not enforced). The caveat is that you would need to translate the method into a MethodClosure when referencing them form a View script. In the following example the action 'action1' is defined as a closure property, whereas the action 'action2' is defined as a method

application(title: 'Action sample', pack: true) {
    gridLayout(cols: 2, rows: 1) {
        button 'Action 1', actionPerformed: controller.action1
        button 'Action 2', actionPerformed: controller.&action2
    }
}

Actions must follow these rules in order to be considered as such:

  • must have public (Java) or default (Groovy) visibility modifier.
  • name does not match an event handler, i.e, it does not begin with on.
  • must pass GriffonClassUtils.isPlainMethod() if it's a method.
  • must have void as return type if it's a method.
  • value must be a closure (including curried method pointers) if it's a property.

Controllers can perform other tasks:

8.1.1 Actions and Threads

A key aspect that you must always keep in mind is proper threading. Often times controller actions will be bound in response to an event driven by the UI. Those actions will usually be invoked in the same thread that triggered the event, which would be the UI thread. When that happens you must make sure that the executed code is short and that it quickly returns control to the UI thread. Failure to do so may result in unresponsive applications.

The following example is the typical use case that must be avoided

class BadController {
    def badAction = {
        def sql = Sql.newInstance(
            app.config.datasource.url,
            model.username,
            model.password,
            app.config.datasource.driver
        )
        model.products.clear()
        sql.eachRow("select * from products") { product ->
            model.products << [product.id, product.name, product.price]
        }
        sql.close()
    }
}

There are two problems here. First the database connection is established inside the UI thread (which takes precious milliseconds or even longer), then a table (which could be arbitrarily large) is queried and each result sent to a List belonging to the model. Assuming that the list is bound to a Table Model then the UI will be updated constantly by each added row; which happens to be done all inside the UI thread. The application will certainly behave slow and sluggish, and to top it off the user won't be able to click on another button or select a menu item until this actions has been processed entirely.

The Threading chapter discusses with further detail the options that you have at your disposal to make use of proper threading constructs. Here's a quick fix for the previous controller

class GoodController {
    def goodAction = {
        execOutsideUI {
            def sql = null
            try {
                sql = Sql.newInstance(
                    app.config.datasource.url,
                    model.username,
                    model.password,
                    app.config.datasource.driver
                )
                List results = []
                sql.eachRow("select * from products") { product ->
                    results << [product.id, product.name, product.price]
                }
                execInsideUIAsync {
                    model.products.clear()
                    model.addAll(results)
                }
            } finally {
                sql?.close()
            }
        }
    }
}

However starting with Griffon 0.9.2 you're no longer required to surround the action code with execOutsideUI as the compiler will do it for you. This feature breaks backward compatibility with previous releases so it's possible to disable it altogether. Please refer to the Disable Threading Injection section. This feature can be partially enabled/disabled too. You can specify with absolute precision which actions should have this feature enabled or disabled, by adding the following settings to griffon-app/conf/BuildConfig.groovy

compiler {
    threading {
        sample {
            SampleController {
                action1 = false
                action2 = true
            }
            FooController = false
        }
        bar = false
    }
}

The compiler will evaluate these settings as follows:

  • the action identified by sample.SampleController.action1 will not have automatic threading injected into its code, while sample.SampleController.action2 (and any other found in the same controller) will have it.
  • all actions belonging to sample.FooController will not have automatic threading injected.
  • all actions belonging to all controllers in the bar package will not have threading injected.

Automatic threading injection only works for Groovy based controllers. You must add appropriate threading code to controller actions that are written in languages other than Groovy.

8.1.2 The Action Manager

Controller actions may automatically be wrapped and exposed as toolkit specific actions in a group's builder; this greatly simplifies how actions can be configured based on i18n concerns.

At the heart of this feature lies the GriffonControllerActionManager. This component is responsible for instantiating, configuring and keeping references to all actions per controller. It will automatically harvest all action candidates from a Controller once it has been instantiated. Each action has all of its properties configured following this strategy:

  • match <controller.class.name>.action.<action.name>.<key>
  • match application.action.<action.name>.<key>

<action.name> should be properly capitalized. In other words, you can configure action properties specifically per Controller or application wide. Available keys are

KeyDefault value
nameGriffonNameUtils.getNaturalName() applied to the action's name - 'Action' suffix (if it exists)
acceleratorundefined
short_descriptionundefined
long_descriptionundefined
mnemonicundefined
small_iconundefined; should point to an URL in the classpath
large_iconundefined; should point to an URL in the classpath
enabledundefined; boolean value
selectedundefined; boolean value

Values must be placed in resources files following the internationalization configuration guidelines.

Example

The following Controller defines four actions, the first two as closure properties while the others as methods. Two actions have an 'Action' suffix in their names.

package sample
import java.awt.event.ActionEvent
class SampleController {
    def newAction = { … }
    def open = { … }
    void close(ActionEvent evt) { … }
    void deleteAction(ActionEvent evt) { … }
}

The actions new and delete use the 'Action' suffix in order to avoid compilation errors given that they make use of reserved keywords. It's all the same to the GriffonControllerActionManager as it will generate the following variables in the group's builder: newAction, openAction, closeAction and deleteAction. ActionManager expects the following keys to be available in the application's i18n resources (i.e. griffon-app/i18n/messages.properties)

sample.SampleController.action.New.name = New
sample.SampleController.action.Open.name = Open
sample.SampleController.action.Close.name = Close
sample.SampleController.action.Delete.name = Delete
# additional keys per action elided

In the case that you'd like the close action to be customized for all controllers, say using the Spanish language, then you'll have to provide a file named griffon-app/i18n/messages_es.properties with the following keys

application.action.Close.name = Cerrar

Make sure to remove any controller specific keys when reaching for application wide configuration.