Featured Post

Applying Email Validation to a JavaFX TextField Using Binding

This example uses the same controller as in a previous post but adds a use case to support email validation.  A Commons Validator object is ...

Friday, April 15, 2016

Saving Changes to a Big JavaFX TableView

This post describes a technique for saving changes to a JavaFX TableView.  OnEditCommit handlers are registered with the TableColumns which add to an update list, allowing a potentially costly save operation to be deferred.  The TableView will update with TableCell edits, but the actual save operation will occur after a deliberate press of the save Button.

To keep the RAM down, the solution relies heavily on POJOs, a PropertyValueFactory, and a shared OnEditCommit handler.


This demo JavaFX Application contains a single TableView with a pair of Buttons.  The TableView contains two TableColumns: id and data.  data is editable by the user and able to be saved.  id is not editable.

Demo Application Screenshot
The application loads 20 million records into a TableView.  Double-clicking on a cell in the Data TableColumn will allow you to change the value.  Press return to commit the value.  When one or more values have been committed, the save Button is enabled.  Pressing the save Button will iterate through the changes to that point and print them out.

Here are the results after making a few edits.  From the Eclipse console.

updating UpdateObject [id=19999992, data=carl, oldData=meZmRhgBDgIvuDgmChaH]
updating UpdateObject [id=19999993, data=demo, oldData=QYfpeQpbnyckqAfYyunv]
updating UpdateObject [id=19999994, data=app, oldData=WQTWqZGGfbvnszPUUvel]

The save clears the list of updates and disables the save Button.

Loading Table

The application creates 20 million objects in a loop with a counter forming the id values and a random string forming the data values.

private List<MyObject> fetchData() {
  
 List<MyObject> objectsFromDataSource = new ArrayList<gt;();
  
 for( int i=0; i<NUM_RECORDS; i++ ) {
  objectsFromDataSource.add( new MyObject( (long)i, RandomStringUtils.randomAlphabetic(20) ) );
 }
  
 return objectsFromDataSource;
}
 
@FXML
public void loadTable() {
  
 //
 // load the data into domain objects
 //
 List<MyObject> objectsFromDataSource = fetchData();
  
 //
 // put in control
 //
 tblObjects.setItems( FXCollections.observableList( objectsFromDataSource ));
}

MyObject is a POJO domain object.  It contains two fields: id and data.

public class MyObject {

 private Long id;
 private String data;

// ...abbreviated

JavaFX Controller

The app is built with SceneBuilder.  The JavaFX Controller contains @FXML fields to manipulate the UI objects, a dirtyFlag field for toggling the save Button, and a Java Collections Queue for gathering updates.

There is a commit handler "dataEditCommit" that will be shared among the TableColumn cells.

I've also added a static utility method "getObjectAtEvent" to make the commit handler more readable.

@FXML
TableColumn<MyObject,Number> tcId;
 
@FXML
TableColumn<MyObject,String> tcData;
 
@FXML
TableView<MyObject> tblObjects;
 
@FXML
Button btnSave;
 
private final BooleanProperty dirtyFlag = new SimpleBooleanProperty();

private final Queue updateList = new LinkedList<>();

static <T, S> T getObjectAtEvent(CellEditEvent<T, S> evt) {
  
 TableView<T> tableView = evt.getTableView();
  
 ObservableList<T> items = tableView.getItems();
  
 TablePosition<T,S> tablePosition = evt.getTablePosition();
 
 int rowId = tablePosition.getRow();
  
 T obj = items.get(rowId);

 return obj;
}

private final EventHandler<TableColumn.CellEditEvent<MyObject,String> > dataEditCommitHandler = (evt) -> {
 if( !dirtyFlag.get() ) {
  dirtyFlag.set(true);
 }
 MyObject obj = getObjectAtEvent(evt);
 String oldData = obj.getData();
 obj.setData( evt.getNewValue() );
 updateList.add( new UpdateObject(obj.getId(), obj.getData(), oldData));
};

When the user commits a change to the table -- which is limited to the single TableColumn "data" -- the dataEditCommit handler fires.  If the dirtyFlag is not set, it is set.  The current domain object of class "MyObject" is retrieved and a new POJO "UpdateObject" is created and put on the Queue.

Here is the code for UpdateObject.

class UpdateObject {
 
 private final Long id;
 private final String data;
 private final String oldData;

// ...abbreviated

initialize() Method

The UI is defined in FXML.  The JavaFX Controller's initialize() method binds the dirtyFlag property to the save Button using the not() operator.  initialize() also registers the commit handler, cell factory, and cell value factories with the TableColumns.

@FXML
public void initialize() {
  
 btnSave.disableProperty().bind( dirtyFlag.not() );
  
 // id is read-only
 tcId.setCellValueFactory(new PropertyValueFactory<MyObject,Number>("id"));  
  
 tcData.setCellValueFactory(new PropertyValueFactory<MyObject,String>("data"));  
 tcData.setCellFactory(TextFieldTableCell.forTableColumn());    
 tcData.setOnEditCommit( dataEditCommitHandler );  
}

btnSave will be disabled whenever the dirtyFlag is not set, indicating that there isn't anything to save.  When dirtyFlag is set in the commit handler, btnSave will automatically be enabled.  On the save operation, the dirtyFlag is cleared and btnSave is disabled again.

tcId is not editable but needs a cell value factory to display the id field in MyObject.  In the FXML, this TableColumn has the editable checkbox de-selected.

tcData also needs a cell value factory, but it is editable.  In the FXML, it's checkbox is selected.  The containing TableView is also set to editable.    The cell factory -- TextFieldTableCell -- tells the TableView to switch over to a TextField from the Label when the current value is double-clicked.  Finally, a return pressed after editting a value calls dataEditCommit handler.

Save Operation

Finally, the save operation pulls UpdateObjects from the Queue.  It clears the dirtyFlag and performs the potentially long-running update operation.  I'm using a simple algorithm which will apply individual updates in the order in which they were received.  Alternatively, you can go through the Queue and discard earlier edits that were updated later during the same save session.

@FXML
public void save() {

 UpdateObject uo = null;
 while( (uo = updateList.poll()) != null ) {
  System.out.println("updating " + uo);
 }
 dirtyFlag.set(false);
}

Memory Digression

I was following an online discussion about the RAM overhead of using JavaFX properties.  Specifically, using Java data types (Long, String) uses less memory than the JavaFX equivalents (LongProperty, StringProperty).

This post didn't try to recast a POJO into a JavaFX class, which is probably an anti-pattern.  That is, JavaFX properties are involved, but they're not packaged into a class duplicate of the POJO.  See the PropertyValueFactory instantiations in the @FXML initialize() method.

As a comparison, if I attempt to create a MyFXObject version of MyObject, I get significantly more overhead.  This is expected since I'm duplicating the input data plus adding the FX property overhead.  Here's the results of a few trials.  The first section of the spreadsheet is based on the following loadTables() method.  The second section shows the memory usage of the blog post solution posted above (no MyFXObject).

@FXML
public void loadTable() {
  
 //
 // load the data into domain objects
 //
 List<MyObject> objectsFromDataSource = fetchData();
  
 //
 // transfer to an FX-ready object
 // 
 List<MyFXObject> fxObjects = new ArrayList< gt&();
 for( MyObject obj : objectsFromDataSource ) {
  fxObjects.add( new MyFXObject(obj.getId(), obj.getData()) );
 }
  
 //
 // put in control
 //
 tblObjects.setItems( FXCollections.observableList( fxObjects ));
}

Comparing Liberal use of JavaFX Properties with more Sparing One
In these trials, I'm both loading the TableView and manually scrolling to the end.  Recall that this is a 20 million record set.

This post showed a coding technique for capturing changes to a large TableView.  Edits to the TableView are saved off into a Java Collections Queue and processed sequentially at the users prompting.  To save RAM, the TableView is backed by a POJO with JavaFX properties used via a factory that creates new objects as they are needed.



No comments:

Post a Comment