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

Poor mans input constraints

For me the best way to prevent the user from annoying errors and error messages is simply in the first line to try not to allow the user e.g. to enter invalid data or to execute actions while data in incomplete (disabled buttons/menu entries).
If only numbers allowed in an input field I think its a better approach to restict user input to numbers only instead of validating and popping up with error messages after applying or “OK”.

Swing has a JFormattedTextField which allows setting a format pattern to verify the input data. Sadly the current JavaFX API doesn’t contain such kind of control but thers is a way by adding an EventHandler for KEY_TYPED-Events and to consume the event at certain conditions. Consuming the event will end up the event bubbling phase. (see Handling JavaFX Events for more informations).
Clearly this can be implemented in a new control or by creating a subclass of TextField. But there is also a simple approach by attaching the wanted constraints to existing instances of standard controls. As you my know the editor controls of Cells and ComboBoxes are also TextFields. Thus such a handler can also be attached e.g. to a ComboBox.

Limitation/Disclaimer:
Don’t sue me for this very simple approach ;-)!
As this is just a limitaion of the INPUT while TYPING it can be bypassed e.g. by copy-paste.
Clearly in the long run a JFormattedTextField like control supporting pattern matching, validation and user feedback is needed!

Runnable demo and code can be found here.

InputConstraintsDemo

        InputConstraints.noBlanks(noBlanksField);
        InputConstraints.noLeadingBlanks(noLeadingBlanksField);
        InputConstraints.numbersOnly(numbersOnlyField);
        InputConstraints.lettersOnly(lettersOnlyField);

        InputConstraints.noLeadingBlanks(valueComboBox.getEditor());
        InputConstraints.noLeadingBlanks(noLeadingBlanksComboBox.getEditor());
        InputConstraints.noBlanks(noBlanksComboBox.getEditor());
        InputConstraints.numbersOnly(numbersOnlyComboBox.getEditor());
        InputConstraints.lettersOnly(lettersOnlyComboBox.getEditor());
public class InputConstraints{

    private InputConstraints() {
        // not needed here
    }

    public static void noLeadingAndTrailingBlanks(final TextField textField) {
        textField.addEventFilter(KeyEvent.KEY_TYPED, createNoLeadingBlanksInputHandler());
        textField.focusedProperty().addListener((Observable observable) -> {
            textField.setText(textField.getText().trim());
        });
    }

    public static void noLeadingBlanks(final TextField textField) {
        textField.addEventFilter(KeyEvent.KEY_TYPED, createNoLeadingBlanksInputHandler());
    }

    public static void noBlanks(final TextField textField) {
        textField.addEventFilter(KeyEvent.KEY_TYPED, createNoBlanksInputHandler());
        textField.focusedProperty().addListener((Observable observable) -> {
            textField.setText(textField.getText().trim());
        });
    }

    public static void numbersOnly(final TextField textField) {
        numbersOnly(textField, Integer.MAX_VALUE);
    }

    public static void numbersOnly(final TextField textField, final Integer maxLenth) {
        textField.addEventFilter(KeyEvent.KEY_TYPED, createNumbersOnlyInputHandler(maxLenth));
        textField.focusedProperty().addListener((Observable observable) -> {
            textField.setText(textField.getText().trim());
        });
    }

    public static void lettersOnly(final TextField textField) {
        lettersOnly(textField, Integer.MAX_VALUE);
    }

    public static void lettersOnly(final TextField textField, final Integer maxLenth) {
        textField.addEventFilter(KeyEvent.KEY_TYPED, createLettersOnlyInputHandler(maxLenth));
        textField.focusedProperty().addListener((Observable observable) -> {
            textField.setText(textField.getText().trim());
        });
    }

    public static EventHandler<KeyEvent> createNoLeadingBlanksInputHandler() {
        return (KeyEvent event) -> {
            if (event.getSource() instanceof TextField) {
                TextField textField = (TextField) event.getSource();
                if (" ".equals(event.getCharacter()) && textField.getCaretPosition() == 0) {
                    event.consume();
                }
            }
        };
    }

    public static EventHandler<KeyEvent> createNumbersOnlyInputHandler(final Integer maxLength) {
        return createPatternInputHandler(maxLength, "[0-9.]");
    }

    public static EventHandler<KeyEvent> createLettersOnlyInputHandler(final Integer maxLength) {
        return createPatternInputHandler(maxLength, "[A-Za-z]");
    }

    public static EventHandler<KeyEvent> createNoBlanksInputHandler() {
        return (KeyEvent event) -> {
            if (event.getSource() instanceof TextField) {
                if (" ".equals(event.getCharacter())) {
                    event.consume();
                }
            }
        };
    }

    public static EventHandler<KeyEvent> createPatternInputHandler(final Integer maxLength, String pattern) {
        return (KeyEvent event) -> {
            if (event.getSource() instanceof TextField) {
                TextField textField = (TextField) event.getSource();
                if (textField.getText().length() >= maxLength || !event.getCharacter().matches(pattern)) {
                    event.consume();
                }
            }
        };
    }
}

JavaFX on Raspberry Pi: GPIO Controller (Part1)

I have started a new leisure time project with my Raspberry Pi.
Target: control the outside lighting around my house (yes, magic words these days: HOME AUTOMATION).

But let’s see how far I will get ;-).

Until now I created a very simple JavaFX UI to control and test the 8 GPIO-out pin states:

GPIO Control UI

But first lets have a look at the setup:

Used Hardware

System setup

Construction

So I armed myself with a soldering gun, brazed the Adafruit Pi Cobbler and sticked together the electical parts on the bread board according to the pin numbering:

WiringPi pin number scheme

gpio-control-example
(Source: pi4j.com)

DSC_3980

DSC_3982

And: Action!

Part2

First steps with JavaFX

This tutorial will introduce

  • Clean separation of application UI and logic
  • Using Builders supporting Fluent API
  • Effects
  • Layouts
  • Property Bindings
  • EventHandler

Abstract

The Golden Ratio (Golden MeanGolden Section) is defined as φ = (√5 + 1) / 2.

Objective

Create an UI to find the golden section by using a slider. Add value labels and an indicator to get green when golden action is hit.

The Model

Create observable double a and b values and a boolean value to indicate whether the golden section has been hit:

private DoubleProperty aValue = new SimpleDoubleProperty(0.0);
private DoubleProperty bValue = new SimpleDoubleProperty(0.0);
private BooleanProperty sectioAurea = new SimpleBooleanProperty(false);

Method to calculate the golden section:

private void reCalculate(){

double b = aValue.substract(100).multiply(-1).get();
setBValue(b);

double result1 = aValue.get()/bValue.get();
result1 = Math.round(result1*100.0)/100.0;

double result2 = (aValue.get()+bValue.get())/aValue.get();
result2 = Math.round(result2*100.0)/100.0;

sectioAurea.set(result1 == result2);
}

Please note: get() returns the primitive double value, getValue() respectively returns the Double instance.

Next attach listeners to re-caculate the golden section indicator on value changes:

private void attachListener(){ChangeListener onChangeListener = new ChangeListener(){
@Override
public void changed(ObservableValue arg0, Number arg1, Number arg2) {
reCalculate();
}
};

getAValueProperty().addListener(onChangeListener);
getBValueProperty().addListener(onChangeListener);
}

Here is the complete model code width appropriate getter and setter methods:

package de.jensd.javafx.aurea;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;

/**
*
* @author Jens Deters
*/
public class SectioModel {

private DoubleProperty aValue = new SimpleDoubleProperty(0.0);
private DoubleProperty bValue = new SimpleDoubleProperty(0.0);
private BooleanProperty sectioAurea = new SimpleBooleanProperty(false);

public SectioModel() {
attachListener();
}

private void attachListener(){

ChangeListener onChangeListener = new ChangeListener(){
@Override
public void changed(ObservableValue arg0, Number arg1, Number arg2) {
reCalculate();
}
};

getAValueProperty().addListener(onChangeListener);
getBValueProperty().addListener(onChangeListener);
}

public DoubleProperty getAValueProperty() {
return aValue;
}

public double getAValue() {
return aValue.get();
}

public void setAValue(double d) {
aValue.set(d);
}

public DoubleProperty getBValueProperty() {
return bValue;
}

public double getBValue() {
return bValue.get();
}

public void setBValue(double d) {
bValue.set(d);
}

public BooleanProperty getSectioAureaProperty() {
return sectioAurea;
}

public boolean isSectioAurea() {
return sectioAurea.get();
}

private void reCalculate(){

double b = aValue.substract(100).multiply(-1).get();
setBValue(b);

double result1 = aValue.get()/bValue.get();
result1 = Math.round(result1*100.0)/100.0;

double result2 = (aValue.get()+bValue.get())/aValue.get();
result2 = Math.round(result2*100.0)/100.0;

sectioAurea.set(result1 == result2);
}
}

The UI 

Title Label and usage hint

Text titleText = TextBuilder.create().
text("Sectio Aurea").
effect(ds).
font(Font.font(null, FontWeight.BOLD, 32)).
build();

Text descriptionText = TextBuilder.create().
text("Move the slider until your hit the golden section.\nHint: use the cursor keys for exact control.").
textAlignment(TextAlignment.CENTER).
build();

Indicator

Stop[] falseStops = new Stop[]{new Stop(0.0, Color.WHITE), new Stop(0.3, Color.RED), new Stop(1.0, Color.DARKRED)};
falsePaint = new RadialGradient(0.0, 0.0, -10.0, -10.0, 50, false, CycleMethod.NO_CYCLE, falseStops);

Stop[] trueStops = new Stop[]{new Stop(0.0, Color.WHITE), new Stop(0.3, Color.GREENYELLOW), new Stop(1.0, Color.DARKGREEN)};
truePaint = new RadialGradient(0.0, 0.0, -10.0, -10.0, 50, false, CycleMethod.NO_CYCLE, trueStops);

ellipse = EllipseBuilder.create().
radiusX(30).
radiusY(30).
fill(falsePaint).
build();

DropShadow ds = new DropShadow();
ds.setOffsetY(3.0f);
ds.setColor(Color.color(0.4f, 0.4f, 0.4f));

ellipse.setEffect(ds);

Slider

slider = SliderBuilder.create().
min(model.getAValue()).
max(model.getBValue()).
showTickMarks(true).
showTickLabels(true).
majorTickUnit(20).
minorTickCount(3).
prefWidth(300).
blockIncrement(0.1f).
build();

Status Labels

aValueText = TextBuilder.create().build();
bValueText = TextBuilder.create().build();
aureaText = TextBuilder.create().build();

Solve Button

solveButton = ButtonBuilder.create().text("Solve").build();

Add the solve action

solveButton.setOnAction(new EventHandler() {
@Override
public void handle(ActionEvent arg0) {
model.setAValue(61.8);
}
});

Put it all together

Using the GridPane layout:

GridPane root = new GridPane();
root.setPadding(new Insets(20, 20, 20, 20));
root.setGridLinesVisible(true);</h2>
RowConstraints rowInfo = new RowConstraints(100);
ColumnConstraints colInfo = new ColumnConstraints(100);

root.getRowConstraints().
add(rowInfo);

for (int j = 0; j &lt;= 2; j++) {
root.getColumnConstraints().
add(colInfo);
}

GridPane.setHalignment(titleText, HPos.CENTER);
GridPane.setHalignment(descriptionText, HPos.CENTER);
GridPane.setHalignment(slider, HPos.CENTER);
GridPane.setHalignment(aValueText, HPos.CENTER);
GridPane.setHalignment(ellipse, HPos.CENTER);
GridPane.setHalignment(bValueText, HPos.CENTER);
GridPane.setHalignment(solveButton, HPos.CENTER);

GridPane.setConstraints(titleText, 0, 0, 3, 1);
GridPane.setConstraints(descriptionText, 0, 1, 3, 1);
GridPane.setConstraints(slider, 0, 2, 3, 1);
GridPane.setConstraints(aValueText, 0, 3);
GridPane.setConstraints(ellipse, 1, 3);
GridPane.setConstraints(bValueText, 2, 3);
GridPane.setConstraints(solveButton, 2, 4);

Connect UI to Model

model.getAValueProperty().
bindBidirectional(slider.valueProperty());

aValueText.textProperty().
bind(model.getAValueProperty().
asString("%.1f"));
bValueText.textProperty().
bind(model.getBValueProperty().
asString("%.1f"));

aureaText.textProperty().
bind(model.getSectioAureaProperty().
asString());
<h2>Connect listener to refresh the indicator:</h2>
<pre>model.getSectioAureaProperty().
            addListener(new ChangeListener() {</pre>
@Override
public void changed(ObservableValue arg0, Boolean arg1, Boolean arg2) {
refreshIndicator();
}
});

The complete UI code

package de.jensd.javafx.aurea;

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBuilder;
import javafx.scene.control.Slider;
import javafx.scene.control.SliderBuilder;
import javafx.scene.effect.DropShadow;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.RowConstraints;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.Paint;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Ellipse;
import javafx.scene.shape.EllipseBuilder;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.scene.text.TextBuilder;
import javafx.stage.Stage;

/**
*
* @author Jens Deters
*/
public class SectioAureaApplication extends Application {

private SectioModel model = new SectioModel();
private Slider slider;
private Text aValueText;
private Text bValueText;
private Text aureaText;
private Ellipse ellipse;
private Paint falsePaint;
private Paint truePaint;
private Button solveButton;

public static void main(String[] args) {
Application.launch(SectioAureaApplication.class, args);
}

@Override
public void start(Stage stage) throws Exception {
stage.setTitle("Sectio Aurea");

model = new SectioModel();
model.setAValue(0);
model.setBValue(100.9);

slider = SliderBuilder.create().
min(model.getAValue()).
max(model.getBValue()).
showTickMarks(true).
showTickLabels(true).
majorTickUnit(20).
minorTickCount(3).
prefWidth(300).
blockIncrement(0.1f).
build();

aValueText = TextBuilder.create().
build();

bValueText = TextBuilder.create().
build();

aureaText = TextBuilder.create().
build();

solveButton = ButtonBuilder.create().
text("Solve").
build();

Stop[] falseStops = new Stop[]{new Stop(0.0, Color.WHITE), new Stop(0.3, Color.RED), new Stop(1.0, Color.DARKRED)};
falsePaint = new RadialGradient(0.0, 0.0, -10.0, -10.0, 50, false, CycleMethod.NO_CYCLE, falseStops);

Stop[] trueStops = new Stop[]{new Stop(0.0, Color.WHITE), new Stop(0.3, Color.GREENYELLOW), new Stop(1.0, Color.DARKGREEN)};
truePaint = new RadialGradient(0.0, 0.0, -10.0, -10.0, 50, false, CycleMethod.NO_CYCLE, trueStops);

ellipse = EllipseBuilder.create().
radiusX(30).
radiusY(30).
fill(falsePaint).
build();

DropShadow ds = new DropShadow();
ds.setOffsetY(3.0f);
ds.setColor(Color.color(0.4f, 0.4f, 0.4f));

ellipse.setEffect(ds);

Text titleText = TextBuilder.create().
text("Sectio Aurea").
effect(ds).
font(Font.font(null, FontWeight.BOLD, 32)).
build();

Text descriptionText = TextBuilder.create().
text("Move the slider until your hit the golden section.\nHint: use the cursor keys for exact control.").
textAlignment(TextAlignment.CENTER).
build();

// Put it all together
GridPane root = new GridPane();
root.setPadding(new Insets(20, 20, 20, 20));
root.setGridLinesVisible(false);

RowConstraints rowInfo = new RowConstraints(100);
ColumnConstraints colInfo = new ColumnConstraints(100);

root.getRowConstraints().
add(rowInfo);
root.getRowConstraints().
add(rowInfo);

for (int j = 0; j <= 2; j++) {
root.getColumnConstraints().
add(colInfo);
}

GridPane.setHalignment(titleText, HPos.CENTER);
GridPane.setHalignment(descriptionText, HPos.CENTER);
GridPane.setHalignment(slider, HPos.CENTER);
GridPane.setHalignment(aValueText, HPos.CENTER);
GridPane.setHalignment(ellipse, HPos.CENTER);
GridPane.setHalignment(bValueText, HPos.CENTER);
GridPane.setHalignment(solveButton, HPos.CENTER);

GridPane.setConstraints(titleText, 0, 0, 3, 1);
GridPane.setConstraints(descriptionText, 0, 1, 3, 1);
GridPane.setConstraints(slider, 0, 2, 3, 1);
GridPane.setConstraints(aValueText, 0, 3);
GridPane.setConstraints(ellipse, 1, 3);
GridPane.setConstraints(bValueText, 2, 3);
GridPane.setConstraints(solveButton, 2, 4);

root.getChildren().
addAll(titleText, descriptionText, slider, aValueText, ellipse, bValueText, solveButton);

bindProperties();

stage.setScene(new Scene(root, 350, 380));
stage.show();

}

private void refreshIndicator() {
if (model.getSectioAureaProperty().
get()) {
ellipse.fillProperty().
set(truePaint);

} else {
ellipse.fillProperty().
set(falsePaint);
}
}

private void bindProperties() {

model.getAValueProperty().
bindBidirectional(slider.valueProperty());

aValueText.textProperty().
bind(model.getAValueProperty().
asString("%.1f"));
bValueText.textProperty().
bind(model.getBValueProperty().
asString("%.1f"));

aureaText.textProperty().
bind(model.getSectioAureaProperty().
asString());

model.getSectioAureaProperty().
addListener(new ChangeListener() {

@Override
public void changed(ObservableValue arg0, Boolean arg1, Boolean arg2) {
refreshIndicator();
}
});

solveButton.setOnAction(new EventHandler() {

@Override
public void handle(ActionEvent arg0) {
model.setAValue(61.8);
}
});

}
}

_____________________________________________________

here’s the NetBeans project: SectioAurea.zip