Reputation: 33
So I am creating a UI with JavaFX for a server controller, what it is does not matter, all that is important is that the server.getClients(); returns an ArrayList of IClients.
I wish to display these clients (they are represented by IP but once again, this doesn't seem relevant) in a ListView. However, clients may connect at any given point in time and when this happens, they get added to the server's IClient ArrayList. When this List is updated, I want the ListView to refresh and show the new client. For some reason, I simply cannot get this to work.
I am very new to JavaFX and I think I might be overseeing something. I'm very sorry if this is a duplicate or obvious, I have searched for a long time over the past couple of days but I might have overlooked a solution.
The following code is the abbreviated version of my FXMLController for the JavaFX application:
/*imports*/
public class FXMLController implements Initializable {//serverUI.FXMLController
@FXML private ListView clientListView;
/*some more (irrelevant) code*/
private IServer server;
/*some more (irrelevant) code*/
private ObservableList<IClient> serverClientsObservableList;
@Override
public void initialize(URL url, ResourceBundle rb) {
System.out.println("initialization...");
/*some more (irrelevant) code*/
//the server was started here
// FXML Controls
initClientListView();
/*some more (irrelevant) code*/
System.out.println("initialized");
}
/*some more (irrelevant) code*/
private void initClientListView() {
System.out.println("clientListView");
serverClientsObservableList = FXCollections.observableList(server.getClients());
serverClientsObservableList.addListener(new ListChangeListener<IClient>() {
@Override
public void onChanged(ListChangeListener.Change<? extends IClient> change) {
System.out.println("list change detected");
//is any of the followin three lines really necessary to update the ListView content?
serverClientsObservableList.setAll(server.getClients());
clientListView.setItems(null);
clientListView.setItems(serverClientsObservableList);
}
});
clientListView.setItems(serverClientsObservableList);
}
/*some more (irrelevant) code*/
}
EDIT:
I don't want to refresh the ListView when something in the IClients changes, nothing changes in them. I want to refresh the ListView when a NEW IClient is ADDED to the server's client list. The ListView should show the NEW IClient
EDIT2:
According to the suggested duplicate I tried the following, however I don't really understand what it's doing and how it works. This did not solve the problem, it gives me an error when I try to create the new Observable[]
Callback<IClient, Observable[]> extractor = new Callback<IClient, Observable[]>() {
@Override
public Observable[] call(IClient c) {
return new Observable[] {c.getNameProperty()};
}
};
ObservableList<IClient> clientOList = FXCollections.observableArrayList(extractor);
Additionally: the code where I add the clients to the server. Long story short, this is an assignment where we have to user RMI in an inverse way, the server commands the clients. Clients register themselves to the server's list and that's where they're added to the IClient list.
package serviceImplementation;
import commandService.ICommand;
import commandServiceImplementation.CommandResult;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import service.IClient;
import service.IServer;
public class ServerService extends UnicastRemoteObject implements IServer {
private ArrayList<IClient> clients;
public ServerService() throws RemoteException {
clients = new ArrayList<>();
}
@Override
public boolean register(IClient client) throws RemoteException {
if(!clients.contains(client)) {
clients.add(client);
return true;
}
return false;
}
@Override
public boolean unRegister(IClient client) throws RemoteException {
if(clients.contains(client)) {
clients.remove(client);
return true;
}
return false;
}
@Override
public String ping() throws RemoteException {
long arrival = System.currentTimeMillis();
System.out.println("Got pinged at [" + arrival + "]");
return ("server ponged [" + arrival + "]");
}
@Override
public CommandResult sendCommand(ICommand command, IClient targetClient) throws RemoteException {
if(clients.contains(targetClient)) {
return clients.get(clients.indexOf(targetClient)).executeCommand(command);
}
return null;
}
@Override
public ArrayList<IClient> getClients() {
return clients;
}
}
Upvotes: 3
Views: 13969
Reputation: 209225
Your observable list is created as a wrapper for the underlying list in the ServerService
using FXCollections.observableList(...)
. The observable list that is returned by this just wraps the underlying list, so it always contains the same elements as the underlying list. However, as noted in the documentation:
mutation operations made directly to the underlying list are not reported to observers of any ObservableList that wraps it.
When clients are registered or unregistered in the server service, you add them to the underlying list. Since the underlying list is not an observable list, no notifications are fired, and so the ListView
does not know to refresh itself.
One possible solution may be to use an ObservableList
in the ServerService
:
public class ServerService extends UnicastRemoteObject implements IServer {
private ObservableList<IClient> clients;
public ServerService() throws RemoteException {
clients = FXCollections.observableArrayList();
}
// ...
@Override
public ObservableList<IClient> getClients() {
return clients;
}
}
and then you do
private void initClientListView() {
clientListView.setItems(server.getClients());
}
Note that this couples your ServerService
to the JavaFX API; this is probably not too bad as the JavaFX Collections API does not rely on any UI elements at all.
However, the code above will not work if your clients are registered/unregistered on a background thread (i.e. not on the FX Application Thread), which is almost certainly the case. Because of this, you need to make the following changes to make this work:
@Override
public boolean register(IClient client) throws RemoteException {
FutureTask<Boolean> register = new FutureTask<>(() ->
if(!clients.contains(client)) {
clients.add(client);
return true;
}
return false;
);
Platform.runLater(register);
return register.get();
}
@Override
public boolean unRegister(IClient client) throws RemoteException {
FutureTask<Boolean> unRegister = new FutureTask<>(() ->
if(clients.contains(client)) {
clients.remove(client);
return true;
}
return false;
);
Platform.runLater(unRegister);
return unRegister.get();
}
Now your ServerService
has a much stronger dependency on JavaFX, because it assume the FX Application Thread is running. You didn't make any specifications about how this is being used, but there's a good chance you don't want this coupling.
An alternative is to support callbacks in the ServerService
. You can represent these pretty simply using a Consumer<IClient>
. This looks something like:
public class ServerService extends UnicastRemoteObject implements IServer {
private ArrayList<IClient> clients;
private Consumer<IClient> registerCallback = client -> {} ;
private Consumer<IClient> unregisterCallback = client -> {} ;
public ServerService() throws RemoteException {
clients = new ArrayList<>();
}
public void setRegisterCallback(Consumer<IClient> registerCallback) {
this.registerCallback = registerCallback ;
}
public void setUnregisterCallback(Consumer<IClient> unregisterCallback) {
this.unregisterCallback = unregisterCallback ;
}
@Override
public boolean register(IClient client) throws RemoteException {
if(!clients.contains(client)) {
clients.add(client);
registerCallback.accept(client);
return true;
}
return false;
}
@Override
public boolean unRegister(IClient client) throws RemoteException {
if(clients.contains(client)) {
clients.remove(client);
unregisterCallback.accept(client);
return true;
}
return false;
}
@Override
public String ping() throws RemoteException {
long arrival = System.currentTimeMillis();
System.out.println("Got pinged at [" + arrival + "]");
return ("server ponged [" + arrival + "]");
}
@Override
public CommandResult sendCommand(ICommand command, IClient targetClient) throws RemoteException {
if(clients.contains(targetClient)) {
return clients.get(clients.indexOf(targetClient)).executeCommand(command);
}
return null;
}
@Override
public ArrayList<IClient> getClients() {
return clients;
}
}
and now in your UI code you do
private void initClientListView() {
System.out.println("clientListView");
serverClientsObservableList = FXCollections.observableArrayList(server.getClients());
server.setRegisterCallback(client -> Platform.runLater(() ->
serverClientsObservableList.add(client)));
server.setUnregisterCallback(client -> Platform.runLater(() ->
serverClientsObservableList.remove(client)));
clientListView.setItems(serverClientsObservableList);
}
Upvotes: 4