Consider the following GUI Screen:
On the left, there is the Person List and on the right, there is the Person. Whenever the selecton of person list changes, the selected person (firstname and lastname) must be shown in the right.
My question is, where the following code belongs? What would be more "MVC"?
personListModel.addPropertyChangeListener("selection", e -> {
personModel.setFromDomainEntity(personListModel.getSelectedPerson().orElse(null));
});
The full example is this:
public class MvcExample {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("Example");
frame.setLayout(new BorderLayout());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
PersonListModel personListModel = new PersonListModel();
PersonListView listView = new PersonListView(personListModel);
frame.add(listView, BorderLayout.LINE_START);
PersonModel personModel = new PersonModel();
PersonView personView = new PersonView(personModel);
frame.add(personView, BorderLayout.CENTER);
personListModel.addPropertyChangeListener("selection", e -> {
personModel.setFromDomainEntity(personListModel.getSelectedPerson().orElse(null));
});
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
});
}
static class PersonModel {
private String firstName, lastName;
private SwingPropertyChangeSupport listeners;
public PersonModel() {
this.firstName = "";
this.lastName = "";
listeners = new SwingPropertyChangeSupport(this);
}
public void setFirstName(String firstName) {
if (firstName.equals(this.firstName))
return;
this.firstName = firstName;
listeners.firePropertyChange("firstname", null, null);
}
public void setLastName(String lastName) {
if (lastName.equals(this.lastName))
return;
this.lastName = lastName;
listeners.firePropertyChange("lastname", null, null);
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public void setFromDomainEntity(Person person) {
setFirstName(person == null ? "" : person.firstName);
setLastName(person == null ? "" : person.lastName);
}
void addPropertyChangeListener(String property, PropertyChangeListener listener) {
listeners.addPropertyChangeListener(property, listener);
}
}
static class PersonView extends JPanel {
private PersonModel model;
private JTextField firstNameField = new JTextField(15);
private JTextField lastNameField = new JTextField(15);
public PersonView(PersonModel model) {
super(new FlowLayout());
setBorder(BorderFactory.createTitledBorder("Person View / Person Model"));
this.model = model;
firstNameField.setText(model.getFirstName());
lastNameField.setText(model.getLastName());
firstNameField.getDocument().addDocumentListener(new RunnableDocumentListener(() -> {
if (!model.getFirstName().equals(firstNameField.getText()))
model.setFirstName(firstNameField.getText());
}));
lastNameField.getDocument().addDocumentListener(new RunnableDocumentListener(() -> {
if (!model.getLastName().equals(lastNameField.getText()))
model.setLastName(lastNameField.getText());
}));
model.addPropertyChangeListener("firstname", e -> {
if (firstNameField.getText().equals(model.getFirstName()))
return;
firstNameField.setText(model.getFirstName());
});
model.addPropertyChangeListener("lastname", e -> {
if (lastNameField.getText().equals(model.getLastName()))
return;
lastNameField.setText(model.getLastName());
});
add(firstNameField);
add(lastNameField);
}
//@formatter:off
private static class RunnableDocumentListener implements DocumentListener{
private Runnable r;
public RunnableDocumentListener(Runnable r) {this.r = r;}
@Override
public void insertUpdate(DocumentEvent e) { this.r.run();}
@Override
public void removeUpdate(DocumentEvent e) { this.r.run();}
@Override
public void changedUpdate(DocumentEvent e) {this.r.run();}
}
//@formatter:on
}
static class PersonListView extends JPanel {
private JList<Person> personList;
private DefaultListModel<Person> listModel;
private PersonListModel model;
public PersonListView(PersonListModel model) {
super(new BorderLayout());
setPreferredSize(new Dimension(400, 400));
setBorder(BorderFactory.createTitledBorder("Person List View / Person List Model"));
this.model = model;
listModel = new DefaultListModel<>();
personList = new JList<>(listModel);
personList.setCellRenderer(new DefaultListCellRenderer() {
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel renderer = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected,
cellHasFocus);
Person person = (Person) value;
renderer.setText(person.firstName + " - " + person.lastName);
return renderer;
}
});
syncData();
syncSelection();
model.addPropertyChangeListener("data", e -> {
syncData();
});
model.addPropertyChangeListener("selection", e -> {
syncSelection();
});
personList.getSelectionModel().addListSelectionListener(e -> {
model.setSelectedPerson(personList.getSelectedValue());
});
add(new JScrollPane(personList));
}
private void syncData() {
listModel.removeAllElements();
for (Person p : model.getPersons()) {
listModel.addElement(p);
}
}
private void syncSelection() {
if (personList.getSelectedValue() == model.getSelectedPerson().orElse(null))
return;
personList.setSelectedValue(model.getSelectedPerson(), true);
}
}
// Domain Entity
static class Person {
private String firstName, lastName;
public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
static class PersonListModel {
private List<Person> persons;
private Person selectedPerson;
private SwingPropertyChangeSupport listeners;
public PersonListModel() {
persons = new ArrayList<>();
persons.add(new Person("Stack", "OverFlow"));
persons.add(new Person("Jackie", "Chan"));
persons.add(new Person("Something", "Else"));
listeners = new SwingPropertyChangeSupport(this);
}
public void setSelectedPerson(Person selectedPerson) {
if (this.selectedPerson == selectedPerson)
return;
this.selectedPerson = selectedPerson;
listeners.firePropertyChange("selection", null, null);
}
public Optional<Person> getSelectedPerson() {
return Optional.ofNullable(selectedPerson);
}
public List<Person> getPersons() {
return Collections.unmodifiableList(persons);
}
void addPropertyChangeListener(String property, PropertyChangeListener listener) {
listeners.addPropertyChangeListener(property, listener);
}
}
}
My thoughts are that there 4 places that I can put it.
Option #1: As in the example. The parent VC part holds it. There is an ApplicationFrame that extends a JFrame and depends on a PersonListView and on a PersonView. And it adds the coordination between the 2 models:
class ApplicationFrame extends JFrame {
public ApplicationFrame(PersonListView personListView, PersonView personView) {
//...
personListView.getModel().addPropertyChangeListener("selection",e->{
personView.getModel().setFromDomainEntity(personListModel.getSelectedPerson().orElse(null));
});
//...
}
}
This approach seems to be approriate, but if there are more models that need to be coordinated like that, ApplicationFrame will end up with too much code and too many reposibilities.
Option #2 One model depends on the other and makes the call explicit (or implicit):
public PersonListModel(PersonModel personModel) {
//...
}
public void setSelectedPerson(Person selectedPerson) {
if (this.selectedPerson == selectedPerson)
return;
this.selectedPerson = selectedPerson;
personModel.setFromDomainEntity(selectedPerson);
listeners.firePropertyChange("selection", null, null);
}
or:
public PersonModel(PersonListModel listToObserve) {
//
listToObserve.addPropertyChangeListener("selection", e->{
setFromDomainEntity(listToObserve.getSelectedPerson().orElse(null));
});
}
Now, if more models get involved, they all get too complicated. Which impacts testability as well.
Option #3: Respect model hierarchy as the views go. PersonView and PersonListView are added on a (say) MainView. So there will be likely a MainModel. And this MainModel depends on PersonModel and PersonListModel. So the code goes there. This will lead the project in a big hierarchy, and I think it will be difficult to debug, or test that. Plus, what will this MainModel end up being if more models get involved?
Option #4: Introduce a new class. Say something like "PersonListToPersonCoordinator" that stands there to coordinate between the 2 models. And if another model needs to "hear" the selected Person on the list, a new class is introduced "PersonListToTheOtherModelCoordinator" that just does the same. This sounds the best option for me till now. The complex/long hierarchies are absent and the testability is there since there are no model dependences between models.
But what does MVC has to say about that?