Saturdays are for coding, right...... Well I had a small epiphany last night and I came up with a solution for the master/detail problem.
I'm more than happy to share the code, but I wanted to validate my approach with the powers that be.
1. I created a DeepCopyBufferedCollectionValueModel. This is only needed if you, like me, have a complex properties dialog where multiple "saves" on the detail form can be cancelled when the containing dialog is cancelled.
2. The DCBCVM uses serialization to implement the copy operation. I couldn't think of a more general mechanism. Obviously, if this code is ever to make it into the RCP code base, I'd probably need to abstract this out to some form of DeepCopyStrategy object.
3. I construct a child CompoundFormModel directly so I can construct the DCBCVM for it to use. Like this:
Code:
ValueModel degreeVM = physFormModel.getPropertyAccessStrategy().getPropertyValueModel("degree");
DeepCopyBufferedCollectionValueModel detailVM = new DeepCopyBufferedCollectionValueModel( degreeVM, degreeVM.getValue().getClass() );
CompoundFormModel subFormModel = new CompoundFormModel(detailVM);
physFormModel.addChildModel("degree", subFormModel);
4. In the master form, I extract the ListListModel availble from DCBCVM.getValue() and use it to construct the master table and I give it to the detail form, which installs it using setEditableFormObjects.
5. Finally, in the postEditCommitted method I tell the table model that a row has been updated.
That seems to be all that is needed - at least for this simple first try at creating a master/detail form.
Here is the actual code for the master/detail form. It's currently a single class with a nested class implementing the detail form. I'll be refactoring this code into a generalized master form and detail form later this weekend. But, I wanted to get any feedback on the approach as early as possible.
Thanks.
Code:
/**
*
*/
package com.fhm.pdbm.ui;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.table.TableColumnModel;
import org.springframework.binding.form.ConfigurableFormModel;
import org.springframework.binding.form.FormModel;
import org.springframework.binding.form.NestingFormModel;
import org.springframework.binding.value.support.ValueHolder;
import org.springframework.richclient.command.AbstractCommand;
import org.springframework.richclient.command.CommandGroup;
import org.springframework.richclient.form.AbstractForm;
import org.springframework.richclient.form.binding.swing.SwingBindingFactory;
import org.springframework.richclient.form.builder.TableFormBuilder;
import org.springframework.richclient.list.ListListModel;
import org.springframework.richclient.table.ListSelectionListenerSupport;
import org.springframework.richclient.table.support.GlazedTableModel;
import org.springframework.richclient.util.GuiStandardUtils;
import ca.odell.glazedlists.BasicEventList;
import ca.odell.glazedlists.EventList;
import com.fhm.pdbm.domain.beans.Degree;
/**
* Form to handle a physician's education, honors, and awards
*
* @author lstreepy
*/
public class EducationForm extends AbstractForm {
public static final String FORM_NAME = "education";
/**
* @param pageFormModel
*/
public EducationForm(NestingFormModel formModel) {
super( formModel, FORM_NAME );
_formModel = formModel;
}
/*
* (non-Javadoc)
*
* @see org.springframework.richclient.form.AbstractForm#createFormControl()
*/
@Override
protected JComponent createFormControl() {
ListListModel detailItems = getListListModel();
EventList eventList = new BasicEventList();
eventList.addAll( detailItems );
String[] colProps = new String[] { "credentialName", "credentialType", "awardingInstitution", "year" };
_masterTableModel = new GlazedTableModel( eventList, getMessageSource(), colProps );
JTable table = new JTable( _masterTableModel );
table.setSelectionMode( ListSelectionModel.SINGLE_SELECTION );
TableColumnModel tcm = table.getColumnModel();
tcm.getColumn( 0 ).setPreferredWidth( 100 );
tcm.getColumn( 1 ).setPreferredWidth( 10 );
tcm.getColumn( 2 ).setPreferredWidth( 300 );
tcm.getColumn( 3 ).setPreferredWidth( 10 );
JScrollPane sp = new JScrollPane( table );
// Setup our selection listener so that it controls the detail form
table.getSelectionModel().addListSelectionListener( new TableSelectionHandler() );
// Now we need to construct a subform and model to handle the detail
// elements of this master table
_detailFormModel = _formModel.createChild( "credentialDetail", new ValueHolder( new Degree() ) );
_detailForm = new CredentialDetailForm( _detailFormModel, detailItems );
// Now put the two forms into a split pane
JSplitPane splitter = new JSplitPane( JSplitPane.VERTICAL_SPLIT );
splitter.add( sp );
splitter.add( _detailForm.getControl() );
splitter.setResizeWeight( .5d );
final SwingBindingFactory sbf = (SwingBindingFactory) getBindingFactory();
TableFormBuilder formBuilder = new TableFormBuilder( sbf );
formBuilder.getLayoutBuilder().cell( splitter );
return formBuilder.getForm();
}
/**
* @return properly typed value model
*/
private ListListModel getListListModel() {
return (ListListModel)getFormModel().getFormObjectHolder().getValue();
}
private NestingFormModel _formModel;
private ConfigurableFormModel _detailFormModel;
private CredentialDetailForm _detailForm;
private GlazedTableModel _masterTableModel;
/**
* Inner class to handle the table selection and installing the selection
* into the detail form.
*/
private class TableSelectionHandler extends ListSelectionListenerSupport {
/**
* Called when nothing gets selected. Override this method to handle
* empty selection
*/
protected void onNoSelection() {
_detailForm.setSelectedIndex(-1);
}
/**
* Called when the user selects a single row. Override this method to
* handle single selection
*
* @param index the selected row
*/
protected void onSingleSelection(int index) {
_detailForm.setSelectedIndex( index );
}
}
/**
* Inner class to handle the detail side of the master/detail view.
*/
private class CredentialDetailForm extends AbstractForm {
public static final String FORM_NAME = "credentialDetail";
/**
* @param pageFormModel
*/
public CredentialDetailForm(FormModel formModel, ListListModel editableItemList) {
super( formModel, FORM_NAME );
// Install the detail data as our editable object list
setEditableFormObjects( editableItemList );
setEditingFormObjectIndexHolder( _indexHolder );
}
@Override
protected JComponent createFormControl() {
final SwingBindingFactory sbf = (SwingBindingFactory) getBindingFactory();
TableFormBuilder formBuilder = new TableFormBuilder( sbf );
formBuilder.row();
formBuilder.add( "credentialName" );
formBuilder.add( "credentialType" );
formBuilder.row();
formBuilder.add( "awardingInstitution" );
// formBuilder.add("year");
formBuilder.add( "country" );
formBuilder.row();
formBuilder.row();
formBuilder.getLayoutBuilder().cell( createButtonBar() );
return formBuilder.getForm();
}
/**
* Set the selected object index.
*
* @param index of selected item
*/
public void setSelectedIndex(int index) {
_indexHolder.setValue( new Integer( index ) );
}
/**
* Commit this forms data back to the master table. Let our super class
* do all the work and then just inform our master table that the value
* has changed.
*/
public void postEditCommitted(Object formObject) {
super.postEditCommitted( formObject );
int index = getEditingFormObjectIndex();
EducationForm.this._masterTableModel.fireTableRowsUpdated( index, index );
}
protected String getRevertCommandFaceDescriptorId() {
return "undo";
}
protected String getCommitCommandFaceDescriptorId() {
return "save";
}
protected final JButton createRevertButton() {
return (JButton) getRevertCommand().createButton();
}
/**
* Return a standardized row of command buttons, right-justified and all
* of the same size, with OK as the default button, and no mnemonics
* used, as per the Java Look and Feel guidelines.
*/
protected JComponent createButtonBar() {
_formCommandGroup = CommandGroup.createCommandGroup( null, new AbstractCommand[] { getRevertCommand(),
getCommitCommand() } );
JComponent buttonBar = _formCommandGroup.createButtonBar();
GuiStandardUtils.attachDialogBorder( buttonBar );
return buttonBar;
}
private ValueHolder _indexHolder = new ValueHolder( new Integer( -1 ) );
private CommandGroup _formCommandGroup;
}
}