JavaFX TableView: Rapid change in items list not reflected - java

It is hard to explain so I'll use an example:
#Override
public void start(Stage primaryStage) throws Exception
{
final VBox vbox = new VBox();
final Scene sc = new Scene(vbox);
primaryStage.setScene(sc);
final TableView<Person> table = new TableView<>();
final TableColumn<Person, String> columnName = new TableColumn<Person, String>("Name");
table.getColumns().add(columnName);
final ObservableList<Person> list = FXCollections.observableArrayList();
list.add(new Person("Hello"));
list.add(new Person("World"));
Bindings.bindContent(table.getItems(), list);
columnName.setCellValueFactory(new PropertyValueFactory<>("name"));
vbox.getChildren().add(table);
final Button button = new Button("test");
button.setOnAction(event ->
{
final Person removed = list.remove(0);
removed.setName("Bye");
list.add(0, removed);
});
vbox.getChildren().add(button);
primaryStage.show();
}
public static class Person
{
private String name = "";
public Person(String n)
{
name = n;
}
public String getName()
{
return name;
}
public void setName(String n)
{
name = n;
}
}
In this example, I show a TableView with a single column named "Name". Running this sample code, you will get two rows: first row with "Hello" in "Name" column; and second row with "World" in "Name" column.
Additionally, there is a button, this button removes the first Person object from the list, then makes some changes to the object, then adds it back in at the same index. Doing so would cause any ListChangeListener added to the ObservableList to be triggered, and I have tested this to be true.
I would expect the row with "Hello" to be replaced with "Bye", but it seems like the TableView continues to show "Hello". If I used a TimeLine to add delay before I add the removed Person object back to the list, it would change to "Bye".
final Timeline tl = new Timeline(new KeyFrame(Duration.millis(30), ae -> list.add(0, removed)));
tl.play();
Is there something weird with the API? Is there any way to do this without this problem?

This is essentially expected behavior.
Note that (and I'm guessing you are trying to work around this issue), if you simply called
list.get(0).setName("Bye");
which has the same effect in terms of the underlying data, the table would not update as it has no way of being notified that the String field name in the element of the list has changed.
The code
Person removed = list.remove(0);
removed.setName("Bye");
list.add(0, removed);
is really equivalent to list.get(0).setName("Bye");: you just temporarily remove the item from the list before changing it, and then add it back. As far as the list is concerned, the net result is the same. I guess you are doing this in the hope that removing and replacing the item from the list will persuade the table to notice the state of the item has changed. There's no guarantee this will be the case. Here is what's happening:
The binding between your two lists:
Bindings.bindContent(table.getItems(), list);
works like any other binding: it defines how to get the value of the binding (the elements of list), and marks the data as invalid if list is invalidated at any time. The latter happens when you add and remove elements from list.
The TableView will not perform layout every time the binding to the list changes; instead, when then binding is invalidated (add or remove an element), then the table view marks itself as potentially needing to be redrawn. Then, on the next rendering pulse, the table will check the data and see if it really needs to be redrawn, and re-render if needed. There are obvious performance-saving features of this implementation.
So what happens with your code is that an item is removed from the list, causing the binding to be marked as invalid. The item is then changed (by calling setName(...)), and the same item is then added back into the list at the same position. This also causes the binding to be marked as invalid, which has no effect (it is already invalid).
No rendering pulse can occur between the removal and re-addition of this element. Consequently, the first time the table actually looks at the changes that were made to the list has to be after the entire remove-change-add process. At that point, the table will see that the list still contains the exact same elements in the exact same order that it previously contained. (The internal state of one of the elements has changed, but since this is not an observable value - not a JavaFX property - the table is unaware of this.) Consequently, the table sees no changes (or sees that all the changes have cancelled each other out), and doesn't re-render.
In the case where you add the pause, then a rendering frame (or two) occurs between the removal of the item and its re-addition. Consequently, the table actually renders one or two frames without the item, and when it is added back in, it adds it back and renders the current value. (You might, possibly, be able to make the behavior unpredictable, by pausing for 16 or 17 milliseconds, which is right on the cusp of the time for one rendering frame.)
It's not clear what you really intend to do. If you are trying to persuade the table to update without using JavaFX properties, you can do
list.get(0).setName("Bye");
table.refresh();
though this is not a very satisfactory solution.
Note too that
list.remove(0);
list.add(0, new Person("Bye"));
will also work (since now the added element is not the same as the removed element).
The better approach is to implement your model class with JavaFX properties:
public static class Person
{
private final StringProperty name = new SimpleStringProperty("");
public Person(String n)
{
setName(n);
}
public StringProperty nameProperty() {
return name ;
}
public final String getName()
{
return nameProperty().get();
}
public final void setName(String n)
{
nameProperty().set(n);
}
}
and then simply calling
list.get(0).setName("Bye");
will update the table (because the cell will be observing the property).

Related

JavaFX Exception in thread "JavaFX Application Thread" java.lang.ArrayIndexOutOfBoundsException when updating search field

I'm writing a small program for a project for Uni and it's basically a library program to manage books anr read/write to JSON file instead of using a database cause it'd be simpler since it's my first proper Java application.
I'm utilizing a TextField to filter a ListView with all the books' titles, and it works, it shows the correct book in the list and updates the corresponding informations on screen when that book is selected, the issue is that even if the program works as intended, it throws an error everytime I update the search field I get an ArrayIndexOutOfBoundsException. The full stack is as follows:
Exception in thread "JavaFX Application Thread" java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 7
at javafx.base#19-ea/javafx.collections.transformation.FilteredList.get(FilteredList.java:172)
at com.libraryproject.javalibrary/com.libraryproject.javalibrary.MainViewController.populateDetails(MainViewController.java:200)
at com.libraryproject.javalibrary/com.libraryproject.javalibrary.MainViewController.lambda$initialize$3(MainViewController.java:127)
at javafx.graphics#19-ea/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:457)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
at javafx.graphics#19-ea/com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:456)
at javafx.graphics#19-ea/com.sun.glass.ui.InvokeLaterDispatcher$Future.run$$$capture(InvokeLaterDispatcher.java:96)
at javafx.graphics#19-ea/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java)
at javafx.graphics#19-ea/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at javafx.graphics#19-ea/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:184)
After some googling, some people suggested that when updating the GUI from user input, one should do it in the Application Thread, which to be honest I'm not absolutely sure what that means, but anyway I followed the advice and wrapped the functions that would then update the UI variables in a Platform.runLater(() -> {} , but the issue still remains, and it's the stack above, at this point I have absolutely no idea what the problem could be, so, following the stack posted, let's see the code of the parts that are shown:
I'm using a FilteredList to, well, filter the listrView using the search, here's the code managing that and most of the initialize method:
private FilteredList<Book> filteredBooks;
...
...
// inside the initialize method
#Override
public void initialize(URL arg0, ResourceBundle arg1) {
// Populate the variable we use throughout the program with the data from the JSON file
filteredBooks = new FilteredList<Book>(handleJSON.getBooks());
// Then update the list view for the first time
populateView(filteredBooks);
...
...
// section of code responsible to check for search changes, when found, fires populateView once more, this time with the variable updated.
searchField.textProperty().addListener((obs, oldText, newText) -> {
filteredBooks.setPredicate(book -> {
if(newText == null || newText.isEmpty() || newText.isBlank()) {
return true;
}
String lowerCaseCompare = newText.toLowerCase();
if(book.getTitle().toLowerCase().contains(lowerCaseCompare)) {
return true;
}
return false;
});
Platform.runLater(() -> populateView(filteredBooks));
}); // Listener
...
...
...
// This one handles the selection of an item in the list, when selected, the fields on the other side of the windows will get populated with the respective data from the book based on the id from the list, since they essentialy share the same FilteredList
listView.getSelectionModel().selectedItemProperty().addListener((obs, oldSel, newSel) -> {
Platform.runLater(() -> {
populateDetails(listView.getSelectionModel().selectedIndexProperty().getValue(), filteredBooks);
editButton.setDisable(false);
});
As you can see I wrapped all of the function that will update the ListView and fields in the window with Platform.runLater, but it doesn't seem to help.
Now for the populateView function that fires the first time the program is opened and everytime there's a change in the searchfiield:
public void populateView(FilteredList<Book> booksList) {
// clears the listview to avoid old elements stacking in the list.
listView.getSelectionModel().clearSelection();
listView.getItems().clear();
ObservableList<String> rawTitles = FXCollections.observableArrayList();
for(Book book: booksList) {
rawTitles.add(book.getTitle());
}
listView.setItems(rawTitles);
} // populateView()
And last but not least the populateDetails function that fills the fields about a book based on the list selection:
public void populateDetails(Integer selectedBookID, FilteredList<Book> books) {
Book currentBook = books.get(selectedBookID);
titleValue.setText(currentBook.getTitle());
authorValue.setText(currentBook.getAuthor());
languageValue.setText(currentBook.getLanguage());
genreValue.setText(currentBook.getGenre());
pagesValue.setText(currentBook.getPages().toString());
yearValue.setText(currentBook.getRelease().toString());
if (currentBook.getAvailable()) {
availableRadio.setSelected(true);
} else {
unavailableRadio.setSelected(true);
}
} // populateDetails
Thats basically I tried to use the runLater in different places just to be sure, I still get the same stack, any idea what could cause this?
The stack trace tells you exactly what the problem is. The ArrayIndexOutOfBoundsException occurs when you call get(..) on a FilteredList with the value -1, which you do on line 200 of MainViewController.java, in the populateDetails(...) method. Looking at your code, this line must be the line
Book currentBook = books.get(selectedBookID);
so selectedBookID must be the culprit, having the value -1.
selectedBookID is a parameter passed to the method, and you call the method from line 127 of MainController.java, in a lambda expression in the initialize() method. (Again, this information is in the stack trace.) The value you pass is
listView.getSelectionModel().selectedIndexProperty().getValue()
The documentation tells you explicitly when this happens:
The selected index is either -1, to represent that there is no selection, or an integer value that is within the range of the underlying data model size.
So your populate details needs to handle the case where nothing is selected (probably by clearing the text fields). I think it's cleaner to listen to the selectedItemProperty() instead of the selectedIndexProperty(), as it directly gives you the selected Book (or null if nothing is selected), and you don't have to retrieve the Book from the list:
public void populateDetails(Book currentBook) {
if (currentBook == null) {
titleValue.setText("");
authorValue.setText("");
languageValue.setText("");
genreValue.setText("");
pagesValue.setText("");
yearValue.setText("");
availableRadio.setSelected(false);
unavailableRadio.setSelected(false);
} else {
titleValue.setText(currentBook.getTitle());
authorValue.setText(currentBook.getAuthor());
languageValue.setText(currentBook.getLanguage());
genreValue.setText(currentBook.getGenre());
pagesValue.setText(currentBook.getPages().toString());
yearValue.setText(currentBook.getRelease().toString());
if (currentBook.getAvailable()) {
availableRadio.setSelected(true);
} else {
unavailableRadio.setSelected(true);
}
}
}
Your code is overkill; there is basically no need for the populateView() method. The filtered list will update its contents when you change the predicate, and notify observers that its content has changed. So you should just set the list view's items list to the filtered list directly. Then your listener for the search field only has to update the predicate, and the list view will automatically update.
Delete the populateView() method and update the initialize() method as:
public void initialize(URL arg0, ResourceBundle arg1) {
// Populate the variable we use throughout the program with the data from the JSON file
filteredBooks = new FilteredList<Book>(handleJSON.getBooks());
listView.setItems(filteredBooks);
// ...
// ...
// section of code responsible to check for search changes, when found, fires populateView once more, this time with the variable updated.
searchField.textProperty().addListener((obs, oldText, newText) -> {
filteredBooks.setPredicate(book -> {
if(newText == null || newText.isEmpty() || newText.isBlank()) {
return true;
}
String lowerCaseCompare = newText.toLowerCase();
return book.getTitle().toLowerCase().contains(lowerCaseCompare)
});
}); // Listener
// ...
// This one handles the selection of an item in the list, when selected, the fields on the other side of the windows will get populated with the respective data from the book based on the id from the list, since they essentialy share the same FilteredList
listView.getSelectionModel().selectedItemProperty().addListener(
(obs, oldSel, newSel) -> populateDetails(newSel)
);
}

How to delete a CheckBox from a TableView in JavaFX?

I'm writing a seating chart program using JavaFX. I have a table that keeps a list of students together that holds their name, grade, and whether they are present or absent (using a checkbox). I have a delete button that allows me to delete the students from the list. This works fine, however, whenever I delete the student object, the checkbox does not go along with it. I'm not sure what I would need to add to get that to work. Here is a snippet of the delete code. There are also two images below that show my problem. This is my first post so please let me know if I missed something. Please help! Thanks!
ObservableList<Student> items, sel;
items = currentTable.getItems();
sel = currentTable.getSelectionModel().getSelectedItems();
Student s = new Student("", "", 0, "");
for (Student p : sel) {
items.remove(p);
s = p;
}
Before Delete
After Delete
This has nothing to do with the delete or remove method. It has to do with what you did in TableColumn.setCellFactory().
To get the checkbox you shown, you should have used (in general) one of the two methods:
Overriding updateItem() in TableCell while setting Cell Factory
There is this empty parameter in updateItem() which indicates whether the row is empty or not. You need to use that to determine when not to show the checkbox.
column.setCellFactory(col -> {
return new TableCell<Foo, Boolean>() {
final CheckBox checkBox = new CheckBox();
#Override
public void updateItem(final Boolean selected, final boolean empty) {
super.updateItem(selected, empty);
if (!this.isEmpty()) {
setGraphic(checkBox);
setText("");
}
else {
setGraphic(null); // Remove checkbox if row is empty
setText("");
}
}
}
}
Using CheckBoxTableCell
JavaFX API has this convenient class CheckBoxTableCell that would do all these for you. Most people find this class hard to use because there are 2 things that you need to ensure to use it correctly:
The TableView that the column belongs to must be editable.
The TableColumn itself must be editable.
Example:
tableView.setEditable(true);
tableColumnSelected.setCellFactory(CheckBoxTableCell.forTableColumn(tableColumnSelected));
tableColumnSelected.setEditable(true);
As for whether which entry you want to be removed with the delete button, you just need to remove the correct items from the TableView.

JavaFX - Cannot Refresh/Update ListView

Well it appears I have been stumped by one of the simplest ListView implementations out there. For a few days I have found it impossible to properly reallocate a JavaFX ListView. I am working on making an EntityView completely dynamic, being able to remove elements whenever needed through a ContextMenu. So I have a ListView that is populated by an ArrayList, which we will refer to as "renderable". When you select "Remove" in the context menu, it removes the Entity from the renderable list, which also happens to be the "value" of the List Cell on whom I right clicked. Afterwards, I wish to refresh the ListView and remove now the nonexistent cell. So by creating a new ObservableList with the new renderable list (which removes the correct entity and that works fine), I set the items in the ListView, which does jack shit. I can set the list to null in this case, which removes all the elements. But I cannot reset the list with the new array and it remove the now missing entity. Somebody point me in the right direction please!
When I use the method I stated above, it removes it from the list, but not visually. There becomes an unusable cell at the bottom of the list, which has its name.
public void createContextMenu(final Entity curr, MouseEvent me){
MenuItem[] items = {new MenuItem("EDIT TYPE"), new MenuItem("REMOVE")};
ContextMenu menu = new ContextMenu(items);
menu.show(list, me.getScreenX(), me.getScreenY());
items[1].setOnAction(new EventHandler<ActionEvent>(){
public void handle(ActionEvent arg0) {
CanvasTab tab = (CanvasTab) core.canvasTabbedPane.getSelectionModel().getSelectedItem();
Kanvas k = tab.canvas;
k.renderable.remove(curr);
System.out.println(k.renderable);
k.redraw();
EntityView.this.list.getItems().remove(curr);
ObservableList<Entity> temp = FXCollections.observableList(k.renderable);
EntityView.this.list.setItems(temp);
}
});
}
This is the context menu:

How to force combobox to render autocomplete options?

This is my zul code:
<combobox id="digitalPublisherCombobox" value="#load(ivm.inventory.digitalPublisherName)"
onOK="#command('setDigitalPublisher', digitalPublisherBox = self)"
onSelect="#command('setDigitalPublisher', digitalPublisherBox = self)"
onChanging="#command('setupQuicksearchByEvent', searchlayout = event, prefix = 'PUB', tags = 'PublisherName, PublisherNameTranslit')"
mold="rounded" hflex="1" buttonVisible="false" autodrop="true">
<comboitem self="#{each=entry}" value="#{entry.key}" label="#{entry.value}"/>
</combobox>
And this is QuickSearch implementations:
#Command
public void setupQuicksearchByEvent(#BindingParam("searchlayout")Event event, #BindingParam("prefix") String prefix, #BindingParam("tags") String tags) throws WrongValueException, SearchException, IOException
{
if(event instanceof InputEvent)
{
InputEvent inputEvent = (InputEvent) event;
String inputText = inputEvent.getValue();
List<String> searchFields = Arrays.asList(tags.split(","));
ListModel model = new ListModelMap(ZKLogic.findDocsStartingWith(prefix, searchFields, "proxy", inputText), true);
ListModel subModel = ListModels.toListSubModel(model, Autocompleter.MAP_VALUE_CONTAINS_COMPARATOR, 10);
Combobox searchBox = (Combobox) event.getTarget();
searchBox.setModel(subModel);
searchBox.setItemRenderer(new ComboitemRenderer()
{
#Override
public void render( Comboitem item, Object data, int pos ) throws Exception
{
String publisherString = data.toString();
UID key = getUidFromPublisherString(publisherString);
int startIndex = publisherString.indexOf('=') + 1;
String publisher = publisherString.substring(startIndex);
item.setLabel(publisher);
item.setValue(key);
}
});
}
}
ZKLogic.findDocsStartingWith return map with UID-key and String-value.
With code above I achieved to get dropdown list when I switch to another window. I need to type something, then select another browser or notepad window - and comboitems will be displayed immediately.
So, my question still need answer, is there are any techniques to reproduce this windows switching in code? Or maybe I should do something with autocomplete, because I've got some ac working with preloaded lists, but this thing should return only 10 records from db, instead of all 70000 entries, every time when user type something in the field.
Edit 20/09/2013: Problem still exist. Rename question a bit, because thing that I need is to call render option by force in code. Is there is any way to do it? Code hasn't changed a lot, but print option in render method said, that method can miss two or more onChange events and suddenly render text for one variant.
Maybe you know another autocomplete options in zk framework where database participating? I'm ready to change implementation, if there is a guide with working implementation of it.
Ok I see two problems, you should solve first.
Setting the Renderer in every call of setupQuicksearchByEvent(...).
that is not logical, cos it is the same every time.
Add to the zul combobox tag something like
itemRenderer="#load(ivm.myRenderer)" ....
If you want just 10 items, do not let the db-request return more then 10.
If you use JPA klick here or for sql here or just google a bit.
After you fixed this two issues, we can exclude these as a reason of the unexpected behavior and fix it, if it is still present.
Edit
Ok, I have two possible ways to fix it.
Call Combobox#invalidate()
This schould force zk to rerender the Combobox, but could
lead to low performance and I would not prefer this.
Use Listbox with the select mold instead of Combobox.
To force the rerender, use Listbox#renderAll()
Try setting the selected item on your combobox or throw its related event
Solution is simple. Really. Nothing is better then brute-force, but I think I tried to avoid it and use it in despair.
#Command
public void setupQuicksearchByEvent(#BindingParam("searchlayout")Event event, #BindingParam("prefix") String prefix, #BindingParam("tags") String tags) throws WrongValueException, SearchException, IOException
{
if(event instanceof InputEvent)
{
InputEvent inputEvent = (InputEvent) event;
String inputText = inputEvent.getValue();
List<String> searchFields = Arrays.asList(tags.split(","));
Map<UID, String> publishers = ZKLogic.findDocsStartingWith(prefix, searchFields, "proxy", inputText);
Combobox searchBox = (Combobox) event.getTarget();
searchBox.getChildren().clear();
for (Map.Entry<UID, String > entry : publishers.entrySet())
{
Comboitem item = new Comboitem();
item.setLabel(entry.getValue());
item.setValue(entry.getKey());
searchBox.appendChild(item);
}
}
}

How do I completely disable row selection in a CellTable?

I want to completely view updates when a row is selected in a CellTable. How can this be done? In the following test case, using NoSelectionModel, the view is still updated: clicking on a row changes the background and border colors of the row until another row is clicked.
CellTable<String> table = new CellTable<String>();
TextColumn<String> column = new TextColumn<String>()
{
#Override
public String getValue(String string)
{
return string;
}
};
table.addColumn(column);
List<String> sampleData = Arrays.asList("foo", "bar", "baz");
table.setRowData(sampleData);
final NoSelectionModel<String> selectionModel = new NoSelectionModel<String>();
table.setSelectionModel(selectionModel);
RootPanel.get().add(table);
I've also attempted to subclass SingleSelectionModel with empty override methods, without success.
I can fake the behavior I want by providing empty CSS stylings for selected rows, but that method seems hack-ish.
What you're seeing is the keyboard-selection highlighting (really useful when you're not using the mouse to interact with the table).
You can disable it using setKeyboardSelectionPolicy(KeyboardSelectionPolicy.DISABLED)

Categories

Resources