Search code examples
javafxtableviewinputstreamreader

Javafx Tableview with InputStreamReader removes row color marker


i can't get any further. I have a standart Tableview and inserted the part from James_D which gives me a colored row and a different colored selected cell see here. This works realy pretty good. Now i need to read the data from an internet site. After inserting the code to read the data with InputStreamReader the colored row is not displayed any more at all. No other error. The read date is ok. About any help or alternatives I would be very glad. Thanks.

  • deleted the first "multi file version" of this problem.

css file: selected-row-table.css

.table-row-cell:selected-row {
    -fx-background-color: lightskyblue ;
}

New all in one Error example based on James_D Tableview without .fxml file. Needed css file (see above) with name "selected-row-table.css". Same error after "new InputStreamreader...".

package com.example.james1;

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.collections.ObservableSet;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;

public class HelloApplication extends Application {

   public static void main(String[] args) {
      launch();
   }

   @Override
   public void start(Stage stage) throws IOException {
      Scene scene = new Scene(new Group());
      scene.getStylesheets().add(getClass().getResource("selected-row-table.css").toExternalForm());
      stage.setTitle("Table View Sample");
      stage.setWidth(450);
      stage.setHeight(600);

      final Label label = new Label("Address Book");
      label.setFont(new Font("Arial", 20));

      final TableView<Person> table = new TableView<>();

      final ObservableList<Person> data =
            FXCollections.observableArrayList(
                  new Person("Jacob", "Smith", "[email protected]"),
                  new Person("Isabella", "Johnson", "[email protected]"),
                  new Person("Ethan", "Williams", "[email protected]"),
                  new Person("Emma", "Jones", "[email protected]"),
                  new Person("Michael", "Brown", "[email protected]")
            );

      table.getSelectionModel().setCellSelectionEnabled(true);
      table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);

      final PseudoClass selectedRowPseudoClass = PseudoClass.getPseudoClass("selected-row");
      final ObservableSet<Integer> selectedRowIndexes = FXCollections.observableSet();
      table.getSelectionModel().getSelectedCells().addListener((Change<? extends TablePosition> change) -> {
         selectedRowIndexes.clear();
         table.getSelectionModel().getSelectedCells().stream().map(TablePosition::getRow).forEach(row -> {
            selectedRowIndexes.add(row);
         });
      });

      table.setRowFactory(tableView -> {
         final TableRow<Person> row = new TableRow<>();
         BooleanBinding selectedRow = Bindings.createBooleanBinding(() ->
               selectedRowIndexes.contains(new Integer(row.getIndex())), row.indexProperty(), selectedRowIndexes);
         selectedRow.addListener((observable, oldValue, newValue) ->
               row.pseudoClassStateChanged(selectedRowPseudoClass, newValue)
         );
         return row;
      });

      TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
      firstNameCol.setMinWidth(100);
      firstNameCol.setCellValueFactory(new PropertyValueFactory<>("firstName"));
      TableColumn<Person, String> lastNameCol = new TableColumn<>("Last Name");
      lastNameCol.setMinWidth(100);
      lastNameCol.setCellValueFactory(new PropertyValueFactory<>("lastName"));
      TableColumn<Person, String> emailCol = new TableColumn<>("Email");
      emailCol.setMinWidth(200);
      emailCol.setCellValueFactory(new PropertyValueFactory<>("email"));

      table.setItems(data);
      table.getColumns().addAll(firstNameCol, lastNameCol, emailCol);

      final VBox vbox = new VBox();
      Button bt1 = new Button("add local");
      Button bt2 = new Button("add internet");

      vbox.setSpacing(5);
      vbox.setPadding(new Insets(10, 0, 0, 10));
      vbox.getChildren().addAll(label, table, bt1, bt2);

      ((Group) scene.getRoot()).getChildren().addAll(vbox);

      stage.setScene(scene);
      stage.show();

      /*************************************************/
      bt1.setOnAction(value -> {
         data.add(new Person("xi.", "2023-01-01", "add Local "));

      });

      /*************************************************/
      bt2.setOnAction(value -> {

         BufferedReader br;
         String strLine;

         data.add(new Person("Do.", "start reading", "readHoli-1"));

         try {
            URL url = new URL("https://www.spiketime.de/feiertagapi/feiertage/csv/2023/2023");
            URLConnection urlCon = url.openConnection();   //feastUrl.openConnection();
            InputStreamReader isr = new InputStreamReader(urlCon.getInputStream());     // ERROR ---- DOES NOT WORK after this statement

            data.add(new Person("Do.", "after creating streamreader", "TEST-2"));

            br = new BufferedReader(isr);
            while ((strLine = br.readLine()) != null) {
               if (!strLine.contains("Bayern"))
                  continue;
               System.out.println(strLine);
               /* split string/check and add to holidays: here only print */
            }
            br.close();
            isr.close();


         } catch (Exception ex) {
            ex.printStackTrace();
         }
         data.add(new Person("Do.", "end reading", "readHoli-2"));
      });
   }

   public static class Person {
      private final StringProperty firstName;
      private final StringProperty lastName;
      private final StringProperty email;

      private Person(String fName, String lName, String email) {
         this.firstName = new SimpleStringProperty(fName);
         this.lastName = new SimpleStringProperty(lName);
         this.email = new SimpleStringProperty(email);
      }

      public String getFirstName() {
         return firstName.get();
      }

      public void setFirstName(String fName) {
         firstName.set(fName);
      }

      public StringProperty firstNameProperty() {
         return firstName;
      }

      public String getLastName() {
         return lastName.get();
      }

      public void setLastName(String fName) {
         lastName.set(fName);
      }

      public StringProperty lastNameProperty() {
         return lastName;
      }

      public String getEmail() {
         return email.get();
      }

      public void setEmail(String fName) {
         email.set(fName);
      }

      public StringProperty emailProperty() {
         return email;
      }
   }
}

Solution

  • All Bindings in javafx are implemented to do their best to not cause memory leaks. They do so (in the internal class BindingHelperObserver) by

    • keep a weak reference to the binding: this allows being claimed by garbage collection as soon as there is no strong reference path to the object
    • remove any listeners to their dependencies if the binding is gc'ed

    The former is the reason for the problem described in the question because the cell factory creates the binding as a local reference which is collectable as soon as the method returns. Requesting an InputStream from a UrlConnection seems to trigger a garbage run, thus collecting the weakly referenced bindings of all rows.

    To make them survive the life-time of its owner, the owner has to keep a strong reference to it, f.i. in the context of a cell factory:

    • subclass the row
    • let it have a field referencing the binding
    • install the binding/listener in its constructor

    Example:

    table.setRowFactory(tableView -> {
        final TableRow<Person> row = new TableRow<>() {
    
            private BooleanBinding selectedRow;
    
            {
                selectedRow = Bindings.createBooleanBinding(
                        () -> selectedRowIndexes.contains(getIndex()), 
                        indexProperty(),
                        selectedRowIndexes);
                selectedRow.addListener((observable, oldValue, newValue) -> {
                    pseudoClassStateChanged(selectedRowPseudoClass, newValue);
                });
    
            }
    
        };
        return row;
    });