For a little while now, I’ve been working on an application that manages a list of documents, providing multiple views that the user can edit.

The application looks something like this:

Image of the document manager main screen
The document manager main screen

The user selects the document they wish to view or edit by selecting it from the large TableView in the middle of the window. The area on the right provides controls to view and edit details. (The area on the left is for filtering the documents displayed in the central table.)

Based on some early advice, I had watchers on the focus property of the fields that could be edited. When a control lost focus, any changes were written to the database. The user didn’t have to do anything to save their work. It just happened.

This worked with Java 7 and JavaFX 2. After the switch to Java 8 and JavaFX 8, things were not quite the same. If a user was making a change somewhere and then selected another document without moving to another editing view, the data was lost. The focus change notification did not arrive before the new document was selected in the table (repopulating the editing control before the data was saved.)

I posted a question about this, along with a SSCCE on StackOverflow. The gist of the few answers I received was that I was doing the updates wrong. Kleopatra informed me that I was probably doing the update wrong since there is no guarantee as to the order of various events and notifications. I had already tried doing the update during the document selection process of the TableView as suggested by eckig, but doing that left the application vulnerable to lost data if the user did an edit and just closed the program.

In the end, I did use a listener on the change of selection in the table and did an override of the stop() method of the Application class to catch needed updates before closing.

But the key was creating a new class MonitoredSimpleStringProperty, derived from SimpleStringProperty, that keeps track of whether it’s contents have been altered. With that information, the application can decide if it even needs to update the database as the user selects a different document to view or edit.

Another, longer SSCCE illustrates how it works:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
package focusproblem;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import static javafx.collections.FXCollections.observableArrayList;
import javafx.collections.ObservableList;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextArea;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class FocusProblem extends Application {
private TextArea notesArea;
private TableView docTable;
private ObservableList<Doc> docList;
private ObservableList<Doc> initDocs() {
docList = observableArrayList();
docList.add(new Doc("Harper Lee", "To Kill a Mockbird",
"Some notes on mockingbirds"));
docList.add(new Doc("John Steinbeck", "Of Mice and Men",
"Some notes about mice"));
docList.add(new Doc("Lewis Carroll", "Jabberwock",
"Some notes about jabberwocks"));
return docList;
}
private Parent initGui(ObservableList<Doc> d) {
notesArea = new TextArea();
notesArea.setId("notesArea");
notesArea.setPromptText("Add notes here");
TableColumn<Doc, String> authorCol = new TableColumn<>("Author");
authorCol.setCellValueFactory(new PropertyValueFactory<Doc, String>("author"));
authorCol.setMinWidth(100.0d);
TableColumn<Doc, String> titleCol = new TableColumn<>("Title");
titleCol.setCellValueFactory(new PropertyValueFactory<Doc, String>("title"));
titleCol.setMinWidth(250.0d);
docTable = new TableView<>(d);
docTable.setEditable(true);
docTable.setPrefHeight(200.0d);
docTable.getColumns().addAll(authorCol, titleCol);
docTable.getSelectionModel().selectedItemProperty().addListener(new SelectionChangeListener());
VBox vb = new VBox();
vb.getChildren().addAll(docTable, notesArea);
return vb;
}
void updateDoc(Doc d) {
for (SimpleStringProperty ssp : d.getDirtyFieldList()) {
System.out.println("Updating field: " + ssp.getName()
+ " with " + ssp.getValue());
d.markClean();
}
}
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Focus Problem");
primaryStage.setScene(new Scene(initGui(initDocs())));
primaryStage.show();
}
@Override
public void stop() {
for (Doc d : docList) {
updateDoc(d);
}
}
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
public class SelectionChangeListener implements ChangeListener<Doc> {
@Override
public void changed(ObservableValue<? extends Doc> observable,
Doc oldDoc, Doc newDoc) {
System.out.println("Changing selected row");
if (oldDoc != null) {
notesArea.textProperty().unbindBidirectional(oldDoc.notesProperty());
updateDoc(oldDoc);
}
if (newDoc != null) {
notesArea.setText(newDoc.getNotes());
newDoc.notesProperty().bindBidirectional(notesArea.textProperty());
}
}
}
public class Doc {
private final MonitoredSimpleStringProperty author;
private final MonitoredSimpleStringProperty title;
private final MonitoredSimpleStringProperty notes;
public Doc(String auth, String ttl, String nts) {
author = new MonitoredSimpleStringProperty(this, "author", auth);
title = new MonitoredSimpleStringProperty(this, "title", ttl);
notes = new MonitoredSimpleStringProperty(this, "notes", nts);
}
public void setAuthor(String value) {
author.set(value);
}
public String getAuthor() {
return author.get();
}
public MonitoredSimpleStringProperty authorProperty() {
return author;
}
public void setTitle(String value) {
title.set(value);
}
public String getTitle() {
return title.get();
}
public MonitoredSimpleStringProperty titleProperty() {
return title;
}
public void setNotes(String value) {
notes.set(value);
}
public String getNotes() {
return notes.get();
}
public MonitoredSimpleStringProperty notesProperty() {
return notes;
}
public boolean isDirty() {
return (author.isDirty() || title.isDirty() || notes.isDirty());
}
public ObservableList<MonitoredSimpleStringProperty> getDirtyFieldList() {
ObservableList<MonitoredSimpleStringProperty> dirtyList = observableArrayList();
if (author.isDirty()) {
dirtyList.add(author);
}
if (title.isDirty()) {
dirtyList.add(title);
}
if (notes.isDirty()) {
dirtyList.add(notes);
}
return dirtyList;
}
public void markClean() {
author.setDirty(false);
title.setDirty(false);
notes.setDirty(false);
}
}
public class MonitoredSimpleStringProperty extends SimpleStringProperty {
SimpleBooleanProperty dirty;
public MonitoredSimpleStringProperty(Object bean, String name, String initValue) {
super(bean, name, initValue);
dirty = new SimpleBooleanProperty(false);
this.addListener(new InvalidationListener() {
@Override
public void invalidated(Observable observable) {
dirty.set(true);
}
});
}
public MonitoredSimpleStringProperty(Object bean, String name) {
this(bean, name, "");
}
public MonitoredSimpleStringProperty(String initialValue) {
this(null, "");
}
public MonitoredSimpleStringProperty() {
this(null, "", "");
}
public boolean isDirty() {
return dirty.get();
}
public void setDirty(boolean newValue) {
dirty.set(newValue);
}
}
}

The actual program is a bit more complicated, but this approach solved my problem. Now on to other things.

An Aside – How this Came About

I started working on this program as a result of something else I was trying to do. I’ve been working on a book – “Molecular Dynamics Simulation for Beginners”. As part of writing that, I need to maintain a list of references to be included for the reader that wants to dig deeper into some areas. The book is being written in LaTeX because of the math involved. Traditionally, LaTeX documents are handled by a program called BibTeX or something similar. There are lots of reference managers. Many are free. Many are open-source. None of them did exactly what I want. So… down the rabbit hole.