15 Tips and Tricks - Reference Documentation
Authors: Andres Almiray
Version: 1.2.0
Table of Contents
15 Tips and Tricks
15.1 Using Artifact Conventions to your Advantage
The Artifact API can be a very powerful ally. Not only it's useful for adding new methods (explained in sections Adding Dynamic Methods at Runtime) but also comes in handy to finding out what application specific attributes an artifact has, for example Controller actions or Model properties. The following screenshot shows a simple application that presents a form based View.
Submit
button a dialog appears
package simple@Bindable class SimpleModel { String firstName String lastName String address }
package simpleimport griffon.util.GriffonNameUtils as GNUclass SimpleController { def model def clear = { model.griffonClass.propertyNames.each { name -> model[name] = '' } } @Threading(Threading.Policy.SKIP) def submit = { javax.swing.JOptionPane.showMessageDialog( app.windowManager.windows.find{it.focused}, model.griffonClass.propertyNames.collect([]) { name -> "${GNU.getNaturalName(name)} = ${model[name]}" }.join('n') ) } }
clear()
action is responsible for reseting the values of each Model property. It does so by iterating over the names of the properties found in the Model. The submit()
action constructs a list fo model property names and their corresponding values, then presents it in a dialog. Notice that the Controller never refers to a Model property directly by its name, i.e, the Controller doesn't really know that the Model has a firstName
property. Finally the Viewpackage simpleimport griffon.util.GriffonNameUtils as GNUapplication(title: 'Simple', pack: true, locationByPlatform:true, iconImage: imageIcon('/griffon-icon-48x48.png').image, iconImages: [imageIcon('/griffon-icon-48x48.png').image, imageIcon('/griffon-icon-32x32.png').image, imageIcon('/griffon-icon-16x16.png').image]) { borderLayout() panel(constraints: CENTER, border: titledBorder(title: 'Person')) { migLayout() model.griffonClass.propertyNames.each { name -> label(GNU.getNaturalName(name), constraints: 'left') textField(columns: 20, constraints: 'growx, wrap', text: bind(name, target: model, mutual: true)) } } panel(constraints: EAST, border: titledBorder(title: 'Actions')) { migLayout() controller.griffonClass.actionNames.each { name -> button(GNU.getNaturalName(name), actionPerformed: controller."$name", constraints: 'growx, wrap') } } }
You must install the MigLayout Plugin before running this application.
15.2 Dealing with Non-Groovy Artifacts
Since version 0.9.1 Griffon supports writing artifacts in JVM languages other than Groovy. The first of such languages is Java and it's supported in core by default. Additional languague support will be provided by plugins.Creating a Non-Groovy Artifact
Many of thecreate-*
scripts that come bundled with Griffon support an additional parameter that can be used to specify the language or filetype of the artifact. Non-Groovy artifacts must extend a particular class in order to receive all the benefits of a typical artifact. The default artifact templates can handle both Groovy and Java types. The following command will create an application that uses Java as the default language for the the initial MVC groupgriffon create-app simple -fileType=java
simple
MVC group we find the following code. First the Modelpackage simple;import org.codehaus.griffon.runtime.core.AbstractGriffonModel;public class SimpleModel extends AbstractGriffonModel { // an observable property // private String input; // public String getInput() { // return input; // } // public void setInput(String input) { // firePropertyChange("input", this.input, this.input = input); // } }
package simple;import java.awt.event.ActionEvent; import org.codehaus.griffon.runtime.core.AbstractGriffonController;public class SimpleController extends AbstractGriffonController { private SimpleModel model; public void setModel(SimpleModel model) { this.model = model; } /* public void action(ActionEvent e) { } */ }
package simple;import java.awt.*; import javax.swing.*; import java.util.Map;import griffon.swing.SwingGriffonApplication; import griffon.swing.WindowManager; import org.codehaus.griffon.runtime.core.AbstractGriffonView;public class SimpleView extends AbstractGriffonView { private SimpleController controller; private SimpleModel model; public void setController(SimpleController controller) { this.controller = controller; } public void setModel(SimpleModel model) { this.model = model; } // build the UI private JComponent init() { JPanel panel = new JPanel(new BorderLayout()); panel.add(new JLabel("Content Goes Here"), BorderLayout.CENTER); return panel; } @Override public void mvcGroupInit(Map<String, Object> args) { execInsideUISync(new Runnable() { public void run() { Container container = (Container) getApp().createApplicationContainer(); if(container instanceof Window) { containerPreInit((Window) container); } container.add(init()); if(container instanceof Window) { containerPostInit((Window) container); } } }); } private void containerPreInit(Window window) { if(window instanceof Frame) ((Frame) window).setTitle("simple"); window.setIconImage(getImage("/griffon-icon-48x48.png")); // uncomment the following lines if targeting +JDK6 // window.setIconImages(java.util.Arrays.asList( // getImage("/griffon-icon-48x48.png"), // getImage("/griffon-icon-32x32.png"), // getImage("/griffon-icon-16x16.png") // )); window.setLocationByPlatform(true); window.setPreferredSize(new Dimension(320, 240)); } private void containerPostInit(Window window) { window.pack(); ((SwingGriffonApplication) getApp()).getWindowManager().attach(window); } private Image getImage(String path) { return Toolkit.getDefaultToolkit() .getImage(SimpleView.class.getResource(path)); } }
15.3 Externalizing Views
Groovy is the default language/format for writing Views, however there might be times where you would rather use a different format for describing a View. It might be the case that you have a legacy View (plain Java code) that you would like to plugin into Griffon. Here are a few tips to get the job done.15.3.1 NetBeans Matisse
NetBeans comes with a visual designer named Matisse which is quite popular among a good number of developers. Matisse views are usually defined by a Java class. Most of the times all UI widgets are exposed as fields on the Java class. Based with this information Griffon can generate a View script that is backed by this particular Java class. Follow these steps to reuse a Matisse view.#1 Place the Matisse View in your application
If you have access to the View's source code then please it somewhere in the application's source tree. A matching package to the traget MVC group insrc/main
is what is preferred. However, if the View is distributed in byte code form the make sure to place the jar that contains the View inside the application's lib
directory. Alternatively you can use the Dependency DSL if the jar is available from a jar file repository (such as Maven or Ivy). Lastly, make sure that you have added the jar that contains GroupLayout
, Matisse's work horse. this is easily accomplished by adding the following confuration in griffon-app/conf/BuildConfig.groovy
griffon.project.dependency.resolution = { repositories { // enable this option in an existing 'repositories' block mavenCentral() } dependencies { // add this to an existing 'dependencies' block compile 'org.swinglabs:swing-layout:1.0.3' } }
#2 Convert the View into a Script
Griffon includes a script commmand target that can read a Matisse View and generate a Groovy View Script from it:generate-view-script
. Execute the command by specifying the name of the Java class that defines the Matisse View, like thisgriffon generate-view-script sample.LoginDialog
griffon-app/views/sample/LoginDialogView.groovy
with the following contents// create instance of view object widget(new LoginDialog(), id:'loginDialog')noparent { // javax.swing.JTextField usernameField declared in LoginDialog bean(loginDialog.usernameField, id:'usernameField') // javax.swing.JPasswordField passwordField declared in LoginDialog bean(loginDialog.passwordField, id:'passwordField') // javax.swing.JButton okButton declared in LoginDialog bean(loginDialog.okButton, id:'okButton') // javax.swing.JButton cancelButton declared in LoginDialog bean(loginDialog.cancelButton, id:'cancelButton') } return loginDialog
#3 Tweak the generated View
From here on you can update the generated View as you see fit, for example by adding bindings to each field and actions to the buttonswidget(new LoginDialog(mainFrame, true), id:'loginDialog') noparent { bean(loginDialog.usernameField, id:'usernameField', text: bind(target: model, 'username')) bean(loginDialog.passwordField, id:'passwordField', text: bind(target: model, 'password')) bean(loginDialog.okButton, id:'okButton', actionPerformed: controller.loginOk) bean(loginDialog.cancelButton, id:'cancelButton', actionPerformed: controller.loginCancel) } return loginDialog
15.3.2 Abeille Forms Designer
Another interesting choice is Abeille Forms, which is supported via a Builder and a plugin. Abeille Forms includes a visual designer that arranges the widgets with either JGoodies FormLayout or the JDK's GridBagLayout. Integrating these kind of views is a bit easier than the previous ones, as Abeille Forms views are usually distributed in either XML or a binary format. The plugin provides a View node that is capable of reading both formats. Follow these steps to setup a View of this type.#1 Install the Abeille Forms plugin
As with any oher plugin, just call theinstall-plugin
command with the name of the plugingriffon install-plugin abeilleform-builder
#2 Place the form definition in your source code
If you have direct access to the files generated by Abeille's designer then place them somewhere undergriffon-app/resources
. Otherwise if the files are packaged in a jar, place the jar in your application's lib
directory. Alternatively you can use the Dependency DSL if the jar is available from a jar file repository (such as Maven or Ivy).#3 Use the formPanel node
As a final step you just need to use theformPanel
node in a regular Groovy View script. All of the form's elements will be exposed to the Script, which means you can tweak their bindings and actions too, like thisdialog(owner: mainFrame, id: "loginDialog", resizable: false, pack: true, locationByPlatform:true, iconImage: imageIcon('/griffon-icon-48x48.png').image, iconImages: [imageIcon('/griffon-icon-48x48.png').image, imageIcon('/griffon-icon-32x32.png').image, imageIcon('/griffon-icon-16x16.png').image]) { formPanel('login.xml') noparent { bean(model, username: bind{ usernameField.text }) bean(model, password: bind{ passwordField.text }) bean(okButton, actionPerformed: controller.loginOk) bean(cancelButton, actionPerformed: controller.loginCancel) } }
15.3.3 XML
Yet another option to externalize a View is a custom XML format that closely ressembles the code that you can find in a Groovy View script. Why XML you ask? Well because it is a ver popular format choice still, some developers prefer writing declarative programming with it. This option is recommended to be paired with Java views, just because if you're writing a Groovy View it makes more sense to use Groovy to write the whole instead. Follow these steps to get it done.#1 Change the Java View class
A typical Java View class will extend from AbstractGriffonView. This super class defines a method namedbuildViewFromXml()
that takes a Map as its sole argument. This map should contain all variables that the builder may require to wire the View, such as 'app', 'controller' and 'model' for example.package sample;import java.util.Map;import org.codehaus.griffon.runtime.core.AbstractGriffonView;public class SampleView extends AbstractGriffonView { private SampleController controller; private SampleModel model; public void setController(SampleController controller) { this.controller = controller; } public void setModel(SampleModel model) { this.model = model; } public void mvcGroupInit(Map<String, Object> args) { buildViewFromXml(args); } }
#2 Define the XML view
ThebuildViewFromXml()
method expects an XML file whose name matches the name of the class from where it's called, in this case it should be SampleViw.xml
. Make sure to place the following contents in griffon-app/resources/sample/SampleView.xml
<application title="app.config.application.title" pack="true"> <actions> <action id="'clickAction'" name="'Click'" closure="{controller.click(it)}"/> </actions> <gridLayout cols="1" rows="3"/> <textField id="'input'" columns="20" text="bind('value', target: model)"/> <textField id="'output'" columns="20" text="bind{model.value}" editable="false"/> <button action="clickAction"/> </application>
application(title: app.config.application.title, pack: true) { actions { action(id: 'clickAction', name: 'Click', closure: {controller.click(it)}) } gridLayout(cols: 1, rows: 3) textField(id: 'input', text: bind('value', target: model), columns: 20) textField(id: 'output', text: bind{model.value}, columns: 20, editable: false) button(action: clickAction) }
15.4 Creating Bindings in Java
Bindings are an effective way to keep two properties in sync. Unfortunately Java does not provide a mechanism nor an API to make bindings, but Griffon does.As shown in the Binding section, Griffon relies onPropertyChangeEvent
and PropertyChangeListener
to keep track of property changes and notify observers. Swing components are already observable by default. You can build your own observable classes by following a convention, or implement the Observable interface (there's a handy partial implementation in AbstractObservable that you can subclass).Bindings can be created by using BindUtils.binding(), like the following example showspackage sample;import java.util.Map; import groovy.util.FactoryBuilderSupport; import griffon.swing.BindUtils; import org.codehaus.griffon.runtime.core.AbstractGriffonView;public class SampleView extends AbstractGriffonView { private SampleController controller; private SampleModel model; public void setController(SampleController controller) { this.controller = controller; } public void setModel(SampleModel model) { this.model = model; } public void mvcGroupInit(Map<String, Object> args) { buildViewFromXml(args); FactoryBuilderSupport builder = getBuilder(); /* * Equivalent Groovy code * bind(source: input, sourceProperty: 'text', * target: model, targetProperty: 'value') */ BindUtils.binding() .withSource(builder.getVariable("input")) .withSourceProperty("text") .withTarget(model)) .withTargetProperty("value") .make(builder); /* * Equivalent Groovy code * bind(source: model, sourceProperty: 'value', * target: input, targetProperty: 'text') */ BindUtils.binding() .withSource(model) .withSourceProperty("value") .withTarget(builder.getVariable("output")) .withTargetProperty("text") .make(builder); } }
- both
source
andtarget
values must be specified. AnIllegalArgumentException
will be thrown if that's not the case. - both
source
andtarget
instances must be observable. This does not imply that both must implement Observable per se, as Swing components do not. - either
sourceProperty
ortargetProperty
can be omitted but not both. The missing value will be taken from the other property. - the
builder
instance must be able to resolve thebind()
node. This is typically the case for the default builder supplied to Views (because Swingbuilder is included).
mutual
, converter
and validator
. The next snippet improves on the previous example by setting a converter and a validator, only numeric values will be accepted.package sample;import java.util.Map; import groovy.util.FactoryBuilderSupport; import griffon.swing.BindUtils; import griffon.util.CallableWithArgs; import org.codehaus.griffon.runtime.core.AbstractGriffonView;public class SampleView extends AbstractGriffonView { private SampleController controller; private SampleModel model; public void setController(SampleController controller) { this.controller = controller; } public void setModel(SampleModel model) { this.model = model; } public void mvcGroupInit(Map<String, Object> args) { buildViewFromXml(args); FactoryBuilderSupport builder = getBuilder(); /* * Equivalent Groovy code * bind(source: input, sourceProperty: 'text', * target: model, targetProperty: 'value', * converter: {v -> v? "FOO $v" : 'BAR'}, * validator: {v -> * if(v == null) true * try { Integer.parseInt(String.valueOf(v)); true } * catch(NumberFormatException e) { false } * }) */ BindUtils.binding() .withSource(builder.getVariable("input")) .withSourceProperty("text") .withTarget(model) .withTargetProperty("value") .withConverter(new CallableWithArgs<String>() { public String call(Object[] args) { return args.length > 0 ? "FOO "+ args[0] : "BAR"; } }) .withValidator(new CallableWithArgs<Boolean>() { public Boolean call(Object[] args) { if(args.length == 0) return Boolean.TRUE; try { Integer.parseInt(String.valueOf(args[0])); return Boolean.TRUE; } catch(NumberFormatException e) { return Boolean.FALSE; } } }) .make(builder); /* * Equivalent Groovy code * bind(source: model, sourceProperty: 'value', * target: input, targetProperty: 'text') */ BindUtils.binding() .withSource(model) .withSourceProperty("value") .withTarget(builder.getVariable("output")) .withTargetProperty("text") .make(builder); } }
<application title="app.config.application.title" pack="true"> <actions> <action id="'clickAction'" name="'Click'" closure="{controller.click(it)}"/> </actions> <gridLayout cols="1" rows="3"/> <textField id="'input'" columns="20"/> <textField id="'output'" columns="20" editable="false"/> <button action="clickAction" </application>