Combobox selection and Model binding - java

Problem overview:
I have two models OrderModel and OrderStatusModel. The OrderStatusModel items are put into a combobox that contains choices for the user to update a foreign key field in an Order table called orderStatusId. When I initialize the data, I want to be able to pull from OrderModel and retrieve the current orderStatusId and then set the combobox (that holds all the user choices) to match the orderStatusId located in the OrderModel(which holds the FK located in the Order table). The combobox needs to be able to update the OrderModel when a new selection is made. While getting data from a database to populate the OrderModel, I pull my FK orderStatusId. From this point I use case statements to select the appropriate item in OrderStatusModel and select the choice on initialization. I know this can't be the greatest method because if I were to add a new row in the backend database for a new type of status (for example: order cancelled) then I need to go and update all my switch statements to allow for that change.
Example Code
Controller
package test.pkg2;
import java.awt.Label;
import java.beans.Statement;
import java.net.URL;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ResourceBundle;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ComboBox;
public class Test2Controller implements Initializable{
#FXML
private Label customerName;
#FXML
private Label orderNumber;
#FXML
private Label orderStatusId;
#FXML
private ComboBox<OrderStatusModel> orderStatusCmb;
private ObservableList<OrderStatusModel> orderStatusModel;
private ObservableList<OrderModel> orderModel;
int statusId;
#Override
public void initialize(URL location, ResourceBundle resources) {
this.orderStatusCmb.getSelectionModel().selectedItemProperty().addListener((obs, newValue, oldValue) ->{
if(newValue != null && newValue != oldValue){
this.orderStatusCmb.getSelectionModel().getSelectedItem().getOrderStatusId();
//TODO UPDATE Order table with new orderStatusId from above code ^^^^^^
}
});
populateOrderStatusCombobox();
getCurrentOrder();
}
private void populateOrderStatusCombobox(){
String SQL = "SELECT Status.statusId, Status.statusName \n"
+"FROM Status;";
orderStatusModel = FXCollections.observableArrayList();
//Status table:
// +-----------+-------------+------+-----+---------+
// | Field | Type | Null | Key | Default |
// +-----------+-------------+------+-----+---------+
// | statusId | int | YES | PK | NULL |
// | statusName| varchar(20) | YES | | NULL |
// +-----------+-------------+------+-----+---------+
//example data: statusId = 1 statusName = Order Taken
//example data: statusId = 2 statusName = Order Processing
//example data: statusId = 3 statusName = Shipped
//Above items are populated into the combobox for selection
try(Connection connection = DBConnection.getConnection()){
PreparedStatement statement = connection.prepareStatement(SQL);
ResultSet resultSet = statement.executeQuery();
while(resultSet.next()){
this.orderStatusModel.add(new OrderStatusModel(resultSet.getInt("statusId"),
resultSet.getString("statusName")));
}
} catch(SQLException e){
}
this.orderStatusCmb.setItems(orderStatusModel);
}
private void getCurrentOrder(){
String SQL = "SELECT Order.orderNumber, Order.customerName, Order.orderStatusId \n"
+"FROM Order"
+"WHERE orderNumber = 123;";
orderModel = FXCollections.observableArrayList();
//Order table:
// +--------------+-------------+------+-----+---------+
// | Field | Type | Null | Key | Default |
// +--------------+-------------+------+-----+---------+
// | orderNumber | int | YES | PK | NULL |
// | customerName | varchar(20) | YES | | NULL |
// | orderStatusId| int | YES | FK | NULL |
// +--------------+-------------+------+-----+---------+
//example data: orderNumber = 123
// customerName = SomeCompany
// orderStatusId = 1
//I usually set the combobox here using the below method:
try(Connection connection = DBConnection.getConnection()){
PreparedStatement statement = connection.prepareStatement(SQL);
ResultSet resultSet = statement.executeQuery();
while(resultSet.next()){
this.orderModel.add(new OrderModel(resultSet.getInt("orderNumber"),
resultSet.getString("customerName"),
resultSet.getInt("orderStatusId")));
statusId = resultSet.getInt("orderStatusId");
}
//HERE is where I initially set the orderStatus to match the orderModel
//I am guessing I need some kind of binding here?
switch(statusId){
case 1: this.orderStatusCmb.getSelectionModel().select(0);
break;
case 2: this.orderStatusCmb.getSelectionModel().select(1);
break;
case 3: this.orderStatusCmb.getSelectionModel().select(3);
break;
}
} catch(SQLException e){
}
}
}
OrderStatusModel
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class OrderStatusModel {
private final IntegerProperty orderStatusId;
private final StringProperty orderStatusName;
public OrderStatusModel(int orderStatusId, String orderStatusName){
this.orderStatusId = new SimpleIntegerProperty(orderStatusId);
this.orderStatusName = new SimpleStringProperty(orderStatusName);
}
public IntegerProperty getOrderStatusId() {
return orderStatusId;
}
public StringProperty getOrderStatusName() {
return orderStatusName;
}
}
OrderModel
package test.pkg2;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class OrderModel {
private final IntegerProperty orderNumber;
private final StringProperty customerName;
private final IntegerProperty orderStatusId;
public OrderModel(int orderNumber, String customerName, int orderStatusId){
this.orderNumber = new SimpleIntegerProperty(orderNumber);
this.customerName = new SimpleStringProperty(customerName);
this.orderStatusId = new SimpleIntegerProperty(orderStatusId);
}
public IntegerProperty getOrderNumber() {
return orderNumber;
}
public StringProperty getCustomerName() {
return customerName;
}
public IntegerProperty getOrderStatusId() {
return orderStatusId;
}
}

You don't need to use any explicit binding to achieve a link between choices and IDs. Instead, backing the combobox by your OrderStatusModel object, which already includes both the ID and friendly string name, then applying an appropriate StringConverter to the combobox will achieve a nice display of the combobox. The id -> friendly name link is still retained in the model class, which can be used in reverse in subsequent database updates as needed.
Sample
What this sample does is a few things:
Extracts database interaction to an interface.
Provides an example in-memory implementation of the database interface (replace this with an implementation that performs your actual database access code).
Defines the database access in a separate class, which moves its logic out of your UI code so that you can more easily independently develop it, test it and swap out its implementation logic (which is all pretty much preferable, even for a small app).
Defines the UI layout in code rather than FXML (using FXML is preferable, but creating it in the code was just for this sample to keep the code relatively short).
Updates the naming convention for the accessors on the model classes to xxxProperty() rather than getXXX(), which is what you were using. Anything which returns a property should have a name suffix of Property by standard JavaFX naming convention. getXXX() methods are for retrieving the actual values of properties, not the properties themselves.
In the OrderModel, the order status is recorded as ObjectProperty, this makes the OrderModel easier to use as it includes within it all of the relevant status information, without requiring joins and queries to get additional info. I know it's not represented that way in your database system, but having the database interface class do the transform and return the all of the relevant info is usually a better way to go for a case like this.
When working with the ComboBox, uses the valueProperty rather than the selectedItemProperty, as valueProperty is a more direct representation of the value of the ComboBox (doesn't matter much in this case where the user always chooses from a select drop-down and can not text edit the value portion, but still, it is a little more direct conceptually to just work with the value).
A StringConverter is defined for the combobox which will translate the OrderStatusModel values to their friendly String names for display in the combobox.
Defines equals and hashcode for the OrderStatusModel so that it can be properly compared and inserted into sets with the Java collections APIs using appropriate matching and duplicate logic (which, otherwise, they may not act as you expect).
Sample Output
ComboApp.java
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import java.util.Optional;
public class ComboApp extends Application {
private Label orderNumber = new Label();
private Label customerName = new Label();
private ComboBox<OrderStatusModel> orderStatus = new ComboBox<>();
private Database database = new InMemoryTestDatabase();
#Override
public void start(Stage stage) throws Exception {
HBox layout = new HBox(
10,
orderNumber,
customerName,
orderStatus
);
layout.setPadding(new Insets(10));
layout.setAlignment(Pos.BASELINE_LEFT);
populateOrderStatusCombobox();
showOrder(123);
updateOrderWhenStatusChanged();
stage.setScene(new Scene(layout));
stage.show();
}
private void updateOrderWhenStatusChanged() {
orderStatus.valueProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null && !newValue.equals(oldValue)) {
database.updateOrder(
new OrderModel(
Integer.parseInt(orderNumber.getText()),
customerName.getText(),
orderStatus.getValue()
)
);
System.out.println(
"Updated order status of order "
+ orderNumber.getText()
+ " to "
+ orderStatus.getValue()
.orderStatusNameProperty()
.getValue()
);
}
});
}
private void populateOrderStatusCombobox(){
orderStatus.setPlaceholder(new Label("None Selected"));
orderStatus.getItems().setAll(database.listAllOrderStatuses());
orderStatus.setConverter(new StringConverter<OrderStatusModel>() {
#Override
public String toString(OrderStatusModel orderStatusModel) {
return orderStatusModel.orderStatusNameProperty().get();
}
#Override
public OrderStatusModel fromString(String orderStatusName) {
Optional<OrderStatusModel> result = database.findOrderStatusWithName(
orderStatusName
);
return result.orElse(null);
}
});
}
private void showOrder(int orderId){
Optional<OrderModel> result = database.findOrderById(orderId);
if (!result.isPresent()) {
orderNumber.setText("");
customerName.setText("");
orderStatus.setValue(null);
return;
}
OrderModel order = result.get();
orderNumber.setText("" + order.orderNumberProperty().get());
customerName.setText(order.customerNameProperty().get());
orderStatus.setValue(order.orderStatusProperty().get());
}
public static void main(String[] args) {
launch(args);
}
}
Database.java
import java.util.List;
import java.util.Optional;
public interface Database {
List<OrderStatusModel> listAllOrderStatuses();
Optional<OrderStatusModel> findOrderStatusWithName(String orderStatusName);
Optional<OrderStatusModel> findOrderStatusById(int orderStatusId);
Optional<OrderModel> findOrderById(int orderId);
void updateOrder(OrderModel order);
}
InMemoryTestDatabase.java
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class InMemoryTestDatabase implements Database {
List<OrderStatusModel> orderStatuses = new ArrayList<>();
List<OrderModel> orders = new ArrayList<>();
public InMemoryTestDatabase() {
// populate with dummy sample data.
orderStatuses.add(new OrderStatusModel(1, "Order Taken"));
orderStatuses.add(new OrderStatusModel(2, "Order Processing"));
orderStatuses.add(new OrderStatusModel(3, "Shipped"));
orders.add(
new OrderModel(
123,
"XYZZY",
findOrderStatusById(1)
.orElse(null)
)
);
}
public List<OrderStatusModel> listAllOrderStatuses() {
return orderStatuses;
}
#Override
public Optional<OrderStatusModel> findOrderStatusWithName(String orderStatusName) {
return orderStatuses.stream()
.filter(orderStatus -> orderStatusName.equals(orderStatus.orderStatusNameProperty().get()))
.findFirst();
}
#Override
public Optional<OrderStatusModel> findOrderStatusById(int orderStatusId) {
return orderStatuses.stream()
.filter(orderStatus -> orderStatusId == orderStatus.orderStatusIdProperty().get())
.findFirst();
}
public Optional<OrderModel> findOrderById(int orderId) {
return orders.stream()
.filter(order -> orderId == order.orderNumberProperty().get())
.findFirst();
}
public void updateOrder(OrderModel order) {
for (int i = 0; i < orders.size(); i++) {
if (orders.get(i).orderNumberProperty().get() == order.orderNumberProperty().get()) {
orders.set(i, order);
break;
}
}
}
}
OrderModel.java
import javafx.beans.property.*;
public class OrderModel {
private final IntegerProperty orderNumber;
private final StringProperty customerName;
private final ObjectProperty<OrderStatusModel> orderStatus;
public OrderModel(int orderNumber, String customerName, OrderStatusModel orderStatus){
this.orderNumber = new SimpleIntegerProperty(orderNumber);
this.customerName = new SimpleStringProperty(customerName);
this.orderStatus = new SimpleObjectProperty<>(orderStatus);
}
public IntegerProperty orderNumberProperty() {
return orderNumber;
}
public StringProperty customerNameProperty() {
return customerName;
}
public ObjectProperty<OrderStatusModel> orderStatusProperty() {
return orderStatus;
}
}
OrderStatusModel.java
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import java.util.Objects;
public class OrderStatusModel {
private final IntegerProperty orderStatusId;
private final StringProperty orderStatusName;
public OrderStatusModel(int orderStatusId, String orderStatusName){
this.orderStatusId = new SimpleIntegerProperty(orderStatusId);
this.orderStatusName = new SimpleStringProperty(orderStatusName);
}
public IntegerProperty orderStatusIdProperty() {
return orderStatusId;
}
public StringProperty orderStatusNameProperty() {
return orderStatusName;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OrderStatusModel that = (OrderStatusModel) o;
return Objects.equals(orderStatusId.get(), that.orderStatusId.get()) &&
Objects.equals(orderStatusName.get(), that.orderStatusName.get());
}
#Override
public int hashCode() {
return Objects.hash(orderStatusId.get(), orderStatusName.get());
}
}
Background
This answer was loosely based on a similar question and answer from a JavaFX forum post, which related to ChoiceBoxes rather than ComboBoxes:
ChoiceBox with Database IDs forum post.
ChoiceBox with Database IDs sample code.
However, this answer is updated to work directly with ComboBoxes rather than ChoiceBoxes.
Unrelated Advice
A couple of unrelated bits of advice about your posted code:
Never catch an exception without rethrowing it, taking action on it or logging it. Even if it is just for example code posted on the internet, don't do it.
You import java.awt.Label which you then try to inject using FXML: that's not right, you should use javafx.scene.control.Label. Don't mix awt code and JavaFX code.

Related

JavaFX ChoiceBox - How can you update the text of the popup items?

I have a ChoiceBox where I can select the language for my program. When I select another language, the label gets translated as desired (because it is recomputed using ChoiceBoxSkin#getDisplayText and my StringConverter takes the language into account), but the elements in the popup list stay the same.
Now, I could do something like
public void updateStrings() {
var converter = getConverter();
setConverter(null);
setConverter(converter);
var selected = valueProperty().getValue();
valueProperty().setValue(null);
valueProperty().setValue(selected);
}
in my ChoiceBox-subclass. This will re-populate the popup list with the correctly translated texts. Setting the value again is necessary beacause ChoiceBoxSkin#updatePopupItems (which is triggered when changing the converter) also resets the toggleGroup. That means that the selected item would no longer be marked as selected in the popup list.
Despite being kind of ugly, this actually works for my current use case. However, it breaks if any listener of the valueProperty does something problematic on either setting it to null or selecting the desired item a second time.
Am I missing a cleaner or just all-around better way to achieve this?
Another approach might be to use a custom ChoiceBoxSkin. Extending that, I'd have access to ChoiceBoxSkin#getChoiceBoxPopup (although that is commented with "Test only purpose") and could actually bind the text properties of the RadioMenuItems to the corresponding translated StringProperty. But that breaks as soon as ChoiceBoxSkin#updatePopupItems is triggered from anywhere else...
A MRP should be:
import javafx.scene.control.ChoiceBox;
import javafx.util.StringConverter;
public class LabelChangeChoiceBox extends ChoiceBox<String> {
private boolean duringUpdate = false;
public LabelChangeChoiceBox() {
getItems().addAll("A", "B", "C");
setConverter(new StringConverter<>() {
#Override
public String toString(String item) {
return item + " selected:" + valueProperty().getValue();
}
#Override
public String fromString(String unused) {
throw new UnsupportedOperationException();
}
});
valueProperty().addListener((observable, oldValue, newValue) -> {
if(duringUpdate) {
return;
}
duringUpdate = true;
updateStrings();
duringUpdate = false;
});
}
public void updateStrings() {
var converter = getConverter();
setConverter(null);
setConverter(converter);
var selected = valueProperty().getValue();
valueProperty().setValue(null);
valueProperty().setValue(selected);
}
}
And an Application-class like
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
import ui.LabelChangeChoiceBox;
public class Launcher extends Application {
#Override
public void start(Stage stage) {
Scene scene = new Scene(new LabelChangeChoiceBox());
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
This works but needs the duringUpdate variable and can break if there is another change listener.
I’m not sure if this meets your needs, as your description of the problem is unclear in a few places.
Here’s a ChoiceBox which updates its converter using its own chosen language, and also retains its value when that change occurs:
import java.util.Locale;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.ChoiceBox;
import javafx.scene.layout.BorderPane;
import javafx.util.StringConverter;
public class FXLocaleSelector
extends Application {
#Override
public void start(Stage stage) {
ChoiceBox<Locale> choiceBox = new ChoiceBox<>();
choiceBox.getItems().addAll(
Locale.ENGLISH,
Locale.FRENCH,
Locale.GERMAN,
Locale.ITALIAN,
Locale.CHINESE,
Locale.JAPANESE,
Locale.KOREAN
);
choiceBox.converterProperty().bind(
Bindings.createObjectBinding(
() -> createConverter(choiceBox.getValue()),
choiceBox.valueProperty()));
BorderPane pane = new BorderPane(choiceBox);
pane.setPadding(new Insets(40));
stage.setScene(new Scene(pane));
stage.setTitle("Locale Selector");
stage.show();
}
private StringConverter<Locale> createConverter(Locale locale) {
Locale conversionLocale =
(locale != null ? locale : Locale.getDefault());
return new StringConverter<Locale>() {
#Override
public String toString(Locale value) {
if (value != null) {
return value.getDisplayName(conversionLocale);
} else {
return "";
}
}
#Override
public Locale fromString(String s) {
return null;
}
};
}
public static void main(String[] args) {
launch(FXLocaleSelector.class, args);
}
}
Not entirely certain whether or not I understand your requirement correctly, my assumptions:
there's a ChoiceBox which contains the "language" for your ui, including the itself: lets say it contains the items Locale.ENGLISH and Locale.GERMAN, the visual representation of its items should be "English", "German" if its value is Locale.ENGLISH and "Englisch", "Deutsch" if its value is Locale.GERMAN
the visual representation is done by a StringConverter configurable with the value
If so, the solution is in separating out concerns - actually, it's not: the problem described (and hacked!) in the question is JDK-8088507: setting the converter doesn't update the selection of the menu items in the drop down. One hack is as bad or good as another, my personal preferenced would go for a custom skin which
adds a change listener to the converter property
reflectively calls updateSelection
Something like:
public static class MyChoiceBoxSkin<T> extends ChoiceBoxSkin<T> {
public MyChoiceBoxSkin(ChoiceBox<T> control) {
super(control);
registerChangeListener(control.converterProperty(), e -> {
// my local reflection helper, use your own
FXUtils.invokeMethod(ChoiceBoxSkin.class, this, "updateSelection");
});
}
}
Note: the hacks - this nor the OP's solution - do not solve the missing offset of the popup on first opening (initially or after selecting an item in the popup).
Not a solution to the question, just one way to have a value-dependent converter ;)
have a StringConverter with a fixed value (for simplicity) for conversion
have a converter controller having that a property with that value and a second property with a converter configured with the value: make sure the converter is replaced on change of the value
bind the controller's value to the box' value and the box' converter to the controller's converter
In (very raw) code:
public static class LanguageConverter<T> extends StringConverter<T> {
private T currentLanguage;
public LanguageConverter(T language) {
currentLanguage = language;
}
#Override
public String toString(T object) {
Object value = currentLanguage;
return "" + object + (value != null ? value : "");
}
#Override
public T fromString(String string) {
return null;
}
}
public static class LanguageController<T> {
private ObjectProperty<StringConverter<T>> currentConverter = new SimpleObjectProperty<>();
private ObjectProperty<T> currentValue = new SimpleObjectProperty<>() {
#Override
protected void invalidated() {
currentConverter.set(new LanguageConverter<>(get()));
}
};
}
Usage:
ChoiceBox<String> box = new ChoiceBox<>();
box.getItems().addAll("A", "B", "C");
box.getSelectionModel().selectFirst();
LanguageController<String> controller = new LanguageController<>();
controller.currentValue.bind(box.valueProperty());
box.converterProperty().bind(controller.currentConverter);

TableView scrolling and sorting results in incorrectly styled rows through RowFactory

I have a TableView which uses a RowFactory to style rows depending on a specific property of the item of the row. The RowFactory uses worker threads to check the validity of this specific property against a call to the database. The problem is that correct rows are sometimes marked as incorrect (red through a PseudoClass) and incorrect rows not marked. I have created a Minimal Reproducible Example below. This example should mark only rows that are even...but it also marks other rows.
Test Entity
public class TestEntity
{
public TestEntity(String firstName, String lastName, int c)
{
setFirstName(firstName);
setLastName(lastName);
setC(c);
}
private StringProperty firstName = new SimpleStringProperty();
private StringProperty lastName = new SimpleStringProperty();
private IntegerProperty c = new SimpleIntegerProperty();
public int getC()
{
return c.get();
}
public IntegerProperty cProperty()
{
return c;
}
public void setC(int c)
{
this.c.set(c);
}
public String getFirstName()
{
return firstName.get();
}
public StringProperty firstNameProperty()
{
return firstName;
}
public void setFirstName(String firstName)
{
this.firstName.set(firstName);
}
public String getLastName()
{
return lastName.get();
}
public StringProperty lastNameProperty()
{
return lastName;
}
public void setLastName(String lastName)
{
this.lastName.set(lastName);
}
}
Main
public class TableViewProblemMain extends Application
{
public static void main(String[] args)
{
launch(args);
AppThreadPool.shutdown();
}
#Override
public void start(Stage stage)
{
TableView<TestEntity> tableView = new TableView();
TableColumn<TestEntity, String> column1 = new TableColumn<>("First Name");
column1.setCellValueFactory(new PropertyValueFactory<>("firstName"));
TableColumn<TestEntity, String> column2 = new TableColumn<>("Last Name");
column2.setCellValueFactory(new PropertyValueFactory<>("lastName"));
TableColumn<TestEntity, String> column3 = new TableColumn<>("C");
column3.setCellValueFactory(new PropertyValueFactory<>("c"));
tableView.getColumns().addAll(column1, column2, column3);
tableView.setRowFactory(new TestRowFactory());
for (int i = 0; i < 300; i++)
{
tableView.getItems().add(new TestEntity("Fname" + i, "Lname" + i, i));
}
VBox vbox = new VBox(tableView);
Scene scene = new Scene(vbox);
scene.getStylesheets().add(this.getClass().getResource("/style.css").toExternalForm());
// Css has only these lines:
/*
.table-row-cell:invalid {
-fx-background-color: rgba(240, 116, 116, 0.18);
}
* */
stage.setScene(scene);
stage.show();
}
}
Row Factory
public class TestRowFactory implements Callback<TableView<TestEntity>, TableRow<TestEntity>>
{
private final PseudoClass INVALID_PCLASS = PseudoClass.getPseudoClass("invalid");
#Override
public TableRow<TestEntity> call(TableView param)
{
TableRow<TestEntity> row = new TableRow();
Thread validationThread = new Thread(() ->
{
try
{
if(row.getItem() != null)
{
Thread.sleep(500); // perform validation and stuff...
if(row.getItem().getC() % 2 == 0)
{
Tooltip t = new Tooltip("I am a new tooltip that should be shown only on red rows");
row.setTooltip(t);
row.pseudoClassStateChanged(INVALID_PCLASS, true);
}
}
} catch (InterruptedException e)
{
e.printStackTrace();
}
});
ChangeListener changeListener = (obs, old, current) ->
{
row.setTooltip(null);
AppThreadPool.perform(validationThread);
};
row.itemProperty().addListener((observable, oldValue, newValue) ->
{
row.setTooltip(null);
if (oldValue != null)
{
oldValue.firstNameProperty().removeListener(changeListener);
}
if (newValue != null)
{
newValue.firstNameProperty().removeListener(changeListener);
AppThreadPool.perform(validationThread);
}
else
{
row.pseudoClassStateChanged(INVALID_PCLASS, false);
}
});
row.focusedProperty().addListener(changeListener);
return row;
}
}
AppThreadPool
public class AppThreadPool
{
private static final int threadCount = Runtime.getRuntime().availableProcessors();
private static final ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(threadCount * 2 + 1);
public static <R extends Runnable> void perform(R runnable)
{
executorService.submit(runnable);
}
public static void shutdown()
{
executorService.shutdown();
}
}
Screenshot
There are several misunderstandings in your code. The first is about cells and reuse (a TableRow is a Cell). Cells can be reused arbitrarily, and potentially frequently (especially during user scrolling) to stop displaying one item and display a different one.
In your code, if the row is used to display an entity that is invalid, the listener on the row's itemProperty will trigger the runnable on the background thread, which will at some point set the pseudoclass state to true.
If the cell is subsequently reused to display a valid item, however, the next runnable that is executed does not change the pseudoclass state. So that state remains true and the row color remains red.
Consequently, a row is red if it ever displayed an invalid item at some point. (Not if it is currently displaying an invalid item.) If you scroll enough, eventually all cells will be red.
Secondly, you must not update any UI that is part of the scene graph from any thread other than the FX Application Thread. Additionally, some other operations, such as creating Window instances (Tooltip is a subclass of Window) must be performed on the FX Application Thread. Note that this includes modifying model properties which are bound to the UI, including properties used in the table columns. You violate this in your validationThread, where you create a Tooltip, set it on the row, and change the pseudoclass state, all in a background thread.
A good approach here is to use the JavaFX concurrency API. Use Tasks which, as far as possible, use only immutable data and return an immutable value. If you do need to update properties which are displayed in the UI, use Platform.runLater(...) to schedule those updates on the FX Application Thread.
In terms of MVC design, it is a good practice for your Model class(es) to store all the data needed by the View. Your design runs into trouble because there is no real place where the validation status is stored. Moreover, the validation status is really more than just "valid" or "invalid"; there is a phase while the thread is running but not completed where the validation status is unknown.
Here's my solution, which addresses these issues. I am assuming:
Your entity has a notion of validity.
Establishing the validity of an entity is a long-running process
Validity depends on one or more properties which may change while the UI is displayed
The validity should be "lazily" established, on an as-need basis.
The UI prefers not to display "unknown" validity, and if an entity is displayed whose validity is unknown, it should be established and redisplayed.
I created an enum for ValidationStatus, which has four values:
public enum ValidationStatus {
VALID, INVALID, UNKNOWN, PENDING ;
}
UNKNOWN indicates the validity is not known and validation has not been requested; PENDING indicates that validation has been requested but is not yet complete.
Then I have a wrapper for your entity which adds the validation status as an observable property. If the property on which validation depends in the underlying entity changes, the validation is reset to UNKNOWN.
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
public class ValidatingTestEntity {
private final TestEntity entity ;
private final ObjectProperty<ValidationStatus> validationStatus = new SimpleObjectProperty<>(ValidationStatus.UNKNOWN);
public ValidatingTestEntity(TestEntity entity) {
this.entity = entity;
entity.firstNameProperty().addListener((obs, oldName, newName) -> setValidationStatus(ValidationStatus.UNKNOWN));
}
public TestEntity getEntity() {
return entity;
}
public ValidationStatus getValidationStatus() {
return validationStatus.get();
}
public ObjectProperty<ValidationStatus> validationStatusProperty() {
return validationStatus;
}
public void setValidationStatus(ValidationStatus validationStatus) {
this.validationStatus.set(validationStatus);
}
}
The ValidationService provides a service to validate entities on a background thread, updating the appropriate properties with the result. This is managed through a thread pool and with JavaFX Tasks. This just mimics a database call by sleeping for a random amount of time and then returning alternating results.
When the task changes state (i.e. as it progresses though its lifecycle), the validation property of the entity is updated: UNKNOWN if the task fails to complete normally, PENDING if the task is in an incomplete state, and either VALID or INVALID, depending on the result of the task, if the task succeeds.
import javafx.application.Platform;
import javafx.concurrent.Task;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
public class ValidationService {
private final Executor exec = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2 + 1,
r -> {
Thread thread = new Thread(r);
thread.setDaemon(true);
return thread;
}
);
public Task<Boolean> validateEntity(ValidatingTestEntity entity) {
// task runs on a background thread and should not access mutable data,
// so make final copies of anything needed here:
final String firstName = entity.getEntity().getFirstName();
final int code =entity.getEntity().getC();
Task<Boolean> task = new Task<Boolean>() {
#Override
protected Boolean call() throws Exception {
try {
Thread.sleep(500 + ThreadLocalRandom.current().nextInt(500));
} catch (InterruptedException exc) {
// if interrupted other than being cancelled, reset thread's interrupt status:
if (! isCancelled()) {
Thread.currentThread().interrupt();
}
}
boolean result = code % 2 == 0;
return result;
}
};
task.stateProperty().addListener((obs, oldState, newState) ->
entity.setValidationStatus(
switch(newState) {
case CANCELLED, FAILED -> ValidationStatus.UNKNOWN;
case READY, RUNNING, SCHEDULED -> ValidationStatus.PENDING ;
case SUCCEEDED ->
task.getValue() ? ValidationStatus.VALID : ValidationStatus.INVALID ;
}
)
);
exec.execute(task);
return task ;
}
}
This is the TableRow implementation. It has a listener which observes the validation status of the current item, if there is one. If the item changes, the listener is removed from the old item (if there is one), and attached to the new item (if there is one). If either the item changes, or the validation state of the current item changes, the row is updated. If the new validation status is UNKNOWN, a request is sent to the service to validate the current item. There are two pseudoclass states: invalid (red) and unknown (orange), which are updated any time the item or its validation status change. The tooltip is set if the item is invalid, and set to null otherwise.
import javafx.beans.value.ChangeListener;
import javafx.css.PseudoClass;
import javafx.scene.control.TableRow;
import javafx.scene.control.Tooltip;
public class ValidatingTableRow extends TableRow<ValidatingTestEntity> {
private final ValidationService validationService ;
private final PseudoClass pending = PseudoClass.getPseudoClass("pending");
private final PseudoClass invalid = PseudoClass.getPseudoClass("invalid");
private final Tooltip tooltip = new Tooltip();
private final ChangeListener<ValidationStatus> listener = (obs, oldStatus, newStatus) -> {
updateValidationStatus();
};
public ValidatingTableRow(ValidationService validationService){
this.validationService = validationService ;
itemProperty().addListener((obs, oldItem, newItem) -> {
setTooltip(null);
if (oldItem != null) {
oldItem.validationStatusProperty().removeListener(listener);
}
if (newItem != null) {
newItem.validationStatusProperty().addListener(listener);
}
updateValidationStatus();
});
}
private void updateValidationStatus() {
if (getItem() == null) {
pseudoClassStateChanged(pending, false);
pseudoClassStateChanged(invalid, false);
setTooltip(null);
return ;
}
ValidationStatus validationStatus = getItem().getValidationStatus();
if( validationStatus == ValidationStatus.UNKNOWN) {
validationService.validateEntity(getItem());
}
if (validationStatus == ValidationStatus.INVALID) {
tooltip.setText("Invalid entity: "+getItem().getEntity().getFirstName() + " " +getItem().getEntity().getC());
setTooltip(tooltip);
} else {
setTooltip(null);
}
pseudoClassStateChanged(pending, validationStatus == ValidationStatus.PENDING);
pseudoClassStateChanged(invalid, validationStatus == ValidationStatus.INVALID);
}
}
Here's the Entity, which is the same as in the question:
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class TestEntity
{
public TestEntity(String firstName, String lastName, int c)
{
setFirstName(firstName);
setLastName(lastName);
setC(c);
}
private StringProperty firstName = new SimpleStringProperty();
private StringProperty lastName = new SimpleStringProperty();
private IntegerProperty c = new SimpleIntegerProperty();
public String getFirstName() {
return firstName.get();
}
public StringProperty firstNameProperty() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName.set(firstName);
}
public String getLastName() {
return lastName.get();
}
public StringProperty lastNameProperty() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName.set(lastName);
}
public int getC() {
return c.get();
}
public IntegerProperty cProperty() {
return c;
}
public void setC(int c) {
this.c.set(c);
}
}
And here's the application class. I added the ability to edit the first name, which lets you see an item reverting to unknown and then re-establishing its validity (you need to change selection quickly after committing the edit).
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TableViewProblemMain extends Application
{
public static void main(String[] args)
{
launch(args);
}
#Override
public void start(Stage stage)
{
TableView<ValidatingTestEntity> tableView = new TableView();
tableView.setEditable(true);
TableColumn<ValidatingTestEntity, String> column1 = new TableColumn<>("First Name");
column1.setCellValueFactory(cellData -> cellData.getValue().getEntity().firstNameProperty());
column1.setEditable(true);
column1.setCellFactory(TextFieldTableCell.forTableColumn());
TableColumn<ValidatingTestEntity, String> column2 = new TableColumn<>("Last Name");
column2.setCellValueFactory(cellData -> cellData.getValue().getEntity().lastNameProperty());
TableColumn<ValidatingTestEntity, Number> column3 = new TableColumn<>("C");
column3.setCellValueFactory(cellData -> cellData.getValue().getEntity().cProperty());
tableView.getColumns().addAll(column1, column2, column3);
ValidationService service = new ValidationService();
tableView.setRowFactory(tv -> new ValidatingTableRow(service));
for (int i = 0; i < 300; i++)
{
tableView.getItems().add(new ValidatingTestEntity(
new TestEntity("Fname" + i, "Lname" + i, i)));
}
VBox vbox = new VBox(tableView);
Scene scene = new Scene(vbox);
scene.getStylesheets().add(this.getClass().getResource("/style.css").toExternalForm());
stage.setScene(scene);
stage.show();
}
}
Finally, for completeness, the stylesheet:
.table-row-cell:invalid {
-fx-background-color: rgba(240, 116, 116, 0.18);
}
.table-row-cell:pending {
-fx-background-color: rgba(240, 120, 0, 0.18);
}

Refreshing a column of comboboxes in a table view, javafx

so i have a table view with 3 columns and one of them is a column of comboboxes, the way i create the column of combobox is as so
Source = new TableColumn<>("Configure Interface as..");
Source.setCellValueFactory(i -> {
final StringProperty value = i.getValue().optionProperty();
// binding to constant value
return Bindings.createObjectBinding(() -> value);
});
Source.setCellFactory(col -> {
TableCell<TableViewTest, StringProperty> c = new TableCell<>();
ComboBox<String> comboBox = new ComboBox<>(options);
c.itemProperty().addListener((observable, oldValue, newValue) -> {
if (oldValue != null) {
comboBox.valueProperty().unbindBidirectional(oldValue);
}
if (newValue != null) {
comboBox.valueProperty().bindBidirectional(newValue);
}
});
c.graphicProperty().bind(Bindings.when(c.emptyProperty()).then((Node) null).otherwise(comboBox));
return c;
});
the column gets its values from the getter method optionProperty() which resides within my TableViewTest class.
So the problem i'm having is I have another combobox (comboBoxA) that is above my tableview table in my gui, and when ever i change the value of comboBoxA i want to change the values of the comboboxes with the column.
I can do this by calling the following code within the method that is listening for the selection change of comboboxA
Source.setCellValueFactory(i -> {
final StringProperty value = i.getValue().optionTwoProperty();
// binding to constant value
return Bindings.createObjectBinding(() -> value);
});
but the values don't change unless is start scrolling down to near the bottom of the table. is there a way to force the comboboxes to change to the new values within the getter method optionTwoProperty() without me having to scroll down?.
EDIT
Okay so the line
final StringProperty value = i.getValue().optionTwoProperty();
doesnt actaully get called until i start scrolling down.
So, with help from fabian, I think I understand that you want the combo box above the table to change the property in your model class that is represented in the cells in the table column.
One way to do this is to make the type of the combo box function that maps the model class to a property, and populate it with functions mapping to each of the properties you want.
Then you can represent the cell value factory for the table column with a binding that observes all the possible properties that could be represented, along with the selected value in the combo box, and returns the value computed by applying the function from the combo box to the model instance (and retrieving its wrapped value).
For the cell factory for the column, you can observe the selected value in the cell's combo box. When it changes, use the selected item in the combo box above the table to figure out which property to update.
Here's a SSCCE:
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class TableWithSetAllComboBox extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Item> table = new TableView<>();
TableColumn<Item, String> itemCol = new TableColumn<>("Item");
itemCol.setCellValueFactory(cellData -> Bindings.createStringBinding(() -> cellData.getValue().getName()));
table.getColumns().add(itemCol);
TableColumn<Item, String> choiceCol = new TableColumn<>("Choice");
ComboBox<Function<Item, StringProperty>> option = new ComboBox<>();
option.getItems().add(Item::choiceProperty);
option.getItems().add(Item::choice2Property);
option.setCellFactory(lv -> createListCell());
option.setButtonCell(createListCell());
option.getSelectionModel().select(0);
ObservableList<String> choices = FXCollections.observableArrayList("First choice", "Second choice", "Third choice");
choiceCol.setCellFactory(col -> {
TableCell<Item, String> cell = new TableCell<>();
ComboBox<String> combo = new ComboBox<>(choices);
cell.graphicProperty().bind(Bindings.when(cell.emptyProperty()).then((Node)null).otherwise(combo));
combo.valueProperty().addListener((obs, oldValue, newValue) -> {
if (! cell.isEmpty() && newValue != null) {
Item item = table.getItems().get(cell.getIndex()) ;
StringProperty property = option.getValue().apply(item);
property.set(newValue);
}
});
cell.itemProperty().addListener((obs, oldItem, newItem) -> combo.setValue(newItem));
return cell ;
});
choiceCol.setPrefWidth(150);
table.getColumns().add(choiceCol);
choiceCol.setCellValueFactory(cellData -> Bindings.createStringBinding(
() -> option.getValue().apply(cellData.getValue()).get(),
cellData.getValue().choiceProperty(),
cellData.getValue().choice2Property(),
option.valueProperty()));
choiceCol.setGraphic(option);
choiceCol.setPrefWidth(200);
for (int i = 1; i <= 30 ; i++) table.getItems().add(new Item("Item "+i ,choices.get(0)));
Button debug = new Button("Debug");
debug.setOnAction(e -> table.getItems().stream().
map(item -> String.format("%s (%s, %s)", item.getName(), item.getChoice(), item.getChoice2())).
forEach(System.out::println));
BorderPane root = new BorderPane(table);
BorderPane.setMargin(debug, new Insets(5));
root.setBottom(debug);
primaryStage.setScene(new Scene(root, 600, 600));
primaryStage.show();
}
private ListCell<Function<Item, StringProperty>> createListCell() {
return new ListCell<Function<Item, StringProperty>>() {
#Override
public void updateItem(Function<Item, StringProperty> item, boolean empty) {
super.updateItem(item, empty);
setText(empty ? null : item.apply(new Item("", "")).getName());
}
};
}
public static class Item {
private final String name ;
private final StringProperty choice ;
private final StringProperty choice2 ;
public Item(String name, String choice) {
this.choice = new SimpleStringProperty(this, "Choice", choice);
this.choice2 = new SimpleStringProperty(this, "Choice 2", "Second choice");
this.name = name ;
}
public final StringProperty choiceProperty() {
return this.choice;
}
public final java.lang.String getChoice() {
return this.choiceProperty().get();
}
public final void setChoice(final java.lang.String choice) {
this.choiceProperty().set(choice);
}
public String getName() {
return name;
}
public final StringProperty choice2Property() {
return this.choice2;
}
public final java.lang.String getChoice2() {
return this.choice2Property().get();
}
public final void setChoice2(final java.lang.String choice2) {
this.choice2Property().set(choice2);
}
}
public static void main(String[] args) {
launch(args);
}
}
The issue is the TableView not listening to modifications of the cellValueFactory property of the elements of it's columns. Therefore the TableView doesn't know it should redraw it's cells. In JavaFX 8u60 the refresh() method was added for this purpose (for some reason I can't find it in the online javadoc though), which allows you to change the code of your method changing the cellValueFactory like this:
Source.setCellValueFactory(i -> {
final StringProperty value = i.getValue().optionTwoProperty();
// binding to constant value
return Bindings.createObjectBinding(() -> value);
});
tableview.refresh();
In older versions you have to use the workaround of setting the column value to trigger a change in the list:
List<TableColumn<TableViewTest, ?>> columns = tableview.getColumns();
columns.set(columns.indexOf(Source), Source);
But this workaround could cease to work in future versions, since the list is not actually modified with this operation and triggering a list change event is not required by the contract of ObservableList (but replacing the TableColumn with a new instance (and copying the properties) should always work).
Hard to say given the code snippets. Maybe you're not on the javaFX thread when doing the update? In that case use Platform.runLater(...), or share some minimal amout of code to reproduce the problem.

javafx: Bindings not working as expected

I am trying to have a property total which is obtained by multiplying two properties together, namely currentPrice and volumeHeld, where currentPrice is actually obtained by downloading google finance stock price every 10 secs. And it automatically updates every 10 seconds.
Now the getCurrentPrice() is initialized at 0, as shown in the code. 10 seconds later, it picks up a new value and this all works fine.
But in the binding method below, the total is not automatically updated when the currentPrice property has changed.
totalBinding = Bindings.createDoubleBinding(() -> {
System.out.println("current price: " + getCurrentPrice() + "vol held: " + getVolumeHeld());
return getCurrentPrice() * getVolumeHeld();
});
total.bind(totalBinding);
Question: I discovered that within the createDoubleBinding statement above, the getCurrentPrice() has a value of 0 (as mentioned above) and when its value is changed, the change is NOT propagated in the total property. By that I mean the total property is not able to pick up the new value from getCurrentPrice() even when the current price has changed.
So the problem is two-fold but I am guessing the solutions for both of my questions below will be similar if not exactly the same:
How can I fix the problem mentioned above?
Later on, I will be binding this total property to another property to work out the total of the total property for all Trade objects). This fails miserably and it is always equal to 0. This method is written in a different class, i.e. not in the Trade class.
UPDATE:
Code shown below:
class SummaryofTrade{
...
sumOfTotals = new ReadOnlyDoubleWrapper();
sumOfTotalsBinding = Bindings.createDoubleBinding(() -> {
double sum = 0;
for(Trade t : this.observableListOfTrades){
sum += t.getTotal();
}
return sum;
}, total); // I cannot put "total" as a second parameter, as it is a property that resides in the Trade class , not this class.
sumOfTotals.bind(sumOfTotalsBinding);
...
}
The error log message:
Caused by: java.lang.Error: Unresolved compilation problem:
total cannot be resolved to a variable
Please note that the sumOfTotalsBinding and sumOfTotals live in another class.
Code for Trade object below:
class Trade{
...
private final ReadOnlyDoubleWrapper total;
private final ReadOnlyDoubleWrapper currentPrice;
private DoubleProperty volumeHeld;
public DoubleBinding totalBinding;
private final ScheduledService<Number> priceService = new ScheduledService<Number>() {
#Override
public Task<Number> createTask(){
return new Task<Number>() {
#Override
public Number call() throws InterruptedException, IOException {
return getCurrentPriceFromGoogle();
}
};
}
};
public Trade(){
...
priceService.setPeriod(Duration.seconds(10));
priceService.setOnFailed(e -> priceService.getException().printStackTrace());
this.currentPrice = new ReadOnlyDoubleWrapper(0);
this.currentPrice.bind(priceService.lastValueProperty());
startMonitoring();
this.total = new ReadOnlyDoubleWrapper();
DoubleBinding totalBinding = Bindings.createDoubleBinding(() ->
getCurrentPrice() * getVolumeHeld(),
currentPriceProperty(), volumeHeldProperty());
total.bind(totalBinding);
}
// volume held
public double getVolumeHeld(){
return this.volumeHeld.get();
}
public DoubleProperty volumeHeldProperty(){
return this.volumeHeld;
}
public void setVolumeHeld(double volumeHeld){
this.volumeHeld.set(volumeHeld);
}
// multi-threading
public final void startMonitoring() {
priceService.restart();
}
public final void stopMonitoring() {
priceService.cancel();
}
public ReadOnlyDoubleProperty currentPriceProperty(){
return this.currentPrice.getReadOnlyProperty();
}
public final double getCurrentPrice(){
return currentPriceProperty().get();
}
// total
public final Double getTotal(){
return totalProperty().getValue();
}
public ReadOnlyDoubleProperty totalProperty(){
return this.total;
}
}
UPDATE 9/15/2015:
I am trying to elaborate my problem in a logical way here. Let me know if this does not make sense. Thanks.
First, in the Trade class above (please note the code above has been updated and specified the property dependency), each Trade object contains a total property, which is the product of currentPrice and VolumeHeld. If the user manually edit the values of current price and volume held. The total property will be updated automatically.
Now, I have an ObservableList of Trade objects, each of them has a total property. My goal is to sum up the the total property of each Trade object in the observable list and bind the sum to a variable called sumOfTotals. This is done in a class called SummaryOfTrade. And whenever the total property of any one of the Trades in the Observable list changes, the sumOfTotals property should also change automatically.
class SummaryofTrade{
...
// within constructor, we have
sumOfTotals = new ReadOnlyDoubleWrapper();
sumOfTotalsBinding = Bindings.createDoubleBinding(() -> {
double sum = 0;
for(Trade t : this.observableListOfTrades){
sum += t.getTotal();
}
return sum;
}, totalProperty());
sumOfTotals.bind(sumOfTotalsBinding);
...
}
This is where the problem comes in. Eclipse is saying that it does not recognise the Trade object's property,totalProperty. Error message shown below.
The error log message:
Caused by: java.lang.Error: Unresolved compilation problem:
The method totalProperty() is undefined for the type SummaryOfTrade
I have specified the property dependency already yet Eclipse is throwing an error. How should I resolve this?
Since both current price and volume held are properties, you could just bind them directly:
total.bind(currentPriceProperty().multiply(volumeHeldProperty()));
If you absolutely need to use a custom double binding, you first need to provide dependencies so that the computation is performed once the dependencies become invalidated as per documentation:
DoubleBinding totalBinding = new DoubleBinding() {
{
super.bind(currentPrice, volumeHeld);
}
#Override
protected double computeValue() {
return currentPrice.get() * volumeHeld.get();
}
};
The following helper function provided by Bindings should also work:
DoubleBinding totalBinding = Bindings.createDoubleBinding(() ->
currentPrice.get() * volumeHeld.get(),
currentPrice, volumeHeld);
You have an ObservableList<Trade>, where each Trade object has an observable totalProperty(). Your sumOfTotals need to be updated when either the content of that list change, or when any of the individual totalProperty()s belonging to any of the elements change.
You can do this by hand:
DoubleBinding sumOfTotalsBinding = new DoubleBinding() {
{
bind(observableListOfTrades);
observableListOfTrades.forEach(trade -> bind(trade.totalProperty());
observableListOfTrades.addListener((Change<? extends Trade> change) -> {
while (change.next()) {
if (change.wasAdded()) {
change.getAddedSubList().forEach(trade -> bind(trade.totalProperty()));
}
if (change.wasRemoved()) {
change.getRemoved().forEach(trade -> unbind(trade.totalProperty()));
}
}
});
}
#Override
public double computeValue() {
return observableListOfTrades.stream().collect(Collectors.summingDouble(Trade::getTotal));
}
};
Or, you can create your list with an extractor. This will cause the list to fire update notifications (thereby labeling it as invalid) when any of the specified properties belonging to the elements change:
ObservableList<Trade> observableListOfTrades =
FXCollections.observableArrayList(trade -> new Observable[] { trade.totalProperty() });
and then you can just do
sumOfTotals.bind(Bindings.createDoubleBinding(() ->
observableListOfTrades.stream().collect(Collectors.summingDouble(Trade::getTotal)),
observableListOfTrades);
since now binding just to the observableListOfTrades will cause recomputation when any of the individual totals change.
Here's an SSCCE:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import java.util.stream.Collectors;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.HPos;
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.TextField;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.stage.Stage;
import javafx.util.converter.DoubleStringConverter;
import javafx.util.converter.IntegerStringConverter;
public class TradeTableExample extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Trade> table = new TableView<>();
table.setEditable(true);
TableColumn<Trade, String> nameCol = column("Name", trade -> new ReadOnlyStringWrapper(trade.getName()));
TableColumn<Trade, Integer> volumeCol = column("Volume", t -> t.volumeProperty().asObject());
TableColumn<Trade, Double> priceCol = column("Price", t -> t.priceProperty().asObject());
TableColumn<Trade, Number> totalCol = column("Total", Trade::totalProperty);
volumeCol.setCellFactory(TextFieldTableCell.forTableColumn(new IntegerStringConverter()));
priceCol.setCellFactory(TextFieldTableCell.forTableColumn(new DoubleStringConverter()));
table.getColumns().addAll(Arrays.asList(nameCol, volumeCol, priceCol, totalCol));
ObservableList<Trade> data = FXCollections.observableArrayList(trade -> new Observable[] {trade.totalProperty()});
DoubleBinding grandTotal = Bindings.createDoubleBinding(() ->
data.stream().collect(Collectors.summingDouble(Trade::getTotal)),
data);
data.addAll(createData());
table.setItems(data);
Label totalLabel = new Label();
totalLabel.textProperty().bind(grandTotal.asString("Total: %,.2f"));
TextField nameField = new TextField();
TextField volumeField = new TextField("0");
TextField priceField = new TextField("0.00");
Button add = new Button("Add");
add.setOnAction(e -> {
data.add(
new Trade(nameField.getText(),
Integer.parseInt(volumeField.getText()),
Double.parseDouble(priceField.getText())));
nameField.setText("");
volumeField.setText("0");
priceField.setText("0.00");
});
Button delete = new Button("Delete");
delete.setOnAction(e -> data.remove(table.getSelectionModel().getSelectedIndex()));
delete.disableProperty().bind(table.getSelectionModel().selectedItemProperty().isNull());
HBox buttons = new HBox(5, add, delete);
GridPane controls = new GridPane();
controls.addRow(0, new Label("Name:"), nameField);
controls.addRow(1, new Label("Volume:"), volumeField);
controls.addRow(2, new Label("Price:"), priceField);
controls.add(buttons, 0, 3, 2, 1);
controls.add(totalLabel, 0, 4, 2, 1);
ColumnConstraints leftCol = new ColumnConstraints();
leftCol.setHalignment(HPos.RIGHT);
ColumnConstraints rightCol = new ColumnConstraints();
rightCol.setHgrow(Priority.ALWAYS);
controls.getColumnConstraints().addAll(leftCol, rightCol);
GridPane.setHalignment(controls, HPos.LEFT);
GridPane.setHalignment(totalLabel, HPos.LEFT);
controls.setHgap(5);
controls.setVgap(5);
BorderPane root = new BorderPane(table, null, null, controls, null);
Scene scene = new Scene(root, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private List<Trade> createData() {
Random rng = new Random();
List<Trade> trades = new ArrayList<>();
for (int i=0; i<10; i++) {
StringBuilder name = new StringBuilder();
for (int c = 0; c < 3; c++) {
name.append(Character.toString((char)(rng.nextInt(26)+'A')));
}
double price = rng.nextInt(100000)/100.0 ;
int volume = rng.nextInt(10000);
trades.add(new Trade(name.toString(), volume, price));
}
return trades ;
}
private <S,T> TableColumn<S,T> column(String text, Function<S, ObservableValue<T>> property) {
TableColumn<S,T> col = new TableColumn<>(text);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
return col ;
}
public static class Trade {
private final String name ;
private final IntegerProperty volume = new SimpleIntegerProperty();
private final DoubleProperty price = new SimpleDoubleProperty();
private final ReadOnlyDoubleWrapper total = new ReadOnlyDoubleWrapper();
public Trade(String name, int volume, double price) {
this.name = name ;
setPrice(price);
setVolume(volume);
total.bind(priceProperty().multiply(volumeProperty()));
}
public final String getName() {
return name ;
}
public final IntegerProperty volumeProperty() {
return this.volume;
}
public final int getVolume() {
return this.volumeProperty().get();
}
public final void setVolume(final int volume) {
this.volumeProperty().set(volume);
}
public final DoubleProperty priceProperty() {
return this.price;
}
public final double getPrice() {
return this.priceProperty().get();
}
public final void setPrice(final double price) {
this.priceProperty().set(price);
}
public final ReadOnlyDoubleProperty totalProperty() {
return this.total.getReadOnlyProperty();
}
public final double getTotal() {
return this.totalProperty().get();
}
}
public static void main(String[] args) {
launch(args);
}
}

how I can put a dynamic filtered xpage view / repeat control into a hashmap?

Can I easily put the entry values of a filtered view into a hashmap?
I have a repeat control, bound to a view, with a dynamic filter.
The user can change the filter by several djFilteringSelect controls, corresponding to the view columns.
Depending on the selection in the 1st djFilteringSelect, the selection in the next djFilteringSelects should be limited to the possible entries ("similar to the data filter in excel"). At the moment I do this with separate #dbcolumn/#dblookup methods for the djFilteringSelects, but I think it is much better and easier to just fill the view entries values into a hashmap and show the hashmap values in the djFilteringSelect.
I found few threads here with repeat controls and hashmaps, but these examples also build the doc collection separatly, which I wish to avoid.
Thanks for any help,
Uwe
There's a reason why all these examples build their document collections separately. Instead of "the view is in the UI", so I must use it, you might have an easier time to build a bean that serves as the source for your repeat control. A bean data source or a managed bean. This will allow for a use case, where you show 2 filter results (e.g England/London and France/Lyon) in one display, something a filtered view can't do.
Update
If you have a lot of reader/author fields, you want to have a view categorized by them, to populate your "backing bean" - there is lot of performance to gain. Holding a few hundred items in memory isn't a big deal.
There are 2 trains of though: make it generic, so every line in the view ends up as a Collection item (Array, List, Map etc.) or to build dedicated line items with clear names. This trains collide quite often, let me take you to the dedicated train for a moment. :-) So your classes (you need 2) would look like this:
package test;
import java.io.Serializable;
import java.util.Vector;
import lotus.domino.Database;
import lotus.domino.Document;
import lotus.domino.NotesException;
import lotus.domino.ViewEntry;
public class Fruit implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String color;
private String shape;
private String taste;
private String unid = null;
public Fruit() {
// default constructor, might not be needed
}
// To make it easy from a view
public Fruit(final ViewEntry ve) {
try {
#SuppressWarnings("rawtypes")
Vector v = ve.getColumnValues();
// 0 would be the user/group/role
this.setName(v.get(1).toString());
this.setColor(v.get(2).toString());
this.setShape(v.get(3).toString());
this.setTaste(v.get(4).toString());
this.unid = ve.getUniversalID();
} catch (NotesException e) {
e.printStackTrace();
}
}
public void save(Database db) throws NotesException {
Document doc;
if (this.unid == null) {
doc = db.createDocument();
} else {
doc = db.getDocumentByUNID(this.unid);
}
doc.replaceItemValue("Color", this.getColor());
// more here
doc.save();
}
public final String getName() {
return this.name;
}
public final void setName(String name) {
this.name = name;
}
public final String getColor() {
return this.color;
}
public final void setColor(String color) {
this.color = color;
}
public final String getShape() {
return this.shape;
}
public final void setShape(String shape) {
this.shape = shape;
}
public final String getTaste() {
return this.taste;
}
public final void setTaste(String taste) {
this.taste = taste;
}
}
That's to hold a line item (using my favourite fruits example). In your code the repeat control variable (or the data table variable) - instead of the view control, would hold one Fruit instance coming from fruitController.getSelectedFruits() (which you can use in EL as fruitController.selectedFruits), so you can bind your columns using varName.color, varname.shape
The class around it looks roughly like:
package test;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Set;
import java.util.TreeSet;
import lotus.domino.Database;
import lotus.domino.NotesException;
import lotus.domino.Session;
import lotus.domino.View;
import lotus.domino.ViewEntry;
import lotus.domino.ViewEntryCollection;
public class FruitController implements Serializable {
private static final long serialVersionUID = 1L;
private static final String FRUIT_VIEW = "(FruitsByUser)";
private final Collection<Fruit> allFruits = new ArrayList<Fruit>();
private final Set<String> colors = new TreeSet<String>();
private final Set<String> shape = new TreeSet<String>();
private final Set<String> taste = new TreeSet<String>();
private String colorFilter = null;
private String tasteFilter = null;
private String shapeFilter = null;
// if you use this controller, you only can use an object data source!
// for a bean you would need an empty controller
public FruitController(final Session s, final Database db) {
this.populateData(s, db);
}
public final String getColorFilter() {
return this.colorFilter;
}
public final String[] getColors() {
return (String[]) this.colors.toArray();
}
public Collection<Fruit> getSelectedFruits() {
Collection<Fruit> result = new ArrayList<Fruit>();
for (Fruit f : this.allFruits) {
if (this.matchesFilter(f)) {
result.add(f);
}
}
return result;
}
public final String[] getShape() {
return (String[]) this.shape.toArray();
}
public final String getShapeFilter() {
return this.shapeFilter;
}
public final String[] getTaste() {
return (String[]) this.taste.toArray();
}
public final String getTasteFilter() {
return this.tasteFilter;
}
public void resetFilters() {
this.setColorFilter(null);
this.setShapeFilter(null);
this.setTasteFilter(null);
}
public final void setColorFilter(String colorFilter) {
this.colorFilter = colorFilter;
}
public final void setShapeFilter(String shapeFilter) {
this.shapeFilter = shapeFilter;
}
public final void setTasteFilter(String tasteFilter) {
this.tasteFilter = tasteFilter;
}
private boolean matchesFilter(Fruit f) {
boolean result = true;
result = ((result == false) ? false : ((this.colorFilter == null || "".equals(this.colorFilter.trim())) ? true
: (this.colorFilter.equals(f.getColor()))));
result = ((result == false) ? false : ((this.tasteFilter == null || "".equals(this.tasteFilter.trim())) ? true
: (this.tasteFilter.equals(f.getTaste()))));
result = ((result == false) ? false : ((this.shapeFilter == null || "".equals(this.shapeFilter.trim())) ? true
: (this.shapeFilter.equals(f.getShape()))));
return result;
}
private void populateData(final Session s, final Database db) {
try {
final View v = db.getView(FRUIT_VIEW);
// You might need to loop a little here to get all the values
final ViewEntryCollection vec = v.getAllEntriesByKey(s.getUserName());
ViewEntry ve = vec.getFirstEntry();
while (ve != null) {
ViewEntry nextVe = vec.getNextEntry(ve);
Fruit f = new Fruit(ve);
this.updateSelectors(f);
this.allFruits.add(f);
ve = nextVe;
nextVe.recycle();
}
vec.recycle();
v.recycle();
} catch (NotesException e) {
// TODO Stacktrace is no error handling
e.printStackTrace();
}
}
private void updateSelectors(Fruit f) {
this.colors.add(f.getColor());
this.shape.add(f.getShape());
this.taste.add(f.getTaste());
}
}
Of course you can make that more sophisticated by filtering the selection values based on the other selections (e.g. after picking a color only offer the shapes that are available in that color). Using the class as object datasource (e.g. fruitController) should be easy. You can bind your dropdowns to fruitController.colorFilter etc. in EL and define the selections in EL as fruitController.Colors.
Update 2
The data source should be an object data source, like this one:
<xp:this.data>
<xe:objectData var="fruitController" ignoreRequestParams="true"
readonly="false" scope="view"
createObject="#{javascript:return new test.FruitController(session, database);}">
</xe:objectData>
</xp:this.data>
For a bean approach you would need to edit the faces-config.xml and change the class to have a parameterless constructor. For the select values you could stick with the toArray() call in your page or better change the class to return an array in the first place. I updated the class above accordingly (so you can still use EL, no need for SSJS).
Now you only need to add a refresh to the repeat in the onChange event of your selects. Since the new values will be send to the object data source (you bound them to colorFilter, shapeFilter, tasteFilter) the refresh will execute #{fruitController.selectedFruits} which delivers the subset back to the panel.
So the concept here is: You fetch all the user data once into the object data source and once that is loaded filter inside that class, no renewed retrieval or lookup required.
Let us know hoe it goes

Categories