Issue
it's been two days that I've been struggling to get TableView cells to refresh its content when adding new data to its underlying data model.
It's a simple app that shows contacts information on a TableView. The list of contacts is an ObservableList that is stored in an XML file. The contact data model uses JavaFX properties for its fields.
The new contacts are added by clicking the 'add' menu item in the toolbar (showAddContactDialog()). A dialog gets shown where we can enter the contact informations. Then the contact gets saved in the contacts list and then stored in an xml file that will be loaded in the next launch of the application. When the app is run the contacts gets rendered correctly to the tableview, but when I add a new one it's not added to the tableview but only gets stored in the contact list and the xml file.
I don't know what I'm missing, any help please?
I've tried many solutions on stackoverflow and others but none of them was successful.
Below is the code for Contact class :
public class Contact {
private SimpleStringProperty mFirstName;
private SimpleStringProperty mLastName;
private SimpleStringProperty mPhoneNumber;
private SimpleStringProperty mNotes;
public Contact(){...}
public Contact(String firstName, String lastName, String phoneNumber, String notes){
this.mFirstName = new SimpleStringProperty(firstName);
this.mLastName = new SimpleStringProperty(lastName);
this.mPhoneNumber = new SimpleStringProperty(phoneNumber);
this.mNotes = new SimpleStringProperty(notes);
}
String getFirstName() { return mFirstName.get(); }
public SimpleStringProperty mFirstNameProperty() { return mFirstName; }
void setFirstName(String value) { mFirstName.set(value); }
String getLastName() { return mLastName.get(); }
public SimpleStringProperty mLastNameProperty() { return mLastName; }
void setLastName(String value) { mLastName.set(value); }
String getPhoneNumber() { return mPhoneNumber.get(); }
public SimpleStringProperty mPhoneNumberProperty() { return mPhoneNumber; }
void setPhoneNumber(String value) { mPhoneNumber.set(value); }
String getNotes() { return mNotes.get(); }
public SimpleStringProperty mNotesProperty() { return mNotes;}
void setNotes(String value) { mNotes.set(value); }
Below is the code for ContactData class that is responsible for loading and saving the contacts from the xml file and the operations of adding and deleting contacts from the list :
public class ContactData {
private static final String CONTACTS_FILE = "contacts.xml";
private static final String CONTACT = "contact";
private static final String FIRST_NAME = "first_name";
private static final String LAST_NAME = "last_name";
private static final String PHONE_NUMBER = "phone_number";
private static final String NOTES = "notes";
private ObservableList<Contact> contacts;
public ContactData() {
contacts = FXCollections.observableArrayList();
loadContacts();
}
public void addContact(Contact newContact){
contacts.add(newContact);
saveContacts();
}
public void deleteContact(Contact contact){
contacts.remove(contact);
saveContacts();
}
public ObservableList<Contact> getContacts() {
return FXCollections.observableList(contacts);
}
public void loadContacts() {
try {
// First, create a new XMLInputFactory
XMLInputFactory inputFactory = XMLInputFactory.newInstance();
// Setup a new eventReader
InputStream in = new FileInputStream(CONTACTS_FILE);
XMLEventReader eventReader = inputFactory.createXMLEventReader(in);
// read the XML document
Contact contact = null;
while (eventReader.hasNext()) {
XMLEvent event = eventReader.nextEvent();
if (event.isStartElement()) {
StartElement startElement = event.asStartElement();
// If we have a contact item, we create a new contact
if (startElement.getName().getLocalPart().equals(CONTACT)) {
contact = new Contact();
continue;
}
if (event.isStartElement()) {
if (event.asStartElement().getName().getLocalPart()
.equals(FIRST_NAME)) {
event = eventReader.nextEvent();
contact.setFirstName(event.asCharacters().getData());
continue;
}
}
if (event.asStartElement().getName().getLocalPart()
.equals(LAST_NAME)) {
event = eventReader.nextEvent();
contact.setLastName(event.asCharacters().getData());
continue;
}
if (event.asStartElement().getName().getLocalPart()
.equals(PHONE_NUMBER)) {
event = eventReader.nextEvent();
contact.setPhoneNumber(event.asCharacters().getData());
continue;
}
if (event.asStartElement().getName().getLocalPart()
.equals(NOTES)) {
event = eventReader.nextEvent();
contact.setNotes(event.asCharacters().getData());
continue;
}
}
// If we reach the end of a contact element, we add it to the list
if (event.isEndElement()) {
EndElement endElement = event.asEndElement();
if (endElement.getName().getLocalPart().equals(CONTACT)) {
contacts.add(contact);
}
}
}
}
catch (FileNotFoundException e) {
//e.printStackTrace();
}
catch (XMLStreamException e) {
e.printStackTrace();
}
}
public void saveContacts() {
try {
// create an XMLOutputFactory
XMLOutputFactory outputFactory = XMLOutputFactory.newInstance();
// create XMLEventWriter
XMLEventWriter eventWriter = outputFactory
.createXMLEventWriter(new FileOutputStream(CONTACTS_FILE));
// create an EventFactory
XMLEventFactory eventFactory = XMLEventFactory.newInstance();
XMLEvent end = eventFactory.createDTD("\n");
// create and write Start Tag
StartDocument startDocument = eventFactory.createStartDocument();
eventWriter.add(startDocument);
eventWriter.add(end);
StartElement contactsStartElement = eventFactory.createStartElement("",
"", "contacts");
eventWriter.add(contactsStartElement);
eventWriter.add(end);
for (Contact contact: contacts) {
saveContact(eventWriter, eventFactory, contact);
}
eventWriter.add(eventFactory.createEndElement("", "", "contacts"));
eventWriter.add(end);
eventWriter.add(eventFactory.createEndDocument());
eventWriter.close();
}
catch (FileNotFoundException e) {
System.out.println("Problem with Contacts file: " + e.getMessage());
e.printStackTrace();
}
catch (XMLStreamException e) {
System.out.println("Problem writing contact: " + e.getMessage());
e.printStackTrace();
}
}
private void saveContact(XMLEventWriter eventWriter, XMLEventFactory eventFactory, Contact contact)
throws FileNotFoundException, XMLStreamException {
XMLEvent end = eventFactory.createDTD("\n");
// create contact open tag
StartElement configStartElement = eventFactory.createStartElement("",
"", CONTACT);
eventWriter.add(configStartElement);
eventWriter.add(end);
// Write the different nodes
createNode(eventWriter, FIRST_NAME, contact.getFirstName());
createNode(eventWriter, LAST_NAME, contact.getLastName());
createNode(eventWriter, PHONE_NUMBER, contact.getPhoneNumber());
createNode(eventWriter, NOTES, contact.getNotes());
eventWriter.add(eventFactory.createEndElement("", "", CONTACT));
eventWriter.add(end);
}
private void createNode(XMLEventWriter eventWriter, String name,
String value) throws XMLStreamException {
XMLEventFactory eventFactory = XMLEventFactory.newInstance();
XMLEvent end = eventFactory.createDTD("\n");
XMLEvent tab = eventFactory.createDTD("\t");
// create Start node
StartElement sElement = eventFactory.createStartElement("", "", name);
eventWriter.add(tab);
eventWriter.add(sElement);
// create Content
Characters characters = eventFactory.createCharacters(value);
eventWriter.add(characters);
// create End node
EndElement eElement = eventFactory.createEndElement("", "", name);
eventWriter.add(eElement);
eventWriter.add(end);
}
}
Below is the code for the Controller class :
public class Controller {
@FXML
private TableView<Contact> mContactsTableView;
@FXML
private BorderPane mMainWindow;
private ContactData mContactData;
private ObservableList<Contact> mContactsList;
public void initialize(){
mContactData = new ContactData();
mContactsList = mContactData.getContacts();
mContactsTableView.setItems(mContactsList);
//Initializing the columns of the TableView
TableColumn<Contact, String> firstNameCol = new TableColumn<>("First Name");
firstNameCol.setCellValueFactory(param -> param.getValue().mFirstNameProperty());
TableColumn<Contact, String> lastNameCol = new TableColumn<>("Last Name");
lastNameCol.setCellValueFactory(new PropertyValueFactory<Contact, String>("mLastName"));
TableColumn<Contact, String> phoneCol = new TableColumn<>("Phone Number");
phoneCol.setCellValueFactory(new PropertyValueFactory<Contact, String>("mPhoneNumber"));
TableColumn<Contact, String> notesCol = new TableColumn<>("Notes");
notesCol.setCellValueFactory(new PropertyValueFactory<Contact, String>("mNotes"));
mContactsTableView.getColumns().setAll(firstNameCol, lastNameCol, phoneCol, notesCol);
}
@FXML
private void showAddContactDialog(){
Dialog<ButtonType> dialog = new Dialog<>();
dialog.initOwner(mMainWindow.getScene().getWindow());
dialog.setTitle("ADD NEW CONTACT");
FXMLLoader fxmlLoader = new FXMLLoader();
fxmlLoader.setLocation(getClass()
.getResource("AddContactDialog.fxml"));
try {
dialog.getDialogPane().setContent(fxmlLoader.load());
} catch (IOException e) {
System.out.println("Couldn't load the dialog");
e.printStackTrace();
}
dialog.getDialogPane().getButtonTypes().add(ButtonType.OK);
dialog.getDialogPane().getButtonTypes().add(ButtonType.CANCEL);
Optional<ButtonType> result = dialog.showAndWait();
if (result.isPresent() && result.get() == ButtonType.OK){
AddContactDialogController dialogController = fxmlLoader.getController();
Contact newContact = dialogController.addContact();
}
Below is the code for the AddContactDialogController class :
public class AddContactDialogController {
@FXML
private TextField firstNameField;
@FXML
private TextField lastNameField;
@FXML
private TextField phoneNumberField;
@FXML
private TextField notesField;
public Contact addContact(){
String firstName = firstNameField.getText().trim();
String lastName = lastNameField.getText().trim();
String phoneNumber = phoneNumberField.getText().trim();
String notes = notesField.getText().trim();
Contact newContact = new Contact(firstName, lastName, phoneNumber, notes);
ContactData contactData = new ContactData();
contactData.addContact(newContact);
return newContact;
}
}
Below is mainLayout.fxml file :
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<BorderPane fx:id="mMainWindow" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.Controller">
<top>
<MenuBar>
<menus>
<Menu text="Contacts">
<items>
<MenuItem onAction="#showAddContactDialog" text="Add" />
<MenuItem onAction="#handleEditContact" text="Edit" />
<MenuItem onAction="#handleDeleteContact" text="Delete" />
</items>
</Menu>
</menus>
</MenuBar>
</top>
<center>
<TableView fx:id="mContactsTableView" />
</center>
</BorderPane>
Below is AddContactDialog.fxml file :
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.text.Text?>
<DialogPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
fx:controller="sample.AddContactDialogController">
<header>
<Text text="Add a New Contact"/>
</header>
<content>
<GridPane vgap="10" hgap="10">
<Label text="First Name" GridPane.rowIndex="0" GridPane.columnIndex="0"/>
<TextField fx:id="firstNameField" GridPane.rowIndex="0" GridPane.columnIndex="1"/>
<Label text="Last Name" GridPane.rowIndex="1" GridPane.columnIndex="0"/>
<TextField fx:id="lastNameField" GridPane.rowIndex="1" GridPane.columnIndex="1"/>
<Label text="Phone Number" GridPane.rowIndex="2" GridPane.columnIndex="0"/>
<TextField fx:id="phoneNumberField" GridPane.rowIndex="2" GridPane.columnIndex="1"/>
<Label text="Notes" GridPane.rowIndex="3" GridPane.columnIndex="0"/>
<TextField fx:id="notesField" GridPane.rowIndex="3" GridPane.columnIndex="1"/>
</GridPane>
</content>
</DialogPane>
Solution
You actually have at least two problems with your implementation.
First, your getContacts()
method is returning a new ObservableList
, not the one being updated in the addContact()
method:
public ObservableList<Contact> getContacts() {
return FXCollections.observableList(contacts);
}
Since this new list is the one you've bound to your TableView
, it never gets updated when you add a new contact.
Update your getContact()
method to simply return contacts
. It is already an ObservableList
so there's no need to call FXCollections.observableList()
.
Secondly, within your AddContactDialogController
, you are also creating a new ContactData
class and that is where you're adding the new contact. Again, this is not the same ContactData
or contact
List that you've bound to your TableView
.
You need to instead pass a reference to you original ContactData
object to the AddContactDialogController.addContact()
method...
Answered By - Zephyr