0
votes

I have two controllers, one main screen that shows a list of parts and a button to open a second controller window to add a new part.

When I add a part, it adds to the arraylist just fine, but the tableview does not update. I have a search field at the top, and if I hit the button it shows me the new item, but nothing I have tried will get the table to refresh and display the new item when it is added.

Here is the MainController

@Override
public void initialize(URL url, ResourceBundle rb) {
    if (firstLoad) {
        inventory.addPart(new Part(1,"Widget",1.13,5,1,8));
        inventory.addPart(new Part(2,"Sprocket",2.88,5,1,8));
        inventory.addPart(new Part(3,"Gear",3.46,5,1,8));

        firstLoad = false;
    }

    colPartID.setCellValueFactory(new PropertyValueFactory<>("partID"));
    colPartName.setCellValueFactory(new PropertyValueFactory<>("name"));
    colPartInvLevel.setCellValueFactory(new PropertyValueFactory<>("inStock"));
    colPartPrice.setCellValueFactory(new PropertyValueFactory<>("price"));

    tblParts.setItems(FXCollections.observableArrayList(inventory.getAllParts()));

}

inventory class has a static arraylist of parts

private static ArrayList<Part> allParts = new ArrayList<>();

and the addpartcontroller just adds to the arraylist, which works just fine

inv.addPart(new Part(1,"test",2,3.46));
stage.close();

After the stage is closed, the main screen doesn't seem to update at all, the parts table view still has the 3 parts in it

If I leave the search textfield blank and hit the search button, the 4th part shows up

        FilteredList<Part> filteredData = new FilteredList<>(FXCollections.observableArrayList(inventory.getAllParts()), p -> true);

        String newValue = txtPartSearch.getText();

        filteredData.setPredicate(part -> {
            if (newValue == null || newValue.isEmpty()) {
                return true;
            }

            String lowerCaseFilter = newValue.toLowerCase();
            if (part.getName().toLowerCase().contains(lowerCaseFilter)) {
                return true;
            }
            return Integer.toString(part.getPartID()).equals(lowerCaseFilter);
        });

        SortedList<Part> sortedData = new SortedList<>(filteredData);
        sortedData.comparatorProperty().bind(tblParts.comparatorProperty());
        tblParts.setItems(sortedData);

I have tried tblParts.refresh(), I have also tried having the addpartcontroller call a method in maincontroller to set the table before closing, but the table never refreshes unless I call the search method.

EDIT:

Everything works fine if all done within the maincontroller. For instance if I remove the entire search code above and replace it with the following two lines (which execute on the main controller when pressed), then the table on the main controller updates to show the new item right away.

inventory.addPart(new Part(6,"Test",5.23,4,2,8));
tblParts.setItems(FXCollections.observableArrayList(inventory.getAllParts()));
1

1 Answers

1
votes

There are a couple things wrong with your code. The most relevant being that you use FXCollections.observableArrayList(Collection). This Javadoc of this method states:

Creates a new observable array list and adds a content of collection col to it.

In other words, it copies the Collection into a new ObservableList that is backed by an ArrayList. Any updates to the original Collection will never even be added to the ObservableList. You should be using FXCollections.observableList(List) if you want the passed List to be the backing List.

The Javadoc for FXCollections.observableList(List) (emphasis mine):

Constructs an ObservableList that is backed by the specified list. Mutation operations on the ObservableList instance will be reported to observers that have registered on that instance. Note that mutation operations made directly to the underlying list are not reported to observers of any ObservableList that wraps it.

This Javadoc hints at the second issue. Unless you are doing differently in code you haven't posted it appears you add the elements to the ArrayList field (named allParts). Because of this the ObservableList is never aware anything changes and thus no change events are fired. The firing of change events is coded in the ObservableList. If you want to be notified of changes you must only access the list via the ObservableList that wraps the ArrayList.

In this case, your code would still work (when you call tableView.refresh()) if it wasn't for the fact you also wrap your ObservableList in a FilteredList. A FilteredList creates a "view" of the backing ObservableList. If the backing ObservableList never fires any changes the FilteredList is never notified of any changes which means it never knows to update this "view". This means when you add elements to the ArrayList these new elements are "outside the view" (if you were to replace an element that is "inside the view" the change would be visible).

You can see this behavior with the following code:

import java.util.*;
import javafx.collections.*;
import javafx.collections.transformation.*;

public class Main {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("Hello");
        list.add("World");

        ObservableList<String> obsList = FXCollections.observableList(list);
        FilteredList<String> filteredList = new FilteredList<>(obsList);

        System.out.println("INITIAL STATE");
        System.out.printf("\tList: %s%n", list);
        System.out.printf("\tFilteredList: %s%n", filteredList);

        list.add("Goodbye");
        list.add("World");

        System.out.println("AFTER ADDING ELEMENTS");
        System.out.printf("\tList: %s%n", list);
        System.out.printf("\tFilteredList: %s%n", filteredList);

        list.set(0, "Foo");
        list.set(1, "Bar");

        System.out.println("AFTER REPLACING ELEMENTS");
        System.out.printf("\tList: %s%n", list);
        System.out.printf("\tFilteredList: %s%n", filteredList);
    }

}

Which prints out:

INITIAL STATE
        List: [Hello, World]
        FilteredList: [Hello, World]
AFTER ADDING ELEMENTS
        List: [Hello, World, Goodbye, World]
        FilteredList: [Hello, World]
AFTER REPLACING ELEMENTS
        List: [Foo, Bar, Goodbye, World]
        FilteredList: [Foo, Bar]

Taking all this into account the easiest way to fix your code would be to make allParts an ObservableList. Otherwise you must take care to only use the ObservableList that you created around the ArrayList.


Edit

You also mention that, "If I leave the search textfield blank and hit the search button, the 4th part shows up". I want to address this. Here is your code with explanations why the new Part shows up in the TableView when you hit search:

/*
 * I extracted the FXCollections.observableArrayList(Collection) out of the FilteredList
 * constructor to more easily see what is going on.
 */

/* 
 * You create a **new** ObservableList using FXCollections.observableArrayList(Collection). This basically
 * creates a *snapshot* of the List returnd by getAllParts() as it currently is. At this point the 4th
 * Part is in that returned List. This means the newly created ObservableList will also contian the new
 * Part (since observableArrayList(Collection) copies the data). However, the *old* ObservableList that
 * was already set on the TableView *does not* contain that 4th Part.
 */
ObservableList<Part> parts = FXCollections.observableArrayList(inventory.getAllParts());

// You create a FilteredList that performs no filtering around "parts". Note
// here that a Predicate that always returns true is equivalent to passing null
// as the Predicate.
FilteredList<Part> filteredData = new FilteredList<>(parts, p -> true);

// Get the search criteria
String newValue = txtPartSearch.getText();

filteredData.setPredicate(part -> {
    if (newValue == null || newValue.isEmpty()) {
        return true; // don't filter if there is no search criteria
                     // since the newValue will be null or blank in this
                     // case no Parts are filtered
    }

    // filter based on lower-case names and search critiera
    String lowerCaseFilter = newValue.toLowerCase();
    if (part.getName().toLowerCase().contains(lowerCaseFilter)) {
        return true;
    }
    // else filter by ID
    return Integer.toString(part.getPartID()).equals(lowerCaseFilter);
});

// Wrap the FilteredList in a SortedList and bind the comparatorProperty to
// the comparatorProperty of the TableView (allows sorting by column).
SortedList<Part> sortedData = new SortedList<>(filteredData);
sortedData.comparatorProperty().bind(tblParts.comparatorProperty());

// Set the sortedData to the TableView
tblParts.setItems(sortedData);

So, the fundamental reason why when you search you see the new Part show up is because you are creating a new ObservableList every time you search. This new ObservableList has the most recent state of the getAllParts() List. Also, as I already mentioned in the comments, your edit is basically doing the same thing as your sorting code. Since you do:

inventory.addPart(new Part(6,"Test",5.23,4,2,8));
tblParts.setItems(FXCollections.observableArrayList(inventory.getAllParts()));

which adds the Part before creating the ObservableList. Again, FXCollections.observableArrayList(Collection) takes a snapshot of the Collection which, when the method is called here, contains that new Part. If you were to flip the code to:

tblParts.setItems(FXCollections.observableArrayList(inventory.getAllParts()));
inventory.addPart(new Part(6, "Test", 5.23, 4, 2, 8));

then the TableView's items property will not contain the new Part. However, the allParts ArrayList in inventory will.