Reputation: 197
I managed to make working ListView
s by overriding updateItem(T item,boolean empty)
but now I need to show in a ListView
a node that is bidirectionally linked with my domain object data. Using the existing system where you request edit mode is really difficult to work with (e.g. entering edit mode removes focus from TextField
, so you can't enter focus by selecting the TextField
, it forces me to define logic in the cell or graphic). I'm trying to make this work with bindings, but I need to know when I should add the bindings, remove them, show the graphic or not.
The following code could give you a better idea of what I want, but it's definitely not good or working. I've tried to get the desired result in a few ways, the only good result was that it does link properly with my domain and its logic. The bad news is that whenever I add, remove or scroll that list the results are unexpected, but definitely not working properly.
MyCell extends ListCell<MyModel> {
private SomethingThatExtendsNode view = new SomethingThatExtendsNode();
public MyCell(){
BooleanBinding remove = itemProperty().isNull()or(emptyProperty());
remove.addListener((observable, oldValue, newValue) -> {setGraphic(null)});
BooleanBinding exists = itemProperty().isNotNull().and(emptyProperty().not);
exists.addListener((observable, oldValue, newValue) -> {
if (newValue){
setGraphic(view);
MonadicObservableValue<MyModel> model = EasyBind.monadic(itemProperty());
view.getTextField1.textProperty().bindBidirectional(model.selectProperty(MyModel::text1));
//etc..
}
});
}
}
Also, is this design the right way to go? I want my views just to have their properties bound to the domain. Business logic is mostly in the domain through more bindings.
Edit: tried to make my setup as in the answer but it still acts erratically, perhaps problem must be somewhere else. Git of MCVE here https://github.com/PopescuStefanRadu/JavaFX-listView-MCVE
Also this error seems to pop up every now and then when providing input in the textfields
Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
at com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:361)
at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
at javafx.scene.control.TextInputControl$TextProperty.fireValueChangedEvent(TextInputControl.java:1389)
at javafx.scene.control.TextInputControl$TextProperty.markInvalid(TextInputControl.java:1393)
at javafx.scene.control.TextInputControl$TextProperty.controlContentHasChanged(TextInputControl.java:1332)
at javafx.scene.control.TextInputControl$TextProperty.access$1600(TextInputControl.java:1300)
at javafx.scene.control.TextInputControl.lambda$new$162(TextInputControl.java:139)
at com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:137)
at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
at javafx.scene.control.TextField$TextFieldContent.insert(TextField.java:87)
at javafx.scene.control.TextInputControl.replaceText(TextInputControl.java:1204)
at javafx.scene.control.TextInputControl.updateContent(TextInputControl.java:556)
at javafx.scene.control.TextInputControl.replaceText(TextInputControl.java:548)
at com.sun.javafx.scene.control.skin.TextFieldSkin.replaceText(TextFieldSkin.java:576)
at com.sun.javafx.scene.control.behavior.TextFieldBehavior.replaceText(TextFieldBehavior.java:202)
at com.sun.javafx.scene.control.behavior.TextInputControlBehavior.defaultKeyTyped(TextInputControlBehavior.java:238)
at com.sun.javafx.scene.control.behavior.TextInputControlBehavior.callAction(TextInputControlBehavior.java:139)
at com.sun.javafx.scene.control.behavior.BehaviorBase.callActionForEvent(BehaviorBase.java:218)
at com.sun.javafx.scene.control.behavior.TextInputControlBehavior.callActionForEvent(TextInputControlBehavior.java:127)
at com.sun.javafx.scene.control.behavior.BehaviorBase.lambda$new$74(BehaviorBase.java:135)
at com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:218)
at com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80)
at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
at com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
at javafx.event.Event.fireEvent(Event.java:198)
at javafx.scene.Scene$KeyHandler.process(Scene.java:3964)
at javafx.scene.Scene$KeyHandler.access$1800(Scene.java:3910)
at javafx.scene.Scene.impl_processKeyEvent(Scene.java:2040)
at javafx.scene.Scene$ScenePeerListener.keyEvent(Scene.java:2501)
at com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:217)
at com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:149)
at java.security.AccessController.doPrivileged(Native Method)
at com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleKeyEvent$353(GlassViewEventHandler.java:248)
at com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:389)
at com.sun.javafx.tk.quantum.GlassViewEventHandler.handleKeyEvent(GlassViewEventHandler.java:247)
at com.sun.glass.ui.View.handleKeyEvent(View.java:546)
at com.sun.glass.ui.View.notifyKey(View.java:966)
at com.sun.glass.ui.gtk.GtkApplication._runLoop(Native Method)
at com.sun.glass.ui.gtk.GtkApplication.lambda$null$49(GtkApplication.java:139)
at java.lang.Thread.run(Thread.java:745)
Upvotes: 1
Views: 1504
Reputation: 197
Problem solved.
ListView
is built so that the graphic reflects the underlying data so there's no need to bind it to the model. Whenever a change in the model happens the UI will update.
As such I had to make sure that the changes in the data of my model made the ObservableList
aware of the change. For that I used an extractor:
ObservableList<Model> myModels = FXCollections.observableArrayList(model -> new Observable[]{model.nameProperty()});
Next I had to make sure that the changes in the UI cause a change in the model as well, so I used a ChangeListener
inside the cellFactory. This can also be placed in the constructor of the class extending ListCell. I've tried to use bindings but they don't seem to work, not sure why, will look into it later. I would like to use something less verbose than listeners on every single property, but nonetheless it works.
Here's code:
@Override
public void start(Stage primaryStage) throws Exception {
ObservableList<Model> myModels = FXCollections.observableArrayList(model -> new Observable[]{model.nameProperty()});
ListView<Model> modelListView = new ListView<>();
modelListView.setCellFactory(c -> {
MyListCell cell = new MyListCell();
//TODO is there a way to replace this listener with something less verbose?
cell.getTextField().textProperty().addListener((observable, oldValue, newValue) -> {
if (newValue!=null){
cell.getItem().setName(newValue);
}
});
return cell;
});
modelListView.setItems(myModels);
myModels.add(new Model());
Button addButton = new Button("Add");
addButton.setOnAction(event -> myModels.add(new Model()));
StackPane root = new StackPane();
root.getChildren().addAll(new HBox(20, modelListView, addButton));
primaryStage.setTitle("ListView with bidirectional binding");
primaryStage.setScene(new Scene(root, 600, 500));
primaryStage.show();
}
private class Model {
private StringProperty name = new SimpleStringProperty(this, "name", "");
private ReadOnlyStringWrapper computedName = new ReadOnlyStringWrapper(this, "computedName");
public Model() {
computedName.bind(Bindings.createStringBinding(() -> name.get().toUpperCase(), name));
}
public String getName() {
return name.get();
}
public StringProperty nameProperty() {
return name;
}
public void setName(String name) {
this.name.set(name);
}
public String getComputedName() {
return computedName.get();
}
public ReadOnlyStringWrapper computedNameProperty() {
return computedName;
}
}
private class MyListCell extends ListCell<Model> {
private HBox content = new HBox(10);
private TextField textField = new TextField();
private Label label = new Label();
public MyListCell() {
content.getChildren().addAll(textField, label);
}
@Override
protected void updateItem(Model item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setGraphic(null);
} else {
setGraphic(content);
textField.textProperty().set(item.getName());
label.textProperty().set(item.getComputedName());
}
}
public TextField getTextField() {
return textField;
}
}
Upvotes: 0
Reputation: 2210
You should never add any bindings or listeners in cell constructor, because list cell once created is reused by list view every time new item has to be shown. When you bind property in constructor, you wouldn't be able to remove it from old item, when list view change it. That's why you have strange behaviour, when you scroll or change list.
Bidirectional bindings and listeners must be removed before updating item in custom list cell. Check following code:
@Override
public void start(Stage primaryStage) {
ObservableList<Model> myModels = FXCollections.observableArrayList(
IntStream.range(0, 100).mapToObj(i -> new Model("MyModel " + i)).collect(Collectors.toList())
);
ListView<Model> modelListView = new ListView<>();
modelListView.setCellFactory(c -> new MyListCell());
modelListView.setItems(myModels);
StackPane root = new StackPane();
root.getChildren().addAll(modelListView);
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("Custom list cell");
primaryStage.setScene(scene);
primaryStage.show();
}
private class MyListCell extends ListCell<Model> {
private HBox content = new HBox();
private TextField textField = new TextField();
private Button actionButton = new Button("Action");
private final ChangeListener<String> textListener = (ObservableValue<? extends String> observable, String oldValue, String newValue) -> {
checkIfDisableButton(newValue);
};
public MyListCell() {
content.getChildren().addAll(textField, actionButton);
content.setSpacing(10);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
setGraphic(content);
}
@Override
protected void updateItem(Model item, boolean empty) {
if (getItem() != null) { // get old item
//remove all bidirectional bindings and listeners
textField.textProperty().unbindBidirectional(getItem().nameProperty());
getItem().nameProperty().removeListener(textListener);
}
super.updateItem(item, empty);
//new item
if (item == null || empty) {
setText(null);
setGraphic(null);
} else {
setGraphic(content);
checkIfDisableButton(item.getName());
item.nameProperty().addListener(textListener);
textField.textProperty().bindBidirectional(item.nameProperty());
actionButton.setOnAction(e -> {
System.out.println(item.getName());
});
}
}
private void checkIfDisableButton(String value) {
actionButton.setDisable("MyModel 2".equals(value));
}
}
private class Model {
StringProperty name = new SimpleStringProperty();
public Model(String name) {
this.name.set(name);
}
public String getName() {
return name.get();
}
public void setName(String name) {
this.name.set(name);
}
public StringProperty nameProperty() {
return this.name;
}
}
Upvotes: 0