Wednesday, March 5, 2008

Experimenting with FCM

Stephen Colebourne has a prototype implementation of the FCM closures proposal. I took some time to download and play with the new syntax and functionality.

I tried to focus on some of the functionality FCM provides that other closure proposals do not. Namely method and field literals. I think FCM can help create flexible models for swing components. In the past I've done this using plain reflection but FCM makes things easier.

This is a screenshot of the demo application.

It displays three lists and one table all of which are backed by the same collection of SuperHero beans.

I found that I could apply FCM in one of the first few lines of my demo.
SwingUtilities.invokeLater(#startApp());
This calls my startApp() method on the event dispatch thread. I have to admit that while I'm not a fan of in-lining this was preferable to the traditional method of building a Runnable and passing that to invokeLater.

Here is a very basic ListModel that can expose any public field of an object.
   /**
* Wraps a list of elements and exposes one field to the list model
* interface
*
* @param <E>
*/
private class FCMFieldListModel<T> extends AbstractListModel {

private List<T> backingList;
private Field targetField;

public FCMFieldListModel(List<T> data, Field target) {
backingList = data;
targetField = target;
}

@Override
public Object getElementAt(int index) {
try {
return targetField.get(backingList.get(index));
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}

@Override
public int getSize() {
return backingList.size();
}

}
In my case I'm building a list of superheroes and wish to expose the last name field.

FCMFieldListModel<SuperHero> lastListModel = new FCMFieldListModel<SuperHero>(heroes, SuperHero#lastName);

The last parameter is the new field literal syntax from FCM. It allows me a type safe way of specifying which field I want to expose.

To create more list models that expose other fields I just need to create another FCMFieldListModel and expose a different field.

FCMFieldListModel<SuperHero> listModel = new FCMFieldListModel<SuperHero>(heroes, SuperHero#firstName);

Now most of the time a bean will not expose a field. Instead getter/setter methods are used to expose this information. FCM has method literals as well as field literals. The code for a FCMMethodListModel is very much like FCMFieldListModel but it takes a Method reference instead of a field refrence.

/**
* Wraps a list of elements and exposes one method to the list model
* interface
*
* @param <E>
*/
private class FCMMethodListModel<E> extends AbstractListModel {

private List<E> backingList;
private Method targetMethod ;

public FCMMethodListModel(List<E> data, Method target) {
backingList = data;
targetMethod = target;
}

@Override
public Object getElementAt(int index) {
try {
Object item = backingList.get(index);
return targetMethod.invoke(item);
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}

@Override
public int getSize() {
return backingList.size();
}

}
Basically this lets you tell the list model which method of your bean to call to represent your bean in the list.

Finally I used this same technique to build a table model.
FCMTableModel<SuperHero> superTableModel = new FCMTableModel<SuperHero>(heroes,
SuperHero#getFirstName(), SuperHero#getLastName(),
SuperHero#getHeroName(), SuperHero#getFirstAppearance());

The constructor takes a list of SuperHeroes and a variable list of methods to use as columns. This code is all it takes to populate the table seen in the center of the screenshot.

The first appearance column of the super hero table is a date without a day. I decided to use an anonymous inner method to implement a cellrendrer to format the date.
TableColumn dateColumn = superHeroTable.getColumn(SuperHero#getFirstAppearance().getName());

// in-line cell renderer for the date column of the superhero table
dateColumn.setCellRenderer(#(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column){
if(isSelected){
dateRenderer.setBackground(UIManager.getColor("Table.selectionBackground"));
}else{
dateRenderer.setBackground(UIManager.getColor("Table.background"));
}
dateRenderer.setText(sdfDateColumn.format((Date) value));
return dateRenderer;
});
Overall I have to say I like FCM. Even in it's incomplete state I feel it's still a step forward. The method and field literals were very convenient and useful. I can see myself creating generalized adapter classes around this functionality. As I said before I'm not a fan of in-lining code. I tend not to in-line inner classes either. But the anonymous inner method syntax seemed straight forward enough. Also I much prefer using the # syntax to pass method to invokeLater.

My examples involved a bean and it's associated methods. Yet there is no way currently to specify that a method used as a parameter come from any specific class. It certainly would be an error to pass a FCMMethodListModel System.out#println(Object). I think this is one of the items on the list of things to do for FCM but I'm not certain.

I've made the source code for this demo available on google docs. Just remember that you need the prototype FCM implementation to run it.

Finally I'd just like to thank
Stephen for all his hard work in creating this prototype. It's been fun to experiment with it.

Note:
This is demo level code. It was written to play around with FCM and a such the models are not as robust as they could be. Their job is to adapt an unchanging list of beans using FCMs method literals. I will probably revisit FCMTableModel at some point to show how it could be enhanced. As it stands now they have no support for events. Also I'm not sure a cell renderer was a good choice for an anonymous inner method example. If you read the javadoc for DefaultTableCellRenderer it talks about overriding some of the JLabel methods for performance reasons.

No comments: