Announcement Announcement Module
Collapse
No announcement yet.
subforms, master/detail, and commit problems Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • subforms, master/detail, and commit problems

    I've set up a master/detail form as part of a larger properties dialog (using the TreeCompositeDialogPage as the dialog container).

    The main formModel is a CompoundFormModel since many forms are used to edit portions of the object properties. One of these forms is page that contains two forms, one a master containing a table, and the other a detail form (extended from AbstractForm) that operates on a single object in the master table. On the detail form I have Save and Revert buttons (as built by AbstractForm).

    I'm running into trouble when I Save a change from the detail form:

    1. The master table is not updating until I change it's selection (I don't know what is needed to make it re-render immediately upon Save).

    2. This is the really critical one: when I Save a detail entry and then Cancel the entire dialog, the change has still been posted to the master object. I would assume that commiting a change in a submodel would not cause a commit of the parent, but that seems to be happening.

    I hope that I've given enough detail on the problem to get some advice. There are no examples of this kind of work, so I'm really flying blind. Any help would be appreciated.

    Thanks.
    Larry.

    In case it will help, here's the code of the master/detail form:

    Code:
    /**
     * 
     */
    package com.fhm.pdbm.ui;
    
    import java.awt.Dimension;
    import java.util.Arrays;
    
    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 org.springframework.util.Assert;
    
    import com.fhm.pdbm.domain.beans.Degree;
    
    import ca.odell.glazedlists.BasicEventList;
    import ca.odell.glazedlists.EventList;
    import ca.odell.glazedlists.FilterList;
    import ca.odell.glazedlists.GlazedLists;
    import ca.odell.glazedlists.Matcher;
    import ca.odell.glazedlists.impl.filter.TextMatcher;
    
    /**
     * 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() {
    
            EventList eventList = new BasicEventList();
            eventList.addAll( Arrays.asList( (Object[]) getFormModel().getFormObject() ) );
    
            String[] colProps = new String[] { "credentialName", "credentialType", "awardingInstitution", "year" };
            GlazedTableModel model = new GlazedTableModel( eventList, getMessageSource(), colProps );
    
            JTable table = new JTable( model );
            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() ) );
            Object[] tableArray = (Object[]) _formModel.getFormObject();
            _detailForm = new CredentialDetailForm( _detailFormModel, tableArray );
    
            // 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();
        }
    
        private NestingFormModel _formModel;
        private ConfigurableFormModel _detailFormModel;
        private CredentialDetailForm _detailForm;
    
        /**
         * Inner class to handle the table selection and installing the selection
         * into the detail form.
         */
        private class TableSelectionHandler extends ListSelectionListenerSupport {
            /**
             * Called when multiple rows are selected. Override this method to
             * handle multiple selection
             * 
             * @param indexes the selected indexes
             */
            protected void onMultiSelection(int[] indexes) {
            }
    
            /**
             * Called when nothing gets selected. Override this method to handle
             * empty selection
             */
            protected void onNoSelection() {
            }
    
            /**
             * 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, Object[] detailData) {
                super( formModel, FORM_NAME );
    
                // Install the detail data as our editable object list
                ListListModel list = new ListListModel( Arrays.asList( detailData ) );
                setEditableFormObjects( list );
                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" );
    
                //JButton btnCommit = createCommitButton();
                //JButton btnRevert = createRevertButton();
                formBuilder.row();
                formBuilder.row();
                formBuilder.getLayoutBuilder().cell(createButtonBar());
                //formBuilder.getLayoutBuilder().cell(btnRevert, "colSpan=5 colSpec=:min align=right");
                //formBuilder.getLayoutBuilder().cell(btnCommit, "colSpan=1 colSpec=:min align=right");
                
                return formBuilder.getForm();
            }
    
            /**
             * Set the selected object index.
             * 
             * @param index of selected item
             */
            public void setSelectedIndex(int index) {
                _indexHolder.setValue( new Integer( 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;
        }
    }

  • #2
    I think the heart of my problem is that the AbstractForm.setEditableFormObjects method requires an ObservableList object as an argument.

    I don't see any way to properly create an ObservableList from some form of buffered value model. Without that, the changes posted by a commit on this subform will directly affect the real model.

    Anyone have a solution for this? Am I using these methods incorrectly?

    Thanks,
    Larry.

    Comment


    • #3
      Still more poking and I think the problem is related to my previous posting, but more subtle.

      The update to the master model (even after a cancel) seems to stem from the fact that all the buffered value holders that deal with collections do not perform any kind of deep copy. They simple copy the contained object references.

      This behavior, coupled with the AbstractForm's handling of editable objects, results in the changes being commited directly back to the objects that were referenced in the array in the master model.

      What I need is some mechanism to deep copy the collection when it is buffered and then store all that back out when commited.

      Is this something that's been dealt with (and I'm just not finding it), or is this new territory I've stumbled into?

      I'm willing to put some work into this (as long as I can get some guiding directions on how to do it right) since this is a complete project stopper for me.

      Thanks,
      Larry.

      Comment


      • #4
        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;
            }
        }

        Comment


        • #5
          Hi,

          This may come a bit late, but I'll give it anyways.

          We had the same problem some time ago: properties that are collections or more complex objects needed deep cloning in their buffering. We created our own bufferedValueModel much like yours but called it CloneBufferedValueModel. What does it do? Well, it checks for collections/maps and the interface PublicCloneable that defines a public clone() method. Then it creates new collections/maps or does a clone as deep as needed.

          Besides this, we use a ListBinding (used to be ListPropertyEditor) that creates the table (glazedlists also) with special commands to edit/add/delete/view the details(rows). When adding/editing a row a dialog pops up and when committing an event is fired from CloneBufferedValueModel.

          For one particular object we use a binding that consists of a table with the detail right next to it. Here, each change in the detail will fire a change on the binding. (detailform is not buffered, parent component that contains the table IS buffered with a deep cloned object)

          Maybe it's not bad to combine some ideas and post it on the rcp-developer list to see what Oliver thinks of this, and possibly have something committed back to the rcp-project.

          Kind Regards,
          Jan

          Comment


          • #6
            Ok, with a weekend of serious hacking, I've produced an implementation of master/detail forms that I think is ready for some more serious inspection.

            It includes the following:

            AbstractMasterForm
            AbstractTableMasterForm
            AbstractDetailForm
            DeepCopyBufferedCollectionValueModel

            And example usage classes (EducationMasterForm and EducationDetailForm).

            What is the best way to post this code? Here as code fragments, or to create an issue and attach the files?

            Also, making this work required a couple of minor changes to AbstractForm and AbstractFormModel.

            Thanks.

            Comment


            • #7
              Larry,

              I think you can post it in the RC wiki at http://opensource2.atlassian.com/con...splay/RCP/Home
              or just post a Jira issue.

              We would like to see it soon!

              Cheers,
              Gustavo.

              Comment


              • #8
                I'm trying to edit the wiki but I keep getting a stack trace (caused by an NPE in some license check). I'll keep trying.

                While we wait - I was planning on making it a sub page of the Forms user documentation. Does that seem reasonable?

                Thanks.
                Larry.

                Comment


                • #9
                  Ok - I've posted the master/detail code to the wiki.

                  Look here: http://opensource2.atlassian.com/con...on?pageId=2579

                  Enjoy!

                  Comment

                  Working...
                  X