Button of Choice: Use ToggleButtons as RadioButtons

For MQTT.fx I wanted to use ToggleButtons to e.g. choose the de
coding of a MQTT Message or the QoS Level:

decode

qos

 

I found out that in context of a ToggleGroup ToggleButtons behave different than RadioButtons in terms of selection/deselection: unlike RadioButtons ToggleButtons can still be set to unselected state.

 

A RadioButton extends ToggleButton and overrides fire() (which is invoked when a user gesture indicates that an event for this ButtonBase should occur aka “Button was clicked”):
RadioButton:

@Override public void fire() {
  // we don't toggle from selected to not selected if part of a group
  if (getToggleGroup() == null || !isSelected()) {
    super.fire();
  }
}

ToggleButton:

@Override public void fire() {
  setSelected(!isSelected());
  fireEvent(new ActionEvent());
}

In a ToogleGroup ToggleButtons should behave like RadioButtons, so IMHO this is a bug worth a pull request for ToggleButton 😉

One way to handle this is for sure to create a custom extension of ToggleButton implementing fire() in respect to the RadioButton.
But I like more to add behavior to existing controls.
This is my tweak to modify default behavior by adding filters to all ToogleButtons of a ToggleGroup consuming unwanted MouseEvents:

public class JavaFXUtil {

    private static JavaFXUtil me;

    private JavaFXUtil() {
    }

    public static JavaFXUtil get() {
        if (me == null) {
            me = new JavaFXUtil();
        }
        return me;
    }

    public EventHandler<MouseEvent> consumeMouseEventfilter = (MouseEvent mouseEvent) -> {
        if (((Toggle) mouseEvent.getSource()).isSelected()) {
            mouseEvent.consume();
        }
    };

    public void addAlwaysOneSelectedSupport(final ToggleGroup toggleGroup) {
        toggleGroup.getToggles().addListener((Change<? extends Toggle> c) -> {
            while (c.next()) {
                for (final Toggle addedToggle : c.getAddedSubList()) {
                    addConsumeMouseEventfilter(addedToggle);
                }
            }
        });
        toggleGroup.getToggles().forEach(t -> {
            addConsumeMouseEventfilter(t);
        });
    }

    private void addConsumeMouseEventfilter(Toggle toggle) {
        ((ToggleButton) toggle).addEventFilter(MouseEvent.MOUSE_PRESSED, consumeMouseEventfilter);
        ((ToggleButton) toggle).addEventFilter(MouseEvent.MOUSE_RELEASED, consumeMouseEventfilter);
        ((ToggleButton) toggle).addEventFilter(MouseEvent.MOUSE_CLICKED, consumeMouseEventfilter);
    }

}
public class ButtonDemoController {
    @FXML
    private ToggleGroup g2;

    @FXML
    private ToggleGroup g3;

    public void initialize() {
        JavaFXUtil.get().addAlwaysOneSelectedSupport(g2);
        JavaFXUtil.get().addAlwaysOneSelectedSupport(g3);
    }

}

buttonofchoice

Example code at GitHub.

Ordering via SortedList (and Properties Extractor)

Quite a while ago (almost 1,5 years 😱) I wrote a post about the JavaFX way to get ListViews and TableViews instantly updated on model data change: using Properties Extractor pattern.

TableColumns have a build-in support for sorting the data per column: just double-click the column header (. But how to order ListViews or get the table content get ordered by certain predicate instantly on data changes?

To sort elements the ObervableList has a sort(Comparator<? super E> c) method to sort the list according to the order induced by the specified Comparator (each time it is called).
Note: If the specified comparator is null then all elements in this list must implement the Comparable interface and the elements’ natural ordering should be used.

I have extended the demo app in terms of sorting:
GET THE APP HERE.
GET THE CODE HERE.
extractor_demo_main

SortedList and FilteredList

SortedList and FilteredList are very interesting and useful wrapper for OberservableLists to keep the content sorted and/or filtered (will add filtering soon).

SortedList: Wraps an ObservableList and sorts it’s content. All changes in the ObservableList are propagated immediately to the SortedList.

FilteredList: Wraps an ObservableList and filters it’s content using the provided Predicate. All changes in the ObservableList are propagated immediately to the FilteredList.

You can create a SortedList by creating it “manually”:

personSortedList = new SortedList(model.getPersonFXBeans());

or let the ObservableList do it for you:

personSortedList = model.getPersonFXBeans().sorted();

“Readonly TableView” and “ListView” are sharing the same SortedList wrapper:

readOnlyListView.setItems(personSortedList);
readOnlyTableView.setItems(personSortedList);

Sorting

extractor_demo_comparators

The “Sorting” ComboBox contains the available comparators for the SortedList:

personComparators = FXCollections.observableArrayList(new FirstNameComparator(),
                new LastNameComparator(),
                new DateOfBirthComparator(),
                new AgeComparator(),
                new DaysToNextBirthdayComparator(),
                new NextBirthdayComparator()
);
personOrderByComboBox.setItems(personComparators);
@FXML
public void onSortByComparator() {
  personSortedList.setComparator(personOrderByComboBox.getSelectionModel().getSelectedItem());
}

Also there is a toggle for the sort direction:

@FXML
public void onChangeSortDirection() {
  personSortedList.setComparator(personSortedList.getComparator().reversed());
}

The “Editable TableView” is still “self-sufficient” in terms of sorting.

All three views are sharing the same data model (model.getPersonFXBeans()) and editing the content is instantly notified by them (via property extractor).

If you play with the app by changing the data: note also the sorting take effect. 

One more thing

If you bind the comparatorProperty() of SortedList to the TableView, the sorting the content is triggered by the build-in ordering options ig TableView (via column header):

personSortedList.comparatorProperty().bind(editableTableView.comparatorProperty());

Note that other ways to trigger the SortedList e.g. by setCompartor() will fail then with exception as comparatorProperty is already bound.

How to allow users to customize the UI

Idea

Take advantage of the declarative design pattern of JavafX/FXML and allow users to customize a certain view without any coding just by opening it with e.g. SceneBuilder to re-arrange the layout or add new controls or even change the style according to the users needs.

The FXML file + CSS can be basically be placed whereever they are reachable via a URL.
The user must only know the interface/methods of the assigned controller class inside the FXML.

RemoteController

Assuming this simple demo controller class provides methods to remotely control devices and to send MQTT messages, a user is able to customize his own remote control.

public class RemoteController{

    @FXML
    public void onTest(){
        Alert alert = new Alert(Alert.AlertType.INFORMATION);
        alert.setContentText("");
        alert.setHeaderText("WORKS!");
        alert.show();
    }
    
    public void onTest(String value){
        Alert alert = new Alert(Alert.AlertType.INFORMATION);
        alert.setHeaderText("WORKS!");
        alert.setContentText(value);
        alert.show();
    }
    
    public void onSwitch(String houseCode, int groudId, int deviceId, String command){
        Alert alert = new Alert(Alert.AlertType.INFORMATION);
        alert.setHeaderText("Switch!");
        alert.setContentText(String.format("Command: send %s %d %d %s", houseCode, groudId, deviceId, command));
        alert.show();
    }
}

remote.fxml and remote.css

Note the referenced de.jensd.shichimifx.demo.ext.RemoteController and remote.css.
So basically controller actions can be called via

onAction="#onTest".

Nice:
If you add

<?language javascript?>

to FXML, it’s also possible to pass parameters by a JavaScript call via the controller-instance.

onAction=controller.onTest('OFF')
onAction=controller.onSwitch('a',1,1,'ON')

Unfortunately I can’t find more documentation about this feature than -> this, but somehow it magically it works ;-). Its even possible to pass different types of parameters.

<?xml version="1.0" encoding="UTF-8"?>

<?language javascript?>
<?import javafx.geometry.*?>
<?import java.lang.*?>
<?import java.net.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<VBox alignment="TOP_CENTER" prefHeight="400.0" prefWidth="600.0" spacing="20.0" styleClass="main-pane" stylesheets="@remote.css" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="de.jensd.shichimifx.demo.ext.RemoteController">
   <children>
      <Label styleClass="title-label" text="Universal Remote" />
      <HBox alignment="CENTER_RIGHT" spacing="20.0">
         <children>
            <Label layoutX="228.0" layoutY="96.0" styleClass="sub-title-label" text="Light Frontdoor" />
            <Button layoutX="43.0" layoutY="86.0" mnemonicParsing="false" onAction="#onTest" prefWidth="150.0" styleClass="button-on" text="ON" />
            <Button layoutX="411.0" layoutY="86.0" mnemonicParsing="false" onAction="#onTest" prefWidth="150.0" styleClass="button-off" text="OFF" />
         </children>
         <padding>
            <Insets left="10.0" right="10.0" />
         </padding>
      </HBox>
      <HBox alignment="CENTER_RIGHT" spacing="20.0">
         <children>
            <Label layoutX="228.0" layoutY="96.0" styleClass="sub-title-label" text="Light Garden" />
            <Button layoutX="43.0" layoutY="86.0" mnemonicParsing="false" onAction="controller.onTest('ON')" prefWidth="150.0" styleClass="button-on" text="ON" />
            <Button layoutX="411.0" layoutY="86.0" mnemonicParsing="false" onAction="controller.onTest('OFF')" prefWidth="150.0" styleClass="button-off" text="OFF" />
         </children>
         <padding>
            <Insets left="10.0" right="10.0" />
         </padding>
      </HBox>
      <HBox alignment="CENTER_RIGHT" spacing="20.0">
         <children>
            <Label layoutX="228.0" layoutY="96.0" styleClass="sub-title-label" text="Light Garden" />
            <Button layoutX="43.0" layoutY="86.0" mnemonicParsing="false" onAction="controller.onSwitch('a', 1,1,'ON')" prefWidth="150.0" styleClass="button-on" text="ON" />
            <Button layoutX="411.0" layoutY="86.0" mnemonicParsing="false" onAction="controller.onTest('OFF')" prefWidth="150.0" styleClass="button-off" text="OFF" />
         </children>
         <padding>
            <Insets left="10.0" right="10.0" />
         </padding>
      </HBox>
   </children>
   <padding>
      <Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
   </padding>
</VBox>

Based on this example a user is able to simple open the FXMl with SceneBuilder and to add new Button calling the controller.onSwitch() method to control different/new devices installed for home automation.

FxmlUtils
The next release of ShichimiFX will contain a new Utilily class to load FXML as shown in the ExternalFXMLDemoController. Note that the loaded Pane is added to the center of the externalPane (BorderPane) of the Demo-Application via onLoadExternalFxml():

public class ExternalFXMLDemoController {

    @FXML
    private ResourceBundle resources;

    @FXML
    private BorderPane externalPane;

    @FXML
    private TextField fxmlFileNameTextField;

    @FXML
    private Button chooseFxmlFileButton;

    @FXML
    private Button loadFxmlFileButton;

    private StringProperty fxmlFileName;

    public void initialize() {
        fxmlFileNameTextField.textProperty().bindBidirectional(fxmlFileNameProperty());
        loadFxmlFileButton.disableProperty().bind(fxmlFileNameProperty().isEmpty());
    }

    public StringProperty fxmlFileNameProperty() {
        if (fxmlFileName == null) {
            fxmlFileName = new SimpleStringProperty("");
        }
        return fxmlFileName;
    }

    public String getFxmlFileName() {
        return fxmlFileNameProperty().getValue();
    }

    public void setFxmlFileName(String fxmlFileName) {
        this.fxmlFileNameProperty().setValue(fxmlFileName);
    }

    @FXML
    public void chooseFxmlFile() {
        FileChooser chooser = new FileChooser();
        chooser.setTitle("Choose FXML file to load");
        if (getFxmlFileName().isEmpty()) {
            chooser.setInitialDirectory(new File(System.getProperty("user.home")));
        } else {
            chooser.setInitialDirectory(new File(getFxmlFileName()).getParentFile());
        }

        File file = chooser.showOpenDialog(chooseFxmlFileButton.getScene().getWindow());
        if (file != null) {
            setFxmlFileName(file.getAbsolutePath());
        }
    }

    @FXML
    public void onLoadExternalFxml() {
        try {
            Optional<URL> url = FxmlUtils.getFxmlUrl(Paths.get(getFxmlFileName()));
            if (url.isPresent()) {
                Pane pane = FxmlUtils.loadFxmlPane(url.get(), resources);
                externalPane.setCenter(pane);
            } else {
                Alert alert = new Alert(Alert.AlertType.WARNING);
                alert.setContentText(getFxmlFileName() + " could not be found!");
                alert.show();
            }
        } catch (IOException ex) {
            Dialogs.create().showException(ex);
        }
    }
}

ShichimiFX 1.0.5 released

I have released ShichimiFX 1.0.5.

Get more info and the code from my ShichimiFX Bitbucket Repo

Maven

<dependency>
  <groupId>de.jensd</groupId>
  <artifactId>shichimifx</artifactId>
  <version>1.0.5</version>
</dependency>

UI for Refuel

This release contains a Frontend for Refuel an application update API written by Dino Tsoumakis which is used with MQTT.fx 0.0.13.

updates-available

updates-load

ShichimiFX 1.0.4 released

I have released ShichimiFX 1.0.4.

Get the code from my ShichimiFX Bitbucket Repo

Maven

<dependency>
  <groupId>de.jensd</groupId>
  <artifactId>shichimifx</artifactId>
  <version>1.0.4</version>
</dependency>

This release contains 2 new utility classes I needed for MQTT.fx‘s script editing function Desktop and Shell.

By now I used to open a script for editing with the system default editor via java.awt.Desktop. So far this works well on my Mac but there were issues on Windows and Linux. To get rid of these issues I create my own Desktop class to open the script directly with the system shell:

Desktop

public class Desktop {

    public static boolean edit(String fileToEdit) {
        return openWithSystem(fileToEdit);
    }

    private static boolean openWithSystem(String fileToOpen) {
        if (OS.isLinux()) {
            if (Shell.canOpen("gnome-open", fileToOpen)) {
                return true;
            } else if (Shell.canOpen("kde-open", fileToOpen)) {
                return true;
            } else if (Shell.canOpen("xdg-open", fileToOpen)) {
                return true;
            }
        } else if (OS.isWindows()) {
            if (Shell.canOpen("explorer", fileToOpen)) {
                return true;
            }
        } else if (OS.isMac()) {
            if (Shell.canOpen("open", fileToOpen)) {
                return true;
            }
        }
        return false;
    }
}

Shell

public class Shell {

    public static boolean canOpen(String... command) {
        try {
            ProcessBuilder builder = new ProcessBuilder(command);
            Process process = builder.directory(new File(System.getProperty("user.home"))).start();
            if (process == null) {
                return false;
            }
            try {
                process.exitValue();
                // if this does not throw an IllegalThreadStateException the process is not running/has exited immediately
                return false;
            } catch (IllegalThreadStateException e) {
                // the process seems to be in a running state
                return true;
            }
        } catch (IOException ex) {
            return false;
        }
    }

    public static List<String> prepareOpenFileLine(String command, String fileToOpen) {
        List<String> commandList = new ArrayList<>();
        for (String s : command.split(" ")) {
            commandList.add(s.trim());
        }
        commandList.add(fileToOpen);
        return commandList;
    }

    public static Optional<Process> execute(List<String> command) throws IOException {
        return execute(new File(System.getProperty("user.home")), command);
    }

    public static Optional<Process> execute(File directory, String... command) throws IOException {
        return execute(directory, Arrays.asList(command));
    }

    public static Optional<Process> execute(String... command) throws IOException {
        return Shell.execute(new File(System.getProperty("user.home")), command);
    }

    public static Optional<Process> execute(File directory, List<String> command) throws IOException {
        ProcessBuilder builder = new ProcessBuilder(command);
        Process process = builder.directory(directory).start();
        return Optional.of(process);
    }
}

ShichimiFX 1.0.3 released

I have released ShichimiFX 1.0.3.

Get the code from my ShichimiFX Bitbucket Repo

Maven

<dependency>
  <groupId>de.jensd</groupId>
  <artifactId>shichimifx</artifactId>
  <version>1.0.3</version>
</dependency>

This release mostly focuses on the ConsoleStream. I have fixed some performance issues. Also optionally now an alternate stream can be given, e.g. to pass through System.out.
This is used for displaying the logging stream in my lastest release of MQTT.fx.

mqttfx-log-tab

public class ConsoleDude {

    public final static PrintStream STD_OUT = System.out;
    public final static PrintStream STD_ERR = System.err;
    private static BooleanProperty stdStreamsHooked = new SimpleBooleanProperty();

    public static void hookStdStreams(TextInputControl outputArea) {
        PrintStream psOut = new PrintStream(new ConsoleStream(outputArea, System.out), true);
        System.setOut(psOut);
        PrintStream psErr = new PrintStream(new ConsoleStream(outputArea, System.err), true);
        System.setOut(psErr);
        stdStreamsHookedProperty().set(true);
    }

    public static void restoreStdStreams() {
        System.setOut(STD_OUT);
        System.setErr(STD_ERR);
        stdStreamsHookedProperty().set(false);
    }

    public static BooleanProperty stdStreamsHookedProperty() {
        if (stdStreamsHooked == null) {
            stdStreamsHooked = new SimpleBooleanProperty();
        }
        return stdStreamsHooked;
    }

    public boolean isHooked() {
        return stdStreamsHookedProperty().get();
    }

    public static ConsoleStreamHandler createConsoleStreamHandler(TextInputControl outputArea) {
        return createConsoleStreamHandler(outputArea, new SimpleFormatter());
    }

    public static ConsoleStreamHandler createConsoleStreamHandler(TextInputControl outputArea, Formatter formatter) {
        return new ConsoleStreamHandler(new ConsoleStream(outputArea), formatter);
    }

}
public class ConsoleStream extends ByteArrayOutputStream {

    private final TextInputControl textOutputComponent;
    private final StringBuilder buffer;
    private final String EOL = System.getProperty("line.separator");
    private final PrintStream printStream;

    public ConsoleStream(TextInputControl ta) {
        this(ta, null);
    }

    public ConsoleStream(TextInputControl ta, PrintStream printStream) {
        this.textOutputComponent = ta;
        this.printStream = printStream;
        buffer = new StringBuilder(80);
    }

    @Override
    public void flush() throws IOException {
        String text = toString();
        if (text.length() == 0) {
            return;
        }
        append(text);
        reset();
    }

    private void append(String text) {
        if (textOutputComponent.getLength() == 0) {
            buffer.setLength(0);
        }
        if (EOL.equals(text)) {
            buffer.append(text);
        } else {
            buffer.append(text);
            clearBuffer();
        }
    }

    private void clearBuffer() {
        String line = buffer.toString();
        Platform.runLater(() -> {
            textOutputComponent.appendText(line);
        });
        if (printStream != null) {
            printStream.print(line);
        }
        buffer.setLength(0);
    }

}

ShichimiFX 1.0.1 released

I have added extracted some more classes from my application projects (e.g. from MQTT.fx) and added them as new features to ShichimiFX (and released 1.0.1).

Get the code from my ShichimiFX Bitbucket Repo

Maven

<dependency>
  <groupId>de.jensd</groupId>
  <artifactId>shichimifx</artifactId>
  <version>1.0.1</version>
</dependency>

AnimationDude
Provides support for Fade-in/Fade-out-Transition of controls:
AnimationDude.addInOutFadeTransition(Duration duration, Node node)
AnimationDude.addInOutFadeTransition(Duration duration, Node nodeToFade, Node trigger)

Make a ListCell (this) a trigger to to fade in a button bar on mouse hover:
AnimationDude.addInOutFadeTransition(Duration.millis(200), buttonsBox, this);

public class AnimationDude {
    
    public final static void  addInOutFadeTransition(Duration duration, Node node) {
        addInOutFadeTransition(duration, node, node);
    }

    public final static void  addInOutFadeTransition(Duration duration, Node nodeToFade, Node trigger) {
        nodeToFade.setOpacity(0.0);
        final FadeTransition fadeOutTransition = new FadeTransition(duration, nodeToFade);
        fadeOutTransition.setFromValue(1.0);
        fadeOutTransition.setToValue(0.0);
        final FadeTransition fadeInTransition = new FadeTransition(duration, nodeToFade);
        fadeInTransition.setFromValue(0.0);
        fadeInTransition.setToValue(1.0);
        trigger.setOnMouseEntered(e->{
                    fadeOutTransition.stop();
                    fadeInTransition.play();
        });
        trigger.setOnMouseExited(e->{
                    fadeInTransition.stop();
                    fadeOutTransition.play();
        });
    }
}

ConsoleDude
Provides support for switching System.out/Sytem.err (ConsoleStream)or bypassing a logging output (ConsoleStreamHandler) a JavaFX text components.

Usage

Use “stdOutMessageArea” for System.out and System.err output:
hook StdOut + StdErr: ConsoleDude.hookStdOutTo(stdOutMessageArea);
restore StdOut + StdErr: ConsoleDude.restoreStdOut();

Also you can observe the status, e. g.:
hookButton.disableProperty().bind(ConsoleDude.stdOutHookedProperty());
releaseButton.disableProperty().bind(ConsoleDude.stdOutHookedProperty().not());

Use “logMessageArea” for logging output:
LOGGER.addHandler(ConsoleDude.createConsoleStreamHandler(logMessageArea));

public class ConsoleStream extends OutputStream {

    private final TextInputControl output;

    public ConsoleStream(TextInputControl ta) {
        this.output = ta;
    }

    @Override
    public void write(int i) throws IOException {
        Platform.runLater(() -> {
            output.appendText(String.valueOf((char) i));
        });
    }
}
public class ConsoleStreamHandler extends StreamHandler{

    public ConsoleStreamHandler(OutputStream out, Formatter formatter) {
        super(out, formatter);
    }

    @Override
    public synchronized void publish(LogRecord record) {
        super.publish(record);
        flush();
    }
}
public class ConsoleDude {

    public final static PrintStream STD_OUT = System.out;
    public final static PrintStream STD_ERR = System.err;
    private static BooleanProperty stdOutHooked = new SimpleBooleanProperty();

    public static void hookStdOutTo(TextInputControl outputArea) {
        PrintStream ps = new PrintStream(new ConsoleStream(outputArea), true);
        System.setOut(ps);
        System.setErr(ps);
        stdOutHookedProperty().set(true);
    }

    public static void restoreStdOut() {
        System.setOut(STD_OUT);
        System.setErr(STD_ERR);
        stdOutHookedProperty().set(false);
    }

    public static BooleanProperty stdOutHookedProperty() {
        if (stdOutHooked == null) {
            stdOutHooked = new SimpleBooleanProperty();
        }
        return stdOutHooked;
    }

    public boolean isHooked() {
        return stdOutHookedProperty().get();
    }

    public static ConsoleStreamHandler createConsoleStreamHandler(TextInputControl outputArea) {
        return createConsoleStreamHandler(outputArea,new SimpleFormatter());
    }

    public static ConsoleStreamHandler createConsoleStreamHandler(TextInputControl outputArea, Formatter formatter) {
        return new ConsoleStreamHandler(new ConsoleStream(outputArea), formatter);
    }

}

Properties Extractor: Best way to get the ListView instantly updating its elements

This post is about how to deal with JavaFX ListViews and TableViews and how these controls are getting informed about changed content of the contained elements.
I wonder why I didn’t find anything about the following pattern in the relevant books as this is a really crucial mechanism.
Many posts out there suggest to force triggering a ChangeEvent getting a ListView to refresh by calling

list.remove(POJO);
list.add(index,POJO);

after each commited change! Brrr ;-)!

But there is a much better way:
Enable your list to report changes on the element by providing an properties extractor.

The Demo App
I have created a small demo app to play with for giving it a try.
Basically two TableViews and one ListView all sharing the same data.
To change properties of the elements one TableView is editable:
PropertiesExtractor Demo1

The DataModel

The compulsive PersonBean folling the JavaFX Bean Pattern/Convention

public class PersonBean {

    private StringProperty firstName;
    private StringProperty lastName;
    private ObjectProperty<LocalDate> birthday;
    private ObjectBinding<Long> age;

    public PersonBean() {
    }

    public PersonBean(String firstName, String lastName, LocalDate birthday) {
        setFirstName(firstName);
        setLastName(lastName);
        setBirthday(birthday);
    }

    public final StringProperty firstNameProperty() {
        if (firstName == null) {
            firstName = new SimpleStringProperty();
        }
        return firstName;
    }

    public final String getFirstName() {
        return firstNameProperty().get();
    }

    public final void setFirstName(final java.lang.String firstName) {
        firstNameProperty().set(firstName);
    }

    public final StringProperty lastNameProperty() {
        if (lastName == null) {
            lastName = new SimpleStringProperty();
        }
        return lastName;
    }

    public final java.lang.String getLastName() {
        return lastNameProperty().get();
    }

    public final void setLastName(final java.lang.String lastName) {
        lastNameProperty().set(lastName);
    }

    public final ObjectProperty<LocalDate> birthdayProperty() {
        if (birthday == null) {
            birthday = new SimpleObjectProperty<>();
        }
        return birthday;
    }

    public final LocalDate getBirthday() {
        return birthdayProperty().get();
    }

    public final void setBirthday(final java.time.LocalDate birthday) {
        birthdayProperty().set(birthday);

    }

    public String stringValue() {
        return String.format("%s %s %s", getFirstName(), getLastName(), getBirthday().format(DateTimeFormatter.ISO_LOCAL_DATE));
    }

    public final ObjectBinding<Long> ageBinding() {
        if (age == null) {
            age = new ObjectBinding<Long>() {
                {
                    bind(birthdayProperty());
                }

                @Override
                protected Long computeValue() {
                    if (getBirthday() == null) {
                        return null;
                    }
                    return getBirthday().until(LocalDate.now(), ChronoUnit.YEARS);
                }
            };
        }
        return age;
    }

    public static Callback<PersonBean, Observable&#91;&#93;> extractor() {
        return (PersonBean p) -> new Observable[]{p.lastNameProperty(), p.firstNameProperty(), p.birthdayProperty(), p.ageBinding()};
    }
}

DataModel containing a List of randomly created PersonBeans:

public class DataModel {

    private ObservableList<PersonBean> personFXBeans;

    public DataModel() {
        init();
    }

    private void init() {
        personFXBeans = DataSource.getRandomPersonBeansList(100);
    }

    public ObservableList<PersonBean> getPersonFXBeans() {
        return personFXBeans;
    }
}

As you may know to assign a DataModel e.g. to a TableView or a ListView in JavaFX you just have to use the setItems(ObvervableList) method.

@FXML
public void onFillWithDemoDataFXBeans() {
  readOnlyListView.setItems(model.getPersonFXBeans());
  readOnlyTableView.setItems(model.getPersonFXBeans());
  editableTableView.setItems(model.getPersonFXBeans());
}

Now getting a TableView informed about property changes of contained elements is already done you for by the binding either in two ways:
via a PropertyValueFactory and by more or less direct property binding:

readOnlyFirstNameColumn.setCellValueFactory(new PropertyValueFactory<>("firstName"));
readOnlyLastNameColumn.setCellValueFactory(new PropertyValueFactory<>("lastName"));
readOnlyBirthdayColumn.setCellValueFactory(new PropertyValueFactory<>("birthday"));
readOnlyAgeColumn.setCellValueFactory(i -> i.getValue().ageBinding());

editableFirstNameColumn.setCellValueFactory(i -> i.getValue().firstNameProperty());
editableLastNameColumn.setCellValueFactory(i -> i.getValue().lastNameProperty());
editableBirthdayColumn.setCellValueFactory(i -> i.getValue().birthdayProperty());
ageColumn.setCellValueFactory(i -> i.getValue().ageBinding());

But the ListView basically only observes the list and not the properties of each element of that list.

When using a ObservableList created by FXCollections.observableArrayList() the ListView will only refresh on ListChange Events like remove() an add() of elements. Therefore

list.remove(POJO);
list.add(index,POJO);

after each commited change.

But there is a much better way:
Enable your list to report changes on the element by providing an properties extractor.
You don’t have to care about refreshing then!

ObservableList persons = FXCollections.observableArrayList(PersonBean.extractor());

See DataSource.getRandomPersonBeansList(int length):

public static ObservableList<PersonBean> getRandomPersonBeansList(int length) {
        ObservableList<PersonBean>  persons = FXCollections.observableArrayList(PersonBean.extractor());
        for (int i = 0; i < length; i++) {
            persons.add(new PersonBean(getRandomName(), getRandomLastname(), getRandomLocalDate()));
        }
return persons;
}

This Extrator is basically a Callback containing an array of Obvervables which are then observed by the Obervablelist (more precicely: ObservableListWrapper):

My PersonBean already provides it’s extrator callback:

public static Callback<PersonBean, Observable[]> extractor() {
   return (PersonBean p) -> new Observable[]{p.lastNameProperty(), p.firstNameProperty(), p.birthdayProperty(), p.ageBinding()};
}

Following this pattern all controls are updated instantly after applying the change.

Edit data…
PropertiesExtractor Demo3

and commit:
PropertiesExtractor Demo4

THE CODE PLEASE!
You can find the complete code at my BitBucket Repo