Question

I am currently trying to write my first JavaFX app that writes messages decoded from a socket into a JavaFX Table. The table needs to be dynamic and if a message has data that currently doesn't exist in the table a new column should be added. Some of the fields are nested in a wrapper class and I was trying to represent these nesting using a Nested Column as seen here: http://docs.oracle.com/javafx/2/ui_controls/table-view.htm#CJABHBEH

My issue is I cannot seem to get the correct data to bind to the nested columns. When I try binding to PropertyValueFactory("one") for my CustomType I am getting Optionals from MessageType1. I have tried a couple of different things as you will see in my sample below:

import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.stage.Stage;

import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;


public class SampleTable extends Application {
  private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
  private final ObservableList<Message> msgList = FXCollections.observableArrayList();

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

  @Override
  public void start(Stage primaryStage) throws Exception {
    final TableView tableView = new TableView<>(msgList);
    tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);


    final TableColumn<Message, MessageType> msgTypeCol = new TableColumn<Message, MessageType>();
    msgTypeCol.setText("Message Type");
    msgTypeCol.setCellValueFactory(new PropertyValueFactory<Message, MessageType>("messageType"));

    final TableColumn<MessageType1, Optional<Integer>> intCol1 = new TableColumn<MessageType1, Optional<Integer>>();
    intCol1.setText("Int1");
    intCol1.setCellValueFactory(new PropertyValueFactory<MessageType1, Optional<Integer>>("one"));

    final TableColumn<MessageType1, Optional<Integer>> intCol2 = new TableColumn<MessageType1, Optional<Integer>>();
    intCol2.setText("Int2");
    intCol2.setCellValueFactory(new PropertyValueFactory<MessageType1, Optional<Integer>>("two"));

    final TableColumn<MessageType1, Optional<Integer>> intCol3 = new TableColumn<MessageType1, Optional<Integer>>();
    intCol3.setText("Int3");
    intCol3.setCellValueFactory(new PropertyValueFactory<MessageType1, Optional<Integer>>("three"));

    final TableColumn<MessageType2, List<String>> strCol = new TableColumn<MessageType2, List<String>>();
    strCol.setText("String List");
    strCol.setCellValueFactory(new PropertyValueFactory<MessageType2, List<String>>("list"));

   //final TableColumn<MessageType3, Optional<CustomType>> customTypeCol = new TableColumn<MessageType3, Optional<CustomType>>();
    final TableColumn customTypeCol = new TableColumn<>();
    customTypeCol.setText("Custom Type");

//    final TableColumn<CustomType, Boolean> customTypeCol1 = new TableColumn<CustomType, Boolean>();
//    customTypeCol1.setText("CT1");
//    customTypeCol1.setCellValueFactory(new PropertyValueFactory<CustomType, Boolean>("one"));
//    final TableColumn<CustomType, Boolean> customTypeCol2 = new TableColumn<CustomType, Boolean>();
//    customTypeCol2.setText("CT2");
//    customTypeCol2.setCellValueFactory(new PropertyValueFactory<CustomType, Boolean>("customTwo"));
//    final TableColumn<CustomType, Integer> customTypeCol3 = new TableColumn<CustomType, Integer>();
//    customTypeCol3.setText("CT3");
//    customTypeCol3.setCellValueFactory(new PropertyValueFactory<CustomType,Integer>("customThree"));
  final TableColumn<MessageType3, Boolean> customTypeCol1 = new TableColumn<MessageType3, Boolean>();
  customTypeCol1.setText("CT1");
  customTypeCol1.setCellValueFactory(new PropertyValueFactory<MessageType3, Boolean>("one"));
  final TableColumn<MessageType3, Boolean> customTypeCol2 = new TableColumn<MessageType3, Boolean>();
  customTypeCol2.setText("CT2");
  customTypeCol2.setCellValueFactory(new PropertyValueFactory<MessageType3, Boolean>("customTwo"));
  final TableColumn<MessageType3, Integer> customTypeCol3 = new TableColumn<MessageType3, Integer>();
  customTypeCol3.setText("CT3");
  customTypeCol3.setCellValueFactory(new PropertyValueFactory<MessageType3,Integer>("customThree"));

    customTypeCol.getColumns().addAll(customTypeCol1, customTypeCol2, customTypeCol3);


//    customTypeCol.setCellValueFactory(new PropertyValueFactory<MessageType3, Optional<CustomType>>("customType"));
//    customTypeCol.setCellFactory(
//        new Callback<TableColumn<MessageType3, Optional<CustomType>>, TableCell<MessageType3, Optional<CustomType>>>() {
//      @Override
//      public TableCell<MessageType3, Optional<CustomType>> call(
//          final TableColumn<MessageType3, Optional<CustomType>> param) {
//        return new TableCell<MessageType3, Optional<CustomType>>() {
//          @Override
//          protected void updateItem(Optional<CustomType> customType, boolean empty) {
//            super.updateItem(customType, empty);
//
//            if(!empty) {
//              //Tried this but didn't work either
////              final TableColumn<CustomType, Boolean> customTypeCol1 = new TableColumn<CustomType, Boolean>();
////              customTypeCol1.setText("CT1");
////              customTypeCol1.setCellValueFactory(new PropertyValueFactory<CustomType, Boolean>("one"));
////              final TableColumn<CustomType, Boolean> customTypeCol2 = new TableColumn<CustomType, Boolean>();
////              customTypeCol2.setText("CT2");
////              customTypeCol2.setCellValueFactory(new PropertyValueFactory<CustomType, Boolean>("customTwo"));
////              final TableColumn<CustomType, Integer> customTypeCol3 = new TableColumn<CustomType, Integer>();
////              customTypeCol3.setText("CT3");
////              customTypeCol3.setCellValueFactory(new PropertyValueFactory<CustomType,Integer>("customThree"));
//              //
//              final TableColumn<MessageType3, Boolean> customTypeCol1 = new TableColumn<MessageType3, Boolean>();
//              customTypeCol1.setText("CT1");
//              customTypeCol1.setCellValueFactory(new PropertyValueFactory<MessageType3, Boolean>("one"));
//              final TableColumn<MessageType3, Boolean> customTypeCol2 = new TableColumn<MessageType3, Boolean>();
//              customTypeCol2.setText("CT2");
//              customTypeCol2.setCellValueFactory(new PropertyValueFactory<MessageType3, Boolean>("customTwo"));
//              final TableColumn<MessageType3, Integer> customTypeCol3 = new TableColumn<MessageType3, Integer>();
//              customTypeCol3.setText("CT3");
//              customTypeCol3.setCellValueFactory(new PropertyValueFactory<MessageType3,Integer>("customThree"));
//
//              param.getColumns().addAll(customTypeCol1, customTypeCol2, customTypeCol3);
//            }
//          }
//        };
//      }
//    });
    tableView.getColumns().addAll(msgTypeCol, intCol1, intCol2, intCol3, strCol, customTypeCol);

    final Group root = new Group();
    primaryStage.setScene(new Scene(root));
    root.getChildren().add(tableView);

    //Simulate incoming DUMMY Messages
    executor.scheduleAtFixedRate(new Runnable() {
      private int i = 0;

      @Override
      public void run() {
        switch(i) {
        case 0:
          msgList.add(new MessageType1());
          i++;
          break;
        case 1:
          msgList.add(new MessageType2());
          i++;
          break;
          //System.exit(0);
        case 2:
          msgList.add(new MessageType3());
          i = 0;
          break;
        }
      }
    }, 0, 1, TimeUnit.SECONDS);

    primaryStage.show();
  }

  public enum MessageType { TYPE1, TYPE2, TYPE3 };
  public interface Message {
    MessageType getMessageType();
  }
  public static final class CustomType {
    private final boolean one = true;
    private final boolean customTwo = true;
    private final int customThree = 3;

    public Boolean getOne() { return one; }
    public Boolean getCustomTwo() { return customTwo; }
    public Integer getCustomThree() { return customThree; }
  }

  public static final class MessageType1 implements Message {
    private final List<Optional<Integer>> intList =
        ImmutableList.<Optional<Integer>>of(Optional.of(1),
            Optional.of(2),
            Optional.<Integer>absent());

    @Override
    public MessageType getMessageType() { return MessageType.TYPE1; }
    public Optional<Integer> getOne() { return intList.get(0); }
    public Optional<Integer> getTwo() { return intList.get(1); }
    public Optional<Integer> getThree() { return intList.get(2); }
  }

  public static final class MessageType2 implements Message {
    List<String> stringList = ImmutableList.of("one", "two", "three");

    @Override
    public MessageType getMessageType() { return MessageType.TYPE2; }
    public List<String> getList() { return stringList; }
  }

  public static final class MessageType3 implements Message {
    private final Optional<CustomType> obj = Optional.of(new CustomType());

    @Override
    public MessageType getMessageType() { return MessageType.TYPE3; }
    public Optional<CustomType> getCustomType() { return obj; }
  }
}

I guess the question boils down to how can I bind my CustomType data to the nested columns? Everything I tried seems to have failed. My gut feeling tells me it has something to do with dropping the generics on my TableColumns before inserting them but I don't see another option. Is there a better recommendation on how to write a table with intermixed data like this example? Maybe I'm attacking this problem in the completely wrong way?

Thanks!

Note: I wrote this code to attempt to show what I was trying to accomplish it is by no means "production" level.

Was it helpful?

Solution

The problem has nothing related to nested columns. You are using the TableView as raw (not parameterized) type. It is not recommended to use raw types in Java, since the compiler cannot check and ensure type conversations and other stuff related to the parameter types used in. IMO when the raw type used, and if there is a conversation error at runtime the JavaFX ignores it and operates as no-op by returning null (the concrete implementation can be seen from source code). More specifically, every

new PropertyValueFactory<MessageType3, Boolean>("field")

seems to be looking for the "getField()" or "fieldProperty()" getters by reflection of the current added item (MessageType 1,2,3) regardless the column's parameterized type. This is the reason for "PropertyValueFactory("one") for my CustomType I am getting Optionals from MessageType1" since both classes has the same named getter.

The workaround could be:
1) change the getter names to be unique between classes shown in table.
2) use instanceof. The code

customTypeCol1.setCellValueFactory(
                 new PropertyValueFactory<MessageType3, Boolean>("one"));

is more or less equivalent to:

customTypeCol1.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<MessageType3, Boolean>, ObservableValue<Boolean>>() {
    @Override
    public ObservableValue<Boolean> call(TableColumn.CellDataFeatures<MessageType3, Boolean> p) {
        return new SimpleBooleanProperty(p.getValue().getOne());
    }
});

and here the column parameterized type is not ignored, so running this results ClassCastException at first attempt of adding MessageType1 item to the table, since here p.getValue() will return MessageType1. Using instanceof will be a workaround,

customTypeCol1.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<MessageType3, Boolean>, ObservableValue<Boolean>>() {
    @Override
    public ObservableValue<Boolean> call(TableColumn.CellDataFeatures<MessageType3, Boolean> p) {
        if (p.getValue() instanceof MessageType3) {
            return new SimpleBooleanProperty(p.getValue().getOne());
        } else {
            return null;
        }
    }
});


After saying that all, the real solution that I prefer will be defining a new datamodel class consisting of fields from the parts of MessageTypes 1, 2 and 3. Construct this data model and give as source to the parameterized TableView. This will help for other coworkers to understand the code easily, do improvements and maintenance.

Good reference for What is a raw type and why shouldn't we use it?

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top