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 |
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 QueueupdateList = 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 |
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