0

Here's the situation: I have an ObservableSet (because the ID field for my data must be unique) that has a listener. That listener updates an ObservableList. That ObservableList is, in turn, listened to by a TableView. (According to the comments, this is all necessary because ObservableSet can't be used to back a TableView in JavaFX.)

What we're finding, however, is that doing multiple add operations to the Set don't trigger the refresh of the TableView.

This works fine for:

  • Initial list population
  • Duplicate set entries (the listener isn't triggered, that's the intended behavior)

However, editing the value, the TableView will only trigger if there's a single add statement (in the example below, commenting out markStructure.add(new MarkStructureItem(2, 15)); makes it work fine, but then you only have a single value). If there's more than one, the TableView doesn't refresh.

This can be worked around by adding a manual refresh at the end of the Listener class (tblTable.refresh()). This is actually how it's functioned up until now, but development now requires the removal of that.

Any idea what's going on? Why is the listener on the TableView not getting triggered for subsequent adds? The data IS being put into the Set and List, that much has been determined; it's just not triggering a refresh.

Code example (not production, but a POC I put together to test and get answers):

public class TestController implements Initializable {
ObservableSet<MarkStructureItem> markStructure = FXCollections.observableSet();
ObservableList<MarkStructureItem> listMarkStructure = FXCollections.observableArrayList();

@FXML
private Pane root;

@FXML
private TableView<MarkStructureItem> tblTable;

@FXML
private TableColumn<MarkStructureItem, Integer> col1;

@FXML
private TableColumn<MarkStructureItem, Integer> col2;

@FXML
private Button btnButton;

private void resetAll() {
    markStructure.clear();
    markStructure.add(new MarkStructureItem(1, 25));
    markStructure.add(new MarkStructureItem(2, 15));
}

@FXML
private void handleButtonAction(ActionEvent event) throws IOException {
    Object source = event.getSource();
    Button btnSource = (Button) source;
    Stage stage = (Stage) root.getScene().getWindow();

    switch (btnSource.getId()) {
        case "btnButton": {
            resetAll();
            break;
        }
    }
}

class MarksUpdater<MarkStructureItem extends configurationeditor.MarkStructureItem> implements SetChangeListener {
    @Override
    public void onChanged(Change change) {
        if (change.wasRemoved()) {
            listMarkStructure.remove(change.getElementRemoved());
        } else if (change.wasAdded()) {
            MarkStructureItem newMarks = (MarkStructureItem) change.getElementAdded();
            listMarkStructure.add(newMarks);
        }
    }
}

@Override
public void initialize(URL url, ResourceBundle rb) {
    markStructure.addListener(new MarksUpdater<MarkStructureItem>());

    col1.setCellValueFactory(
            new PropertyValueFactory<MarkStructureItem, Integer>("id")
    );
    col2.setCellValueFactory(
            new PropertyValueFactory<MarkStructureItem, Integer>("marks")
    );
    col2.setCellFactory(TextFieldTableCell.forTableColumn(new IntegerStringConverter()));
    col2.setOnEditCommit(
            new EventHandler<CellEditEvent<MarkStructureItem, Integer>>() {
                @Override
                public void handle(CellEditEvent<MarkStructureItem, Integer> t) {
                    ((MarkStructureItem) t.getTableView().getItems().get(
                            t.getTablePosition().getRow())
                    ).setMarks(t.getNewValue());
                }
            }
    );
    tblTable.setItems(listMarkStructure);

    resetAll();
}
}

markStructure class:

public class MarkStructureItem {
final SimpleIntegerProperty marks;
final SimpleIntegerProperty id;

@Override
public int hashCode() {
    return this.getId().hashCode();
}

@Override
public boolean equals(Object obj) {
    if (obj.getClass() == this.getClass()) {
        MarkStructureItem thisObj = (MarkStructureItem) obj;
        return thisObj.getId() == this.getId();
    }
    return false;
}

public MarkStructureItem(Integer finishPosition, Integer marks) {
    this.marks = new SimpleIntegerProperty(marks);
    this.id = new SimpleIntegerProperty(finishPosition);
}

public Integer getId() {
    return id.get();
}

public void setMarks(Integer value) {
    marks.set(value);
}

public Integer getMarks() {
    return marks.get();
}

public void setId(Integer value) {
    id.set(value);
}
}
Paka
  • 185
  • 2
  • 8
  • Can you post the `MarkStructureItem` class? Is the set change listener's `onChanged` method getting called for both additions to the set? (Aside: your use of generics is seriously flawed, though that is completely separate from the actual question.) – James_D Oct 13 '16 at 23:11
  • Added it to the original question. The custom onChanged is being called for each addition. It's the refresh trigger that doesn't seem to be called from within that onChanged. I'd be interested in direction on the proper use of generics (PM or whatnot)... this is inherited code and any way we can make it better is appreciated! THANKS! – Paka Oct 13 '16 at 23:34
  • 2
    `return thisObj.getId() == this.getId();` is likely causing problems, as getId() returns an object. Since you’re using an IntegerProperty internally, the type of the id property really should be int, not Integer, which would be the best way to resolve the problem. – VGR Oct 14 '16 at 00:09
  • 2
    And in addition, it's probably better to follow the [JavaFX Properties pattern](http://www.oracle.com/pls/topic/lookup?ctx=javase80&id=JFXBD107), i.e. add `idProperty()` and `marksProperty()` methods to your `MarkStructureItem` class. Having said that, I tested your code verbatim, and it worked exactly as expected... so maybe there is something else somewhere that is not correct. Can you expand to a [MCVE]? Your handler method is weird too, why switch on the id of the button? You should (almost always) just have one control with that method as the handler anyway. – James_D Oct 14 '16 at 00:17
  • 2
    For the generics, you should have `public class MarksUpdater implements SetChangeListener` with `public void onChanged(Change extends MarkStructureItem> change) { ... }`. Then just instantiate it as `new MarksUpdater()`, and you can remove the cast on `change.getElementAdded()`. – James_D Oct 14 '16 at 01:06
  • Possible duplicate of [Autoupdating rows in TableView from model](http://stackoverflow.com/questions/10912690/autoupdating-rows-in-tableview-from-model) – Sergey Grinev Oct 14 '16 at 10:24

1 Answers1

2

Issue 1:

Your equals implementation is not null-safe and you're comparing the Integers using reference equality (==) instead of equals. This will probably work as long as you're inside the range of cached values and use autoboxing, but cease to work outside that range (this range is only guarantied to be -128 to 127, see Integer.valueOf(int)).
Since you're using the Object in a Set, I recommend making the id unmodifiable anyways. Otherwise you need to remove the value from the set before modifying it and adding it back to the Set (the hash code changes), which would mess up the order in the list unless you temporarily prevent the SetChangeListener from updating the list.
Also using a primitive type will prevent issues with ==.

Issue 2:

For any changes in the marks property to be visible in the TableView, you need to provide a marksProperty() method returning the property itself. This is necessary for the TableView to listen to changes of the property.

public class MarkStructureItem {

    final SimpleIntegerProperty marks;
    final int id;

    @Override
    public int hashCode() {
        return id;
    }

    @Override
    public boolean equals(Object obj) {
        return obj != null
                  && obj.getClass() == this.getClass()
                  && this.id == ((MarkStructureItem) obj).id; // primitive type can be compared using ==
                  // && this.getId().equals(((MarkStructureItem) obj).getId()); // alternative for non-primitive types
    }

    public MarkStructureItem(int finishPosition, Integer marks) {
        this.marks = new SimpleIntegerProperty(marks);
        this.id = finishPosition;
    }

    public int getId() {
        return id;
    }

    public IntegerProperty marksProperty() {
        return marks;
    }
fabian
  • 80,457
  • 12
  • 86
  • 114
  • So, I've finally figured it out: It wasn't exactly anything that folks suggested, but through all of your feedback, I managed to whittle the problem down. – Paka Oct 14 '16 at 18:19
  • (Ugh, premature "Enter" key.) Okay, anyway, the problem is with the custom "equals" method that was being used to test uniqueness for the objects in the set. It was ostensibly implemented because ID was unique, but Marks wasn't. This worked as intended: Items with duplicate IDs were rejected without any business logic outside the custom "equals" method. However, this also caused the problem as JavaFX also uses the "equals" method: Changing Marks without changing ID, JavaFX didn't see that the row was "different" (even though it was), so it wasn't triggering a refresh. – Paka Oct 14 '16 at 18:22