8 Controllers and Services - Reference Documentation
Authors: Andres Almiray
Version: 1.2.0
Table of Contents
8 Controllers and Services
This section describes the artifacts that hold the logic of a Griffon application.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 oneclass MyController { def someAction = { evt = null -> // do some stuff } }
application(title: 'Action sample', pack: true) {
gridLayout(cols: 2, rows: 1) {
button 'Action 1', actionPerformed: controller.action1
button 'Action 2', actionPerformed: controller.&action2
}
}
- 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.
- listen to application events.
- create and destroy MVC groups via a pair of methods (createMVCGroup, destroyMVCGroup).
- react to MVC initialization/destruction via a pair of methods (mvcGroupInit, mvcGroupDestroy).
- hold service references.
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 avoidedclass 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()
}
}
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() } } } }
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 action identified by
sample.SampleController.action1
will not have automatic threading injected into its code, whilesample.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 theGriffonControllerActionManager
. 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 areKey | Default value |
---|---|
name | GriffonNameUtils.getNaturalName() applied to the action's name - 'Action' suffix (if it exists) |
accelerator | undefined |
short_description | undefined |
long_description | undefined |
mnemonic | undefined |
small_icon | undefined; should point to an URL in the classpath |
large_icon | undefined; should point to an URL in the classpath |
enabled | undefined; boolean value |
selected | undefined; boolean value |
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) { … } }
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
griffon-app/i18n/messages_es.properties
with the following keysapplication.action.Close.name = Cerrar
8.2 Services
Services are responsible for the application logic that does not belong to a single controller. They are meant to be treated as singletons, injected to MVC members by following a naming convention. Services are optional artifacts, and as such there is no default folder created for them when a new application is created.Services must be located inside thegriffon-app/services
directory with a Service
suffix. The create-service command performs this job for you; also adding a unit test for the given service.Let's say you need to create a Math service, the command to invoke would begriffon create-service math
griffon-app/services/MathService.groovy
- the service class.test/unit/MathServiceTests.groovy
- service unit test.
class MathService { def addition(a, b) { a + b } }
class MyController { def mathService def action = { model.result = mathService.addition model.a, model.b } }
Service injection is trivial, it does not provide a full blown DI, in other words further service dependencies will not be resolved. You will need a richer DI solution in order to achieve this, fortunately there are Spring and Weld plugins that do this and more.Given that services are inherently treated as singletons they are also automatically registered as application event listeners. Be aware that services will be instantiated lazily which means that some events might not reach a particular service if it has not been instantiated by the framework by the time of event publication. It's also discouraged to use the @Singleton annotation on a Service class as it will cause trouble with the automatic singleton management Griffon has in place.Lastly, all services instances will become available through an instance of type
griffon.core.ServiceManager
. This helper class exposes available services via a Map. You can query all currently available services in the following mannerapp.serviceManager.services.each { name, instance ->
// do something cool with services
}
def fooService = app.serviceManager.findService('foo')
Config.groovy
griffon.services.eager.instantiation = true
8.2.1 Service Lifecycle
Services participate in a lifecycle as they are automatically managed by the application. TheGriffonService
interface defines a pair of methods that every service may override.public interface GriffonService extends GriffonArtifact { void serviceInit(); void serviceDestroy(); }
ServiceManager
. This is the right place to put initialization code. The app
instance should be already set on the service instance, giving you direct access to the application's configuration and i18n facilities.The second method is also called by ServiceManager
when the application is shutting down. Be aware that this method will be called before MVC groups are destroyed.
8.2.2 Service Configuration DSL
Services may have some of its properties defined in external configuration files, for example inConfig.groovy
, using a simple DSL. Take for example the following service classclass NetworkService { String host int port private Server server void connect() { if (!server) { server = new Server(host, port) } } }
host
and port
) but does not define any values for them. Inside Config.groovy
we find the following definitionsservices { network { host = 'http://acme.com' port = 1234 } }
- The entry point is the top
services
section. - Each child node identifies the target service by name. Notice that the
Service
suffix is omitted. - Each property within a service block will be set on the service instance.