문제

I'm sure it's a common programming paradigm to have a tableview backed by a database (JPA 2 using EclipseLink in our case). But getting the UI right is proving very difficult. I don't have much experience with modern UI design which I'm sure is causing me to miss an otherwise obvious solution.

Our tables are supposed to allow the user to insert, delete, make changes, and then save or discard the set of changes. Inserts and changes need to be validated. We currently do this by committing the change, and on failure rollback and replay the set of changes to the database except for the failing one and leaving the UI unchanged so that users can fix the error without retyping everything.

Our TableView is backed by an ObservableList of data obtained from the JPA source. Inserts and deletes are pretty straight forward. We can obtain information about what has been added to the list or removed using change listeners on the list. However, I haven't been able to come up with a reliable way to detect changes to an existing item.

The current design is a hack job done by someone else and has to be rearchitected that depends on TableRow focus changes listeners, but it very unreliable. What is the usual means of monitoring table changes, validating each change, and when the changes are invalid, rolling back the database changes, and reapplying the visible changes to the table?

An existing example application would be superb but I haven't found any available that support transactions. Barring that, a model diagram would be very helpful.

도움이 되었습니까?

해결책

In theory, you can monitor items in an observable list for changes by creating the list with an extractor. The idea is, for a TableView, you specify a Callback which provides an array of Observables for each object in the list. The observables will be observed by the list and any ListChangeListeners registered with the list will be notified (via a change with wasUpdated() returning true) when existing items have changes to the specified properties.

However, this only seems to work in Java 8; there's likely a bug filed somewhere for JavaFX 2.2.

Here's an example, based on the usual TableView sample.

import java.util.Arrays;
import java.util.List;

import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.Callback;

public class TableUpdatePropertyExample extends Application {

    @Override
    public void start(Stage primaryStage) {
        final TableView<Person> table = new TableView<>();
        table.setEditable(true);
        final TableColumn<Person, String> firstNameCol = createTableColumn("First Name");
        final TableColumn<Person, String> lastNameCol = createTableColumn("Last Name");
        final TableColumn<Person, String> emailCol = createTableColumn("Email");
        table.getColumns().addAll(Arrays.asList(firstNameCol, lastNameCol, emailCol));
        final List<Person> data = Arrays.asList(
                new Person("Jacob", "Smith", "jacob.smith@example.com"),
                new Person("Isabella", "Johnson", "isabella.johnson@example.com"),
                new Person("Ethan", "Williams", "ethan.williams@example.com"),
                new Person("Emma", "Jones", "emma.jones@example.com"),
                new Person("Michael", "Brown", "michael.brown@example.com")
        );
        table.setItems(FXCollections.observableList(data, new Callback<Person, Observable[]>() {
            @Override
            public Observable[] call(Person person) {
                return new Observable[] {person.firstNameProperty(), person.lastNameProperty(), person.emailProperty()};
            }
        }));

        table.getItems().addListener(new ListChangeListener<Person>() {
            @Override
            public void onChanged(
                    javafx.collections.ListChangeListener.Change<? extends Person> change) {
                while (change.next()) {
                    if (change.wasUpdated()) {
                        Person updatedPerson = table.getItems().get(change.getFrom());
                        System.out.println(updatedPerson+" was updated");
                    }
                }
            }
        });
        final BorderPane root = new BorderPane();
        root.setCenter(table);
        final Scene scene = new Scene(root, 600, 400);
        primaryStage.setScene(scene);
        primaryStage.show();

    }

    private TableColumn<Person, String> createTableColumn(String title) {
        TableColumn<Person, String> col = new TableColumn<>(title);
        col.setCellValueFactory(new PropertyValueFactory<Person, String>(makePropertyName(title)));
        col.setCellFactory(TextFieldTableCell.<Person>forTableColumn());
        return col ;
    }

    private String makePropertyName(String text) {
        boolean first = true ;
        StringBuilder prop = new StringBuilder();
        for (String word : text.split("\\s")) {
            if (first) {
                prop.append(word.toLowerCase());
            } else {
                prop.append(Character.toUpperCase(word.charAt(0)));
                if (word.length() > 1) {
                    prop.append(word.substring(1));
                }
            }
            first=false ;
        }
        return prop.toString();
    }

    public static class Person {
        private final StringProperty firstName ;
        private final StringProperty lastName ;
        private final StringProperty email ;
        public Person(String firstName, String lastName, String email) {
            this.firstName = new SimpleStringProperty(this, "firstName", firstName);
            this.lastName = new SimpleStringProperty(this, "lastName", lastName);
            this.email = new SimpleStringProperty(this, "email", email);
        }
        public String getFirstName() {
            return firstName.get();
        }
        public void setFirstName(String firstName) {
            this.firstName.set(firstName);
        }
        public StringProperty firstNameProperty() {
            return firstName ;
        }
        public String getLastName() {
            return lastName.get();
        }
        public void setLastName(String lastName) {
            this.lastName.set(lastName);
        }
        public StringProperty lastNameProperty() {
            return lastName ;
        }
        public String getEmail() {
            return email.get();
        }
        public void setEmail(String email) {
            this.email.set(email);
        }
        public StringProperty emailProperty() {
            return email ;
        }
        @Override
        public String toString() {
            return getFirstName() + " " + getLastName();
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}

For validation, you might consider overriding the set and setValue methods in the model class's property. I haven't tried this, and I suspect you may need to mess with the TableCell in order to make it work properly, but something along the lines of:

this.email = new StringPropertyBase(email) {

            final Pattern pattern = Pattern.compile("[a-zA-Z_0-9]+@[a-zA-Z0-9.]+");

            @Override
            public String getName() {
                return "email";
            }

            @Override
            public Object getBean() {
                return Person.this;
            }

            @Override
            public void set(String email) {
                if (pattern.matcher(email).matches()) {
                    super.set(email);
                }
            }

            @Override
            public void setValue(String email) {
                if (pattern.matcher(email).matches()) {
                    super.setValue(email);
                }
            }
        };

in place of the one liner in the Person class above.

UPDATE: To make this work in JavaFX 2.2, you can basically roll your own version of the "extractor". It's a bit of work but not too bad. Something like the following seems to work, for example:

final List<Person> data = Arrays.asList(
            new Person("Jacob", "Smith", "jacob.smith@example.com"),
            new Person("Isabella", "Johnson", "isabella.johnson@example.com"),
            new Person("Ethan", "Williams", "ethan.williams@example.com"),
            new Person("Emma", "Jones", "emma.jones@example.com"),
            new Person("Michael", "Brown", "michael.brown@example.com")
    );

    final ChangeListener<String> firstNameListener = new ChangeListener<String>() {
        @Override
        public void changed(ObservableValue<? extends String> obs,
                String oldFirstName, String newFirstName) {
            Person person = (Person)((StringProperty)obs).getBean();
            System.out.println("First name for "+person+" changed from "+oldFirstName+" to "+newFirstName);
        }
    };

    final ChangeListener<String> lastNameListener = new ChangeListener<String>() {
        @Override
        public void changed(ObservableValue<? extends String> obs,
                String oldLastName, String newLastName) {
            Person person = (Person)((StringProperty)obs).getBean();
            System.out.println("Last name for "+person+" changed from "+oldLastName+" to "+oldLastName);
        }
    };

    final ChangeListener<String> emailListener = new ChangeListener<String>() {
        @Override
        public void changed(ObservableValue<? extends String> obs,
                String oldEmail, String newEmail) {
            Person person = (Person)((StringProperty)obs).getBean();
            System.out.println("Email for "+person+" changed from "+oldEmail+" to "+oldEmail);
        }
    };

    table.getItems().addListener(new ListChangeListener<Person>() {
        @Override
        public void onChanged(
                javafx.collections.ListChangeListener.Change<? extends Person> change) {
            while (change.next()) {
                for (Person person : change.getAddedSubList()) {
                    person.firstNameProperty().addListener(
                            firstNameListener);
                    person.lastNameProperty().addListener(lastNameListener);
                    person.emailProperty().addListener(emailListener);
                }
                for (Person person : change.getRemoved()) {
                    person.firstNameProperty().removeListener(
                            firstNameListener);
                    person.lastNameProperty().removeListener(
                            lastNameListener);
                    person.emailProperty().removeListener(emailListener);
                }
            }
        }
    });

    table.getItems().addAll(data);

다른 팁

You should check Granite Data Services which is a framework that has a great integration of JPA features within JavaFX: https://www.granitedataservices.com/

It can deal with lazy-loading, entity conflicts, EJB cache in the JavaFX side, bean validation, synchronisation of data on multiple clients...

You should check the example which is on github, I think it is a great start: https://github.com/graniteds/shop-admin-javafx

It is a bit complex, but JPA management is indeed complex. I have used Granite for years in Flex projects and probably will use it with JavaFX, it is already production-ready.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top