JavaFX TableView rows not refreshing when list item added - java

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()));

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.

Related

Fetching and modifying an object in a set in Java

I have MyFinalSalad class consisting of the following elements:
AppleClass apple;
BananaClass banana;
PearClass pear;
List<SpicesClass> spices;
I have equals implemented such as 2 MyFinalSalad objects are equal, if they have same AppleClass, BananaClass, PearClass objects in them.
Now, I am creating a set of MyFinalSalad objects.
And I have the following code:
MyFinalSalad mySalad = new MyFinalSalad(apple, banana, pear);
SpiceClass cinnamon = new SpiceClass("cinnamon");
if (mySet.contains(mySalad)) {
// I want to fetch mySalad object in the set and add cinnamon to the list of spices
} else {
List<SpiceClass> spices = new ArrayList<>();
spices.add(cinnamon);
mySalad.setSpices(spices);
mySet.add(mySalad);
}
To summarize, if mySalad is already present in mySet, add the spice object to the list of spices in mySalad from mySet, else add mySalad to mySet after creating a new spice list, adding cinnamon to it and inserting list in mySalad.
My question is, if set already has mySalad and I want to add a new spice to the list in that object, how do I achieve it?
From https://stackoverflow.com/a/7283419/887235 I have the following:
mySet.stream().filter(mySalad::equals).findAny().orElse(null).getSpices().add(cinnamon);
Is this the only way or the right way to do it? Or is there a better way?
I was thinking that as I am already entering if after doing a contains check, orElse(null) will never be encountered. Thus null.getSpices() will never occur. Is this assumption correct?
Is there a better way to do it?
I cannot change Set to Map.
Your assumption is correct. The orElse(null) will never take place since you check if the set contains the salad right before. You could replace it with get().
However, I would also go one level before and handle it as an Optional, taking the advantage of isPresent and get method.
Salad mySalad = new Salad();
Optional<Salad> possibleSalad = set.stream().filter(mySalad::equals).findAny();
if (possibleSalad.isPresent()) {
Salad alreadyExistingSalad = possibleSalad.get();
// combine spices
} else {
// add new salad
}

Separate a list of items into new, present and present-and-needs-stuff-done-to-it

I have a list of items which I want in my database. Some of the items are new and have to be saved, others are already in the database, but have to be updated and some of those that need an update might be eligible for a special treatment.
Now I just run through them and put them in other lists according to their properties and then hand the lists to the respective database (or special) methods that deal with it.
I just don't think it's pretty, it does a bit much and it has nested ifs.
But can't really come up with a nicer way of doing this.
Here is the (slightly simplified) code
List<Item> newItemList = new ArrayList<>();
List<Item> existingItemList = new ArrayList<>();
List<Item> specialItemList = new ArrayList<>();
for(Item item : items)
{
if(item.isNew())
{
newItemList.add(item);
}
else
{
if(item.isSpecial())
{
specialItemList.add(item);
}
existingItemList.add(item);
}
}
itemHandler.saveItems(newItemList);
itemHandler.updateItems(existingItemList);
specialManager.specialStuff(specialItemList);
Your current code does exactly what is required. Your requirement calls for three different lists, and you iterate items once to create them. It is the most efficient approach. You could write the code in three lines - see below - (the helper method is for clarity) - but then you iterate items three times (but don't require 3 lists). I believe any attempt to make 'nicer' code loses some optimization.
itemHandler.saveItems(createList(items, i -> i.isNew()));
specialManager.specialStuff(createList(items, i -> !i.isNew() && i.isSpecial()));
itemHandler.updateItems(createList(items, i -> !i.isNew()));
Helper method:
public List<Item> createList(List<Item> allItems, Predicate<Item> p) {
return allItems.stream().filter(p).collect(Collectors.toList());
}
With Java 8, you could simplify by grouping the Lists by State (new or existing).
An enum could represent the state : enum State {NEW, EXISTING} and the Item class should declare a State getState() method.
Map<ItemState, List<Item>> itemListByState =
items.stream()
.collect(Collectors.groupingBy(Item::getState));
itemHandler.saveItems(itemsByState.get(State.NEW));
itemHandler.updateItems(itemsByState.get(State.EXISTING));
specialManager.specialStuff(itemsByState.get(State.EXISTING).stream()
.filter(Item::isSpecial)
.collect(Collectors.toList()));
You could of course introduce intermediary variables for the Lists but I don't think that it is really required and it reduces potential side effects between them.
Try creating another method like:
boolean add(List<Item> items, Item item, boolean doAdd) {
if (doAdd) {
items.add(item);
}
}
And call it like:
add (newItemList, item, item.isNew());
add (specialItemList, item, item.isSpecial() & !item.isNew());
add (existingItemList, item, !item.isNew());

Javafx How to make a Binding to a list of Properties

I want to order a random number of Nodes according to their width. But I fail to calculate the sum of the width (using their properties), I have the following example code - I fail to get informed about the change of one of the properties:
#Override
public void start(Stage arg0) throws Exception {
List<SimpleIntegerProperty> l = IntStream.range(0, 10)
.mapToObj(SimpleIntegerProperty::new)
.collect(Collectors.toList());
ObservableList<IntegerProperty> widthAr = FXCollections.observableArrayList();
widthAr.addAll(l);
IntegerBinding nextheight = Bindings.createIntegerBinding(() -> widthAr.stream()
.mapToInt(IntegerProperty::get)
.sum(), widthAr);
nextheight.addListener((v, o, n) -> System.out.println("New value: " + v.getValue()));
//Now change randomly one of the IntegerProperties
ScheduledExecutorService tfsqueryScheduler = Executors.newScheduledThreadPool(1);
tfsqueryScheduler.scheduleAtFixedRate(() -> {
System.out.println("Changing");
int i = (int) Math.round(Math.random() * 9.4);
SimpleIntegerProperty v = l.get(i);
v.set(0);
}, 0, 3, TimeUnit.SECONDS);
System.out.println("Start...");
}
The nextheight.addListener is never called :( ... any ideas?
Thanks!
By default, ObservableLists only fire updates if the structure of the list changes (e.g. items are added to or removed from the list), not if the state of individual elements in the list changes. To create a list which fires notifications if properties belonging to any of its elements change, you need to create the list with an extractor.
In this case the property you are interested in is just the list element itself, so you need to replace
ObservableList<IntegerProperty> widthAr = FXCollections.observableArrayList();
with
ObservableList<IntegerProperty> widthAr =
FXCollections.observableArrayList(w -> new Observable[] {w});
Note also that, depending on your real use case, you may need to ensure your binding is not prematurely garbage collected, by making it a field instead of a local variable.
When creating the IntegerProperty here:
Bindings.createIntegerBinding(() -> widthAr.stream()
.mapToInt(IntegerProperty::get)
.sum(), widthAr);
you also need to add all the elements in widthAr as dependencies, because the list will not notify should it's element change, only if an element gets added or removed.
NOTE: This would not work if removing or adding elements from the List, but you don't do that.
Here's a example i used in a project. The model is about a Group with a list of recipes, and each Recipe has a list of Materials with name and amount. This only holds the structure and doesn't have any prices loaded.
The prices are pulled from a external list of reagents. So first i had to map all reagents to each material.
grupos.stream().map(Grupo::getRecetas).flatMap(List::stream).map(Receta::getObsMateriales).flatMap(List::stream)
.forEach(material -> material.setReagent(buscarReagent(material.getNombre())));
Next, after all prices are loaded to the material's reagentProperty. It's time create a binding that sums the cost from all material*amount. I also setted the recipe's reagentProperty while streaming but it has nothing to do with the sum. Since everything is done with bindings if the source reagent updates it's price this sum should update dynamically.
grupos.stream().map(Grupo::getRecetas).flatMap(List::stream).forEach(receta -> {
receta.setReagent(buscarReagent(receta.getNombre()));//doesn't affect the sum binding
receta.costoProduccionProperty()
.bind(Bindings.createLongBinding(
() -> receta.getObsMateriales().stream().mapToLong(m -> m.getReagent().getValue()*m.getCantidad()).sum(),
receta.getObsMateriales()));
});

Fitler JTable of contents of an ArrayList

I have a simple library application, and one thing it needs to accomplish is checkout/return books. In that checkout/return window, I have two tables, a "Book List" table, which starts full of books I already have stored, and a "Checkout/Return" table, which starts out empty. The Checkout/Return table is populated by double clicking items in the Book List table, and then depopulated by double clicking items displayed within itself. Double clicking adds or removes the book items to/from a checkout/return ArrayList (which is then displayed in the table).
What I'm looking for is a way to filter out books in the Book List table by the contents of the Checkout/Return ArrayList. (Unable to post images of desired effect at this time).
Is this even possible, and if so, how similar is this to filtering by the contents of a JTextField?
Edit: I've since found this approach: http://pulkitsinghal.blogspot.com/2010/06/multiple-row-filters-for-jtable.html.
RowFilter<TableModel, Object> firstFiler = null;
RowFilter<TableModel, Object> secondFilter = null;
List<RowFilter<TableModel,Object>> filters = new ArrayList<RowFilter<TableModel,Object>>();
RowFilter<TableModel, Object> compoundRowFilter = null;
try {
firstFiler = RowFilter.regexFilter(yourRegexString, columnIndex);
secondFilter = RowFilter.regexFilter(yourRegexString, columnIndex);
filters.add(firstFiler);
filters.add(secondFilter);
compoundRowFilter = RowFilter.andFilter(filters); // you may also choose the OR filter
} catch (java.util.regex.PatternSyntaxException e) {
return;
}
sorter.setRowFilter(compoundRowFilter);
However, this achieves the opposite affect I want. Rather than filtering out everything that doesn't match the filter input, I want to filter out everything that does match the filter input.

removing multiple selections from JList

I have a JList with some elements where multiple selection is allowed. Before these elements are added to the JList, some information about them is being stored in a static HashMap in a separate class. When more than 1 items are selected and the 'Remove selected' button is pressed, I am trying to remove the selected items (which works fine) and also delete their records from the HashMap. For some reason though, if I select more than 1 elements, only the first record in the HashMap is removed. I don't understand how this works for the JList but doesn't work for the HashMap. My code below:
remove.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
Object[] selectedElementsValues = jList.getSelectedValues();
for (int i = 0; i < selectedElementsValues.length; i++) {
System.out.println(jList.getSelectedValue().toString());
System.out.println(PersonClass.map.get(jList.getSelectedValue().toString()));
PersonClass.map.remove(jList.getSelectedValue().toString());
System.out.println(PersonClass.map);
}
It works fine if I select only one item at a time and remove it. But not with multiple selection. The items from the JList are removed properly, though, so I don't see why it doesn't do the same for the map.
Thx
The problem is that the loop that removes items from the map uses jList.getSelectedValue().toString(), when the jList selection is not modified. You can use the selection array you obtained earlier:
for (Object o : selectedValues) {
PersonClass.map.remove(o.toString());
}
Note that getSelectedValues() is deprecated, and you should use getSelectedValuesList() instead.

Categories