JavaFX Concurrency : Update multiple messages from Service - java

How can we update multiple controls from a single Service. Right now there is only one single updateMessage() in Service, whose value can be bound to just one control and hence update just that. How can we update values for multiple controls ?
My instance of Service Class:
//run a background thread
threadTimeChecker = new Service<Void>() {
#Override
protected Task<Void> createTask() {
return new Task<Void>() {
#Override
protected Void call() throws Exception {
while (!isDone) {
DataHelper.setCurrentDate(LocalDate.now());
if(!DataHelper.getOldDate().equals(DataHelper.getCurrentDate())) {
DataHelper.setIntIndex(DataHelper.getIntIndex()+1);
DataHelper.setOldDate(DataHelper.getCurrentDate());
DataHelper.saveData();
System.out.println("Saved!");
}
//Thread.currentThread().sleep(2000);
updateMessage(wordString.getValue());
}
return null;
}
};
}
};
threadTimeChecker.restart();
//bind string properties to labels
word.textProperty().bind(threadTimeChecker.messageProperty());
This only updates one message i.e. I can only bind one label. Is there any way I can update multiple messages from the same thread so I can bind multiple labels in my UI?
EDITED - More Information according to comments
My runnable is:
#Override
protected Task<Void> createTask() {
return new Task<Void>() {
#Override
protected Void call() throws Exception {
//loop run until the program is closed
while (!isDone) {
DataHelper.setCurrentDate(LocalDate.now());
if (!DataHelper.getOldDate().equals(DataHelper.getCurrentDate())) {
Platform.runLater(new Runnable(){
#Override
public void run() {
DataHelper.setIntIndex(DataHelper.getIntIndex()+1);
}
});
DataHelper.setOldDate(DataHelper.getCurrentDate());
DataHelper.saveData();
}
Thread.currentThread().sleep(5000);
}
return null;
}
};
}
I run a thread to change DataHelper.IntIndex which invokes a listener that changes the 'String Property' as per the index like:
//listener to detect change in index and assign strings of word,meaning, and sentence, accordingly
DataHelper.intIndexProperty().addListener(
(v, oldValue, newValue) -> {
wordString.setValue(DataHelper.getListOfWords().get((int) newValue).get("word"));
meaningString.setValue(DataHelper.getListOfWords().get((int) newValue).get("meaning"));
sentenceString.setValue(DataHelper.getListOfWords().get((int) newValue).get("sentence"));
System.out.print("kjbmmj");
}
);
And I have used these 'String Properties' to bind to three different labels correspondingly like:
//bind string properties to labels
word.textProperty().bind(wordString);
meaning.textProperty().bind(meaningString);
sentence.textProperty().bind(sentenceString);
Now what I want to do is to use more JavaFX inclined updateMessage to achieve the same.

Instead of updating multiple messages, we just need to update a single instance of DataHelper. DataHelper has contents which will update multiple labels. For instance, let us consider we have the following labels which we want to update after each call of service :
wordLabel
meaningLabel
sentenceLabel
To keep things simple, let us consider that you've a class DataHelper which has three properties word, meaning and sentence.
private class DataHelper {
public DataHelper(String word, String meaning, String sentence) {
this.word.setValue(word);
this.meaning.setValue(meaning);
this.sentence.setValue(sentence);
}
StringProperty word = new SimpleStringProperty();
StringProperty meaning = new SimpleStringProperty();
StringProperty sentence = new SimpleStringProperty();
// setters and getters
}
We call the service for some background task and whenever the service is done with the background task it can return us the updated DataHelper.
Service<DataHelper> service = new Service<DataHelper>() {
#Override
protected Task<DataHelper> createTask() {
return new Task<DataHelper>() {
#Override
protected DataHelper call() throws Exception {
i.incrementAndGet(); // Don't worry about i here
return new DataHelper("Word " + i, "Meaning " + i, "Sentence " + i);
}
};
}
};
Now, every time we call the server we get an updated DataHelper which we want to show on the label(s).
To approach this, we declare a variable dataHelper and bind its properties to the textProperty() of various labels :
DataHelper dataHelper = new DataHelper("Word", "Meaning", "Sentence");
wordLabel.textProperty().bind(dataHelper.wordProperty());
meaningLabel.textProperty().bind(dataHelper.meaningProperty());
sentenceLabel.textProperty().bind(dataHelper.sentenceProperty());
Now you must be wondering, how will we update dataHelper, right? Well, that's the easy part. This can be can taken care in setOnSucceeded() of Service, where getValue() will return us a new instance of DataHelper with updated values.
service.setOnSucceeded(event -> {
dataHelper.setWord(service.getValue().getWord());
dataHelper.setMeaning(service.getValue().getMeaning());
dataHelper.setSentence(service.getValue().getSentence());
});
Complete MCVE :
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.concurrent.atomic.AtomicInteger;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
// Properties
DataHelper dataHelper = new DataHelper("Word", "Meaning", "Sentence");
AtomicInteger i = new AtomicInteger(0);
// UI elements
Label wordLabel = new Label();
Label meaningLabel = new Label();
Label sentenceLabel = new Label();
Button startService = new Button("Start");
Service<DataHelper> service = new Service<DataHelper>() {
#Override
protected Task<DataHelper> createTask() {
return new Task<DataHelper>() {
#Override
protected DataHelper call() throws Exception {
i.incrementAndGet();
return new DataHelper("Word " + i, "Meaning " + i, "Sentence " + i);
}
};
}
};
startService.setOnAction(e -> {
if(service.getState().equals(Worker.State.READY) || service.getState().equals(Worker.State.SUCCEEDED)) {
service.restart();
}
});
service.setOnSucceeded(event -> {
dataHelper.setWord(service.getValue().getWord());
dataHelper.setMeaning(service.getValue().getMeaning());
dataHelper.setSentence(service.getValue().getSentence());
});
wordLabel.textProperty().bind(dataHelper.wordProperty());
meaningLabel.textProperty().bind(dataHelper.meaningProperty());
sentenceLabel.textProperty().bind(dataHelper.sentenceProperty());
VBox box = new VBox(10, wordLabel, meaningLabel, sentenceLabel, startService);
box.setAlignment(Pos.CENTER);
Scene scene = new Scene(box, 200, 200);
primaryStage.setScene(scene);
primaryStage.show();
}
private class DataHelper {
StringProperty word = new SimpleStringProperty();
StringProperty meaning = new SimpleStringProperty();
StringProperty sentence = new SimpleStringProperty();
public DataHelper(String word, String meaning, String sentence) {
this.word.setValue(word);
this.meaning.setValue(meaning);
this.sentence.setValue(sentence);
}
public String getMeaning() {
return meaning.get();
}
public StringProperty meaningProperty() {
return meaning;
}
public void setMeaning(String meaning) {
this.meaning.set(meaning);
}
public String getSentence() {
return sentence.get();
}
public StringProperty sentenceProperty() {
return sentence;
}
public void setSentence(String sentence) {
this.sentence.set(sentence);
}
public String getWord() {
return word.get();
}
public StringProperty wordProperty() {
return word;
}
public void setWord(String word) {
this.word.set(word);
}
}
public static void main(String[] args) {
launch(args);
}
}

Related

Running a thread by toggling a flag externally (from GUI)

I am making a simple JavaFX college course project and I need a good way of dealing with threads, mainly running them while a certain flag is activated.
This is a simple sketch I came up with:
public class ListenerService extends Thread {
private static ArrayList<ListenerService> listeners = new ArrayList<>();
private ToggleButton button;
private File folder;
private SimpleBooleanProperty active = new SimpleBooleanProperty();
ListenerService(ToggleButton button, String pathname) {
this.button = button;
this.folder = new File(pathname);
button.setOnAction(event -> active.set(button.isSelected()));
active.addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue)
-> {if (newValue.booleanValue()) start();});
listeners.add(this);
}
#Override
public void run() {
while(active.get())
System.out.print(".");
}
The process is as following:
The user dynamically creates a ToggleButton on the form. A
ListenerService object is created, to which a button and a directory
are assigned.
A listener is assigned to the button - if it's clicked - activate
the flag. Otherwise, deactivate. The flag here is a
SimpleBooleanProperty instance.
If the flag is switched on, run the thread. The thread will run
while the flag is active. If the user toggles the button again and
deactivates it, the condition in the while loop would fail and
thread should stop running.
As soon as I run the program, it freezes. I tried making the flag volatile, but nothing changed. Since the flag is controlled externally (from GUI), there isn't a way to make this method synchronized.
What am I doing wrong?
You basically create a new Thread that runs as long as the button is selected and exits when the button is not selected.
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.scene.Scene;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class ThreadApp extends Application {
public class Worker implements Runnable{
private boolean active;
public void setActive(boolean active) {
this.active = active;
}
#Override
public void run() {
while(active)
{
System.out.println("Active! " + System.currentTimeMillis());
}
}
}
public class WorkerToggle extends ToggleButton {
Worker worker;
public WorkerToggle(String text) {
super(text);
this.worker = new Worker();
setOnAction((event) -> {
if(isSelected())
{
worker.setActive(true);
Thread thread = new Thread(worker);
thread.setDaemon(true);
thread.start();
}else
{
worker.setActive(false);
}
});
}
}
#Override
public void start(Stage primaryStage) {
BorderPane rootPane = new BorderPane();
Scene scene = new Scene(rootPane);
rootPane.setCenter(new WorkerToggle("toggle me"));
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
This should work fine, but creating Threads can be expensive, so you might want to look into ThreadPoolExecutor if you notice some performance problems there.
In JavaFX you have the ability to use a scheduled service to run things off the main FX thread. Here is simple sample that might help.
public class JavaFXApplication3 extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
PollingService service = new PollingService();
service.setPeriod(Duration.millis(1000)); // sysout every second
ToggleButton tb = new ToggleButton("Start Process");
tb.setOnAction(event -> {
System.out.println(tb.isSelected());
if(tb.isSelected()){
service.reset();
service.start();
}else {
service.cancel();
}
});
VBox vbox = new VBox(tb);
Scene scene = new Scene(vbox);
primaryStage.setScene(scene);
primaryStage.show();;
}
private class PollingService extends ScheduledService<Void> {
#Override
protected Task<Void> createTask() {
return new Task<Void>() {
#Override
protected Void call() {
System.out.print(".#.");
return null;
}
};
}
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}

TableVew - Edit cell when KeyEvent is thrown

I have an event listener on a TableView that listens for keyboard event.
// Add event listener to table
table.setOnKeyTyped(event -> {
TablePosition<SimpleStringProperty, String> focusedCell = table.getFocusModel().getFocusedCell();
if (focusedCell != null)
{
table.getItems().get(focusedCell.getRow()).set(event.getCharacter());
table.edit(focusedCell.getRow(), focusedCell.getTableColumn());
}
});
I am having problems with updating the cell with the new data when a user clicks enter or changes focus to another cell. When you click enter or change focus, the cell becomes empty. I'm not sure why. How can I save the data and update the cell with the new data.
// Here is the full code.
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class TableViewEdit extends Application
{
#Override
public void start(Stage primaryStage)
{
TableView<SimpleStringProperty> table = new TableView<SimpleStringProperty>();
table.getSelectionModel().setCellSelectionEnabled(true);
table.setEditable(true);
table.getColumns().add(this.createColumn());
ObservableList<SimpleStringProperty> rowData = FXCollections.observableArrayList();
//table.getItems().addAll(rowData);
for (int j = 0; j < 10; j++)
{
rowData.add(new SimpleStringProperty(String.format("Cell [%d", j)));
}
table.setItems(rowData);
table.setOnKeyTyped(event -> {
TablePosition<SimpleStringProperty, String> focusedCell = table.getFocusModel().getFocusedCell();
if (focusedCell != null)
{
table.getItems().get(focusedCell.getRow()).set(event.getCharacter());
table.edit(focusedCell.getRow(), focusedCell.getTableColumn());
}
});
Scene scene = new Scene(new BorderPane(table), 880, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private TableColumn<SimpleStringProperty, String> createColumn()
{
TableColumn<SimpleStringProperty, String> col = new TableColumn<>("Column ");
col.setCellValueFactory(cellData -> cellData.getValue());
col.setCellFactory(column -> new EditCell());
return col;
}
private static class EditCell extends TableCell<SimpleStringProperty, String>
{
private final TextField textField = new TextField();
EditCell()
{
this.textProperty().bind(this.itemProperty());
this.setGraphic(this.textField);
this.setContentDisplay(ContentDisplay.TEXT_ONLY);
this.textField.setOnAction(evt -> this.commitEdit(this.textField.getText()));
this.textField.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> {
if (!isNowFocused)
{
this.commitEdit(this.textField.getText());
}
});
}
#Override
public void startEdit()
{
super.startEdit();
this.textField.setText(this.getItem());
this.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
this.textField.requestFocus();
}
#Override
public void cancelEdit()
{
super.cancelEdit();
this.setContentDisplay(ContentDisplay.TEXT_ONLY);
}
#Override
public void commitEdit(String text)
{
super.commitEdit(text);
this.setContentDisplay(ContentDisplay.TEXT_ONLY);
}
}
public static void main(String[] args)
{
launch(args);
}
}
These get really tricky; I think anything "behavior-related" (i.e. standard controls reacting to user input) is hard to change and generally not well supported in JavaFX. Hopefully this is an area of the API that will be improved...
There seem to be a couple of different issues. I think that what is happening with the Enter key, is that although this generates an ActionEvent on the text field, which commits the edit, etc, the keyTyped event still propagates back to the table, causing it to re-enter editing mode. A fix for this seems to be to use a keyPressed handler on the table instead (though to be honest this doesn't feel very robust).
The code relies on the default onEditCommit handler on the table column to actually change the property value. The onEditCommit handler is invoked by the default table cell's commitEdit method. The problem with calling commitEdit(...) on losing focus is that the default commitEdit method first checks if the cell is in an editing state, and does nothing if it's not. It appears that when the cell loses focus, it is taken out of the editing state before the focusProperty listener is invoked, so the onEditCommit handler is never called. (As an aside, this also prevents example 13-11 "Alternative solution of cell editing" (sic) from working correctly in the JDK 8 u25 (the current version).)
The only fix I can see for this second issue is to directly update the property from the commitEdit(...) method. This requires the cell have a reference to the property, which breaks the nice separation between the cell and the cell value.
I rewrote the example using the usual Person example and incorporated these two fixes. This example works quite well, though as I said some parts feel as though they are not very robust:
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class TableViewEditOnType extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Person> table = new TableView<>();
table.getSelectionModel().setCellSelectionEnabled(true);
table.setEditable(true);
table.getColumns().add(createColumn("First Name", Person::firstNameProperty));
table.getColumns().add(createColumn("Last Name", Person::lastNameProperty));
table.getColumns().add(createColumn("Email", Person::emailProperty));
table.getItems().addAll(
new Person("Jacob", "Smith", "jacob.smith#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Michael", "Brown", "michael.brown#example.com")
);
table.setOnKeyPressed(event -> {
TablePosition<Person, ?> pos = table.getFocusModel().getFocusedCell() ;
if (pos != null) {
table.edit(pos.getRow(), pos.getTableColumn());
}
});
Scene scene = new Scene(new BorderPane(table), 880, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private TableColumn<Person, String> createColumn(String title, Function<Person, StringProperty> property) {
TableColumn<Person, String> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
col.setCellFactory(column -> new EditCell(property));
return col ;
}
private static class EditCell extends TableCell<Person, String> {
private final TextField textField = new TextField();
private final Function<Person, StringProperty> property ;
EditCell(Function<Person, StringProperty> property) {
this.property = property ;
textProperty().bind(itemProperty());
setGraphic(textField);
setContentDisplay(ContentDisplay.TEXT_ONLY);
textField.setOnAction(evt -> {
commitEdit(textField.getText());
});
textField.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> {
if (! isNowFocused) {
commitEdit(textField.getText());
}
});
}
#Override
public void startEdit() {
super.startEdit();
textField.setText(getItem());
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
textField.requestFocus();
}
#Override
public void cancelEdit() {
super.cancelEdit();
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
#Override
public void commitEdit(String text) {
super.commitEdit(text);
Person person = getTableView().getItems().get(getIndex()) ;
StringProperty cellProperty = property.apply(person);
cellProperty.set(text);
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
}
public static class Person {
private final StringProperty firstName = new SimpleStringProperty();
private final StringProperty lastName = new SimpleStringProperty();
private final StringProperty email = new SimpleStringProperty();
public Person(String firstName, String lastName, String email) {
setFirstName(firstName);
setLastName(lastName);
setEmail(email);
}
public final StringProperty firstNameProperty() {
return this.firstName;
}
public final java.lang.String getFirstName() {
return this.firstNameProperty().get();
}
public final void setFirstName(final java.lang.String firstName) {
this.firstNameProperty().set(firstName);
}
public final StringProperty lastNameProperty() {
return this.lastName;
}
public final java.lang.String getLastName() {
return this.lastNameProperty().get();
}
public final void setLastName(final java.lang.String lastName) {
this.lastNameProperty().set(lastName);
}
public final StringProperty emailProperty() {
return this.email;
}
public final java.lang.String getEmail() {
return this.emailProperty().get();
}
public final void setEmail(final java.lang.String email) {
this.emailProperty().set(email);
}
}
public static void main(String[] args) {
launch(args);
}
}

Synchronizing a sequence of asynchronous calls

I'm using JavaFX's WebView to parse a website. The site contains a bunch of links - I need to open each of them separately, in a given order, and retrieve one information from each of them.
In order to make sure that WebView has loaded the whole site, I'm listening to changed event of WebEngine and waiting for newState == Worker.State.SUCCEEDED. The problem is that this call is asynchronous. When I'm calling webEngine.load(firstAddress);, the code immediately returns and before this page will have been loaded, my code will call another webEngine.load(secondAddress);, and so on.
I understand why it's done this way (why async is better than sync), but I'm a beginner in Java and I'm not sure what's the best solution to this problem. I somehow understand multithreading and stuff, so I've already tried a semaphore (CountDownLatch class). But the code hangs on await and I'm not sure what I'm doing wrong.
Could someone please show me how it should be done the right way? Maybe some universal pattern how to cope with scenarios like this?
A pseudocode of what I want to achieve:
WebEngine webEngine = new WebEngine();
webEngine.loadPage("http://www.something.com/list-of-cars");
webEngine.waitForThePageToLoad(); // I need an equivalent of this. In the real code, this is done asynchronously as a callback
// ... some HTML parsing or DOM traversing ...
List<String> allCarsOnTheWebsite = webEngine.getDocument()....getChildNodes()...;
// allCarsOnTheWebsite contains URLs to the pages I want to analyze
for (String url : allCarsOnTheWebsite)
{
webEngine.loadPage(url);
webEngine.waitForThePageToLoad(); // same as in line 3
String someDataImInterestedIn = webEngine.getDocument()....getChildNodes()...Value();
System.out.println(url + " : " + someDataImInterestedIn);
}
System.out.println("Done, all cars have been analyzed");
You should use listeners which get invoked when the page is loaded, instead of blocking until it's done.
Something like:
WebEngine webEngine = new WebEngine();
ChangeListener<State> initialListener = new ChangeListener<State>() {
#Override
public void changed(ObservableValue<? extends State> obs, State oldState, State newState) {
if (newState == State.SUCCEEDED) {
webEngine.getLoadWorker().stateProperty().removeListener(this);
List<String> allCarsOnTheWebsite = webEngine.getDocument()... ;
loadPagesConsecutively(allCarsOnTheWebsite, webEngine);
}
}
};
webEngine.getLoadWorker().addListener(initialListener);
webEngine.loadPage("http://www.something.com/list-of-cars");
// ...
private void loadPagesConsecutively(List<String> pages, WebEngine webEngine) {
LinkedList<String> pageStack = new LinkedList<>(pages);
ChangeListener<State> nextPageListener = new ChangeListener<State>() {
#Override
public void changed(ObservableValue<? extends State> obs, State oldState, State newState) {
if (newState == State.SUCCEEDED ) {
// process current page data
// ...
if (pageStack.isEmpty()) {
webEngine.getLoadWorker().stateProperty().removeListener(this);
} else {
// load next page:
webEngine.load(pageStack.pop());
}
}
}
};
webEngine.getLoadWorker().stateProperty().addListener(nextPageListener);
// load first page (assumes pages is not empty):
webEngine.load(pageStack.pop());
}
If you want to run all the tasks concurrently, but process them in the order they were submitted, have a look at the following example:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class ProcessTaskResultsSequentially extends Application {
#Override
public void start(Stage primaryStage) {
ListView<String> results = new ListView<>();
List<Task<Integer>> taskList = new ArrayList<>();
for (int i = 1; i<= 10 ; i++) {
taskList.add(new SimpleTask(i));
}
ExecutorService exec = Executors.newCachedThreadPool(r -> {
Thread t = new Thread(r);
t.setDaemon(true);
return t ;
});
Thread processThread = new Thread(() -> {
for (Task<Integer> task : taskList) {
try {
int result = task.get();
Platform.runLater(() -> {
results.getItems().add("Result: "+result);
});
} catch (Exception e) {
e.printStackTrace();
}
}
});
processThread.setDaemon(true);
processThread.start();
taskList.forEach(exec::submit);
primaryStage.setScene(new Scene(new BorderPane(results), 250, 400));
primaryStage.show();
}
public static class SimpleTask extends Task<Integer> {
private final int index ;
private final static Random rng = new Random();
public SimpleTask(int index) {
this.index = index ;
}
#Override
public Integer call() throws Exception {
System.out.println("Task "+index+" called");
Thread.sleep(rng.nextInt(1000)+1000);
System.out.println("Task "+index+" finished");
return index ;
}
}
public static void main(String[] args) {
launch(args);
}
}

How to add listener to the checkbox inside a listview that uses CheckBoxListCell

I have a listview that uses a CheckBoxListCell to display a list with checkboxes next to the items. How do I add a listener to this checkbox to know when an item as been selected or unselected?
Solution
You don't add a listener to the checkbox. You add a listener to the observable property of the object which was associated with the checkbox by the CheckBoxListCell.forListView routine.
Setting up the association:
ListView<Task> checklist = new ListView<>(tasks);
checklist.setCellFactory(CheckBoxListCell.forListView(Task::selectedProperty));
Adding a listener for all items:
tasks.forEach(task -> task.selectedProperty().addListener((observable, wasSelected, isSelected) -> {
if (isSelected) {
// . . .
} else {
// . . .
}
}));
Documentation
The process is described in the CheckBoxListCell.forListView javadoc like so:
getSelectedProperty - A Callback that, given an object of type T
(which is a value taken out of the ListView.items list), will
return an ObservableValue that represents whether the given
item is selected or not. This ObservableValue will be bound
bidirectionally (meaning that the CheckBox in the cell will set/unset
this property based on user interactions, and the CheckBox will
reflect the state of the ObservableValue, if it changes externally).
Sample Program
A sample program which demonstrated some of the patterns which could be used with CheckBoxListCell:
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.collections.*;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.control.cell.CheckBoxListCell;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import java.util.*;
import java.util.stream.Collectors;
public class CheckList extends Application {
#Override
public void start(Stage stage) throws Exception{
ObservableList<Task> tasks = FXCollections.observableArrayList(
Arrays.stream(taskNames).map(Task::new).collect(Collectors.toList())
);
ListView<String> reactionLog = new ListView<>();
tasks.forEach(task -> task.selectedProperty().addListener((observable, wasSelected, isSelected) -> {
if (isSelected) {
reactionLog.getItems().add(reactionStrings.get(task.getName()));
reactionLog.scrollTo(reactionLog.getItems().size() - 1);
}
}));
ListView<Task> checklist = new ListView<>(tasks);
checklist.setCellFactory(CheckBoxListCell.forListView(Task::selectedProperty, new StringConverter<Task>() {
#Override
public String toString(Task object) {
return object.getName();
}
#Override
public Task fromString(String string) {
return null;
}
}));
HBox layout = new HBox(10, checklist, reactionLog);
layout.setPrefSize(350, 150);
layout.setPadding(new Insets(10));
Scene scene = new Scene(layout);
stage.setScene(scene);
stage.show();
}
public static class Task {
private ReadOnlyStringWrapper name = new ReadOnlyStringWrapper();
private BooleanProperty selected = new SimpleBooleanProperty(false);
public Task(String name) {
this.name.set(name);
}
public String getName() {
return name.get();
}
public ReadOnlyStringProperty nameProperty() {
return name.getReadOnlyProperty();
}
public BooleanProperty selectedProperty() {
return selected;
}
public boolean isSelected() {
return selected.get();
}
public void setSelected(boolean selected) {
this.selected.set(selected);
}
}
public static void main(String[] args) {
launch(args);
}
private static final String[] taskNames = {
"Walk the dog",
"Skin the cat",
"Feed the pig"
};
private static final Map<String, String> reactionStrings = new HashMap<>();
static {
reactionStrings.put("Walk the dog", "The dog thanks you");
reactionStrings.put("Skin the cat", "The cat hates you");
reactionStrings.put("Feed the pig", "The pig wants more");
}
}
Sample output after selecting the first item once and the third item three times.
Here is an alternative if the item does not already have a property that indicates if it has been selected or not:
public class CheckedListViewCheckObserver<T> extends SimpleObjectProperty<Pair<T, Boolean>> {
BooleanProperty getObserverForObject(T object) {
BooleanProperty value = new SimpleBooleanProperty(false);
value.addListener((observable, oldValue, newValue) -> {
CheckedListViewCheckObserver.this.set(new Pair<>(object, newValue));
});
return value;
}
}
Then to use it, you simply do:
CheckedListViewCheckObserver observer = new CheckedListViewCheckObserver<>();
checklist.setCellFactory(CheckBoxListCell.forListView(observer::getObserverForObject));
Now you can set a listener to listen for any changes:
observer.addListener((obs, old, curr) -> {
if (curr.getValue()) {
System.out.println("You have checked " + curr.getKey());
} else {
System.out.println("You have unchecked " + curr.getKey());
}
});
The advantage of this method is that it does not depend on the objects being used; Instead, since it is generic, you can simply attach it to an already existing listview and it starts working off the bat.
Hope this helps someone.

Programmatically change the TableView row appearance

After doing a Oracle tutorial about the TableView, I was wondering if there's a way to programmatically apply different CSS style to the selected TableView row. For example, user selects a certain row, clicks the "Highlight" button and the selected row gets brown background, white text fill, etc. I've read the JavaFX tableview colors, Updating TableView row appearance and Background with 2 colors in JavaFX?, but to no avail =/
Here's the source:
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;
public class TableViewSample extends Application {
private TableView<Person> table = new TableView<Person>();
private final ObservableList<Person> data =
FXCollections.observableArrayList(
new Person("Jacob", "Smith", "jacob.smith#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Michael", "Brown", "michael.brown#example.com")
);
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
Scene scene = new Scene(new Group());
stage.setTitle("Table View Sample");
stage.setWidth(450);
stage.setHeight(600);
final Label label = new Label("Address Book");
label.setFont(new Font("Arial", 20));
TableColumn firstNameCol = new TableColumn("First Name");
firstNameCol.setMinWidth(100);
firstNameCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("firstName"));
TableColumn lastNameCol = new TableColumn("Last Name");
lastNameCol.setMinWidth(100);
lastNameCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("lastName"));
TableColumn emailCol = new TableColumn("Email");
emailCol.setMinWidth(200);
emailCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("email"));
table.setItems(data);
table.getColumns().addAll(firstNameCol, lastNameCol, emailCol);
final Button btnHighlight = new Button("Highlight selected row");
btnHighlight.setMaxWidth(Double.MAX_VALUE);
btnHighlight.setPrefHeight(30);
btnHighlight.setOnAction(new EventHandler<ActionEvent>(){
public void handle(ActionEvent e){
// this is where the CSS should be applied
}
});
final VBox vbox = new VBox();
vbox.setSpacing(5);
vbox.setPadding(new Insets(10, 0, 0, 10));
vbox.getChildren().addAll(label, table, btnHighlight);
((Group) scene.getRoot()).getChildren().addAll(vbox);
stage.setScene(scene);
stage.show();
}
public static class Person {
private final SimpleStringProperty firstName;
private final SimpleStringProperty lastName;
private final SimpleStringProperty email;
private Person(String fName, String lName, String email) {
this.firstName = new SimpleStringProperty(fName);
this.lastName = new SimpleStringProperty(lName);
this.email = new SimpleStringProperty(email);
}
public String getFirstName() {
return firstName.get();
}
public void setFirstName(String fName) {
firstName.set(fName);
}
public String getLastName() {
return lastName.get();
}
public void setLastName(String fName) {
lastName.set(fName);
}
public String getEmail() {
return email.get();
}
public void setEmail(String fName) {
email.set(fName);
}
}
}
And the application.css from which the "Highlight selected row" button applies the highlightedRow class to the selected table row:
.highlightedRow {
-fx-background-color: brown;
-fx-background-insets: 0, 1, 2;
-fx-background: -fx-accent;
-fx-text-fill: -fx-selection-bar-text;
}
Edit:
After several hours of trying, the best thing I could come up is this using the code below:
firstNameCol.setCellFactory(new Callback<TableColumn<Person, String>, TableCell<Person, String>>() {
#Override
public TableCell<Person, String> call(TableColumn<Person, String> personStringTableColumn) {
return new TableCell<Person, String>() {
#Override
protected void updateItem(String name, boolean empty) {
super.updateItem(name, empty);
if (!empty) {
if (name.toLowerCase().startsWith("e") || name.toLowerCase().startsWith("i")) {
getStyleClass().add("highlightedRow");
}
setText(name);
} else {
setText("empty"); // for debugging purposes
}
}
};
}
});
The part I don't really understand is why I can't do that from inside the setOnAction method of the btnHighlight? I also tried refreshing the table afterwards (described here), but it didn't seem to work. Also, my "solution" only works for the firstNameCol column, so does one have to set new cell factory for each column in order to apply a certain style, or is there a smarter solution?
Edit: Updated version of this (old) post is at https://stackoverflow.com/a/73764770/2189127
If you don't want the reusability of the solution I posted here, this is really the same thing but using an anonymous inner class for the row factory instead of a standalone class. Perhaps the code is easier to follow as it's all in one place. It's kind of a hybrid between Jonathan's solution and mine, but will automatically update the highlights without forcing it with a sort.
I used a list of integers so it supports multiple selection, but if you don't need that you could obviously just use an IntegerProperty instead.
Here's the row factory:
final ObservableList<Integer> highlightRows = FXCollections.observableArrayList();
table.setRowFactory(new Callback<TableView<Person>, TableRow<Person>>() {
#Override
public TableRow<Person> call(TableView<Person> tableView) {
final TableRow<Person> row = new TableRow<Person>() {
#Override
protected void updateItem(Person person, boolean empty){
super.updateItem(person, empty);
if (highlightRows.contains(getIndex())) {
if (! getStyleClass().contains("highlightedRow")) {
getStyleClass().add("highlightedRow");
}
} else {
getStyleClass().removeAll(Collections.singleton("highlightedRow"));
}
}
};
highlightRows.addListener(new ListChangeListener<Integer>() {
#Override
public void onChanged(Change<? extends Integer> change) {
if (highlightRows.contains(row.getIndex())) {
if (! row.getStyleClass().contains("highlightedRow")) {
row.getStyleClass().add("highlightedRow");
}
} else {
row.getStyleClass().removeAll(Collections.singleton("highlightedRow"));
}
}
});
return row;
}
});
And here are what some buttons might look like:
final Button btnHighlight = new Button("Highlight");
btnHighlight.disableProperty().bind(Bindings.isEmpty(table.getSelectionModel().getSelectedIndices()));
btnHighlight.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
highlightRows.setAll(table.getSelectionModel().getSelectedIndices());
}
});
final Button btnClearHighlight = new Button("Clear Highlights");
btnClearHighlight.disableProperty().bind(Bindings.isEmpty(highlightRows));
btnClearHighlight.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
highlightRows.clear();
}
});
How about creating a row factory which exposes an observable list of the indexes of table rows which are to be highlighted? That way you can simply update the list with the indexes you need to highlight: for example by calling the getSelectedIndices() on the selection model and passing it to the list's setAll(...) method.
This could look something like:
import java.util.Collections;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.util.Callback;
public class StyleChangingRowFactory<T> implements
Callback<TableView<T>, TableRow<T>> {
private final String styleClass ;
private final ObservableList<Integer> styledRowIndices ;
private final Callback<TableView<T>, TableRow<T>> baseFactory ;
public StyleChangingRowFactory(String styleClass, Callback<TableView<T>, TableRow<T>> baseFactory) {
this.styleClass = styleClass ;
this.baseFactory = baseFactory ;
this.styledRowIndices = FXCollections.observableArrayList();
}
public StyleChangingRowFactory(String styleClass) {
this(styleClass, null);
}
#Override
public TableRow<T> call(TableView<T> tableView) {
final TableRow<T> row ;
if (baseFactory == null) {
row = new TableRow<>();
} else {
row = baseFactory.call(tableView);
}
row.indexProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> obs,
Number oldValue, Number newValue) {
updateStyleClass(row);
}
});
styledRowIndices.addListener(new ListChangeListener<Integer>() {
#Override
public void onChanged(Change<? extends Integer> change) {
updateStyleClass(row);
}
});
return row;
}
public ObservableList<Integer> getStyledRowIndices() {
return styledRowIndices ;
}
private void updateStyleClass(TableRow<T> row) {
final ObservableList<String> rowStyleClasses = row.getStyleClass();
if (styledRowIndices.contains(row.getIndex()) ) {
if (! rowStyleClasses.contains(styleClass)) {
rowStyleClasses.add(styleClass);
}
} else {
// remove all occurrences of styleClass:
rowStyleClasses.removeAll(Collections.singleton(styleClass));
}
}
}
Now you can do
final StyleChangingRowFactory<Person> rowFactory = new StyleChangingRowFactory<>("highlightedRow");
table.setRowFactory(rowFactory);
And in your button's action handler do
rowFactory.getStyledRowIndices().setAll(table.getSelectionModel().getSelectedIndices());
Because StyleChangingRowFactory wraps another row factory, you can still use it if you already have a custom row factory implementation you want to use. For example:
final StyleChangingRowFactory<Person> rowFactory = new StyleChangingRowFactory<Person>(
"highlightedRow",
new Callback<TableView<Person>, TableRow<Person>>() {
#Override
public TableRow<Person> call(TableView<Person> tableView) {
final TableRow<Person> row = new TableRow<Person>();
ContextMenu menu = new ContextMenu();
MenuItem removeMenuItem = new MenuItem("Remove");
removeMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
table.getItems().remove(row.getItem());
}
});
menu.getItems().add(removeMenuItem);
row.contextMenuProperty().bind(
Bindings.when(row.emptyProperty())
.then((ContextMenu) null)
.otherwise(menu));
return row;
}
});
table.setRowFactory(rowFactory);
Here is a complete code example.
Here's an ugly hack solution. Firstly, define an int field called highlightedRow. Then set a row factory on the TableView:
table.setRowFactory(new Callback<TableView<Person>, TableRow<Person>>() {
#Override public TableRow<Person> call(TableView<Person> param) {
return new TableRow<Person>() {
#Override protected void updateItem(Person item, boolean empty) {
super.updateItem(item, empty);
if (getIndex() == highlightedRow) {
getStyleClass().add("highlightedRow");
} else {
getStyleClass().remove("highlightedRow");
}
}
};
}
});
Then add the following code in your button on action (and this is where the ugly hack comes into play):
btnHighlight.setOnAction(new EventHandler<ActionEvent>(){
public void handle(ActionEvent e){
// set the highlightedRow integer to the selection index
highlightedRow = table.getSelectionModel().getSelectedIndex();
// force a tableview refresh - HACK
List<Person> items = new ArrayList<>(table.getItems());
table.getItems().setAll(items);
}
});
Once that is done, you get the brown highlight on the selected row. You could of course easily support multiple brown highlights by replacing the int with a list of itns.
The best way I find to do this:
In my CSS
.table-row-cell:feederChecked{
-fx-background-color: #06FF00;
}
In my table initialization with a SimpleBooleanProperty of an Object content in my ObservableList:
// The pseudo classes feederChecked that were defined in the css file.
PseudoClass feederChecked = PseudoClass.getPseudoClass("feederChecked");
// Set a rowFactory for the table view.
tableView.setRowFactory(tableView -> {
TableRow<Feeder> row = new TableRow<>();
ChangeListener<Boolean> changeListener = (obs, oldFeeder, newFeeder) -> {
row.pseudoClassStateChanged(feederChecked, newFeeder);
};
row.itemProperty().addListener((obs, previousFeeder, currentFeeder) -> {
if (previousFeeder != null) {
previousFeeder.feederCheckedProperty().removeListener(changeListener);
}
if (currentFeeder != null) {
currentFeeder.feederCheckedProperty().addListener(changeListener);
row.pseudoClassStateChanged(feederChecked, currentFeeder.getFeederChecked());
} else {
row.pseudoClassStateChanged(feederChecked, false);
}
});
return row;
});
Code adapting from this complete exemple
I might have found something that works:
With this code added, if you press the button the highlighted row changes color, when you select a different row the color changes back to default, when you press the button again, it changes the color of the new row to brown.
final String css = getClass().getResource("style.css").toExternalForm();
final Scene scene = new Scene(new Group());
btnHighlight.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent e) {
scene.getStylesheets().add(css);
}
});
table.getSelectionModel().selectedIndexProperty()
.addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> ov, Number t, Number t1) {
scene.getStylesheets().remove(css);
}
});
css:
.table-row-cell:selected
{
-fx-background-color: brown;
-fx-text-inner-color: white;
}
Only problem with this solution is that if you press the button twice in a row, your next row selected is already brown. You would have to use a seperate css file for this, else at startup of the application no css rules would be applied untill you press the button.
I found that the best solution would be to listen for row.itemProperty() changes because when you sort for example the rows change indexes, so rows get notified automatically.

Categories