Consider the following GUI screen (Java Swing) with a list of persons where the user can select a person and delete it.
According to MVC, the view observes (observer pattern) the model and updates itself. Business logic and domain data in MVC goes to model (as far as I am aware of). My question is, should the model be like a service and have a personList.delete(Person p)
method or have a selectedPerson field and be personList.delete()
.
I have two alternate versions of this GUI and I want to know which one is "more MVC".
The one is:
public class Example {
public static void main(String[] args) {
SwingUtilities.invokeLater(Example::runGui);
}
public static void runGui() {
Persons persons = new Persons();
DefaultListModel<Person> personsSwingModel = new DefaultListModel<>();
JList<Person> personsJList = new JList<>(personsSwingModel);
personsJList.setVisibleRowCount(15);
personsJList.setCellRenderer(new DefaultListCellRenderer());
persons.addPropertyChangeListener("data", e -> {
syncListData(personsSwingModel, persons);
});
syncListData(personsSwingModel, persons);
personsJList.getSelectionModel().addListSelectionListener(e -> {
persons.setSelectedPerson(personsJList.getSelectedValue());
});
persons.addPropertyChangeListener("selection", e -> {
personsJList.setSelectedValue(persons.getSelectedPerson().orElse(null), true);
});
JButton deleteButton = new JButton("Delete selected");
deleteButton.addActionListener(e -> {
persons.delete();
});
JFrame frame = new JFrame("Example");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new JScrollPane(personsJList), BorderLayout.CENTER);
frame.add(deleteButton, BorderLayout.PAGE_END);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
private static void syncListData(DefaultListModel<Person> personsSwingModel, Persons persons) {
personsSwingModel.removeAllElements();
persons.getPersons().forEach(personsSwingModel::addElement);
}
private static class Persons {
private List<Person> persons;
private SwingPropertyChangeSupport listeners;
private Person selectedPerson;
public Persons() {
persons = new ArrayList<>();
persons.add(new Person("Mike", "Random"));
persons.add(new Person("Alice", "Something"));
persons.add(new Person("John", "Rambo"));
persons.add(new Person("Jack", "Sparrow"));
listeners = new SwingPropertyChangeSupport(this);
}
public List<Person> getPersons() {
return persons;
}
public void delete() {
if (selectedPerson == null)
return;
persons.remove(selectedPerson);
listeners.firePropertyChange("data", null, null);
}
public void setSelectedPerson(Person selectedPerson) {
this.selectedPerson = selectedPerson;
listeners.firePropertyChange("selection", null, null);
}
public Optional<Person> getSelectedPerson() {
return Optional.ofNullable(selectedPerson);
}
public void addPropertyChangeListener(String property, PropertyChangeListener listener) {
listeners.addPropertyChangeListener(property, listener);
}
}
private static class Person {
String firstName;
String lastName;
public Person(String first, String last) {
this.firstName = first;
this.lastName = last;
}
@Override
public String toString() {
return firstName + " - " + lastName;
}
}
}
And the other:
public class Example {
public static void main(String[] args) {
SwingUtilities.invokeLater(Example::runGui);
}
public static void runGui() {
Persons persons = new Persons();
DefaultListModel<Person> personsSwingModel = new DefaultListModel<>();
JList<Person> personsJList = new JList<>(personsSwingModel);
personsJList.setVisibleRowCount(15);
personsJList.setCellRenderer(new DefaultListCellRenderer());
persons.addPropertyChangeListener("data", e -> {
syncListData(personsSwingModel, persons);
});
syncListData(personsSwingModel, persons);
JButton deleteButton = new JButton("Delete selected");
deleteButton.addActionListener(e -> {
persons.delete(personsJList.getSelectedValue());
});
JFrame frame = new JFrame("Example");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new JScrollPane(personsJList), BorderLayout.CENTER);
frame.add(deleteButton, BorderLayout.PAGE_END);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
private static void syncListData(DefaultListModel<Person> personsSwingModel, Persons persons) {
personsSwingModel.removeAllElements();
persons.getPersons().forEach(personsSwingModel::addElement);
}
private static class Persons {
private List<Person> persons;
private SwingPropertyChangeSupport listeners;
public Persons() {
persons = new ArrayList<>();
persons.add(new Person("Mike", "Random"));
persons.add(new Person("Alice", "Something"));
persons.add(new Person("John", "Rambo"));
persons.add(new Person("Jack", "Sparrow"));
listeners = new SwingPropertyChangeSupport(this);
}
public List<Person> getPersons() {
return persons;
}
public void delete(Person p) {
persons.remove(p);
listeners.firePropertyChange("data", null, null);
}
public void addPropertyChangeListener(String property, PropertyChangeListener listener) {
listeners.addPropertyChangeListener(property, listener);
}
}
private static class Person {
String firstName;
String lastName;
public Person(String first, String last) {
this.firstName = first;
this.lastName = last;
}
@Override
public String toString() {
return firstName + " - " + lastName;
}
}
}
The two examples differ on "selectedPerson" property. At first, the first approach seems to be more appropriate since other views can listen (observe) to this selection as well. However, what I find weird in a real scenario is that if I follow this approach, I end up with too much "gui properties" in my models. Should my domain models look like this? Should selection be part of my domain model? And if selection goes in this model, how is that different from a Presentation Model? (ignoring the data binding difference). Finally, the first approach, I can say that makes the use case more clear ("the user can delete the selected person").
The second approach, the model looks like a service and it is definitely different from a Presentation Model. Zero GUI concerns. The model fetches the data from the database, and handles the use case "user can delete the selected person" with the controller providing the selected person. However, I do not know what happens when other views want to observe the selection. (Technically they can by accessing the Swing's list selection model, but yikes).
Now, say I am adding a search field at the top to let user search fast for persons:
If the first approach is taken, the domain model must be touched and have a "searchText" property and sync with the view. But should it be? While in the second approach, only view and the controllers are edited. The controller just asks getPersons
filter them based on search text and shows the result.
I do not want to follow something like "oh, if other views want to observe the X property, it goes in a model, otherwise the state of X stays as a GUI concern".