I have a method called rxToProperty() that turns an Observable into a JavaFX Property.
public static <T> ReadOnlyObjectProperty<T> rxToProperty(Observable<T> obs) {
ReadOnlyObjectWrapper<T> property = new ReadOnlyObjectWrapper<>();
obs.onBackpressureLatest().serialize().subscribe(v -> {
synchronized(property) {
property.set(v);
}
});
return property.getReadOnlyProperty();
}
How do I ensure that the returned Property is always up-to-date especially when it is bound to controls in JavaFX? I have noticed some very bizarre, random behaviors that seem to indicate thread safety has been compromised with the schedulers and JavaFX.
Like for example, I try to use this method to show one source Observable scheduled in several different ways, and the results are random and haphazard when used in a TableView, with cells alternating between having values and not having values, as well as thread activity that sometimes will never end.
Whenever I try to schedule on the FX thread using Platform.invokeLater() it only makes the behavior more crazy. What is wrong with my rxToProperty() method?
public class ReactiveTableViewTest extends Application {
#Override
public void start(Stage stage) throws Exception {
Group root = new Group();
Scene scene = new Scene(root);
root.getChildren().add(new ReactiveTable());
stage.setScene(scene);
stage.sizeToScene();
stage.show();
}
private static final class ReactiveRecord {
private final Observable<Number> obs = Observable.just(10,20,30,40,50,60).cast(Number.class);
public Observable<Number> subscribeImmediate() {
return obs;
}
public Observable<Number> subscribeComputation() {
return obs.subscribeOn(Schedulers.computation());
}
public Observable<Number> subscribeNewthread() {
return obs.subscribeOn(Schedulers.newThread());
}
public Observable<Number> subscribeIo() {
return obs.subscribeOn(Schedulers.io());
}
public Observable<Number> subscribeParallel() {
return obs.flatMap(i -> Observable.just(i).subscribeOn(Schedulers.computation()));
}
public Observable<Number> subscribeTrampoline() {
return obs.subscribeOn(Schedulers.trampoline());
}
public ImmutableList<Observable<Number>> getAll() {
return ImmutableList.of(subscribeImmediate(),subscribeComputation(),subscribeNewthread(),subscribeIo(),subscribeParallel(),subscribeTrampoline());
}
public ImmutableList<String> getHeaders() {
return ImmutableList.of("IMMEDIATE","COMPUTATION","NEW","IO","PARALLEL","TRAMPOLINE");
}
}
private static final class ReactiveTable extends TableView<ReactiveRecord> {
private ReactiveTable() {
ReactiveRecord record = new ReactiveRecord();
this.getItems().add(record);
ImmutableList<Observable<Number>> observables = record.getAll();
ImmutableList<String> headers = record.getHeaders();
for (int i = 0; i < observables.size(); i++) {
TableColumn<ReactiveRecord,Number> col = new TableColumn<>(headers.get(i));
final int index = i;
col.setCellValueFactory(cb -> rxToProperty(observables.get(index)));
this.getColumns().add(col);
}
}
}
public static <T> ReadOnlyObjectProperty<T> rxToProperty(Observable<T> obs) {
ReadOnlyObjectWrapper<T> property = new ReadOnlyObjectWrapper<>();
obs.onBackpressureLatest().serialize().subscribe(v -> {
synchronized(property) {
System.out.println("Emitting val " + v + " on " + Thread.currentThread().getName());
property.set(v);
}
});
return property.getReadOnlyProperty();
}
public static void main(String[] args) {
launch(args);
}
}
Tomas Mikula gave some very helpful insight to this behavior, as well as the solution, on the ReactFX GitHub project.
https://github.com/TomasMikula/ReactFX/issues/22
Related
Today i was working on my homework, which it was making simple apps with retrofit calls and learning new things for code improvement, and somehow i saw there are so many ways to write less code and do better with OOP. So to improve my code experiment I'm trying to do my retrofit calls with OOP. So this is my issue right now:
Consider a simple retrofit call with CompositeDisposable( I'm developing my simples with MVP ) :
mView.showProgress(1);
RequestRemainingProductsRequest requestRemainingProductsRequest = new RequestRemainingProductsRequest();
requestRemainingProductsRequest.distributorId = distributorId;
requestRemainingProductsRequest.requestCode = requestCode;
requestRemainingProductsRequest.requestType = 1;
NetworkCalls.getObservableList();
compositeDisposable.add(getApi().requestRemainingProducts(requestRemainingProductsRequest)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<List<Products>>() {
#Override
public void accept(List<Products> products) throws Throwable {
mView.hideProgress(1);
mView.getRemainingProducts(products);
}
}, new Consumer<Throwable>() {
#Override
public void accept(Throwable throwable) throws Exception {
mView.hideProgress(1);
mView.showLog(throwable.getMessage().toString());
}
}));
And, Another retrofit call without CompositeDisposable :
ProductSellerRequest productSellerRequest = new ProductSellerRequest();
productSellerRequest.centralId = centralsList.get(i).requestCentralId;
productSellerRequest.requestType = 0;
productSellerRequest.productId = Constant.currentProduct.productId;
getApi().checkProductExistInRequest(productSellerRequest)
.enqueue(new Callback<ProductSellerCallback>() {
#Override
public void onResponse(Call<ProductSellerCallback> call, Response<ProductSellerCallback> response) {
hideProgress(myViewHolder);
showAddDialog(myViewHolder, v, response, i);
}
#Override
public void onFailure(Call<ProductSellerCallback> call, Throwable t) {
hideProgress(myViewHolder);
}
});
So let's say I created a java class with NetworkCalls.java, and created 2 voids like this:
public static void getObservableList()
{
}
public static void getWithoutObservableList()
{
}
How to handle my response to return to my Presenter/Activity?
This is how i using StringBuilder and returning my String, but I'm trying do similiar way to make repository for my Network Calls, then learn all all i should know about Repository Pattern.
public static String TotalPriceStringBuilder(int Price){
String DecimalPrice = String.format("%,d", Price);
String FinalString = new StringBuilder("Price : ").append(DecimalPrice).append(" $").toString();
return String.valueOf(FinalString);
}
This is what I've tried, but i still don't know how to fix it or make it work, what to return, and how to return and etc... :
private static ApiClient mApi;
private List<Products> receivedProducts;
private int hideProgress;
private boolean status;
private String message;
public void getObservableList(RequestRemainingProductsRequest requestRemainingProductsRequest, CompositeDisposable compositeDisposable)
{
compositeDisposable.add(getApi().requestRemainingProducts(requestRemainingProductsRequest)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<List<Products>>() {
#Override
public void accept(List<Products> products) throws Throwable {
hideProgress = 1;
receivedProducts = products;
status = TRUE;
}
}, new Consumer<Throwable>() {
#Override
public void accept(Throwable throwable) throws Exception {
hideProgress = 1;
status = FALSE;
message = throwable.getMessage().toString();
}
}));
if (status == TRUE) {
return hideProgress, receivedProducts, status;
} else {
return hideProgress, message, status;
}
}
public ApiClient getApi() {
if (mApi == null) {
mApi = ApiService.getClient().create(ApiClient.class);
}
return mApi;
}
If i use static method I'll get bunch of errors like can't be refrenced from a static context or etc...
I believe I've seen variants of this question, but no "definitive answer". In the code below, I understand that SomeEventManager holds a reference to someImplClassTwo.myEventListenerA and someImplClassTwo.myEventListenerB, and that this does not allow for someImplClassTwo to be garbage collected, and this results in the output generated the second time someEventManager.notifyListeners() is invoked.
But, I'd really like for users of SomeImplClass not to have to know that there are listeners involved in the implementation, and that these listeners need to be manually un-registered (i.e., SomeImplClass.releaseListeners()) before releasing the SomeImplClass object.
Is there a clean/accepted way of doing this?
p.s. I've already played with finalize(), just for fun, and confirmed that GC is not even attempted in this case, for either instance of SomeImplClass. So, that seems to be a non-starter as a potential solution.
Test Driver
public class TestDriver {
public static void main(String[] args) {
SomeEventManager someEventManager = SomeEventManager.getInstance();
SomeImplClass someImplClassOne = new SomeImplClass("One");
SomeImplClass someImplClassTwo = new SomeImplClass("Two");
someEventManager.notifyListeners();
someImplClassOne.releaseListeners();
someImplClassOne = null;
someImplClassTwo = null;
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
}
someEventManager.notifyListeners();
}
}
Event Interface
public interface SomeEventListener {
public void handleSomeEvent();
}
Event Manager
import java.util.ArrayList;
import java.util.List;
public class SomeEventManager {
private static SomeEventManager eventManager = null;
private List<SomeEventListener> listeners = null;
private SomeEventManager() {
listeners = new ArrayList<SomeEventListener>();
}
public static SomeEventManager getInstance() {
if (eventManager == null) {
eventManager = new SomeEventManager();
}
return eventManager;
}
public void addListener(SomeEventListener listener) {
if (!listeners.contains(listener)) {
listeners.add(listener);
}
}
public void removeListener(SomeEventListener listener) {
listeners.remove(listener);
}
public void notifyListeners() {
for(SomeEventListener listener : listeners) {
listener.handleSomeEvent();
}
}
}
Event Listener Implementation
public class SomeImplClass {
private InnerEventListener myEventListenerA = null;
private InnerEventListener myEventListenerB = null;
private String id = null;
public SomeImplClass(String id) {
this.id = id;
myEventListenerA = new InnerEventListener(id + "_A");
myEventListenerB = new InnerEventListener(id + "_B");
}
public void releaseListeners() {
myEventListenerA.unregisterListener();
myEventListenerB.unregisterListener();
}
private class InnerEventListener implements SomeEventListener {
private SomeEventManager someEventManager = null;
private String id = null;
public InnerEventListener(String id) {
someEventManager = SomeEventManager.getInstance();
this.id = id;
registerListener();
}
public void registerListener() {
someEventManager.addListener(this);
}
public void unregisterListener() {
someEventManager.removeListener(this);
}
public void handleSomeEvent() {
System.out.println("InnerEventListener->" + id);
}
}
}
The solution we use is to have the listener automatically unregister itself if it gets called and the thing it's updating has been collected.
It looks a bit like this:
private static class InnerEventListener implements SomeEventListener {
private final WeakReference<ThingToUpdate> thingRef;
public InnerEventListener(ThingToUpdate thing) {
thingRef = new WeakReference<>(thing);
}
#Override
public void handleSomeEvent(SomeEvent event) {
ThingToUpdate thing = thingRef.get();
if (thing != null) {
thing.updateSomehow();
} else {
((SomeEventedThing) event.getSource())
.removeSomeEventListener(this);
}
}
}
//...
SomeEventedThing eventedThing;
ThingToUpdate thingToUpdate;
//...
eventedThing.addListener(new InnerEventListener(thingToUpdate));
I wouldn't say it's a perfect solution because the listener sticks around until it gets an event, and it's still somewhat dependent on garbage collection. We've been trying to replace it with explicit removal where possible, usually on addNotify/removeNotify on GUI components.
I'm using a multiplayer Game Client that's called AppWarp (http://appwarp.shephertz.com), where you can add event listeners to be called back when event's happen, let's assume we'll be talking about the Connection Listener, where you need to implement this interface:
public interface ConnectionRequestListener {
void onConnectDone(ConnectEvent var1);
void onDisconnectDone(ConnectEvent var1);
void onInitUDPDone(byte var1);
}
My goal here is to mainly create a Reactive version of this client to be used in my Apps Internally instead of using the Client itself directly (I'll also rely on interfaces later instead of just depending on the WarpClient itself as in the example, but that's not the important point, please read my question at the very end).
So what I did is as follows:
1) I introduced a new event, named it RxConnectionEvent (Which mainly groups Connection-Related events) as follows:
public class RxConnectionEvent {
// This is the original connection event from the source client
private final ConnectEvent connectEvent;
// this is to identify if it was Connection / Disconnection
private final int eventType;
public RxConnectionEvent(ConnectEvent connectEvent, int eventType) {
this.connectEvent = connectEvent;
this.eventType = eventType;
}
public ConnectEvent getConnectEvent() {
return connectEvent;
}
public int getEventType() {
return eventType;
}
}
2) Created some event types as follows:
public class RxEventType {
// Connection Events
public final static int CONNECTION_CONNECTED = 20;
public final static int CONNECTION_DISCONNECTED = 30;
}
3) Created the following observable which emits my new RxConnectionEvent
import com.shephertz.app42.gaming.multiplayer.client.WarpClient;
import com.shephertz.app42.gaming.multiplayer.client.events.ConnectEvent;
import rx.Observable;
import rx.Subscriber;
import rx.functions.Action0;
import rx.subscriptions.Subscriptions;
public class ConnectionObservable extends BaseObservable<RxConnectionEvent> {
private ConnectionRequestListener connectionListener;
// This is going to be called from my ReactiveWarpClient (Factory) Later.
public static Observable<RxConnectionEvent> createConnectionListener(WarpClient warpClient) {
return Observable.create(new ConnectionObservable(warpClient));
}
private ConnectionObservable(WarpClient warpClient) {
super(warpClient);
}
#Override
public void call(final Subscriber<? super RxConnectionEvent> subscriber) {
subscriber.onStart();
connectionListener = new ConnectionRequestListener() {
#Override
public void onConnectDone(ConnectEvent connectEvent) {
super.onConnectDone(connectEvent);
callback(new RxConnectionEvent(connectEvent, RxEventType.CONNECTION_CONNECTED));
}
#Override
public void onDisconnectDone(ConnectEvent connectEvent) {
super.onDisconnectDone(connectEvent);
callback(new RxConnectionEvent(connectEvent, RxEventType.CONNECTION_DISCONNECTED));
}
// not interested in this method (for now)
#Override
public void onInitUDPDone(byte var1) { }
private void callback(RxConnectionEvent rxConnectionEvent)
{
if (!subscriber.isUnsubscribed()) {
subscriber.onNext(rxConnectionEvent);
} else {
warpClient.removeConnectionRequestListener(connectionListener);
}
}
};
warpClient.addConnectionRequestListener(connectionListener);
subscriber.add(Subscriptions.create(new Action0() {
#Override
public void call() {
onUnsubscribed(warpClient);
}
}));
}
#Override
protected void onUnsubscribed(WarpClient warpClient) {
warpClient.removeConnectionRequestListener(connectionListener);
}
}
4) and finally my BaseObservable looks like the following:
public abstract class BaseObservable<T> implements Observable.OnSubscribe<T> {
protected WarpClient warpClient;
protected BaseObservable (WarpClient warpClient)
{
this.warpClient = warpClient;
}
#Override
public abstract void call(Subscriber<? super T> subscriber);
protected abstract void onUnsubscribed(WarpClient warpClient);
}
My question is mainly: is my implementation above correct or should I instead create separate observable for each event, but if so, this client has more than 40-50 events do I have to create separate observable for each event?
I also use the code above as follows (used it in a simple "non-final" integration test):
public void testConnectDisconnect() {
connectionSubscription = reactiveWarpClient.createOnConnectObservable(client)
.subscribe(new Action1<RxConnectionEvent>() {
#Override
public void call(RxConnectionEvent rxEvent) {
assertEquals(WarpResponseResultCode.SUCCESS, rxEvent.getConnectEvent().getResult());
if (rxEvent.getEventType() == RxEventType.CONNECTION_CONNECTED) {
connectionStatus = connectionStatus | 0b0001;
client.disconnect();
} else {
connectionStatus = connectionStatus | 0b0010;
connectionSubscription.unsubscribe();
haltExecution = true;
}
}
}, new Action1<Throwable>() {
#Override
public void call(Throwable throwable) {
fail("Unexpected error: " + throwable.getMessage());
haltExecution = true;
}
});
client.connectWithUserName("test user");
waitForSomeTime();
assertEquals(0b0011, connectionStatus);
assertEquals(true, connectionSubscription.isUnsubscribed());
}
I suggest you avoid extending the BaseObservable directly since it's very error prone. Instead, try using the tools Rx itself gives you to create your observable.
The easiest solution is using a PublishSubject, which is both an Observable and a Subscriber. The listener simply needs to invoke the subject's onNext, and the subject will emit the event. Here's a simplified working example:
public class PublishSubjectWarpperDemo {
public interface ConnectionRequestListener {
void onConnectDone();
void onDisconnectDone();
void onInitUDPDone();
}
public static class RxConnectionEvent {
private int type;
public RxConnectionEvent(int type) {
this.type = type;
}
public int getType() {
return type;
}
public String toString() {
return "Event of Type " + type;
}
}
public static class SimpleCallbackWrapper {
private final PublishSubject<RxConnectionEvent> subject = PublishSubject.create();
public ConnectionRequestListener getListener() {
return new ConnectionRequestListener() {
#Override
public void onConnectDone() {
subject.onNext(new RxConnectionEvent(1));
}
#Override
public void onDisconnectDone() {
subject.onNext(new RxConnectionEvent(2));
}
#Override
public void onInitUDPDone() {
subject.onNext(new RxConnectionEvent(3));
}
};
}
public Observable<RxConnectionEvent> getObservable() {
return subject;
}
}
public static void main(String[] args) throws IOException {
SimpleCallbackWrapper myWrapper = new SimpleCallbackWrapper();
ConnectionRequestListener listner = myWrapper.getListener();// Get the listener and attach it to the game here.
myWrapper.getObservable().observeOn(Schedulers.newThread()).subscribe(event -> System.out.println(event));
listner.onConnectDone(); // Call the listener a few times, the observable should print the event
listner.onDisconnectDone();
listner.onInitUDPDone();
System.in.read(); // Wait for enter
}
}
A more complex solution would be to use one of the onSubscribe implementations to create an observable using Observable.create(). For example AsyncOnSubscibe. This solution has the benefit of handling backperssure properly, so your event subscriber doesn't become overwhelmed with events. But in your case, that sounds like an unlikely scenario, so the added complexity is probably not worth it.
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"));
I created a TableView a while back and registered Properties to each of the TableColumns. Editing of the internal data reflected itself back in the TableView just fine.
With a ListView, however, it is a different story. The changes are not being shown right away unless I close the frame and open it again.
My ListView consists of ActionSteps. Note that I used the Javafx beans properties.
package application.objects;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.function.IntPredicate;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class ActionStep {
private StringProperty actionStepID;
private ObjectProperty<LocalDate> dateSet, dateFinished;
private StringProperty stepName;
private IntegerProperty completion;
private ArrayList<StepComment> comments;
public ActionStep(String name) {
actionStepID = new SimpleStringProperty();
stepName = new SimpleStringProperty();
dateSet = new SimpleObjectProperty<LocalDate>();
dateFinished = new SimpleObjectProperty<LocalDate>();
completion = new SimpleIntegerProperty();
stepName.setValue(name);
}
public void setName(String name) {
stepName.setValue(name);
}
public String getName() {
return stepName.getValue();
}
public StringProperty stepNameProperty() {
return actionStepID;
}
public void setID(String id) {
actionStepID.setValue(id);
}
public String getID() {
return actionStepID.get();
}
public StringProperty actionStepIDProperty() {
return actionStepID;
}
public void setCompletion(int percent) {
if (percent < 0 || percent > 100)
return;
completion.set(percent);
}
public int getCompletion() {
return completion.get();
}
public IntegerProperty completionProperty() {
return completion;
}
public void setDateSet(LocalDate date) {
dateSet.set(date);
}
public LocalDate getDateSet() {
return dateSet.get();
}
public ObjectProperty<LocalDate> dateSetProperty() {
return dateSet;
}
public void setDateFinished(LocalDate date) {
dateFinished.set(date);
}
public LocalDate getDateFinished() {
return dateFinished.get();
}
public ObjectProperty<LocalDate> dateFinishedProperty() {
return dateFinished;
}
public String toString() {
return stepNameProperty().get();
}
}
My ListView uses an ObservableList as well.
#FXML
private ListView<ActionStep> actionStepsListView;
private ObservableList<ActionStep> listOfSteps;
listOfSteps = FXCollections.observableArrayList();
actionStepsListView.setItems(listOfSteps);
if (plan != null) {
ArrayList<ActionStep> arrayOfSteps = plan.getStepsArrayList();
for (int i = 0; i < arrayOfSteps.size(); i++)
listOfSteps.add(arrayOfSteps.get(i));
} else
plan = new ActionPlan();
How come changes made to the ObservableList do not reflect themselves in the ListView? I noticed that the ListView called upon every object's toString() to display their values in the ListView, rather than binding it to their Properties.
What am I doing wrong? Am I supposed to override a cell factory or something?
Note that you're trying to do something more complex with the cells in your ListView than you were with the cells in the TableView. In the TableView, the objects displayed in the cells were changing, so it was easy for the cells to observe this. In the ListView, you want the cells to notice when properties that belong to the objects displayed in the cells change; this is one further step removed, so you have to do a bit of extra coding (though not much, as you'll see).
You could create a custom cell factory to bind to the stepNameProperty(), but it's tricky (you have to make sure to unbind/remove listeners from old items in the updateItem() method).
The easier way, though, which isn't well documented is to use an ObservableList with an extractor defined.
First, fix your method names: you have some weird mismatches in the code you posted. The getX/setX/xProperty method names should all match correctly. I.e. instead of
public void setName(String name) {
stepName.setValue(name);
}
public String getName() {
return stepName.getValue();
}
public StringProperty stepNameProperty() {
return actionStepID;
}
you should have
public final void setName(String name) {
stepName.setValue(name);
}
public final String getName() {
return stepName.getValue();
}
public StringProperty nameProperty() {
return stepName;
}
and similarly for the other property accessor methods. (Obviously, the names of the fields can be anything you like, as they're private.) Making the get/set methods final is good practice.
Then, create the list with an extractor. The extractor is a function that maps each element in the list to an array of Observables which the list will observe. If those values change, it will fire list updates to the list's observers. Since your ActionStep's toString() method references the nameProperty(), I assume you want the ListView to update if the nameProperty() changes. So you want to do
listOfSteps = FXCollections.observableArrayList(
actionStep -> new Observable[] { actionStep.nameProperty() } // the "extractor"
);
actionStepsListView.setItems(listOfSteps);
Note that in earlier versions of JavaFX 2.2 the ListView did not properly observe the list for update events; this was fixed (if I remember correctly) shortly prior to the release of Java 8. (Since you tagged the question JavaFX8, I assume you're using Java 8 and so you should be fine here.)
If you are not using Java 8, you can use the following (equivalent but more verbose) code:
listOfSteps = FXCollections.observableArrayList(
new Callback<ActionStep, Observable[]>() {
#Override
public Observable[] call(ActionStep actionStep) {
return new Observable[] { actionStep.nameProperty() } ;
}
});
actionStepListView.setItems(listOfSteps);
Here is sample how make listview with custom objects:
public class JavaFX_ListView extends Application {
class MyObject {
String day;
int number;
MyObject(String d, int n) {
day = d;
number = n;
}
String getDay() {
return day;
}
int getNumber() {
return number;
}
#Override
public String toString() {
return number + " " + day;
}
}
ObservableList<MyObject> myList;
// Create dummy list of MyObject
private void prepareMyList() {
myList = FXCollections.observableArrayList();
myList.add(new MyObject("Sunday", 50));
myList.add(new MyObject("Monday", 60));
myList.add(new MyObject("Tuesday", 20));
}
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("sample");
prepareMyList();
ListView<MyObject> listView = new ListView<>();
listView.setItems(myList);
Pane root = new Pane();
root.getChildren().add(listView);
primaryStage.setScene(new Scene(root, 300, 250));
primaryStage.show();
// testing
Timer timer = new Timer();
timer.schedule(new UpdateListTask(), 1000, 1000);
}
public static void main(String[] args) {
launch(args);
}
// testing
public class UpdateListTask extends TimerTask {
#Override
public void run() {
Platform.runLater(new Runnable() {
#Override
public void run() {
myList.add(new MyObject("sample", Calendar.getInstance()
.getTime().getSeconds()));
}
});
}
}
}