001    /*
002     * Copyright 2002-2005 the original author or authors.
003     *
004     * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005     * use this file except in compliance with the License. You may obtain a copy of
006     * the License at
007     *
008     * http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012     * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013     * License for the specific language governing permissions and limitations under
014     * the License.
015     */
016    package org.springframework.richclient.form;
017    
018    import java.beans.PropertyChangeEvent;
019    import java.beans.PropertyChangeListener;
020    import java.util.List;
021    
022    import javax.swing.JButton;
023    import javax.swing.JComponent;
024    import javax.swing.JPopupMenu;
025    import javax.swing.ListSelectionModel;
026    import javax.swing.event.ListSelectionListener;
027    
028    import org.springframework.beans.BeanUtils;
029    import org.springframework.binding.form.HierarchicalFormModel;
030    import org.springframework.binding.form.ValidatingFormModel;
031    import org.springframework.binding.validation.ValidationResultsModel;
032    import org.springframework.binding.validation.support.DefaultValidationResultsModel;
033    import org.springframework.binding.value.ValueModel;
034    import org.springframework.binding.value.support.DeepCopyBufferedCollectionValueModel;
035    import org.springframework.binding.value.support.DirtyTrackingValueModel;
036    import org.springframework.binding.value.support.ObservableEventList;
037    import org.springframework.binding.value.support.ObservableList;
038    import org.springframework.binding.value.support.ValueHolder;
039    import org.springframework.richclient.command.AbstractCommand;
040    import org.springframework.richclient.command.ActionCommand;
041    import org.springframework.richclient.command.CommandGroup;
042    import org.springframework.richclient.dialog.ConfirmationDialog;
043    import org.springframework.richclient.dialog.Messagable;
044    import org.springframework.richclient.table.ListSelectionListenerSupport;
045    import org.springframework.richclient.util.GuiStandardUtils;
046    import org.springframework.util.Assert;
047    import org.springframework.util.ClassUtils;
048    import org.springframework.util.StringUtils;
049    
050    import ca.odell.glazedlists.BasicEventList;
051    import ca.odell.glazedlists.EventList;
052    
053    /**
054     * Abstract base for the Master form of a Master/Detail pair. Derived types must implement
055     * two methods:
056     * <dt>{@link #createDetailForm}</dt>
057     * <dd>To construct the detail half of this master/detail pair</dd>
058     * <dt>{@link #getSelectionModel()}</dt>
059     * <dd>To return the selection model of the object rendering the list</dd>
060     * <p>
061     * <strong>Important note:</strong> Any subclass that implements
062     * {@link AbstractForm#createControl()} <strong>MUST</strong> call {@link #configure()}
063     * prior to its work in order to have the detail form properly prepared.
064     *
065     * @author Larry Streepy
066     * @see AbstractDetailForm
067     * @see #creatingNewObject()
068     */
069    public abstract class AbstractMasterForm extends AbstractForm {
070    
071        /** Property name for indicating changes in our selected index. */
072        public static final String SELECTION_INDEX_PROPERTY = "selectionIndex";
073    
074        /**
075         * Property name for indicating changes in our "is creating new object" state.
076         */
077        public static final String IS_CREATING_PROPERTY = "isCreating";
078    
079        private DirtyTrackingDCBCVM collectionVM;
080        private EventList rootEventList;
081        private ObservableEventList masterEventList;
082        private AbstractDetailForm detailForm;
083        private Class detailType;
084        private ActionCommand newFormObjectCommand;
085        private ActionCommand deleteCommand;
086        private CommandGroup commandGroup;
087        private boolean confirmDelete = true;
088        private ListSelectionHandler selectionHandler = new ListSelectionHandler();
089        private PropertyChangeListener parentFormPropertyChangeHandler = new ParentFormPropertyChangeHandler();
090    
091        /**
092         * Construct a new AbstractMasterForm using the given parent form model and property
093         * path. The form model for this class will be constructed by getting the value model
094         * of the specified property from the parent form model and constructing a
095         * DeepCopyBufferedCollectionValueModel on top of it.
096         *
097         * @param parentFormModel Parent form model to access for this form's data
098         * @param property containing this forms data (must be a collection or an array)
099         * @param formId Id of this form
100         * @param detailType Type of detail object managed by this master form
101         */
102        protected AbstractMasterForm(HierarchicalFormModel parentFormModel, String property, String formId, Class detailType) {
103            super( formId );
104            this.detailType = detailType;
105    
106            ValueModel propertyVM = parentFormModel.getValueModel( property );
107    
108            // Now construct the dirty tracking model
109            Class collectionType = getMasterCollectionType( propertyVM );
110    
111            collectionVM = new DirtyTrackingDCBCVM( propertyVM, collectionType );
112            ValidatingFormModel formModel = FormModelHelper.createChildPageFormModel( parentFormModel, formId,
113                    collectionVM);
114            setFormModel( formModel );
115    
116            // Install a handler to detect when the parents form model changes
117            propertyVM.addValueChangeListener(parentFormPropertyChangeHandler);
118        }
119    
120        /**
121         * Get the value model representing the collection we are managing. This value model
122         * can be used to register vlue change listeners and to update the collection
123         * contents.
124         * <p>
125         * You must use this method to get the value model since calling getValueModel on the
126         * parent form model will not get you what you want.
127         *
128         * @return collection value model
129         */
130        public ValueModel getCollectionValueModel() {
131            return collectionVM;
132        }
133    
134        /**
135         * Determine the type of the collection holding the detail items. This will be used to
136         * create the value model for the collection.
137         * <p>
138         * <b>Note to Hibernate users:</b> You will most likely need to override this method
139         * in order to force the use of a simple <code>List</code> class instead of the
140         * default implementation that would return <code>PersistentList</code>. Creating a
141         * new instance of this type would result in a somewhat misleading error regarding
142         * lazy instantiation since the new PersistentList instance would not have been
143         * properly initialized by Hibernate.
144         *
145         * @param collectionPropertyVM ValueModel holding the master collection
146         * @return Type of collection to use
147         */
148        protected Class getMasterCollectionType(ValueModel collectionPropertyVM) {
149            return collectionPropertyVM.getValue().getClass();
150        }
151    
152        /**
153         * Configure this master form's data and prepare the detail form.
154         */
155        protected void configure() {
156            // Just configure a basic event list to handle our data
157            installEventList( getRootEventList() );
158    
159            // Now we need to construct a subform and value model to handle the
160            // detail elements of this master table
161    
162            Object detailObject = BeanUtils.instantiateClass(detailType);
163            ValueModel valueHolder = new ValueHolder( detailObject );
164            detailForm = createDetailForm( getFormModel(), valueHolder, masterEventList);
165    
166            // Start the form disabled and not validating until the form is actually in use.
167            detailForm.setEnabled( false );
168            detailForm.getFormModel().setValidating( false );
169    
170            // Wire up the monitor to track the selected index and edit state so we
171            // can keep the delete and add button states up to date
172            detailForm.getEditingIndexHolder().addValueChangeListener( new EditingIndexMonitor() );
173            detailForm.addPropertyChangeListener( new EditStateMonitor() );
174        }
175    
176        /**
177         * Construct the detail half of this master/detail pair.
178         *
179         * @param parentFormModel
180         * @param valueHolder BufferedValueModel holding an object of the type configured for
181         *            this master form.
182         * @param masterList The ObservableList of data to from the master form (this will
183         *            constitute the editable object list for the detail form).
184         */
185        protected abstract AbstractDetailForm createDetailForm(HierarchicalFormModel parentFormModel,
186                ValueModel valueHolder, ObservableList masterList);
187    
188        /**
189         * Install an EventList for use as our master list data. The event list must have been
190         * created on top of the event list obtained from {@link #getRootEventList()} so that
191         * all changes are propery proxied onto the actual form data. The event list provided
192         * will be wrapped in a {@link ObservableEventList}.
193         *
194         * @param eventList new EventList to install
195         */
196        protected void installEventList(EventList eventList) {
197            masterEventList = new ObservableEventList( eventList );
198    
199            // Propogate this down to the detail form
200            if( detailForm != null ) {
201                detailForm.setMasterList(masterEventList);
202            }
203        }
204    
205        /**
206         * Get the root event list for this model. This event list will be constructed from
207         * the form objects value (assumed to be an EventList). Any subclasses that are
208         * installing additional transformed lists should use this method to obtain the
209         * original event list on top of which all the other lists are constructed.
210         */
211        protected EventList getRootEventList() {
212            if( rootEventList == null ) {
213                rootEventList = (EventList) getFormModel().getFormObjectHolder().getValue();
214            }
215            return rootEventList;
216        }
217    
218        /**
219         * Handle the root event list being changed externally. This is normally invoked
220         * when the value model holding this forms source property has changed (which can
221         * occur when the parent form object is changed). This method is normally invoked from
222         * the parent form object listener.
223         *
224         * @see #parentFormPropertyChangeHandler
225         */
226        protected void handleExternalRootEventListChange() {
227            if( masterEventList != null ) {
228                // While we do this, we need to disable our normal list listener since it's
229                // too late to interact with the user due to unsaved changes (the underlying
230                // value model has already changed).
231                uninstallSelectionHandler();
232    
233                // Clean up the detail form
234                if( detailForm != null ) {
235                    detailForm.reset();
236                    detailForm.setSelectedIndex( -1 );
237                }
238    
239                if( isControlCreated() ) {
240                    updateControlsForState(); // Ensure our controls are properly updated
241                }
242    
243                installSelectionHandler(); // Reinstate the handler
244            }
245        }
246    
247        /**
248         * Get the form data we are operating upon. The form object must be castable to List
249         * for this to work.
250         *
251         * @return List The form object's value
252         */
253        public List getFormData() {
254            return (List) getFormModel().getFormObjectHolder().getValue();
255        }
256    
257        /**
258         * Get the master EventList (which proxies the real form data).
259         *
260         * @return EventList
261         */
262        public ObservableEventList getMasterEventList() {
263            return masterEventList;
264        }
265    
266        /**
267         * Get the selection model for the master list representation.
268         *
269         * @return selection model
270         */
271        protected abstract ListSelectionModel getSelectionModel();
272    
273        /**
274         * Install our selection handler.
275         */
276        protected void installSelectionHandler() {
277            ListSelectionModel lsm = getSelectionModel();
278            if( lsm != null ) {
279                lsm.addListSelectionListener( getSelectionHandler() );
280            }
281        }
282    
283        /**
284         * Uninstall our selection handler.
285         */
286        protected void uninstallSelectionHandler() {
287            ListSelectionModel lsm = getSelectionModel();
288            if( lsm != null ) {
289                lsm.removeListSelectionListener( getSelectionHandler() );
290            }
291        }
292    
293        /**
294         * Indicates that we are creating a new detail object. Default behavior is to just
295         * clear the selection on the master set.
296         */
297        public void creatingNewObject() {
298            getSelectionModel().clearSelection();
299        }
300    
301        /**
302         * Get the selection handler for the master list. Default implementation.
303         *
304         * @return listener to handle master table selection events
305         */
306        protected ListSelectionListener getSelectionHandler() {
307            return selectionHandler;
308        }
309    
310        /**
311         * Get the command group for the master table (the add and delete commands).
312         *
313         * @return command group
314         */
315        protected CommandGroup getCommandGroup() {
316            if( commandGroup == null ) {
317                commandGroup = CommandGroup.createCommandGroup( null, new AbstractCommand[] { getDeleteCommand(),
318                        getNewFormObjectCommand() } );
319            }
320            return commandGroup;
321        }
322    
323        /**
324         * Return a standardized row of command buttons, right-justified and all of the same
325         * size, with OK as the default button, and no mnemonics used, as per the Java Look
326         * and Feel guidelines.
327         */
328        protected JComponent createButtonBar() {
329            JComponent buttonBar = getCommandGroup().createButtonBar();
330            GuiStandardUtils.attachDialogBorder( buttonBar );
331            return buttonBar;
332        }
333    
334        /**
335         * Get the popup menu for the master table. This is built from the command group
336         * returned from {@link #getCommandGroup()}.
337         *
338         * @return popup menu
339         */
340        protected JPopupMenu getPopupMenu() {
341            return getCommandGroup().createPopupMenu();
342        }
343    
344        /**
345         * Get the action command to creating a new detail object. Note that we have to
346         * override this method in order to call our own createNewDetailObjectCommand method
347         * since the AbstractForm's implementation of createNewFormObjectCommand is private!
348         */
349        public ActionCommand getNewFormObjectCommand() {
350            if( newFormObjectCommand == null ) {
351                newFormObjectCommand = createNewFormObjectCommand();
352            }
353            return newFormObjectCommand;
354        }
355    
356        /**
357         * Create the "new detail object" command. This will encapsulate the action from our
358         * detail forms {@link AbstractForm#getNewFormObjectCommand} as well as controlling
359         * the state of the detail form.
360         *
361         * @return command
362         */
363        protected ActionCommand createNewFormObjectCommand() {
364            String commandId = getNewFormObjectCommandId();
365            if( !StringUtils.hasText( commandId ) ) {
366                return null;
367            }
368    
369            ActionCommand newDetailObjectCommand = new ActionCommand( commandId ) {
370                protected void doExecuteCommand() {
371                    maybeCreateNewObject(); // Avoid losing user edits
372                }
373            };
374            String scid = constructSecurityControllerId( commandId );
375            newDetailObjectCommand.setSecurityControllerId( scid );
376            return (ActionCommand) getCommandConfigurer().configure( newDetailObjectCommand );
377        }
378    
379        /**
380         * Construct the "new detail object" command id as
381         * <code>new[detailTypeName]Command</code>
382         *
383         * @return constructed command id
384         */
385        protected String getNewFormObjectCommandId() {
386            return "new" + StringUtils.capitalize( ClassUtils.getShortName( detailType + "Command" ) );
387        }
388    
389        /**
390         * Return the command to delete the currently selected item in the master set.
391         *
392         * @return command, created on demand
393         */
394        public ActionCommand getDeleteCommand() {
395            if( deleteCommand == null ) {
396                deleteCommand = createDeleteCommand();
397            }
398            return deleteCommand;
399        }
400    
401        /**
402         * Create the "delete object" command.
403         *
404         * @return command
405         */
406        protected ActionCommand createDeleteCommand() {
407            String commandId = getDeleteCommandId();
408            if( !StringUtils.hasText( commandId ) ) {
409                return null;
410            }
411    
412            final ActionCommand deleteCommand = new ActionCommand( commandId ) {
413                protected void doExecuteCommand() {
414                    maybeDeleteSelectedItems();
415                }
416            };
417    
418            String scid = constructSecurityControllerId( commandId );
419            deleteCommand.setSecurityControllerId( scid );
420            return (ActionCommand) getCommandConfigurer().configure( deleteCommand );
421        }
422    
423        /**
424         * Get the message to present to the user when confirming the delete of selected
425         * detail items. This default implementation just obtains the message with key
426         * <code>&lt;formId&gt;.confirmDelete.message</code> or
427         * <code>masterForm.confirmDelete.message</code>. Subclasses can use the selected
428         * item(s) to construct a more meaningful message.
429         */
430        protected String getConfirmDeleteMessage() {
431            return getMessage(new String[] { getId() + ".confirmDelete.message", "masterForm.confirmDelete.message" });
432        }
433    
434        /**
435         * Maybe delete the selected items. If we are configured to confirm the delete, then
436         * do so. If the user confirms, then delete the selected items.
437         */
438        protected void maybeDeleteSelectedItems() {
439    
440            // If configured, have the user confirm the delete operation
441            if( isConfirmDelete() ) {
442                String title = getMessage( new String[] { getId() + ".confirmDelete.title", "masterForm.confirmDelete.title" } );
443                String message = getConfirmDeleteMessage();
444                ConfirmationDialog dlg = new ConfirmationDialog( title, message ) {
445    
446                    protected void onConfirm() {
447                        deleteSelectedItems();
448                        getSelectionModel().clearSelection();
449                    }
450                };
451                dlg.showDialog();
452            } else {
453                deleteSelectedItems();
454                getSelectionModel().clearSelection();
455            }
456        }
457    
458        /**
459         * Delete the detail item at the specified index.
460         */
461        protected void deleteSelectedItems() {
462            ListSelectionModel sm = getSelectionModel();
463    
464            if( sm.isSelectionEmpty() ) {
465                return;
466            }
467    
468            detailForm.reset();
469    
470            int min = sm.getMinSelectionIndex();
471            int max = sm.getMaxSelectionIndex();
472    
473            // Loop backwards and delete each selected item in the interval
474            for( int index = max; index >= min; index-- ) {
475                if( sm.isSelectedIndex( index ) ) {
476                    getMasterEventList().remove( index );
477                }
478            }
479        }
480    
481        /**
482         * Construct the button to invoke the delete command.
483         *
484         * @return button
485         */
486        protected JButton createDeleteButton() {
487            Assert.state( deleteCommand != null, "Delete command has not been created!" );
488            return (JButton) deleteCommand.createButton();
489        }
490    
491        /**
492         * Construct the "delete detail object" command id as
493         * <code>delete[DetailTypeName]Command</code>
494         *
495         * @return constructed command id
496         */
497        protected String getDeleteCommandId() {
498            return "delete" + StringUtils.capitalize( ClassUtils.getShortName( detailType + "Command" ) );
499        }
500    
501        /**
502         * @return Returns the detailForm.
503         */
504        protected AbstractDetailForm getDetailForm() {
505            return detailForm;
506        }
507    
508        /**
509         * @param form The detailForm to set.
510         */
511        protected void setDetailForm(AbstractDetailForm form) {
512            detailForm = form;
513        }
514    
515        /**
516         * @return Returns the detailFormModel.
517         */
518        protected ValidatingFormModel getDetailFormModel() {
519            return detailForm.getFormModel();
520        }
521    
522        /**
523         * @return Returns the detailType.
524         */
525        protected Class getDetailType() {
526            return detailType;
527        }
528    
529        /**
530         * @param type The detailType to set.
531         */
532        protected void setDetailType(Class type) {
533            detailType = type;
534        }
535    
536        /**
537         * Return confirm delete setting.
538         * @return confirm delete setting.
539         */
540        public boolean isConfirmDelete() {
541            return confirmDelete;
542        }
543    
544        /**
545         * Set confirm delete. If this is <code>true</code> then the master form will
546         * confirm with the user prior to deleting a detail item.
547         * @param confirmDelete
548         */
549        public void setConfirmDelete(boolean confirmDelete) {
550            this.confirmDelete = confirmDelete;
551        }
552    
553        /**
554         * Deal with the user invoking a "new object" command. If we have unsaved changes,
555         * then we need to query the user to ensure they want to really make the change.
556         */
557        protected void maybeCreateNewObject() {
558            if( getDetailForm().isEditingNewFormObject() ) {
559                return; // Already creating a new object, just bail
560            }
561    
562            final ActionCommand detailNewObjectCommand = detailForm.getNewFormObjectCommand();
563    
564            if( getDetailForm().isDirty() ) {
565                String title = getMessage( new String[] { getId() + ".dirtyNew.title", "masterForm.dirtyNew.title" } );
566                String message = getMessage( new String[] { getId() + ".dirtyNew.message", "masterForm.dirtyNew.message" } );
567                ConfirmationDialog dlg = new ConfirmationDialog( title, message ) {
568                    protected void onConfirm() {
569                        // Tell both forms that we are creating a new object
570                        detailNewObjectCommand.execute(); // Do subform action first
571                        creatingNewObject();
572                        detailForm.creatingNewObject();
573                    }
574                };
575                dlg.showDialog();
576            } else {
577                // Tell both forms that we are creating a new object
578                detailNewObjectCommand.execute(); // Do subform action first
579                creatingNewObject();
580                detailForm.creatingNewObject();
581            }
582        }
583    
584        /**
585         * Update our controls based on our state.
586         */
587        protected void updateControlsForState() {
588            int state = getDetailForm().getEditState();
589            boolean isCreating = state == AbstractDetailForm.STATE_CREATE;
590    
591            getDeleteCommand().setEnabled( getDetailForm().getEditingFormObjectIndex() >= 0 );
592            getNewFormObjectCommand().setEnabled( !isCreating );
593    
594            // If we are in the CLEAR state, then we need to disable validations on the form
595            getDetailFormModel().setValidating( state != AbstractDetailForm.STATE_CLEAR );
596        }
597    
598        /**
599         * When the results reporter is setup on the master form, we need to capture it and
600         * forward it on to the detail form as well.
601         */
602        public ValidationResultsReporter newSingleLineResultsReporter(Messagable messageReceiver) {
603            // create a resultsModel container which receives events from detail and master
604            ValidationResultsModel validationResultsModel = new DefaultValidationResultsModel();
605            validationResultsModel.add(getFormModel().getValidationResults());
606            validationResultsModel.add(getDetailFormModel().getValidationResults());
607            ValidationResultsReporter reporter = new SimpleValidationResultsReporter(validationResultsModel, messageReceiver);
608            return reporter;
609        }
610    
611        /**
612         * Inner class to handle the list selection and installing the selection into the
613         * detail form.
614         */
615        protected class ListSelectionHandler extends ListSelectionListenerSupport {
616            /**
617             * Called when nothing gets selected. Override this method to handle empty
618             * selection
619             */
620            protected void onNoSelection() {
621                maybeChangeSelection( -1 );
622            }
623    
624            /**
625             * Called when the user selects a single row. Override this method to handle
626             * single selection
627             *
628             * @param index the selected row
629             */
630            protected void onSingleSelection(final int index) {
631                maybeChangeSelection( index );
632            }
633    
634            /**
635             * Deal with a change in the selected index. If we have unsaved changes, then we
636             * need to query the user to ensure they want to really make the change.
637             *
638             * @param newIndex The new selection index, may be -1 to clear the selection
639             */
640            protected void maybeChangeSelection(final int newIndex) {
641                if( newIndex == getDetailForm().getSelectedIndex() ) {
642                    return;
643                }
644                if( getDetailForm().isDirty() ) {
645                    String title = getMessage( new String[] { getId() + ".dirtyChange.title", "masterForm.dirtyChange.title" } );
646                    String message = getMessage( new String[] { getId() + ".dirtyChange.message", "masterForm.dirtyChange.message" } );
647                    ConfirmationDialog dlg = new ConfirmationDialog( title, message ) {
648    
649                        protected void onConfirm() {
650                            getDetailForm().setSelectedIndex( newIndex );
651                        }
652    
653                        protected void onCancel() {
654                            // Force the selction back
655                            super.onCancel();
656                            if( getDetailForm().isEditingNewFormObject() ) {
657                                // Since they were editing a new object, we just
658                                // need to clear the selection
659                                getSelectionModel().clearSelection();
660                            } else {
661                                int index = getDetailForm().getSelectedIndex();
662                                getSelectionModel().setSelectionInterval( index, index );
663                            }
664                        }
665    
666                    };
667                    dlg.showDialog();
668                } else {
669                    getDetailForm().setSelectedIndex( newIndex );
670                }
671            }
672        }
673    
674        /**
675         * Inner class to monitor the editing index on the detail form and update the state of
676         * the delete command whenever it changes.
677         */
678        private class EditingIndexMonitor implements PropertyChangeListener {
679            public void propertyChange(PropertyChangeEvent evt) {
680                updateControlsForState();
681            }
682        }
683    
684        /**
685         * Inner class to monitor the edit state of the detail form and update the state of
686         * the commands whenever it changes.
687         */
688        private class EditStateMonitor implements PropertyChangeListener {
689            public void propertyChange(PropertyChangeEvent evt) {
690                updateControlsForState();
691            }
692        }
693    
694        /**
695         * This class handles changes in the property that this master form is editing. This
696         * can occur when the parent form's form object is changed. When that occurs, our
697         * collection data will change automatically and we then need to update our event list
698         * accordingly.
699         */
700        private class ParentFormPropertyChangeHandler implements PropertyChangeListener {
701            public void propertyChange(PropertyChangeEvent evt) {
702                handleExternalRootEventListChange();
703            }
704    
705        }
706    
707        /**
708         * Specialized DCBCVM to provide dirty tracking semantics. This will allow the form
709         * model built on this value model to properly track our dirty status.
710         */
711        private class DirtyTrackingDCBCVM extends DeepCopyBufferedCollectionValueModel implements DirtyTrackingValueModel {
712    
713            private boolean dirty = false;
714            private boolean oldDirty = false;
715    
716            /**
717             * Constructs a new DirtyTrackingDCBCVM.
718             *
719             * @param wrappedModel the value model to wrap
720             * @param wrappedType the class of the value contained by wrappedModel; this must
721             *            be assignable to <code>java.util.Collection</code> or
722             *            <code>Object[]</code>.
723             */
724            public DirtyTrackingDCBCVM(ValueModel wrappedModel, Class wrappedType) {
725                super( wrappedModel, wrappedType );
726                // FIXME: make DCBCVM do dirty tracking on its own
727                dirty = false; // We should never start life as dirty
728            }
729    
730            /**
731             * Create the buffered list model. We want to use an ObservableEventList so that
732             * it can be used as the root event list of the master form model.
733             * @return ObservableList to use
734             */
735            protected ObservableList createBufferedListModel() {
736                return new ObservableEventList( new BasicEventList() );
737            }
738    
739            /**
740             * Set the value. If this is our original value, then clear dirty.
741             * @param value New value
742             */
743            public void setValue(Object value) {
744                super.setValue( value );
745                if( value == getWrappedValueModel().getValue() ) {
746                    // this is a revert
747                    dirty = false;
748                    valueUpdated();
749                }
750            }
751    
752            /**
753             * Our underlying list has changed, we are now dirty.
754             */
755            protected void fireListModelChanged() {
756                super.fireListModelChanged();
757                dirty = true;
758                valueUpdated();
759            }
760    
761            /**
762             * Return our dirty status.
763             * @return dirty
764             */
765            public boolean isDirty() {
766                return dirty;
767            }
768    
769            /**
770             * Clear the dirty status
771             */
772            public void clearDirty() {
773                dirty = false;
774                valueUpdated();
775            }
776    
777            /**
778             * Revert to original value.
779             */
780            public void revertToOriginal() {
781                revert();
782            }
783    
784            protected void valueUpdated() {
785                boolean dirty = isDirty();
786                if( oldDirty != dirty ) {
787                    oldDirty = dirty;
788                    firePropertyChange( DIRTY_PROPERTY, !dirty, dirty );
789                }
790            }
791        }
792    }