Let's say I'm building a Java Swing GUI, and I've got a frame which contains a panel, containing another panel, which contains a button. (Assume the panels are reusable, and so I've made them into individual classes.)
Frame → FirstPanel → SecondPanel → Button
In reality, the line of children could be more complex, but I just want to keep this example simple.
If I want the button to control one of its parent components (eg. resize the frame), what is the best way to implement functionality between two GUI classes that aren't necessarily one directly inside the other?
I don't like the idea of stringing getParent()
methods together, or of passing the instance of Frame
all the way down through its children so that it can be accessed from SecondPanel
. Basically, I don't want to daisy-chain my classes together one way or another.
Is this an instance in which the button should be updating a model and not the parent component directly? Then the parent is notified of the model's change and updates itself accordingly?
I've put together a little example that should compile and run on its own to illustrate my problem. This is two JButtons in a JPanel, in another JPanel, in a JFrame. The buttons control the size of the JFrame.
import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class MVCExample
{
public static void main(String[] args)
{
Model model = new Model();
Controller ctrl = new Controller();
ctrl.registerModel(model);
View view = new View(ctrl);
view.setVisible(true);
model.init();
}
/**
* Model class
*/
static class Model
{
private ArrayList<PropertyChangeListener> listeners =
new ArrayList<PropertyChangeListener>();
private Dimension windowSize;
public Dimension getWindowSize(){ return windowSize; }
public void setWindowSize(Dimension windowSize)
{
if(!windowSize.equals(getWindowSize()))
{
firePropertyChangeEvent(getWindowSize(), windowSize);
this.windowSize = windowSize;
}
}
public void init()
{
setWindowSize(new Dimension(400, 400));
}
public void addListener(PropertyChangeListener listener)
{
listeners.add(listener);
}
public void firePropertyChangeEvent(Object oldValue, Object newValue)
{
for(PropertyChangeListener listener : listeners)
{
listener.propertyChange(new PropertyChangeEvent(
this, null, oldValue, newValue));
}
}
}
/**
* Controller class
*/
static class Controller implements PropertyChangeListener
{
private Model model;
private View view;
public void registerModel(Model model)
{
this.model = model;
model.addListener(this);
}
public void registerView(View view)
{
this.view = view;
}
// Called from view
public void updateWindowSize(Dimension windowSize)
{
model.setWindowSize(windowSize);
}
// Called from model
public void propertyChange(PropertyChangeEvent pce)
{
view.processEvent(pce);
}
}
/**
* View classes
*/
static class View extends JFrame
{
public View(Controller ctrl)
{
super("JFrame");
ctrl.registerView(this);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
getContentPane().add(new FirstPanel(ctrl));
pack();
}
public void processEvent(PropertyChangeEvent pce)
{
setPreferredSize((Dimension)pce.getNewValue());
pack();
}
}
static class FirstPanel extends JPanel
{
public FirstPanel(Controller ctrl)
{
setBorder(BorderFactory.createTitledBorder(
BorderFactory.createLineBorder(
Color.RED, 2), "First Panel"));
add(new SecondPanel(ctrl));
}
}
static class SecondPanel extends JPanel
{
private Controller controller;
private JButton smallButton = new JButton("400x400");
private JButton largeButton = new JButton("800x800");
public SecondPanel(Controller ctrl)
{
this.controller = ctrl;
setBorder(BorderFactory.createTitledBorder(
BorderFactory.createLineBorder(
Color.BLUE, 2), "Second Panel"));
add(smallButton);
add(largeButton);
smallButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent ae)
{
controller.updateWindowSize(new Dimension(400, 400));
}
});
largeButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent ae)
{
controller.updateWindowSize(new Dimension(800, 800));
}
});
}
}
}
What I don't like, is that the controller needs to exist in the JFrame so that the frame can register itself to receive events. But the controller then has to be passed all the way down to the SecondPanel (lines 112, 131, and 143) so that the panel can communicate with the model.
I feel like there is something inefficient going on here (and the classes become too tightly coupled). Let me know if my problem isn't clear.
If you want your classes to remain decoupled, you can add a ViewFactory which handles linking all the pieces together. Something like this might work:
Then you have the code that links all your classes together in a separate place, and it can vary independently of your controller and your View implementations.
HTH,
In Swing, the controller and view typically belong to the UI delegate, and the model is separate. The view can construct a complex hierarchy of components to represent the model, and the controller listens on them as necessary. The component is just used for the various bookkeeping that ties together the two parts.
So, for example, in a combobox, the JCombobox is where you set the UI and the model. The ComboboxUI assembles the components that make up the combobox - the renderer or editor and the button, as well as the popup and list - and provides layout and possibly custom rendering. This is the View logic. It also listens on all those components and modifies the model as appropriate. This is the Controller level. The changes to the model bubble up to the component through events.
So, in your case, there is no reason the view code can't build the whole component hierarchy. I would have the model provide Actions for the buttons that change its own property, and then have the view listen on that property change and resize the window: