Geertjan is a DZone Zone Leader and has posted 468 posts at DZone. You can read more from them at their website. View Full User Profile

Porting to Griffon

09.15.2008
| 29231 views |
  • submit to reddit

The View

As can be seen in the previous screenshot, the original view consisted of two JFrames. The first provides the user interface for the Anagram game; the second provides the About box. Both were created in the Matisse GUI Builder, resulting in an application that is locked into NetBeans IDE.

The original view was very tightly connected to the code that evaluates the user's input and to the code used for dealing with the user's actions. Porting to Griffon means spending a lot of time unwiring your code and carefully considering which bit does what. The aim for the view is for it to be as "viewy" as possible, with no processing code of any kind cluttering it up.

A Griffon view can itself be split into three parts. First, there's the main view that the user sees. Supplemental views, such as, in this case, the About box, are defined in a separate file, ending in "Dialogs" (although that is currently not a rule, but just a convention that makes sense in the context of everything else). Similarly, all the actions find themselves defined in a separate file, with a name ending in "Actions". Why is all this necessary? So that you (and, especially, all the maintainers of your code) can know immediately where to find things, which is a central concern of "convention over configuration", but this time brought to the world of the Java desktop.

So, here's my view file, called "AnagramGameGriffonView.groovy", which is automatically created (and registered in the appropriate places) by means of the "griffon create-app" command: 

I rewrote the Anagram game user interface as follows, using the same LayoutManager as the original, i.e., GridBagLayout:

build(AnagramGameGriffonActions)

application(title:'Anagrams', minimumSize:[297, 200], location:[50,50],
    pack:true, locationByPlatform:true) {
    menuBar( id: 'menuBar') {
        menu(text: 'File', mnemonic: 'F') {
            menuItem(aboutAction)
            menuItem(exitAction)
        }
    }
    panel(border:emptyBorder(12)) {
        gridBagLayout()
        label(text: 'Scrambled Word:',
            constraints: gridBagConstraints(
                gridwidth: 1, gridheight: 1,
                fill: HORIZONTAL, anchor: WEST,
                weightx: 0.0, weighty: 0.0,
                insets: [0,0,12,6]))
        textField(id: 'scrambledWord', text: bind {model.scrambledWord},
            columns: 20, editable: false,
            constraints: gridBagConstraints(
                gridwidth: REMAINDER, gridheight: 1,
                fill: HORIZONTAL, anchor: CENTER,
                weightx: 1.0, weighty: 0.0,
                insets: [0,0,12,0]))
        label(text: 'Your Guess:',
            constraints: gridBagConstraints(
                gridwidth: 1, gridheight: 1,
                fill: HORIZONTAL, anchor: WEST,
                weightx: 0.0, weighty: 0.0,
                insets: [0,0,20,6]))
        textField(id: 'guessedWord',
            columns: 20,
            constraints: gridBagConstraints(
                gridwidth: REMAINDER, gridheight: 1,
                fill: HORIZONTAL, anchor: CENTER,
                weightx: 1.0, weighty: 0.0,
                insets: [0,0,20,0]))
        label(id:'feedbackLabel', text: bind {model.feedback},
            constraints: gridBagConstraints(
                gridx: 1, gridy: RELATIVE,
                gridwidth: REMAINDER, gridheight: 1,
                fill: HORIZONTAL, anchor: CENTER,
                weightx: 1.0, weighty: 0.0,
                insets: [0,0,20,0]))
        button(action: guessWordAction, constraints: gridBagConstraints(
                gridx: 1, gridy: RELATIVE,
                gridwidth: 1, gridheight: REMAINDER,
                fill: NONE, anchor: SOUTHEAST,
                weightx: 1.0, weighty: 1.0,
                insets: [0,0,0,6]))
        button(action: nextWordAction, constraints: gridBagConstraints(
                gridwidth: REMAINDER, gridheight: REMAINDER,
                fill: NONE, anchor: SOUTHEAST,
                weightx: 0.0, weighty: 1.0,
                insets: [0,0,0,0]))
        bean( model, guessedWord: bind {guessedWord.text} )
    }
}

All that GridBagLayout stuff is pretty cumbersome and I'd be happy with a different LayoutManager, but since I'm trying to replicate the original as closely as possible, I simply went along with this complexity. At some point I'd like to see how GroupLayout would work in this context, as well as MiGLayout.

Next, look at lines 19, 39, and 57. What's all that "binding" about? The properties "text: bind {model.scrambledWord}" and "text: bind {model.feedback}" bind the text of the textfield (in the first case) and the text of the feedback label (in the second case) to a property defined in the model. When the model changes, via something that happens in the controller, the texts of these two components will automatically be updated.

Now look again at the definitions of the two buttons above, as well as the line with which the above lines begin. As explained in "Flying with Griffon", the combination of that initial statement with the two "action:" declarations in the buttons, lets the buttons be filled with the descriptions specified in a separate file, which as indicated above is called "AnagramGameGriffonActions.groovy":

actions {

action( id: 'guessWordAction',
name: "Guess",
closure: controller.guessWord,
accelerator: shortcut('G'),
mnemonic: 'G',
shortDescription: "Guess the given scrambled word"
)

action( id: 'nextWordAction',
name: "New Word",
closure: controller.nextWord,
accelerator: shortcut('N'),
mnemonic: 'N',
shortDescription: "Move to the next word"
)

action( id: 'exitAction',
name: "Exit",
closure: controller.exit,
mnemonic: 'E',
)

action( id: 'aboutAction',
name: "About",
closure: controller.about,
mnemonic: 'A',
)

}

Above, you see the action descriptions for 4 actions, two ("guessWordAction" and "nextWordAction") are used in the main view of the Anagram game, while the other two are used for the menu items under the "File" menu (lines 5 to 10). Since we are still in the view, we are not concerned in any way with the action events. As you can see above, the "closure" property points to the controller (which is automatically defined within the configuration files as being "AnagramGameGriffonController"), which is where we'll later see what the actions in question actually do.

Finally, in the same way as the actions are defined in a list of closures, the same is true for dialogs. Here's the "AnagramGameGriffonDialogs" file, which in this case defines only one dialog:

dialog(
title: "About Anagram Game", minimumSize: [123,141],
preferredSize: [298,216], id: "aboutDialog",
modal: true, pack:true, locationByPlatform:true) {

panel(){
gridBagLayout()
textArea(
columns: 25,
editable: false,
lineWrap: true,
rows: 8,
text:"Anagrams\n\nCopyright (c) 2003 Irritable Enterprises, Inc.",
wrapStyleWord: true,
border: null,
focusable: false)
button(text: "Close", actionPerformed: {event ->
aboutDialog.dispose()
},
constraints: gridBagConstraints(
gridx: 0, gridy: 1,
gridwidth: 1, gridheight: 1,
fill: NONE, anchor: SOUTHEAST,
weightx: 0.0, weighty: 0.0,
insets: [0,0,0,0]))
}

}

Notice that the button's "actionPerformed" is here defined within the Dialogs file, instead of in the controller. That's kind of cheating, but since what needs to happen is so trivial, it can be found here instead of in the controller. (At least, that's my interpretation, based on one of the samples that came with the Griffon distribution.) Here I again used the GridBagLayout because that's what the original used too.

AttachmentSize
fig-1.png11.5 KB
fig-2.png11.43 KB
fig-3.png58.63 KB
fig-4.png11.42 KB
Published at DZone with permission of its author, Geertjan Wielenga.