I'm studying advanced Java and trying to write a program that utilizes the MVC design pattern. The program needs to draw a string which can be modified by a user's input in a JTextField
. The user can also adjust the color and font size of the text through a JComboBox
and a JSpinner
respectively.
Here is what I have so far:
public class MVCDemo extends JApplet {
private JButton jBtnController = new JButton("Show Controller");
private JButton jBtnView = new JButton("Show View");
private TextModel model = new TextModel();
//constructor
public MVCDemo(){
//set layout and add buttons
setLayout(new FlowLayout());
add(jBtnController);
add(jBtnView);
jBtnController.addActionListener(new ActionListener(){
@Override
public void actionPerformed(ActionEvent e){
JFrame frame = new JFrame("Controllor");
TextController controller = new TextController();
controller.setModel(model);
frame.add(controller);
frame.setSize(200, 100);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
jBtnView.addActionListener(new ActionListener(){
@Override
public void actionPerformed(ActionEvent e){
JFrame frame = new JFrame("View");
TextView view = new TextView();
view.setModel(model);
frame.add(view);
frame.setSize(500, 200);
frame.setLocation(200, 200);
frame.setVisible(true);
}
});
}
public static void main(String[] args){
MVCDemo applet = new MVCDemo();
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setTitle("MVCDemo");
frame.getContentPane().add(applet, BorderLayout.CENTER);
frame.setSize(400, 90);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
public class TextModel {
private String text = "Your Student ID #";
//utility field used by event firing mechanism
private ArrayList<ActionListener> actionListenerList;
public void setText(String text){
this.text = text;
//notify the listener for the change on text
processEvent(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "text"));
}
public String getText(){
return text;
}
//register an action event listener
public synchronized void addActionListener(ActionListener l){
if (actionListenerList == null)
actionListenerList = new ArrayList<ActionListener>();
}
//remove an action event listener
public synchronized void removeActionListener(ActionListener l){
if (actionListenerList != null && actionListenerList.contains(l))
actionListenerList.remove(l);
}
//fire TickEvent
private void processEvent(ActionEvent e){
ArrayList<ActionListener> list;
synchronized (this){
if (actionListenerList == null)
return;
list = (ArrayList<ActionListener>)(actionListenerList.clone());
}
}
}
public class TextView extends JPanel{
private TextModel model;
//set a model
public void setModel(TextModel model){
this.model = model;
if (model != null)
//register the view as listener for the model
model.addActionListener(new ActionListener(){
@Override
public void actionPerformed(ActionEvent e){
repaint();
}
});
}
public TextModel getModel(){
return model;
}
@Override
public void paintComponent(Graphics g){
if (model != null){
super.paintComponent(g);
//g.setColor(model.getColor());
g.drawString(model.getText(), 190, 90);
}
}
}
public class TextController extends JPanel {
String[] colorStrings = { "Black", "Blue", "Red" };
private TextModel model;
private JTextField jtfText = new JTextField();
private JComboBox jcboColorList = new JComboBox(colorStrings);
//constructor
public TextController(){
//panel to group labels
JPanel panel1 = new JPanel();
panel1.setLayout(new GridLayout(3, 1));
panel1.add(new JLabel("Text"));
panel1.add(new JLabel("Color"));
panel1.add(new JLabel("Size"));
//panel to group text field, combo box and spinner
JPanel panel2 = new JPanel();
panel2.setLayout(new GridLayout(3, 1));
panel2.add(jtfText);
panel2.add(jcboColorList);
setLayout(new BorderLayout());
add(panel1, BorderLayout.WEST);
add(panel2, BorderLayout.CENTER);
//register listeners
jtfText.addActionListener(new ActionListener(){
@Override
public void actionPerformed(ActionEvent e){
if (model != null)
model.setText(jtfText.getText());
}
});
/*jcboColorList.addActionListener(new ActionListener(){
@Override
public void actionPerformed(ActionEvent e){
if (model != null)
model.set
}
});*/
}
public void setModel(TextModel model){
this.model = model;
}
public TextModel getModel(){
return model;
}
}
At this moment I've only implemented the JTextField
component (yet to figure out how to do the JComboBox
and JSpinner
properly), and even that is hardly perfect.
When I first launch the program and turn on both view and controller panels, the default string of "Your Student ID #" is shown correctly in the view. But when I type some other string into the JTextField
and hit enter, the output string in TextView
does not update unless I close the view panel and reopen it. Could someone point out what is causing this behavior?
I suspect it probably has something to do with the event handling part of my program. But I'm still quite new to GUI programming and have a very basic understanding of how events are triggered and handled. So I would be really grateful if someone could explain the underlying cause of the problem in a beginner-friendly fashion.
Your mixing your layers, both the model and controller are non visual entities. I'm in my phone so I've not inspected your code in any depth, but, your view should notify the controller (directly or in directly) when the values change, the controller will update the model accordingly which will notify the controller which will further notify the view
In a formal MVC, the model and view should never know about each other, the controller is used to bridge them together. Swing doesn't follow a strict MVC (it's more of a MV-C) and sometimes trying to wrap a strict MVC around it can cause no end of headaches.
Instead, what I do, is wrap the MVC around Swing, this means that the view doesn't need to expose its UI elements, but instead, relies on a contract between the controller and the view to determine what each party can do
Let's start with an example.
Start by defining the contracts. These should be interfaces, as it allows you to decouple the code in a way that allows the physical implementation to change without affecting the other parts of the API, maybe something like...
public interface TextModel {
public void setText(String text);
public String getText();
public void addChangeListener(ChangeListener listener);
public void removeChangeListener(ChangeListener listener);
}
public interface TextController {
public String getText();
public void setText(String text);
}
public interface TextView {
public TextController getController();
public void setController(TextController controller);
public void setText(String text);
}
Now, normally, I'd consider making some abstract
versions, to wrap up the common functionality, but for the sake of the example, I've jumped straight to the default implementations...
public class DefaultTextModel implements TextModel {
private String text;
private Set<ChangeListener> listeners;
public DefaultTextModel() {
listeners = new HashSet<>(25);
}
@Override
public String getText() {
return text;
}
@Override
public void setText(String value) {
if (text == null ? value != null : !text.equals(value)) {
this.text = value;
fireStateChanged();
}
}
@Override
public void addChangeListener(ChangeListener listener) {
listeners.add(listener);
}
@Override
public void removeChangeListener(ChangeListener listener) {
listeners.remove(listener);
}
protected void fireStateChanged() {
ChangeListener[] changeListeners = listeners.toArray(new ChangeListener[0]);
if (changeListeners != null && changeListeners.length > 0) {
ChangeEvent evt = new ChangeEvent(this);
for (ChangeListener listener : changeListeners) {
listener.stateChanged(evt);
}
}
}
}
public class DefaultTextController implements TextController {
private TextModel model;
private TextView view;
public DefaultTextController(TextModel model, TextView view) {
this.model = model;
this.view = view;
this.view.setController(this);
this.model.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
// You could simply make a "textWasChanged" method on the view
// and make the view ask the controller for the value, but where's
// the fun in that :P
getView().setText(getText());
}
});
}
public TextModel getModel() {
return model;
}
public TextView getView() {
return view;
}
@Override
public String getText() {
return getModel().getText();
}
@Override
public void setText(String text) {
getModel().setText(text);
}
}
Now, you should be asking yourself, how is this all going to work, you have an input and an output view. The reality is, it will work really, really well, but first, we need the two different views...
public class InputTextView extends JPanel implements TextView {
private TextController controller;
public InputTextView() {
setLayout(new GridBagLayout());
JTextField field = new JTextField(10);
add(field);
field.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
getController().setText(field.getText());
}
});
}
@Override
public TextController getController() {
return controller;
}
@Override
public void setController(TextController controller) {
this.controller = controller;
}
@Override
public void setText(String text) {
// We kind of don't care, because we're responsible for changing the
// text anyway :P
}
}
public class OutputTextView extends JPanel implements TextView {
private TextController controller;
public OutputTextView() {
}
@Override
public TextController getController() {
return controller;
}
@Override
public void setController(TextController controller) {
this.controller = controller;
}
@Override
public void setText(String text) {
revalidate();
repaint();
}
@Override
public Dimension getPreferredSize() {
Dimension size = new Dimension(200, 40);
TextController controller = getController();
if (controller != null) {
String text = controller.getText();
FontMetrics fm = getFontMetrics(getFont());
if (text == null || text.trim().isEmpty()) {
size.width = fm.stringWidth("M") * 10;
} else {
size.width = fm.stringWidth(text);
}
size.height = fm.getHeight();
}
return size;
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
TextController controller = getController();
String text = "";
if (controller != null) {
text = controller.getText();
}
if (text == null) {
text = "";
}
FontMetrics fm = g.getFontMetrics();
int x = (getWidth() - fm.stringWidth(text)) / 2;
int y = ((getHeight() - fm.getHeight()) / 2) + fm.getAscent();
g.drawString(text, x, y);
}
}
These are both implementations of the TextView
, the difference is, one view (the input) only sets the text and ignores changes to the text and one only responds to the changes in the text and never sets it...
Brain still not coping? Let me demonstrate....
InputTextView inputView = new InputTextView();
OutputTextView outputView = new OutputTextView();
TextModel model = new DefaultTextModel();
// Shared model!!
TextController inputController = new DefaultTextController(model, inputView);
TextController outputController = new DefaultTextController(model, outputView);
Basically, here, we one two views, two controllers and ONE, shared, model. When the input side of things changes the text, the model notifies the output side of things and they are updated
And because I know what fun it is to copy separate pieces of code and stich them together...
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.HashSet;
import java.util.Set;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
InputTextView inputView = new InputTextView();
OutputTextView outputView = new OutputTextView();
TextModel model = new DefaultTextModel();
// Shared model!!
TextController inputController = new DefaultTextController(model, inputView);
TextController outputController = new DefaultTextController(model, outputView);
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new GridLayout(2, 0));
frame.add(inputView);
frame.add(outputView);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public interface TextModel {
public void setText(String text);
public String getText();
public void addChangeListener(ChangeListener listener);
public void removeChangeListener(ChangeListener listener);
}
public interface TextController {
public String getText();
public void setText(String text);
}
public interface TextView {
public TextController getController();
public void setController(TextController controller);
public void setText(String text);
}
public class DefaultTextModel implements TextModel {
private String text;
private Set<ChangeListener> listeners;
public DefaultTextModel() {
listeners = new HashSet<>(25);
}
@Override
public String getText() {
return text;
}
@Override
public void setText(String value) {
if (text == null ? value != null : !text.equals(value)) {
this.text = value;
fireStateChanged();
}
}
@Override
public void addChangeListener(ChangeListener listener) {
listeners.add(listener);
}
@Override
public void removeChangeListener(ChangeListener listener) {
listeners.remove(listener);
}
protected void fireStateChanged() {
ChangeListener[] changeListeners = listeners.toArray(new ChangeListener[0]);
if (changeListeners != null && changeListeners.length > 0) {
ChangeEvent evt = new ChangeEvent(this);
for (ChangeListener listener : changeListeners) {
listener.stateChanged(evt);
}
}
}
}
public class DefaultTextController implements TextController {
private TextModel model;
private TextView view;
public DefaultTextController(TextModel model, TextView view) {
this.model = model;
this.view = view;
this.view.setController(this);
this.model.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
// You could simply make a "textWasChanged" method on the view
// and make the view ask the controller for the value, but where's
// the fun in that :P
getView().setText(getText());
}
});
}
public TextModel getModel() {
return model;
}
public TextView getView() {
return view;
}
@Override
public String getText() {
return getModel().getText();
}
@Override
public void setText(String text) {
getModel().setText(text);
}
}
public class InputTextView extends JPanel implements TextView {
private TextController controller;
public InputTextView() {
setLayout(new GridBagLayout());
JTextField field = new JTextField(10);
add(field);
field.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
getController().setText(field.getText());
}
});
}
@Override
public TextController getController() {
return controller;
}
@Override
public void setController(TextController controller) {
this.controller = controller;
}
@Override
public void setText(String text) {
// We kind of don't care, because we're responsible for changing the
// text anyway :P
}
}
public class OutputTextView extends JPanel implements TextView {
private TextController controller;
public OutputTextView() {
}
@Override
public TextController getController() {
return controller;
}
@Override
public void setController(TextController controller) {
this.controller = controller;
}
@Override
public void setText(String text) {
revalidate();
repaint();
}
@Override
public Dimension getPreferredSize() {
Dimension size = new Dimension(200, 40);
TextController controller = getController();
if (controller != null) {
String text = controller.getText();
FontMetrics fm = getFontMetrics(getFont());
if (text == null || text.trim().isEmpty()) {
size.width = fm.stringWidth("M") * 10;
} else {
size.width = fm.stringWidth(text);
}
size.height = fm.getHeight();
}
return size;
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
TextController controller = getController();
String text = "";
if (controller != null) {
text = controller.getText();
}
if (text == null) {
text = "";
}
FontMetrics fm = g.getFontMetrics();
int x = (getWidth() - fm.stringWidth(text)) / 2;
int y = ((getHeight() - fm.getHeight()) / 2) + fm.getAscent();
g.drawString(text, x, y);
}
}
}