How to define which property ListView should use to render - java

I have a java bean object list which I would like to display in ListView control. By default ListView uses toString method.
How can I define which property to use for rendering in ListView?
I want to achieve same functionality as in TableView can be achieved by PropertyValueFactory in this code:
#FXML
private TableView<Person> mainTableView;
//...
TableColumn<Person,String> personColumn = new TableColumn<>("Name");List
personColumn.setCellValueFactory(new PropertyValueFactory("name"));
mainTableView.getColumns().add(personColumn);
Edit
It looks like there is no easy(out of the box) solution. Based on code from James_D I created generic class to deal with the problem. It wraps PropertyValueFactory - note that PropertyValueFactory firstly looks for method [NAME]Property() trying to get observable, only when it is not found it tries to access standard bean properties.
public class PropertyValueFactoryWrapperCellFactory<T> implements Callback<ListView<T>, ListCell<T>> {
private final PropertyValueFactory<T, String> pvf;
public PropertyValueFactoryWrapperCellFactory(String propertyName) {
super();
pvf = new PropertyValueFactory(propertyName);
}
#Override
public ListCell<T> call(ListView<T> param) {
return new ListCell<T>() {
#Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
textProperty().unbind();
if (item == null) {
return;
}
TableColumn.CellDataFeatures<T, String> cdf = new TableColumn.CellDataFeatures<>(null, null, item);
textProperty().bind(pvf.call(cdf));
}
};
}
}
Usage:
#FXML
private ListView<Person> mainListView;
//...
mainListView.setCellFactory(new PropertyValueFactoryWrapperCellFactory("name"));

Use a cell factory.
If the property is immutable, it's pretty straightforward:
ListView<MyDataType> listView = new ListView<>();
listView.setCellFactory(new Callback<ListView<MyDataType>, ListCell<MyDataType>>() {
#Override
public ListCell<MyDataType> call(ListView<MyDataType> lv) {
return new ListCell<MyDataType>() {
#Override
public void updateItem(MyDataType item, boolean empty) {
super.updateItem(item, empty);
if (item == null) {
setText(null);
} else {
// assume MyDataType.getSomeProperty() returns a string
setText(item.getSomeProperty());
}
}
};
}
});
If the property can change its value and the list view needs to update dynamically in response to these changes, you need to bind the textProperty of the list cell:
listView.setCellFactory(new Callback<ListView<MyDataType>, ListCell<MyDataType>>() {
#Override
public ListCell<MyDataType> call(ListView<MyDataType> lv) {
return new ListCell<MyDataType>() {
#Override
public void updateItem(MyDataType item, boolean empty) {
super.updateItem(item, empty);
textProperty().unbind();
if (item == null) {
setText(null);
} else {
// assumes MyDataType.someProperty() returns a StringProperty:
textProperty.bind(item.someProperty());
}
}
};
}
});

Binding String Property
This code allows you to choose what JavaFX property is displayed in a ListView. I use an anonymous class inside a lambda to add some clarity to what is happening. In this example:
Display - The underlying object behind the ListView
titleProperty - The JavaFX property to be displayed
listView.setCellFactory(
listview -> {
return new ListCell<Display>() {
#Override
public void updateItem(Display item, boolean empty) {
super.updateItem(item, empty);
textProperty().unbind();
if(item != null)
textProperty().bind(item.titleProperty());
else
setText(null);
}
};
}
);
Explanation
With this code, we are basically making a custom ListCell. When it is updated and the item it is display is null, we clear the display text. Otherwise, we set the text to be whatever the title property of our item is.
TL;DR :: Modified James_D's Solution
I based this on James_D's second example. I wanted to bind a SimpleStringProperty to be the displayed text in a ListView. James_D's solution worked great but didn't update when I deleted an object from the ObservableList in the ListView, so I modified it. I also thought having a cleaner lambda example would be good.

Related

JavaFX: ComboBox using Object property

Lets say I have a class:
public class Dummy {
private String name;
private String someOtherProperty;
public String getName() {
return name;
}
}
I have an ArrayList of this class ArrayList<Dummy> dummyList;
Can I create a JavaFX ComboBox with the Object name property as selection options without creating a new ArrayList<String> with the object names?
Pseudocode:
ObservableList<Dummy> dummyO = FXCollections.observableArrayList(dummyList);
final ComboBox combo = new ComboBox(dummyO); // -> here dummyO.name?
(Optional) Ideally, while the name should be displayed, when an option has been selected, the combo.getValue() should return me the reference of the selected Dummy and not only the name. Is that possible?
You can use a custom cellFactory to display the items in a way that suits your needs:
ComboBox<Dummy> comboBox = ...
Callback<ListView<Dummy>, ListCell<Dummy>> factory = lv -> new ListCell<Dummy>() {
#Override
protected void updateItem(Dummy item, boolean empty) {
super.updateItem(item, empty);
setText(empty ? "" : item.getName());
}
};
comboBox.setCellFactory(factory);
comboBox.setButtonCell(factory.call(null));
I'm assuming the ComboBox you're referring to is this: http://docs.oracle.com/javase/8/javafx/api/javafx/scene/control/ComboBoxBase.html. As getValue() is public, you can do:
public class MyComboBox<T> extends ComboBox<T> {
private final Dummy dummy;
public MyComboBox(Dummy dummy) {
this.dummy = dummy;
}
public T getValue() {
return dummy.getName();
}
}

JavaFX listview duplication of rendered items when added to observableList

I have a ListView with observableList attached to it,
#FXML
private ListView<Weapon> listViewWeapons;
....
//initialize
listViewWeapons.setCellFactory(lv -> new CustomWeaponDetailListCell<>());
listViewWeapons.setItems(CsgoRr.getModel().getWeaponCache());
Custom cell:
public class CustomWeaponDetailListCell<T extends Weapon> extends ListCell<T> {
private final StringBuilder sb = new StringBuilder();
#Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
} else {
sb.append(item.getName()).append(" Detail:")
.append((String) CsgoRr.objectToJsonString(item.getRecoilPattern()));
setText(sb.toString());
}
}
}
Function for creating new Weapon and adding it to a database and a list:
private static int newWeaponNameIncrement = 1;
#FXML
private void newWeaponOnAction() {
try {
System.out.println("DEBUG WEAPON NAME TRYING TO BE CREATED IS :" + "newWeapon" + newWeaponNameIncrement);
Weapon newWeapon = Weapon.createWeapon("newWeapon" + newWeaponNameIncrement,
new RecoilPattern());
newWeapon.setId(DbUtil.storeWeapon(newWeapon));
CsgoRr.getModel().getWeaponCache().add(newWeapon);
} catch (SQLException ex) {
if (ex.getErrorCode() == 23505) {//duplicate name
System.out.println("DEBUG :Duplicate name on add new weapon");
newWeaponNameIncrement++;
newWeaponOnAction();
}
Logger.getLogger(WeaponViewController.class.getName()).log(Level.SEVERE, null, ex);
} catch (AWTException ex) {
Logger.getLogger(WeaponViewController.class.getName()).log(Level.SEVERE, null, ex);
}
}
Everything works fine, data is added to a database, but the problem once again is with how I see listView behave.The problem is demonstrated here in a GIF (GIF got a bit too big, you have to click link, can't embed it here).
As you can see from image, the problem is that it duplicates items in a list at least visually, so change is refreshed but not in a proper way. Once I change view to something else and then go back, which calls constructor and initialize method, everything looks as it should. Anyone knows what the problem is with this?
Weird part is I have similar code in other controller which points to my previous SO question which I fixed no problem and works flawlessly, but when I do this almost same way I have different results here. JavaFX ListView adding item into observable list doesn't reflect change and it's not selectable
It's a different problem since before I didn't had any update feedback now I have feedback but not the correct one.
You reuse the same StringBuilder every time the item is swapped without clearing it. This means the resulting String will be the concatenation of all values for items that were stored in the Cell.
You need to use different StringBuilders every time or clear the StringBuilder:
public class CustomWeaponDetailListCell<T extends Weapon> extends ListCell<T> {
private final StringBuilder sb = new StringBuilder();
#Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
} else {
sb.append(item.getName()).append(" Detail:")
.append((String) CsgoRr.objectToJsonString(item.getRecoilPattern()));
setText(sb.toString());
// clear StringBuilder content
sb.delete(0, sb.length());
}
}
}

JavaFX ComboBox - How to get different Prompt Text and Selected Item Text?

I've searched a bit, but couldn't find an answer. The Combobox is editable. How can I show different text in the Combobox prompt text and in the list of Objects below? In the list I want the toString method of the Object to be used, but when I select it, I want only one attribute of the selected Object to be shown in the prompt text.
How can I do this? Is it possible to display the value of an object differently in the prompt text field and in the list below?
An example of the usage would be with songs. Let's say I search a song by title, then it shows me the song with the title, composer and instrument below. When I select the song, I only want the title to be shown in the prompt text (because I display the composer and instrument Information somewhere else).
Use a converter that uses the short version for the conversion and a custom cellFactory to create cells displaying the extended version:
static class Item {
private final String full, part;
public Item(String full, String part) {
this.full = full;
this.part = part;
}
public String getFull() {
return full;
}
public String getPart() {
return part;
}
}
#Override
public void start(Stage primaryStage) {
ComboBox<Item> comboBox = new ComboBox<>(FXCollections.observableArrayList(
new Item("AB", "A"),
new Item("CD", "C")
));
comboBox.setEditable(true);
// use short text in textfield
comboBox.setConverter(new StringConverter<Item>(){
#Override
public String toString(Item object) {
return object == null ? null : object.getPart();
}
#Override
public Item fromString(String string) {
return comboBox.getItems().stream().filter(i -> i.getPart().equals(string)).findAny().orElse(null);
}
});
comboBox.setCellFactory(lv -> new ListCell<Item>() {
#Override
protected void updateItem(Item item, boolean empty) {
super.updateItem(item, empty);
// use full text in list cell (list popup)
setText(item == null ? null : item.getFull());
}
});
Scene scene = new Scene(comboBox);
primaryStage.setScene(scene);
primaryStage.show();
}

TableView doesn't refresh

I've got a project written in JavaFX and I'm trying to get a refresh on a tableview without result.
I've googled around and tried some examples I've found but it still doesn't work.
I populate a tableview with information each row in this table can have new comments added to by double click on the row. The a new Tabpane is opened and the new comment can be added there. On close of this tabpane I'd like the one I clicked from to be refreshed.
I must be doing something wrong. I just don't know what.
In my StoreController
private void populateTableView(List<Store> stores) {
ObservableList<Store> data = FXCollections.observableArrayList(stores);
storeNumberColumn.setCellValueFactory(
new PropertyValueFactory<Store, String>("id"));
storePhoneColumn.setCellValueFactory(
new PropertyValueFactory<Store, String>("phoneNbr"));
chainColumn.setCellValueFactory(
new PropertyValueFactory<Store, String>("chainId"));
commentColumn.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Store, ImageView>, ObservableValue<String>>() {
#Override
public ObservableValue<String> call(TableColumn.CellDataFeatures<Store, ImageView> p) {
Integer numberOfComments = p.getValue().getCommentsCount();
ReadOnlyObjectWrapper wrapper = null;
if (numberOfComments == 0) {
wrapper = null;
} else if (numberOfComments == 1) {
wrapper = new ReadOnlyObjectWrapper(new ImageView(COMMENT_SINGLE_FLAG_SOURCE));
} else {
wrapper = new ReadOnlyObjectWrapper(new ImageView(COMMENT_DOUBLE_FLAG_SOURCE));
}
return wrapper;
}
});
storeTable.setItems(data);
sortTable(storeTable, missedColumn);
}
#FXML
public void handleTableAction(MouseEvent event) {
if (event.getClickCount() == 2) {
showNewCommentStage();
}
}
private void showNewCommentStage() {
initCommentController();
Store store
= storeTable.getSelectionModel().selectedItemProperty().getValue();
commentController.showNewStage(commentPane, store);
}
It seems like the call-function doesn't get called when the commentpane is closed.
CommentController
public void showNewStage(Pane pane, Store store) {
this.store = store;
initStage(pane);
windowHandler = new WindowHandler(stage);
effectHandler.playEffect(pane);
constructCommentHeaders();
List<Comment> comments;
comments = commentService.listByStoreId(store.getId());
populateCommentTable(comments);
}
Like I said I've tried a lot of the solutions found here on Stackoverflow but with no results. The Tableview doesn't refresh. The Stores and the Comments are in different database tables if that's important
Can someone point me in the right direction?
Thanks!
****EDIT****
The Store.class
public class Store extends CommentEntity {
private String id;
private String chainId;
private String phoneNbr;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getChainId() {
return chainId;
}
public void setChainId(String chainId) {
this.chainId = chainId;
}
public String getPhoneNbr() {
return phoneNbr;
}
public void setPhoneNbr(String phoneNbr) {
this.phoneNbr = phoneNbr;
}
#Override
public String toString() {
return "Store{" + "id=" + id + ", chainId=" + chainId + '}';
}
#Override
public String getCommentIdentifier() {
return id;
}
}
The CommentEntity.Class
public abstract class CommentEntity {
private int commentsCount;
public int getCommentsCount() {
return commentsCount;
}
public void setCommentsCount(int commentsCount) {
this.commentsCount = commentsCount;
}
public abstract String getCommentIdentifier();
}
Thank you for input, I hadn't even reflected over the ImageView / String.
Two issues:
First, you need to distinguish between the data the cells in your column are displaying, and the cells that actually display those data. The cellValueFactory determines the data that are displayed. The PropertyValueFactory is a cellValueFactory implementation that references a JavaFX Property, so when you call
storeNumberColumn.setCellValueFactory(new PropertyValueFactory<Store, String>("id"));
it effectively tells the cells in the storeNumberColumn to call the idProperty() method on the Store object in the current row to get the data for the cell. (If no such method exists, it will try to use getId() as a backup plan.)
By default, you get a cellFactory that displays text resulting from calling toString() on the data generated by the cellValueFactory. In the case where your data are simply Strings, this is usually what you need. In other cases, you often need to provide a cellFactory of your own to get the correct way to display the data.
In your case, the data for the commentColumn are simply the number of comments. You are going to display that by choosing an image based on that numeric value.
So you should have
TableColumn<Store, Number> commentColumn = new TableColumn<>("Comments");
For the cellValueFactory, you can just use
commentColumn.setCellValueFactory(new PropertyValueFactory<>("commentsCount"));
Then you need a cellFactory that displays the appropriate ImageView:
commentColumn.setCellFactory(new Callback<TableColumn<Store, Number>, new TableCell<Store, Number>>() {
#Override
public TableCell<Store, Number>() {
private ImageView imageView = new ImageView();
#Override
public void updateItem(Number numberOfComments, boolean empty) {
super.updateItem(count, empty) ;
if (empty) {
setGraphic(null);
} else {
if (numberOfComments.intValue() == 0) {
setGraphic(null);
} else if (numberOfComments.intValue() == 1) {
imageView.setImage(new Image(COMMENT_SINGLE_FLAG_SOURCE));
setGraphic(imageView);
} else {
imageView.setImage(new Image(COMMENT_DOUBLE_FLAG_SOURCE));
setGraphic(imageView);
}
}
}
}
});
The second issue is actually about the update. A TableView keeps its contents "live" by observing JavaFX properties that are provided by the cellValueFactory as ObservableValues. If the value might change while the table is displayed, you must provide an actual property that can be observed: using a ReadOnlyObjectWrapper is no good (because it's read only, so it's wrapped value will not change). The PropertyValueFactory will also return a ReadOnlyObjectWrapper if you do not have JavaFX property accessor methods (i.e. if it is only using getXXX() methods to access the data). So your model class must provide JavaFX Properties.
You can make an immediate fix to this by updating CommentEntity to use an IntegerProperty:
public abstract class CommentEntity {
private final IntegerProperty commentsCount = new SimpleIntegerProperty();
public final int getCommentsCount() {
return commentsCountProperty().get();
}
public final void setCommentsCount(int commentsCount) {
commentsCountProperty().set(commentsCount);
}
public IntegerProperty commensCountProperty() {
return commentsCount ;
}
public abstract String getCommentIdentifier();
}
I would also strongly recommend updating the Store class to use JavaFX Properties in a similar manner.

JavaFX 8 Custom TableCell implementation is rendering three times

I've developed a simple custom TableCell to enable the edition of the values in a table. The behaviour of the component is show a BigDecimalTextField no matter if the user is editing or not that cell, it should enable the edition all the time. The component is working fine, there is only a strange problem: when the table is rendered, instead of show only a single line, three lines are shown:
The code of the component is this:
public class BigDecimalEditingCell extends TableCell {
private BigDecimalField spinner = new BigDecimalField(new BigDecimal("0.00"));
private ObjectProperty<BigDecimal> campoLigado = null;
public BigDecimalEditingCell() {
this.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
createField();
}
private void createField() {
spinner.setStepwidth(new BigDecimal("0.01"));
spinner.setMinValue(new BigDecimal("0.00"));
spinner.setFormat(NumberFormat.getInstance());
spinner.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
}
#Override
public void updateItem(Object item, boolean empty) {
super.updateItem(item, empty);
criarBind();
setGraphic(spinner);
setText(null);
}
private void criarBind() {
ObservableValue<BigDecimal> valor = getTableColumn().getCellObservableValue(getIndex());
if (valor != null) {
ObjectProperty<BigDecimal> propriedade = (ObjectProperty<BigDecimal>) valor;
if (campoLigado == null) {
spinner.numberProperty().bindBidirectional(propriedade);
campoLigado = propriedade;
} else if (campoLigado != propriedade) {
spinner.numberProperty().unbindBidirectional(campoLigado);
spinner.numberProperty().bindBidirectional(propriedade);
campoLigado = propriedade;
}
}
}
}
If I use the default TextFieldTableCell, the table is rendered correctly. I have another component (like this) that uses JavaFX's DatePicker and the same problem happens.
What I'm doing wrong?
ADDED
Here is the usage of this component:
public class ControladorPainelFormaPagamento extends ControladorPainelSubmeter {
#FXML
private TableView<ParcelaBean> tabela;
#FXML
private TableColumn<ParcelaBean, LocalDate> colunaVencimento;
#FXML
private TableColumn<ParcelaBean, BigDecimal> colunaValor;
private FormaPagamentoBean bean;
.
.
.
private void configurarColunaValor() {
colunaValor.setCellValueFactory(new PropertyValueFactory<ParcelaBean, BigDecimal>("valor"));
colunaValor.setCellFactory(new Callback<TableColumn<ParcelaBean, BigDecimal>, TableCell<ParcelaBean, BigDecimal>>() {
#Override
public TableCell<ParcelaBean, BigDecimal> call(TableColumn<ParcelaBean, BigDecimal> parcelaBeanStringTableColumn) {
return new BigDecimalEditingCell();
}
});
}
private void configurarColunaVencimento() {
colunaVencimento.setCellValueFactory(new PropertyValueFactory<ParcelaBean, LocalDate>("dataVencimento"));
}
public void carregar(ModoExibicao modoExibicao, FormaPagamentoBean formaPagamento) {
this.bean=formaPagamento;
tabela.setItems(bean.getParcelas());
.
.
.
}
.
.
.
}
I've checked, inclusive using debug, if there was more than one bean in the list used by the table. Every time only one was there.
I was looking at another table in the same system and noticed this: when there is no item in the table, no one row is rendered.
but when you add just one row, the table renders empty row until reach the height of the table (repair the light grey rows):
So, it was obvious that the extra rows were added by TableView, this way a simple null check solve the problem:
public class BigDecimalEditingCell extends TableCell {
private BigDecimalField element = new BigDecimalField(new BigDecimal("0.00"));
private ObjectProperty<BigDecimal> campoLigado = null;
#Override
protected void updateItem(Object item, boolean empty) {
super.updateItem(item,empty);
if (getIndex() < getTableView().getItems().size() && !empty) {
createField();
createBind();
setGraphic(element);
setText(null);
} else {
removeBind();
setGraphic(null);
}
}
.
.
.
}

Categories