Wednesday, September 17, 2008

My First Griffon App

Sorry it's been so long since I posted here. Work keeps me busy.

Recently I had been given several GB of raw data from our two most recent scientific drilling expeditions in Antarctica. This data needs a fair amount of quality control processing to turn it into a usable datasets for the scientists. To do this, I needed to write a tool for the drillers to interactively plot and explore the data to determine regions of interest. Given the recent buzz about Griffon, I thought I'd give it a try.

I started by downloading and installing Griffon. Once I had everything setup, I created an app:

griffon create-app DrillingAnalytics

If you've done any Grails development, this will be a familiar idiom to you. The results of this command is a straightforward directory structure, focused around the MVC pattern. You'll recognize directories for models, views, and controllers.

My next step was to flesh out my model. When you create an app, Griffon automatically creates a model class called ${}Model (DrillingAnalyticsModel for me) in the griffon-app/models directory. The main purpose of my app is to plot time series data so I defined two fields, startDate and endDate in my model:

import groovy.beans.Bindable

class DrillingAnalyticsModel {
@Bindable String startDate = "2006-11-07 00:00"
@Bindable String endDate = "2006-11-08 00:00"

You'll notice the @Bindable annotations on these fields. These fields will tie to these components in the UI, and the @Bindable annotation will automatically take care of keeping the UI in sync with the model via PropertyChangeEvents.

The model class is also where you can put other fields to maintain applications state:
def plot = new CombinedDomainXYPlot(new DateAxis())
def subplots = []
def chart = new JFreeChart(null, JFreeChart.DEFAULT_TITLE_FONT, plot, false)

With the model sorted out, I moved on to developing the view. As with model, Griffon creates a ${}View class for you in the griffon-app/views directory. Griffon puts the full power of SwingBuilder, SwingXBuilder, and GraphicsBuilder (with more on the way) at your fingertips for developing the UI.

I spent the majority of my time on the UI. It was a seemingly endless cycle of tweaking the code and testing with griffon run-app to get it to look the way I wanted. This is no knock on Griffon; writing Java UIs, especially by hand, just plain sucks.

After far too long trying to get the standard Java layout managers to do what I want, I did myself a favor and downloaded MigLayout. Despite not being built into SwingBuilder, MigLayout integrates nicely with SwingBuilder:

application(title:'Drilling Analytics', pack:true, locationByPlatform:true) {
panel(layout: new MigLayout('fill')) {
// chart panel
widget(chartPanel, constraints:'span, grow')

// our runs and time
panel(layout: new MigLayout('fill'), border: titledBorder('Time'), constraints: 'grow 100 1') {
scrollPane(constraints:'span 3 2, growx, h 75px') {
runs = list(listData: model.mis.keySet().toArray())
label('Start:', constraints: 'right, gapbefore 50px')
textField(id:"startDate", text: bind { model.startDate }, action: plotAction, constraints:'wrap, right, growx')
label('End:', constraints: 'right, top')
textField(id:"endDate", text: bind { model.endDate }, action: plotAction, constraints:'wrap, right, top, growx')
label("+/-", constraints: 'right')
textField(id:"padding", text: "30", constraints: 'growx')
button(action: plotAction, constraints:'span 2, bottom, right')

// our plots panel
panel(layout: new MigLayout(), border: titledBorder('Plots'), constraints: 'grow 100 1') { { id, map ->
checkBox(id: id, selected: false, action: plotAction, text: map.title, constraints:'wrap')

SwingBuilder gets rid of all the boilerplate code and MigLayout makes it possible to code decent Java UIs by hand:

We've covered the Model and the View, now it's time to focus on the Controller. The controller mediates between the model and view. It contains all of the logic for handling events from the UI and manipulating the model.

One common pattern in the existing Griffon examples is the use of Swing Action objects to trigger actions from the UI. My UI was pretty simple so I could reuse a single action on all of the components to refresh the plot:

actions {
action(id: 'plotAction',
name: 'Update',
closure: controller.plot)

I put this code in my DrillingAnalyticsView class, but it could just as easily be defined in its own file and imported into the view via the build() method. You'll notice that I give the action an id--plotAction--which I use to reference it from the components:

button(action: plotAction, constraints:'span 2, bottom, right')

You can also see that the action just delegates to the controller.plot closure. This is convenient because it keeps all of the logic in one place and the controller has access to both the model and view. The actual code of the controller.plot is unremarkable. The big consideration is to properly manage your threading. Don't do long running actions in the EDT as it will freeze the UI, and don't update the UI from outside the EDT as Swing is not thread safe. Andres Almiray has a good description of how Griffon makes this easy.

Since my app is fairly niche (I doubt there's many of you visualizing drilling data), I'm not going to post the whole source code here. However, I want to point out that the source code consists of just 327 lines of code, and that's including blank lines and comments! The bulk of that code is the logic to query the database and update the JFreeChart plots. This truly demonstrates how simple and easy it is to build an app with Griffon.

If you're looking for more Griffon examples, check out the samples included in the samples directory of the Griffon distribution, and keep an eye on Griffon posts


hansamann said...

I just tried a new griffon 0.1.2 app with MigLayout. I just placed the MigLayout in the lib directort and added teh MigLayout class. Unfortunately my views fail, is there a special trick other than adding the jar and importing the classes in the views?

Josh Reed said...

Is there an exception message or does it just not look right?

I've always just:
import net.miginfocom.swing.MigLayout

and then used new MigLayout() for the layout property on the app/view:

application(title:'Scheme Editor', layout: new MigLayout('fill'))

The only problem I've run into is if I try to use an incorrect constraint on a component, like 'fill' on a label.