13. Tips and Tricks

13.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 section 5.7.2 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.

When the user clicks the Submit button a dialog appears

Believe it or not both the View and the Controller know nothing about the specific property names found in the Model. Let's have a look at the Model first

package simple

@Bindable class SimpleModel { String firstName String lastName String address }

There are 3 observable properties defined in the Model. Note the usage of default imports to avoid an extra line for importing groovy.beans.Bindable. Now, the Controller on the other hand defines two actions

package simple

import griffon.util.GriffonNameUtils as GNU

class 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') ) } }

The 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 View

package simple

import griffon.util.GriffonNameUtils as GNU

application(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') } } }

The View also iterates over the Model's property names in order to construct the form. It follows a similar approach to dynamically discover the actions that the Controller exposes.

You must install the MigLayout Plugin before running this application.

13.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 the create-* 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 group

griffon create-app simple -fileType=java

The fileType switch indicates that the templates must pick a Java based template first. If no suitable template is found then a Groovy based template will be used. The setting of this flag is saved in the application's build configuration, this way you don't need to specific the fileType switch again if your intention is to create another artifact of the same type. Of course you can specify the flag at any time with a different value. It's worth mentioning that the default Groovy based template will be used if a suitable template for the specified fileType cannot be found.

Peeking into each member of the simple MVC group we find the following code. First the Model

package 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); // } }

Next is the Controller

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) { } */ }

And finally the View

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)); } }

13.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.

13.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 in src/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 this

griffon generate-view-script sample.LoginDialog

This command should generate the file 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 buttons

widget(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

13.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 the install-plugin command with the name of the plugin

griffon 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 under griffon-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 the formPanel 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 this

dialog(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)
    }
}

13.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 named buildViewFromXml() 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

The buildViewFromXml() 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>

Notice that every string literal value must be escaped with additional quotes otherwise the builder will have trouble setting the appropriate values. When the time comes for the builder to parse the view it will translate the XML to a Groovy scritpt then interpret it. This is the generated Groovy script that matches the previous XML definition

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)
}

13.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 section 6.2 Binding, Griffon relies on PropertyChangeEvent 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 shows

package 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); } }

The following rules apply:

Bindings created in this way also support the following attributes: 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); } }

The View for these examples is defined in XML format (as described in the previous section)

<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>

However you can build the View in any way, bindings do not require an specific View construction mechanism in order to work.