Straight away I need to apologise for a bit lengthy post, but this has been bugging me for quite some time now. I have read a lot about MVC recently, and how it has its place within the Swing world of Java, and I still cannot understand why this is even remotely useful in any application being slightly more complex than the simple toy examples the tutorials provide. But let me start from the beginning...
I did all my GUI programming in C#/.Net 4.0, which was not extensive but extensive enough to get a good understanding of MVVM - this is a new version of MVC. It is quite a simple concept: You define you GUI using XAML (XML like description of comnponents), specifying bindings between e.g. table and its model, string values of text fields. These bindings correspond to object properties, which you define completely separately. That way, you have a complete decoupling between view and the rest of the world. On top, all changes within the model are "almost" automatically fired back to corresponding controls, the event driven design is much more central etc. etc.
Now, coming back to Java, we need to use an old school MVC. Let me begin with a very simple example: I am trying to have a panel with two combo boxes and one button. Selecting value in the first combo would drive the values of the second combo box, selecting a value in the second would call an external service, based on values in both combo boxes, and the button would reset values in the first combo, using external service as well. If I were to do it using "my" approach, I would proceed as follows:
public class TestGUI {
private JComboBox<String> firstCombo;
private JComboBox<String> secondCombo;
private JButton button;
private ExternalReloadService reloadService;
private ExternalProcessingService processingService;
public TestGUI(ExternalReloadService reloadService, ExternalProcessingService processingService) {
initialise();
this.reloadService = reloadService;
this.processingService = processingService;
}
private void initialise() {
firstCombo = new JComboBox<>();
secondCombo = new JComboBox<>();
button = new JButton("Refresh");
firstCombo.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String value = (String) ((JComboBox) e.getSource()).getSelectedItem();
reloadSecondCombo(value);
}
});
secondCombo.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getPropertyName().equals("model")) {
ComboBoxModel model = (ComboBoxModel) evt.getNewValue();
if (model.getSize() == 0) {
String value = (String) model.getSelectedItem();
processValues((String) firstCombo.getSelectedItem(), value);
}
}
}
});
secondCombo.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
processValues((String) firstCombo.getSelectedItem(), (String) secondCombo.getSelectedItem());
}
});
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
resetValues()
}
});
}
private void processValues(String selectedItem, String value) {
processingService.process(selectedItem, value);
//possibly do sth with result and update ui
}
private void reloadSecondCombo(String value) {
secondCombo.setModel(new CustomModel(reloadService.reload(value)));
}
private void resetValues() {
//Call other external service to pull default data, possibly from DB
}
}
It's obvious that this is not a simple piece of code, although short. Now, if we were to do it using MVC my first step would be to use some sort of controller, that would do all the work, e.g.
public class TestGUI {
private JComboBox<String> firstCombo;
private JComboBox<String> secondCombo;
private JButton button;
private Constroller controller;
public TestGUI(Controller controller) {
this.controller = controller;
initialise();
}
private void initialise() {
firstCombo = new JComboBox<>();
secondCombo = new JComboBox<>();
button = new JButton("Refresh");
firstCombo.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String value = (String) ((JComboBox) e.getSource()).getSelectedItem();
Data d = controller.getReloadedData(value);
//assiign to combobox
}
});
Problem 1: View should not know anything about the Controller, but should rather respond to the updates from the model.
To overcome the above, we could as the model. Model would simply have two List, one for each combobox. So we have a model (completely uselsess), a view, and controller...
Problem 2 How should we wire this? There are at least 2 separate techniques: direct vs Observer pattern
Problem 3 Direct wiring - isn't that just rewriting everything in the starting setup into three separate classes? In this approach, View registers a model, and Controller has both view and Model. It would look as sth like:
public class TestGUI {
private JComboBox<String> firstCombo;
private JComboBox<String> secondCombo;
private JButton button;
private Model model;
public TestGUI(Model m) {
model = m;
}
public void updateSecondValues(){
model.getSecondValues();
//do sth
}
}
public class Controller {
private TestGUI view;
private Model model;
public reloadSecondValues(){
firstValues = ...//reload using external service
model.setSecondValues(firstValues);
view.updateSecondValues();
}
}
public class Model {
private Set<String> firstValues;
private Set<String> secondValues;
public Set<String> getFirstValues() {
return firstValues;
}
public void setFirstValues(Set<String> firstValues) {
this.firstValues = firstValues;
}
public Set<String> getSecondValues() {
return secondValues;
}
public void setSecondValues(Set<String> secondValues) {
this.secondValues = secondValues;
}
}
This is way more complicated than it needs to, IMHO, making model and controller call each other all the time: view -> (do sth) controller -> (update yourself) view
Problem 4 Observer pattern - this is even worse in my opinion, although it allows us to decouple view and model. View would be registered as a listener on the model, which would inform view about any changes. SO now, we need a method like:
public void addListener(ViewListener listener);
and we need a ViewListener. Now, we could potentially have one method with some event params, but we cannot cater for ALL scenarios with one method. For instance, how would View know that we are just updating the second combobox and not resetting all values, or not disabling something, or not removing an item froma table??? We would therefore, need a separate method for each update, (pretty much copy and paste the methods we would have on the gui into the listener) making a listener huge.
Main Problems
As I have raised a few issues here, I wanted to summarise that a bit.
Main Probelm 1 Splitting loginc into several objects: If you imagine you have multiple panels, with many controls, you would have a view, model and views for all of them, resulting in three times as many classes as you would have normally, by allowing to do the work on the UI class.
Main problem 2 No matter what wiring technique you use, you end up adding methods on all objects to allow the communication, which would be redundant, had you simply placed everything in the UI.
As "placing everything in the UI" is NOT a solution, I am trying to get your help and comments on this. Many thanks in advance for your ideas.