Dashboard > Spring Rich Client Project (Spring Rich) > ... > Contributions > Master-Detail Forms
  Spring Rich Client Project (Spring Rich) Log In View a printable version of the current page.  
  Master-Detail Forms
Added by Larry Streepy, last edited by Larry Streepy on Oct 03, 2005  (view change)
Labels: 
(None)

Master/Detail Forms

Common among many applications is the need to handle a master/detail relationship. Typically, the master data set is either a table or a tree and the detail is used to edit attributes of an object selected in the master set. Although there is no obvious support for master/detail forms within Spring RCP, there are underlying functions in AbstractForm that support the implementation of a master/detail form model.

Code Updates:

  • The code has now been checked into the sandbox, please get the code from CVS.
  • The latest code provides a number of enhancements, including support for sorted and filtered master data sets.
  • 03 Oct 2005 - constructor rework to remove the need to create the form model by hand.

Implementation Notes

The implementation consists of several files:

  • AbstractMasterForm - The abstract base class for master forms. It takes care of the handling of the list of elements that make up the master set and the interactions with the detail form when one of the master elements is selected/updated. It also provides a mechanism to add a new detail element.
  • AbstractDetailForm - The abstract base class of the detail form. This form is used to edit an element from the master list. The operation of this form is controlled by the master form. It contains "Save", "Revert", and "Cancel" buttons for managing the state of the data on the page.
  • AbstractTableMasterForm - an implementation of AbstractMasterForm that renders the master set as a table (using a GlazedTableModel). The columns displayed are specified as property names.
  • DeepCopyBufferedCollectionValueModel - Since the master/detail relationship inherently deals with a set of objects, there needs to be some way to deal with changes to the elements of the set in a buffered fashion. This class provides that buffering.
  • ObservableEventList - an implementation of a glazed EventList that implements the ObservableList interface. This is the object used to handle the master data set (and is constructed automatically by the master form).

Usage Examples

Below are samples of how these components would be used in a real application:

Assuming that we have a large object (Physician) that contains numerous complex properties (including lists of data that should be managed by a master/detail form), we start by creating a proper value model and form model. In this example, the Physician object contains a property, degree, that is an array of Degree objects that we want to handle using a master/detail form arrangement:

final Physician phys = getSelectedPhysician();
physFormModel = FormModelHelper.createFormModel( phys );

Form form = new EducationForm( physFormModel, "degree", Degree.class );

compositePage.addForm( labelPage, form );

It makes use of a master/detail form, EducationForm, that combines both the master and detail implementation. These could be in two classes, but it is often simpler to maintain them together. The code looks like this:

public class EducationForm extends AbstractTableMasterForm {

    public static final String FORM_NAME = "education";

    /**
     * Construct a new EducationForm using the given parent model and detail object type.
     * 
     * @param parentFormModel Parent form model
     * @param property containing this forms data (must be a collection or an array)
     */
    public EducationForm(HierarchicalFormModel parentFormModel, String property) {
        super( parentFormModel, property, FORM_NAME, Degree.class );
        setSortProperty( "credentialName" );
    }

    /**
     * Get the property names to show in columns of the master table.
     * 
     * @return String[] array of property names
     */
    protected String[] getColumnPropertyNames() {
        return new String[] { "credentialName", "credentialType", "awardingInstitution", "year" };
    }

    /*
     * (non-Javadoc)
     * @see com.fhm.pdbm.ui.form.AbstractMasterForm#createDetailForm(org.springframework.binding.form.NestingFormModel,
     *      org.springframework.binding.value.ValueModel)
     */
    @Override
    protected AbstractDetailForm createDetailForm(HierarchicalFormModel parentFormModel, ValueModel valueHolder,
            ObservableList masterList) {

        return new AbstractDetailForm( parentFormModel, "educationDetail", valueHolder, masterList ) {
            protected JComponent createFormControl() {
                final PdbBindingFactory bf = (PdbBindingFactory) getBindingFactory();
                TableFormBuilder formBuilder = new TableFormBuilder( bf );
                formBuilder.setLabelAttributes( "colGrId=label colSpec=right:pref" );

                formBuilder.row();
                formBuilder.add( bf.createBoundComboBox( "credentialType", MasterLists.CREDENTIAL_TYPE ),
                    "colSpan=1 align=left" );
                formBuilder.add( "credentialName", "colSpan=1 align=left" );
                formBuilder.row();
                formBuilder.add( "awardingInstitution", "colSpan=1 colSpec=f:150dlu:g" );
                JComponent yearField = formBuilder.add( "year", "colSpan=1 align=left" )[1];
                ((JTextField) yearField).setColumns( 5 );
                formBuilder.row();
                formBuilder.add( "country", "colSpan=1 align=left" );
                formBuilder.row();
                formBuilder.addTextArea( "notes", "colSpan=2" );

                formBuilder.row();
                formBuilder.row();
                formBuilder.getLayoutBuilder().cell( createButtonBar() );

                updateControlsForState();

                return formBuilder.getForm();
            }
        };
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.richclient.form.AbstractForm#createFormControl()
     */
    @Override
    protected JComponent createFormControl() {
        JComponent comp = super.createFormControl();

        TableColumnModel tcm = getMasterTable().getColumnModel();
        tcm.getColumn( 0 ).setPreferredWidth( 100 );
        tcm.getColumn( 1 ).setPreferredWidth( 10 );
        tcm.getColumn( 2 ).setPreferredWidth( 300 );
        tcm.getColumn( 3 ).setPreferredWidth( 10 );

        return comp;
    }
}

The final form (contained in a tree composite dialog) looks like this: master-detail.png

Message Configuration

A number of buttons and commands are created by the master/detail forms. You must configure the messages (labels) used for the various buttons. Here is an example of the entries that I have in my messages.properties file:

save.label=Save
revert.label=Revert
cancelNew.label=Cancel
newDegreeCommand.label=New Degree
deleteDegreeCommand.label=Delete

Note: The use of "Degree" (in the new and delete commands) is the short name of the detail object type declared in the master form. So you would need to create one pair of entries for every type of detail object being handled in your master/detail forms.

The master form can show a couple of different dialogs:

1. A dialog is shown when the user tries to change the selection or create a new object when the current detail form has unsaved changes (is dirty). In this case, a dialog is shown asking the user to confirm the change (and the loss of the unsaved changes). The message properties used (and example text) for this dialog are:

masterForm.dirtyChange.title=Unsaved Changes
masterForm.dirtyChange.message=Selecting a different item will cause you to lose your unsaved changes.\nAre you sure you want to select a different item?

masterForm.dirtyNew.title=Unsaved Changes
masterForm.dirtyNew.message=Creating a new item will cause you to lose your unsaved changes.\nAre you sure you want to do this?

2. The master form will, conditionally, query the user to confirm a delete operation. This is configured with the setConfirmDelete(boolean) method on AbstractMasterForm. If confirmation is set true, then a confirmation dialog is presented prior to deleting the selected items. The message properties used (and example text) for this dialog are:

masterForm.confirmDelete.title=Confirm Delete
masterForm.confirmDelete.message=Are you sure you want to delete these items?

To further customize the confirmation message, take a look at AbstractMasterForm.getConfirmDeleteMessage. You can override this in a subclass to include any information you'd like in the message.

3. The master table headers are configured based on the master formId "education" and the properties put in the table. Example:

education.credentialName.label=Credential Name
education.credentialType.label=Type
education.awardingInstitution.label=Institution
education.year.label=Year

4. The detail form labels are configured as usual, based on the name of the form id, ie "educationDetail"

educationDetail.credentialName.label=Credential Name
educationDetail.credentialType.label=Type
educationDetail.awardingInstitution.label=Institution
educationDetail.country.label=Country

Alternate Master Implementations

In order to show how derived master forms can control the layout and rendering of the master set and detail forms, I've put together a SimpleListMasterForm that renders the master set as a JList and combines the master and detail forms together with a row of buttons below them.

The code is here: SimpleListMasterDetailForm.java

An example of what this might look like inside another form is here: ListMasterDetail.png

SimpleListMasterDetailForm is a nearly complete implementation, derived classes only need to provide the definition of the detail form. Here's a sample of code that uses SimpleListMasterDetailForm and uses an anonymous class for the detail form.

/**
 * This form provides a reusable template for entering contact information using a
 * simplified master/detail form to handle the data.
 * 
 * @author lstreepy
 * 
 */
public class GenericContactForm extends SimpleListMasterDetailForm {

    public static final String FORM_NAME = "contact";

    /**
     * Construct a new GenericContactForm using the given parent model and detail object
     * type.
     * 
     * @param parentFormModel Parent form model
     * @param property containing this forms data (must be a collection or an array)
     */
    public GenericContactForm(HierarchicalFormModel parentFormModel, String property) {
        super( parentFormModel, property, FORM_NAME, Contact.class );
        setCellRenderer( new JoiningBeanPropertyValueListRenderer( new String[] { "typeId", "data" }, ": " ) );
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.richclient.form.AbstractMasterForm#createDetailForm(org.springframework.binding.form.NestingFormModel,
     *      org.springframework.binding.value.ValueModel)
     */
    @Override
    protected AbstractDetailForm createDetailForm(HierarchicalFormModel parentFormModel, ValueModel valueHolder,
            ObservableList masterList) {
        return new AbstractDetailForm( parentFormModel, "contactDetail", valueHolder, masterList ) {

            @Override
            protected JComponent createFormControl() {
                final PdbBindingFactory bf = (PdbBindingFactory) getBindingFactory();
                TableFormBuilder formBuilder = new TableFormBuilder( bf );
                formBuilder.setLabelAttributes( "colGrId=label colSpec=right:pref" );

                formBuilder.row();
                formBuilder.add( bf.createBoundComboBox( "typeId", MasterLists.CONTACT_TYPE ), "align=left" );
                formBuilder.row();
                formBuilder.add( "data" );

                updateControlsForState();

                return formBuilder.getForm();
            }
        };
    }

    /**
     * Get the number of visible rows to display on the list.
     * 
     * @return number of rows to show
     */
    public int getVisibleRowCount() {
        return 4;
    }
}

Site running on a free Atlassian Confluence Open Source Project License granted to Spring Framework. Evaluate Confluence today.
Powered by Atlassian Confluence, the Enterprise Wiki. (Version: 2.5.5 Build:#811 Jul 25, 2007) - Bug/feature request - Contact Administrators