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 firstpackage 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 actionspackage 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')
)
}
}
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 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')
}
}
}
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.
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 groupgriffon 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 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);
// }
}
Next is the Controllerpackage 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 Viewpackage 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));
}
}
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.
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 thisgriffon 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 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
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 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 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 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)
}
}
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 definitionapplication(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)
}
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 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);
}
}
The following rules apply:
- both
source
and target
values must be specified. An IllegalArgumentException
will be thrown if that's not the case.
- both
source
and target
instances must be observable. This does not imply that both must implement Observable per se, as Swing components do not.
- either
sourceProperty
or targetProperty
can be omitted but not both. The missing value will be taken from the other property.
- the
builder
instance must be able to resolve the bind()
node. This is typically the case for the default builder supplied to Views (because Swingbuilder is included).
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.