How to bind two Spinner controls into a TableView ? According to the sreenshot below, I would like to do something : colA = colB / 2 (and colB = colA x 2...) :
Here the snippet (deliberately simple) used to expose the problem :
TestApp.java
public class TestApp extends Application {
#Override
public void start(Stage stage) throws Exception {
final TableView<MyBean> tableView = new TableView<>();
final TableColumn<MyBean, Integer> colA = new TableColumn<>("Col A");
final TableColumn<MyBean, Integer> colB = new TableColumn<>("Col B");
colA.setCellFactory(col -> new SpinnerCell<MyBean, Integer>());
colA.setCellValueFactory(new PropertyValueFactory<MyBean, Integer>("valA"));
colB.setCellFactory(col -> new SpinnerCell<MyBean, Integer>());
colB.setCellValueFactory(new PropertyValueFactory<MyBean, Integer>("valB"));
tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
tableView.setItems(FXCollections.observableArrayList(new MyBean(1, 2)));
tableView.getColumns().addAll(colA, colB);
stage.setScene(new Scene(new VBox(tableView), 500, 300));
stage.show();
}
public static void main(String[] args) {
Application.launch();
}
}
SpinnerCell.java
public class SpinnerCell<S, T> extends TableCell<S, T> {
private Spinner<Integer> spinner;
private ObservableValue<T> ov;
public SpinnerCell() {
this.spinner = new Spinner<Integer>(0, 100, 1);
setAlignment(Pos.CENTER);
}
#Override
protected void updateItem(Integer item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
setText(null);
setGraphic(this.spinner);
if(this.ov instanceof IntegerProperty) {
this.spinner.getValueFactory().valueProperty().unbindBidirectional(((IntegerProperty) this.ov).asObject());
}
this.ov = getTableColumn().getCellObservableValue(getIndex());
if(this.ov instanceof IntegerProperty) {
this.spinner.getValueFactory().valueProperty().bindBidirectional(((IntegerProperty) this.ov).asObject());
}
}
}
}
MyBean.java
public class MyBean {
private IntegerProperty valA, valB;
public MyBean(int valA, int valB) {
this.valA = new SimpleIntegerProperty(this, "valA", valA);
this.valB = new SimpleIntegerProperty(this, "valB", valB);
}
public IntegerProperty valAProperty() {
return this.valA;
}
public void setValA(int valA) {
this.valA.set(valA);
}
public int getValA() {
return valA.get();
}
public IntegerProperty valBProperty() {
return this.valB;
}
public void setValB(int valB) {
this.valB.set(valB);
}
public int getValB() {
return valB.get();
}
}
Here's an example of using the extended bidirectional binding support in MyBean:
public static class MyBean {
private IntegerProperty valA;
private IntegerProperty valB;
public MyBean(int valA) {
this.valA = new SimpleIntegerProperty(this, "valA", valA);
this.valB = new SimpleIntegerProperty(this, "valB", 0);
updateB(this.valA, null, this.valA.get());
BidirectionalBinding.<Number, Number>bindBidirectional(
this.valA, this.valB, this::updateB, this::updateA);
}
protected void updateB(ObservableValue<? extends Number> source, Number old, Number value) {
setValB(value.intValue() * 2);
}
protected void updateA(ObservableValue<? extends Number> source, Number old, Number value) {
setValA(value.intValue() / 2);
}
... // same as in OP's code
}
Plus in the SpinnerCell, bind to the bean property directly (vs. to its asObject wrapper) - there's a typing issue that I don't understand entirely [update, see below] (me and generics will never become friends ;-) which stands in the way of successful bidi-binding:
public static class SpinnerCell<S, T extends Number> extends TableCell<S, T> {
private Spinner<T> spinner;
private ObservableValue<T> ov;
public SpinnerCell() {
this(1);
}
public SpinnerCell(int step) {
this.spinner = new Spinner<>(0, 100, step);
setAlignment(Pos.CENTER);
}
#Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
setText(null);
setGraphic(this.spinner);
if(this.ov instanceof Property) {
this.spinner.getValueFactory().valueProperty().unbindBidirectional(((Property) this.ov));
}
this.ov = getTableColumn().getCellObservableValue(getIndex());
if(this.ov instanceof Property) {
this.spinner.getValueFactory().valueProperty().bindBidirectional(((Property) this.ov));
}
}
}
}
Update (in understanding the problem with .asObject)
The problem is not the typing as such, but (struck again!) the weak listener registration in bidi-binding:
// spinner type
Spinner<Integer> spinner;
// value type (in valueFactory):
ObjectProperty<Integer> valueProperty;
// value type in bean:
IntegerProperty valXProperty;
// to be bindeable to spinner's value, needs to be wrapped
// into ObjectProperty<Integer>
// intuitively ... WRONG!
valueProperty.bindBidirectional(bean.valXProperty().asObject());
The wrapper created on the fly is a local reference which can (and is) garbage collected as soon as containing method is left ... As always with these weak listening contexts, none (? at least none that I'm aware of) of the alternatives is satisfying:
relax on typing of the Spinner: using Number (vs.Integer) doesn't require the wrapper because InterProperty instanceOf ObjectProperty<Number>
keep a strong reference to the wrapper somewhere
Try :
valA.bind(valB.divide(2));
Related
I'm trying to realize a personal abstractMap. I have 2 types of map in my project, one <String , List<Box>> and one <String , Box> (box is a class for encase an item with his quantity). But when I try the put method of map, it's showing me "UnsupportedOperationException - if the put operation is not supported by this map"
This is the abstract class for map
public abstract class TamagotchiMap<X> extends AbstractMap<String, X> {
#Override
public abstract Set<Entry<String, X>> entrySet();
public abstract void attachCategories(Set<String> categories);
public abstract void addItemForCategory(String category, Box box);
public abstract String getCategory(Box box);
public Collection<String> getAllCategories() {
return this.keySet();
}
}
and there are the 2 classes that extends this class
1)
public class InventoryMainMap extends TamagotchiMap<Box> {
private final Set<Entry<String, Box>> entry = new HashSet<>();
#Override
public Set<Entry<String, Box>> entrySet() {
return entry;
}
#Override
public void attachCategories(final Set<String> categories) {
for (String category: categories) {
this.put(category, null);
}
}
/**
*
* #return main item for this category
* #param category is the category of item
*/
public Box getMainItem(final String category) {
return this.get(category);
}
#Override
public String getCategory(final Box box) {
for (String category: this.getAllCategories()) {
if (this.get(category).containsItem(box.getItem())) {
return category;
}
}
return null;
}
#Override
public void addItemForCategory(final String category, final Box box) {
if (this.containsKey(category)) {
this.replace(category, box);
}
}
}
2)
public class ItemContainerMap extends TamagotchiMap<List<Box>> {
private final Set<Entry<String, List<Box>>> entry = new HashSet<>();
#Override
public Set<Entry<String, List<Box>>> entrySet() {
return entry;
}
#Override
public String getCategory(final Box box) {
for (String category: this.getAllCategories()) {
for (Box boxIterator: this.get(category)) {
if (boxIterator.containsItem(box.getItem())) {
return category;
}
}
}
return null;
}
#Override
public void addItemForCategory(final String category, final Box box) {
if (this.containsKey(category)) {
if (!this.get(category).contains(box)) {
System.out.println("nuovo item");
this.get(category).add(box);
} else {
System.out.println("item esistente");
this.get(category).stream().filter(iterBox -> iterBox.equals(box)).forEach(iterBox -> iterBox.increaseQuantity());
}
} else {
throw new NoSuchElementException();
}
}
#Override
public void attachCategories(final Set<String> categories) {
for (String category: categories) {
this.put(category, new LinkedList<Box>());
}
}
}
When I do
class Test {
TamagotchiMap map = new ItemContainerMap();
map.put(string, box);
}
it makes me see the Exception that I have already said.
As if the put was not usable, do you know where I was wrong?
throwing an UnsupportedOperationException is the default implementation of put(K, V) you inherit from AbstractMap. You could either implement it yourself, or, have your TamagotchiMap extend a concrete Map class, such as HashMap instead of AbstractMap.
I can use an extractor (Callback<E, Observable[]> extractor) to make a ListProperty fire change events if one of its elements changed one of its properties (update event).
Update Change Event in ObservableList
Is there an equivalent for ObjectProperty<>? I have an SimpleObjectProperty which I want to fire events when properties of it's value (another bean type) change (update change events).
Sample code:
public class TestBean {
public static <T extends TestBean> Callback<T, Observable[]> extractor() {
return (final T o) -> new Observable[] { o.testPropertyProperty() };
}
private final StringProperty testProperty = new SimpleStringProperty();
public final StringProperty testPropertyProperty() {
return this.testProperty;
}
public final String getTestProperty() {
return this.testPropertyProperty().get();
}
public final void setTestProperty(final String testProperty) {
this.testPropertyProperty().set(testProperty);
}
}
public class SomeType {
/**
* How can I listen for changes of TestBean#testProperty?
*/
private final ObjectProperty<TestBean> property = new SimpleObjectProperty<>();
}
I want to receive change events if the value of SomeType#property changes, but also, if SomeType#property#testProperty changes.
I cannot just listen for SomeType#property#testProperty, since I would not be notified when SomeType#property was changed (I would then listen on the wrong object for changes).
I want to receive change events if value of SomeType#property changes, but also, if SomeType#property#testProperty changes.
I cannot just listen for SomeType#property#testProperty, since I would not be notified, when SomeType#property was changed (I would then listen on the wrong object for changes).
This is a limitation of sorts of the current iteration of JavaFX. The built-in way is unreliable and you're better off using 3rd party libraries. See this answer for more information.
For you case, ReactFX can be utilized in a similar way:
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.reactfx.value.Val;
import org.reactfx.value.Var;
class TestBean {
private final StringProperty testProperty = new SimpleStringProperty();
public final StringProperty testPropertyProperty() { return testProperty; }
public final String getTestProperty() { return testProperty.get(); }
public final void setTestProperty(String newTestProperty) { testProperty.set(newTestProperty); }
}
public class SomeType {
private final ObjectProperty<TestBean> property = new SimpleObjectProperty<>();
public final ObjectProperty<TestBean> propertyProperty() { return property; }
public final TestBean getProperty() { return property.get(); }
public final void setProperty(TestBean newProperty) { property.set(newProperty); }
public static void main(String[] args) {
SomeType someType = new SomeType();
Var<String> chainedTestProperty = Val.selectVar(someType.propertyProperty(), TestBean::testPropertyProperty);
chainedTestProperty.addListener((obs, oldVal, newVal) -> System.out.println(obs + " " + oldVal + "->" + newVal));
//Tests
someType.setProperty(new TestBean());
someType.getProperty().setTestProperty("s1");
TestBean bean2 = new TestBean();
bean2.setTestProperty("s2");
someType.setProperty(bean2);
someType.setProperty(new TestBean());
}
}
Output:
org.reactfx.value.FlatMappedVar#7aec35a null->s1
org.reactfx.value.FlatMappedVar#7aec35a s1->s2
org.reactfx.value.FlatMappedVar#7aec35a s2->null
The key line
Var<String> chainedTestProperty = Val.selectVar(someType.propertyProperty(), TestBean::testPropertyProperty);
is a sort of listener chaining. The first argument is a property (OvservableValue) of some type Type. The second argument is the "sub"-property of some other type Type2 inside Type, which is given as a function from Type to that property.
Now whenever any "links" in the chain change, you are notified. You can continue to listen to changes in sub-sub-... properties by continuously chaining ovservables this way.
I came up with the following:
public class ObservableValueProperty<T> extends SimpleObjectProperty<T> {
private InvalidationListener listener = null;
private final Callback<T, Observable[]> extractor;
public ObservableValueProperty() {
this(null);
}
public ObservableValueProperty(final Callback<T, Observable[]> extractor) {
this.extractor = extractor;
}
#Override
protected void fireValueChangedEvent() {
super.fireValueChangedEvent();
}
#Override
public void setValue(final T v) {
if (extractor != null) {
final T oldValue = super.get();
if (oldValue != null) {
for (final Observable o : extractor.call(oldValue)) {
o.removeListener(listener);
}
}
listener = o -> fireValueChangedEvent();
for (final Observable o : extractor.call(v)) {
o.addListener(listener);
}
}
super.setValue(v);
}
}
public class ObservableValuePropertyTest4 implements ChangeListener<Object> {
#BeforeClass
public static void setUpBeforeClass() throws Exception {
}
#AfterClass
public static void tearDownAfterClass() throws Exception {
}
#Before
public void setUp() throws Exception {
}
#After
public void tearDown() throws Exception {
}
static class NestedBean {
StringProperty nestedProperty = new SimpleStringProperty("hans");
public static <T extends NestedBean> Callback<T, Observable[]> extractor() {
return (final T o) -> new Observable[] { o.nestedProperty };
}
#Override
public boolean equals(final Object obj) {
if (obj instanceof NestedBean) {
System.err.println(this.nestedProperty.get() + " " + ((NestedBean) obj).nestedProperty.get());
return Objects.equal(this.nestedProperty.get(), ((NestedBean) obj).nestedProperty.get());
}
return false;
}
}
private ObservableValueProperty<NestedBean> p;
private NestedBean nestedBean;
private String newNestedValue = null;
#Test
public void test01() {
p = new ObservableValueProperty<>(NestedBean.extractor());
nestedBean = new NestedBean();
p.setValue(nestedBean);
p.addListener(this);
nestedBean.nestedProperty.set("peter");
assertEquals("peter", newNestedValue);
}
#Override
public void changed(final ObservableValue<? extends Object> observable, final Object oldValue,
final Object newValue) {
System.err.println("Changed");
newNestedValue = nestedBean.nestedProperty.get();
}
}
Unfortunately, this does not fire any change events because of ExpressionHelper$SingleChange:
#Override
protected void fireValueChangedEvent() {
final T oldValue = currentValue;
currentValue = observable.getValue();
final boolean changed = (currentValue == null)? (oldValue != null) : !currentValue.equals(oldValue);
if (changed) {
try {
listener.changed(observable, oldValue, currentValue);
} catch (Exception e) {
Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
}
}
}
This checks for equality and only if not equal, notifies all listeners. When I trigger fireValueChangedEvent() the value has already changed, and new- and old values are equal, therefore no notification to listeners.
I had the same problem last week, and after many tries, I found a solution that seems to work as expected:
I created a new class called ObjectXProperty<E>, that has the same interface of an ObjectProperty<E>;
It has constructors that can accept a Callback<E,Observable[]>, our extractor function;
Inside the ObjectXProperty, I use a SimpleObjectProperty that deleguates all methods;
The magic trick lies in the set(E value) methods : I create an ObjectBinding that simply send back the value, but it uses the extractor function to decide when it's become invalidated!
This trick will not be applied if the bind method was used previously on the ObjectXProperty, to let the "real" binding do his job; it will work again if the unbind method is called;
Here's my new class ObjectXProperty<E> :
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.util.Callback;
/**
*
* #author Claude Bouchard - 2017
*/
public class ObjectXProperty<E> extends ObjectProperty<E> {
SimpleObjectProperty<E> p;
Callback<E, Observable[]> extractor;
boolean externalBound = false;
public ObjectXProperty(Callback<E, Observable[]> extractor) {
this.extractor = extractor;
}
public ObjectXProperty(E init, Callback<E, Observable[]> extractor) {
p = new SimpleObjectProperty();
this.extractor = extractor;
set(init);
}
public ObjectXProperty(Object bean, String name, Callback<E, Observable[]> extractor) {
p = new SimpleObjectProperty(bean, name);
this.extractor = extractor;
}
public ObjectXProperty(Object bean, String name, E init, Callback<E, Observable[]> extractor) {
p = new SimpleObjectProperty(bean, name);
this.extractor = extractor;
set(init);
}
#Override
public void set(E value) {
if (!externalBound) {
if (value != null) {
p.bind(Bindings.createObjectBinding(() -> {
return value;
}, extractor.call(value)));
} else {
p.bind(Bindings.createObjectBinding(() -> {
return value;
}, new Observable[]{}));
}
} else {
p.set(value); //As expected, it will throw a java.lang.RuntimeException
}
}
#Override
public E get() {
return p.get();
}
#Override
public void addListener(ChangeListener<? super E> listener) {
p.addListener(listener);
}
#Override
public void removeListener(ChangeListener<? super E> listener) {
p.removeListener(listener);
}
#Override
public void addListener(InvalidationListener listener) {
p.addListener(listener);
}
#Override
public void removeListener(InvalidationListener listener) {
p.removeListener(listener);
}
#Override
public Object getBean() {
return p.getBean();
}
#Override
public String getName() {
return p.getName();
}
#Override
public void bind(ObservableValue<? extends E> observable) {
p.bind(observable);
externalBound = true;
}
#Override
public void unbind() {
p.unbind();
externalBound = false;
set(get()); //to reactivate the extractor on the last value
}
#Override
public boolean isBound() {
return externalBound;
}
}
I think you need to add a listener to your object. This can be done simply. First of all you should write your class with a constructor and with getters this way:
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
public class SomeType {
public ObjectProperty<TestProperty> property;
public SomeType(TestProperty testProperty) {
this.property = new SimpleObjectProperty<>(testProperty);
}
public TestProperty getProperty() {
return property.get();
}
public ObjectProperty<TestProperty> propertyProperty() {
return property;
}
}
Then anywhere you have an instance of SomeType you can chain the properties, so you get the property the property's testProperty() and then simply add a listener to it.
someType.getProperty().testProperty().addListener((observable, oldValue, newValue) -> {
// Do whatever you want if the its value changed.
// You can also use its old or new value.
});
I created an editor with two editor pages. I added tool bar items for the two pages with property testers. On the first page the property testers work fine. But if I open the editor, select the second page and select tree item, the property testers are not working. Manual requesting the evaluation of the property testers does not work. After selecting another view or a tree item on first page, the property testers work fine on second page too. Whats the problem? Here an example for editor:
public class MyEditor extends FormEditor {
#Override
protected void addPages() {
try {
addPage(new MyPage(this));
addPage(new MyPage(this));
} catch (PartInitException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
#Override
public boolean isSaveAsAllowed() {
return false;
}
#Override
public void doSave(IProgressMonitor monitor) {
// nothing to do
}
#Override
public void doSaveAs() {
// nothing to do
}
}
class MyPage extends FormPage {
private static final String TOOLBAR_URI = "toolbar:my.page";
public MyPage(final FormEditor editor) {
super(editor, MyPage.class.getName(), "Test");
}
#Override
protected void createFormContent(final IManagedForm managedForm) {
final ScrolledForm form = managedForm.getForm();
final ToolBarManager toolbarManager = (ToolBarManager) form.getToolBarManager();
final IMenuService menuService = (IMenuService) getSite().getService(IMenuService.class);
menuService.populateContributionManager(toolbarManager, TOOLBAR_URI);
final Composite body = managedForm.getForm().getBody();
body.setLayout(new GridLayout());
final TreeViewer treeViewer = new TreeViewer(body);
treeViewer.setContentProvider(new MyContentProvider());
treeViewer.setLabelProvider(new ColumnLabelProvider());
treeViewer.setInput(Arrays.asList(new String[] { "A", "B", "C" }));
getSite().setSelectionProvider(treeViewer);
}
}
class MyContentProvider implements ITreeContentProvider {
#Override
public Object[] getElements(final Object inputElement) {
return ((List<String>) inputElement).toArray();
}
#Override
public Object[] getChildren(final Object parentElement) {
return new Object[] {};
}
#Override
public Object getParent(final Object element) {
return null;
}
#Override
public boolean hasChildren(final Object element) {
return false;
}
#Override
public void dispose() {
// nothing to do
}
#Override
public void inputChanged(final Viewer viewer, final Object oldInput, final Object newInput) {
// nothing to do
}
}
Handler:
<handler
class="EditHandler"
commandId="myEdit">
<activeWhen>
<with
variable="selection">
<test
property="my.active">
</test>
</with>
</activeWhen>
</handler>
Property tester registration:
<propertyTester
class="MyPropertyTester"
id="MyPropertyTester"
namespace="my"
properties="active"
type="java.lang.Object">
</propertyTester>
Property tester:
public class MyPropertyTester extends PropertyTester {
public static final String ID = MyPropertyTester.class.getName();
public static final String ACTIVE = "active";
#Override
public boolean test(final Object receiver, final String property, final Object[] args, final Object expectedValue) {
if (receiver instanceof IStructuredSelection) {
final IStructuredSelection selection = (IStructuredSelection) receiver;
if (selection.size() == 1) {
return isMatch(property);
}
}
return false;
}
private static boolean isMatch(final String property) {
if (ACTIVE.equals(property)) {
return true;
}
return false;
}
}
I "fixed" it via hack on editor page change:
#Override
protected void pageChange(int newPageIndex) {
List<IWorkbenchPart> views = ... // find all opened IWorkbenchPart
if (!views.isEmpty()) {
views.get(0).setFocus();
this.setFocus();
}
super.pageChange(newPageIndex);
}
I have a TableView that is sourced from an attribute in a legacy Java Bean of type java.util.Date. I wish to customize the formatting of the date String to HH:mm:ss
I'm looking for is a native JavaFX utility to create an ObservableValue wrapper taking a java.util.DateFormat or javafx.util.StringConverter
I've found Bindings.format() class which could be used to wrap the ObservableValue, however this only allows printf format patterns like %04d etc, not any custom date specific formatting.
The best I've come up with is to use Bindings.bindBidirectional(property, property, format) with a dummy StringProperty which the CellFactory returns. Can this be simplified? Could this cause a memory leak?
public class OldBeanTableView extends Application {
public class OldBean {
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
public static final String PROPERTY_NAME_FOO = "foo";
private Date foo = new Date();
public Date getFoo() {
return foo;
}
public void setFoo(Date foo) {
Date oldValue = this.foo;
this.foo = foo;
pcs.firePropertyChange(PROPERTY_NAME_FOO, oldValue, foo);
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
pcs.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
pcs.removePropertyChangeListener(listener);
}
}
private class LegacyValueFactory<T, F> implements Callback<CellDataFeatures<T, String>, ObservableValue<String>> {
private String propertyName;
private Format format;
public LegacyValueFactory(String propertyName, Format format) {
this.propertyName = propertyName;
this.format = format;
}
#Override
public ObservableValue<String> call(CellDataFeatures<T, String> param) {
try {
Property<String> formattedString = new SimpleStringProperty();
Property<F> original = JavaBeanObjectPropertyBuilder.create().name(propertyName).bean(param.getValue()).build();
Bindings.bindBidirectional(formattedString, original, format);
return formattedString;
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
return null;
}
}
#Override
public void start(Stage primaryStage) throws Exception {
ObservableList<OldBean> beans = FXCollections.observableArrayList();
beans.add(new OldBean());
TableView<OldBean> tableView = new TableView<>();
TableColumn<OldBean, String> column = new TableColumn<OldBeanTableView.OldBean, String>();
tableView.getColumns().add(column);
column.setCellValueFactory(new LegacyValueFactory<OldBean, String>("foo", new SimpleDateFormat("HH:mm:ss")));
tableView.setItems(beans);
primaryStage.setScene(new Scene(tableView));
primaryStage.show();
Executors.newScheduledThreadPool(1).scheduleAtFixedRate(
() -> beans.get(0).setFoo(new Date(beans.get(0).getFoo().getTime() + 1000)), 0, 1, TimeUnit.SECONDS);
}
public static void main(String[] args) {
launch(args);
}
}
Based on James_D suggestion this can be done using a CellFactory instead of a CellValueFactory...
public class FormattedTableCell<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>> {
private Format format;
public FormattedTableCell(Format format) {
super();
this.format = format;
}
#Override
public TableCell<S, T> call(TableColumn<S, T> param) {
return new TableCell<S, T>() {
#Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
} else {
setText(format.format(item));
}
}
};
}
}
Usage
column.setCellFactory(new FormattedTableCell<OldBean, Date>(new SimpleDateFormat("HH:mm:ss")));
We're integrating JavaFX onto a large legacy code base containing many "original" Java beans, i.e. the type implemented using java.beans.PropertyChangeSupport.
JavaFX does not support update of these style of beans, only initial value, as documented in javafx.scene.control.cell.PropertyValueFactory
If no method matching this pattern exists, there is fall-through
support for attempting to call get() or is() (that
is, getFirstName() or isFirstName() in the example above). If a method
matching this pattern exists, the value returned from this method is
wrapped in a ReadOnlyObjectWrapper and returned to the TableCell.
However, in this situation, this means that the TableCell will not be
able to observe the ObservableValue for changes (as is the case in the
first approach above).
Upgrading the beans to the property API is not an option as they live in a separate code base which we don't wish to add JavaFX dependencies on as it is still used by legacy Java 6 projects.
My question, how can I get a TableView to update when properties are changed without having to add/remove listeners onto all the individual beans in the table.
I was considering creating my own version of PropertyValueFactory which supports this, but I'd like to know if there are any other possible solutions.
I've produced two examples to illustrate this.
TableView using old-school beans
public class OldBeanTableView extends Application {
public class OldBean {
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
public static final String PROPERTY_NAME_FOO = "foo";
private int foo = 99;
public int getFoo() {
return foo;
}
public void setFoo(int foo) {
int oldValue = this.foo;
this.foo = foo;
pcs.firePropertyChange(PROPERTY_NAME_FOO, oldValue, foo);
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
pcs.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
pcs.removePropertyChangeListener(listener);
}
}
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
ObservableList<OldBean> beans = FXCollections.observableArrayList();
beans.add(new OldBean());
TableView<OldBean> tableView = new TableView<>();
TableColumn<OldBean, Integer> column = new TableColumn<OldBeanTableView.OldBean, Integer>();
tableView.getColumns().add(column);
column.setCellValueFactory(new PropertyValueFactory<>("foo"));
tableView.setItems(beans);
primaryStage.setScene(new Scene(tableView));
primaryStage.show();
Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> beans.get(0).setFoo(beans.get(0).getFoo() + 1), 0,
1, TimeUnit.SECONDS);
}
}
TableView using new beans
public class NewBeanTableView extends Application {
public class NewBean {
private IntegerProperty fooProperty = new SimpleIntegerProperty(0);
public int getFoo() {
return fooProperty.get();
}
public void setFoo(int foo) {
fooProperty.set(foo);
}
public IntegerProperty fooProperty() {
return fooProperty;
}
}
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
ObservableList<NewBean> beans = FXCollections.observableArrayList();
beans.add(new NewBean());
TableView<NewBean> tableView = new TableView<>();
TableColumn<NewBean, Integer> column = new TableColumn<NewBeanTableView.NewBean, Integer>();
tableView.getColumns().add(column);
column.setCellValueFactory(new PropertyValueFactory<>("foo"));
tableView.setItems(beans);
primaryStage.setScene(new Scene(tableView));
primaryStage.show();
Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> beans.get(0).setFoo(beans.get(0).getFoo() + 1), 0,
1, TimeUnit.SECONDS);
}
}
A very quick example for using JavaBeanProperty as valueFactory:
Callback<CellDataFeatures<OldBean, Integer>, ObservableValue<Integer>> valueFactory = cdf -> {
OldBean bean = cdf.getValue();
JavaBeanObjectProperty<Integer> wrappee;
try {
wrappee = JavaBeanObjectPropertyBuilder.create()
.name("foo").bean(bean).build();
return wrappee;
} catch (Exception e) {
e.printStackTrace();
}
return null;
};
column.setCellValueFactory(valueFactory);
Note that the bean must have methods add/removePropertyChangeListeners (which your real beans will have anyway :-) to work.
Extrapolating kleopatra's answer to the generic solution.
public class LegacyValueFactory<T, F> implements Callback<CellDataFeatures<T, F>, ObservableValue<F>> {
private String propertyName;
public LegacyValueFactory(String propertyName) {
this.propertyName = propertyName;
}
#Override
public ObservableValue<F> call(CellDataFeatures<T, F> param) {
try {
return JavaBeanObjectPropertyBuilder.create().name(propertyName).bean(param.getValue()).build();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
return null;
}
}
Usage
column.setCellValueFactory(new LegacyValueFactory<OldBean, Integer>("foo"));