In the Passive View Model View Presenter pattern, who has the responsibility for displaying the view? I have found related answers for other MVP versions, but they don't seem applicable to the passive view version.
I have a concrete example using Java Swing. It's pretty simple, but basically we have a SwingCustomersView which internally builds a JPanel with a table (list of customers) and a label displaying the currently selected customers age. When a customer is selected in the table, the presenter retrieves the selected customer age from the model. I think the example is a correct implementation of MVP Passive View, but correct me if I'm wrong.
The question is how do we bootstrap these classes? For example, if we wanted to display the SwingCustomersView in a JFrame. How would one do that? I imagine something along the lines of:
void launcher() {
CustomersModel model = new CustomersModel();
SwingCustomersView view = new SwingCustomersView();
CustomersPresenter presenter = new CustomersPresenter(view, model);
}
This is the initial wiring, but nothing is displayed yet. How do we actually display the view? Is it the responsibility of (1) launcher() , (2) SwingCustomersView or (3) CustomersPresenter to display the view? Unfortunately I don't believe any of those are very good as you can see from my thoughts below. Perhaps there's another way?
(1.a): launcher
Make SwingCustomersView extend JFrame and make it add it's internal JPanel to the content pane of itself. Then we can do this:
void launcher() {
CustomersModel model = new CustomersModel();
SwingCustomersView view = new SwingCustomersView();
CustomersPresenter presenter = new CustomersPresenter(view, model);
view.setVisible(true); // Displays the view
}
However in this case we don't use the presenter instance for anything. Isn't that strange? It's just there for wiring, we could just as well delete the variable and just do new CustomersPresenter(view, model).
(2): SwingCustomersView
Make SwingCustomersView take a Container in the constructor to which it should add it's internal JPanel:
void launcher() {
CustomersModel model = new CustomersModel();
JFrame frame = new JFrame("Some title");
SwingCustomersView view = new SwingCustomersView(frame.getContentPane());
CustomersPresenter presenter = new CustomersPresenter(view, model);
frame.pack();
frame.setVisible(true) // Displays the view
}
However, same problem as (1): the presenter instance does nothing. It seems strange. Furthermore with both (1) and (2) it is possible to display the view before the presenter is hooked up, which I imagine could cause strange results in some situations.
(3): CustomersPresenter
Make CustomersPresenter responsible for displaying the view somwhow. Then we could do this:
void launcher() {
CustomersModel model = new CustomersModel();
SwingCustomersView view = new SwingCustomersView();
CustomersPresenter presenter = new CustomersPresenter(view, model);
presenter.show() // Displays the view
}
This would solve the problem of not using it for anything after construction. But I don't see how do to this without either changing the CustomersView interface or making CustomersPresenter too dependent on the underlying GUI implementation. Furthermore, displaying a view doesn't sound like presentation logic and thus doesn't seem to belong in the presenter.
Example
public class CustomersModel {
private List<Customer> customers;
public CustomersModel() {
customers = new ArrayList<Customer>();
customers.add(new Customer("SomeCustomer", "31"));
customers.add(new Customer("SomeCustomer", "32"));
}
public List<Customer> getCustomers() {
return customers;
}
}
public class Customer {
public String name;
public String age;
public Customer(String name, String age) {
this.name = name;
this.age = age;
}
}
public interface CustomersView {
void addCustomerSelectionChangeListener(ItemListener listener);
void onNewActiveCustomer(String age);
void onNewCustomers(List<String> newCustomers);
}
public class SwingCustomersView implements CustomersView {
// Swing components here all put into a main JPanel
public void addCustomerSelectionChangeListener(ItemListener listener) {
// Add event listener to table
}
public void onNewActiveCustomer(String age) {
// Display age in label beneath table
}
public void onNewCustomers(List<String> newCustomers) {
// Display customers in table
}
}
public class CustomersPresenter {
private final CustomersView view;
private final CustomersModel model;
public CustomersPresenter(CustomersView view, CustomersModel model) {
this.view = view;
this.model = model;
initPresentationLogic();
populateView();
}
private void initPresentationLogic() {
view.addCustomerSelectionChangeListener(new ItemListener() {
#Override
public void itemStateChanged(ItemEvent e) {
String selectedName = (String)e.getItem();
List<Customer> customers = model.getCustomers();
for (Customer c : customers)
if (c.name.equals(selectedName))
view.onNewActiveCustomer(c.age);
}
});
}
private void populateView() {
List<Customer> customers = model.getCustomers();
List<String> names = new ArrayList<String>();
for (Customer c : customers)
names.add(c.name);
// View will now populate its table, which in turn will call customerSelectionChangeListener
// so the view 'automagically' updates the selected contact age too
view.onNewCustomers(names);
}
}
Option (3) all the way. It is the presenter's jobs for "controlling" the view, which includes making it visible. Yes, you'll need to add to the view's interface to allow this to happen, but that's not a big deal. Remember, you can make the view is as passive as possible. No logic whatsoever!
Working Example:
I stumbled upon this example of a simple Swing game using an MVC architecture. Since I write my Swing apps using MVP instead of MVC, I can't say with authority if this example is a true and pure example of MVC. It looks okay to me, and the author trashgod has more than proven himself here on SO using Swing, so I'll accept it as reasonable.
As an exercise, I decided to rewrite it using an MVP architecture.
The Driver:
As you can see in the code below, this is pretty simple. What should jump out at you are the separation of concerns (by inspecting the constructors):
The Model class is standalone and has no knowledge of Views or Presenters.
The View interface is implemented by a standalone GUI class, neither of which have any knowledge of Models or Presenters.
The Presenter class knows about both Models and Views.
Code:
import java.awt.*;
/**
* MVP version of https://stackoverflow.com/q/3066590/230513
*/
public class MVPGame implements Runnable
{
public static void main(String[] args)
{
EventQueue.invokeLater(new MVPGame());
}
#Override
public void run()
{
Model model = new Model();
View view = new Gui();
Presenter presenter = new Presenter(model, view);
presenter.start();
}
}
and the GamePiece that we'll be using for the game:
import java.awt.*;
public enum GamePiece
{
Red(Color.red), Green(Color.green), Blue(Color.blue);
public Color color;
private GamePiece(Color color)
{
this.color = color;
}
}
The Model: Primarily, the job of the Model is to:
Provide data for the UI (upon request)
Validation of data (upon request)
Long-term storage of data (upon request)
Code:
import java.util.*;
public class Model
{
private static final Random rnd = new Random();
private static final GamePiece[] pieces = GamePiece.values();
private GamePiece selection;
public Model()
{
reset();
}
public void reset()
{
selection = pieces[randomInt(0, pieces.length)];
}
public boolean check(GamePiece guess)
{
return selection.equals(guess);
}
public List<GamePiece> getAllPieces()
{
return Arrays.asList(GamePiece.values());
}
private static int randomInt(int min, int max)
{
return rnd.nextInt((max - min) + 1) + min;
}
}
The View: The idea here is to make it as "dumb" as possible by stripping out as much application logic as you can (the goal is to have none). Advantages:
The app now be 100% JUnit testable since no application logic is mixed in with Swing code
You can launch the GUI without launching the entire app, which makes prototyping much faster
Code:
import java.awt.*;
import java.awt.event.*;
import java.util.List;
public interface View
{
public void addPieceActionListener(GamePiece piece, ActionListener listener);
public void addResetActionListener(ActionListener listener);
public void setGamePieces(List<GamePiece> pieces);
public void setResult(Color color, String message);
}
and the GUI:
import java.awt.*;
import java.awt.event.*;
import java.util.List;
import javax.swing.*;
/**
* View is "dumb". It has no reference to Model or Presenter.
* No application code - Swing code only!
*/
public class Gui implements View
{
private JFrame frame;
private ColorIcon icon;
private JLabel resultLabel;
private JButton resetButton;
private JButton[] pieceButtons;
private List<GamePiece> pieceChoices;
public Gui()
{
frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
icon = new ColorIcon(80, Color.WHITE);
}
public void setGamePieces(List<GamePiece> pieces)
{
this.pieceChoices = pieces;
frame.add(getMainPanel());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public void setResult(Color color, String message)
{
icon.color = color;
resultLabel.setText(message);
resultLabel.repaint();
}
private JPanel getMainPanel()
{
JPanel panel = new JPanel(new BorderLayout());
panel.add(getInstructionPanel(), BorderLayout.NORTH);
panel.add(getGamePanel(), BorderLayout.CENTER);
panel.add(getResetPanel(), BorderLayout.SOUTH);
return panel;
}
private JPanel getInstructionPanel()
{
JPanel panel = new JPanel();
panel.add(new JLabel("Guess what color!", JLabel.CENTER));
return panel;
}
private JPanel getGamePanel()
{
resultLabel = new JLabel("No selection made", icon, JLabel.CENTER);
resultLabel.setVerticalTextPosition(JLabel.BOTTOM);
resultLabel.setHorizontalTextPosition(JLabel.CENTER);
JPanel piecePanel = new JPanel();
int pieceCount = pieceChoices.size();
pieceButtons = new JButton[pieceCount];
for (int i = 0; i < pieceCount; i++)
{
pieceButtons[i] = createPiece(pieceChoices.get(i));
piecePanel.add(pieceButtons[i]);
}
JPanel panel = new JPanel(new BorderLayout());
panel.add(resultLabel, BorderLayout.CENTER);
panel.add(piecePanel, BorderLayout.SOUTH);
return panel;
}
private JPanel getResetPanel()
{
resetButton = new JButton("Reset");
JPanel panel = new JPanel();
panel.add(resetButton);
return panel;
}
private JButton createPiece(GamePiece piece)
{
JButton btn = new JButton();
btn.setIcon(new ColorIcon(16, piece.color));
btn.setActionCommand(piece.name());
return btn;
}
public void addPieceActionListener(GamePiece piece, ActionListener listener)
{
for (JButton button : pieceButtons)
{
if (button.getActionCommand().equals(piece.name()))
{
button.addActionListener(listener);
break;
}
}
}
public void addResetActionListener(ActionListener listener)
{
resetButton.addActionListener(listener);
}
private class ColorIcon implements Icon
{
private int size;
private Color color;
public ColorIcon(int size, Color color)
{
this.size = size;
this.color = color;
}
#Override
public void paintIcon(Component c, Graphics g, int x, int y)
{
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setColor(color);
g2d.fillOval(x, y, size, size);
}
#Override
public int getIconWidth()
{
return size;
}
#Override
public int getIconHeight()
{
return size;
}
}
}
What might not be so obvious right away is how large the View interface can get. For each Swing component on the GUI, you may want to:
Add/Remove a listener to the component, of which there are many types (ActionListener, FocusListener, MouseListener, etc.)
Get/Set the data on the component
Set the "usability" state of the component (enabled, visible, editable, focusable, etc.)
This can get unwieldy really fast. As a solution (not shown in this example), a key is created for each field, and the GUI registers each component with it's key (a HashMap is used). Then, instead of the View defining methods such as:
public void addResetActionListener(ActionListener listener);
// and then repeat for every field that needs an ActionListener
you would have a single method:
public void addActionListener(SomeEnum someField, ActionListener listener);
where "SomeEnum" is an enum that defines all fields on a given UI. Then, when the GUI receives that call, it looks up the appropriate component to call that method on. All of this heavy lifting would get done in an abstract super class that implements View.
The Presenter: The responsibilities are:
Initialize the View with it's starting values
Respond to all user interactions on the View by attaching the appropriate listeners
Update the state of the View whenever necessary
Fetch all data from the View and pass to Model for saving (if necessary)
Code (note that there's no Swing in here):
import java.awt.*;
import java.awt.event.*;
public class Presenter
{
private Model model;
private View view;
public Presenter()
{
System.out.println("ctor");
}
public Presenter(Model model, View view)
{
this.model = model;
this.view = view;
}
public void start()
{
view.setGamePieces(model.getAllPieces());
reset();
view.addResetActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
reset();
}
});
for (int i = 0; i < GamePiece.values().length; i++)
{
final GamePiece aPiece = GamePiece.values()[i];
view.addPieceActionListener(aPiece, new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
pieceSelected(aPiece);
}
});
}
}
private void reset()
{
model.reset();
view.setResult(Color.GRAY, "Click a button.");
}
private void pieceSelected(GamePiece piece)
{
boolean valid = model.check(piece);
view.setResult(piece.color, valid ? "Win!" : "Keep trying.");
}
}
Keep in mind that each portion of the MVP architecture can/will be delegating to other classes (that are hidden to the other 2 portions) to perform many of its tasks. The Model, View, and Presenter classes are just the upper divisions in your code base heirarchy.
Related
I wanted to do really simple MVC project, when i click on the button, I'd like to see "napis" on my console, but the ActionListener doesn't work properly. Any thoughts ? (If i've done something wrong with post please be understanding its my first post here :))
public class Model {
public String napis (){
return "napis";
}
}
public class View {
private JFrame frame;
private JLabel label;
private JButton button;
public View (){
frame = new JFrame();
label = new JLabel("Napis");
button = new JButton("click");
frame.add(label);
frame.add(button);
frame.setSize(500,500);
button.setSize(30,30);
frame.setVisible(true);
}
public void addActionListener(ActionListener click){
button.addActionListener(click);
}
}
public class Controller {
private Model model;
private View view = new View();
public Controller (final Model model, View view) {
view.addActionListener(
new ActionListener(){
#Override
public void actionPerformed(ActionEvent e) {
model.napis();
}
}
);{
}
}
}
ofc all imports are fixed.
model.napis(); doesn't write anything in the console :
public String napis (){
return "napis";
}
It returns simply a String.
This should do it :
#Override
public void actionPerformed(ActionEvent e) {
System.out.println(model.napis());
}
In your controller class, change
model.napis();
to
System.out.println(model.napis());
What your problem is that the action listener takes the String but does nothing with it. What you have to do is to print the returned String.
Okay, so your code is a little confusing. Your Controller is creating an instance of View, but it's also been passed an instance of View.
The instance it create's does not have the ActionListener attached to it, but the one you pass in does.
Both Views are create and showing a JFrame
Because the instance of View you're passing gets created first, it appears first, but the instance of View which your Controller creates gets created second, it appears over the top - but it does not have an ActionListener attached to it...
So, the first thing you should do, is get rid of the instance of the View that the Controller is creating
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
Controller controller = new Controller(new Model(), new View());
}
public class Model {
public String napis() {
return "napis";
}
}
public class View {
private JFrame frame;
private JLabel label;
private JButton button;
public View() {
frame = new JFrame();
label = new JLabel("Napis");
button = new JButton("click");
frame.add(label);
frame.add(button);
frame.setSize(500, 500);
button.setSize(30, 30);
frame.setVisible(true);
}
public void addActionListener(ActionListener click) {
button.addActionListener(click);
}
}
public class Controller {
private Model model;
private View view;
public Controller(final Model model, View view) {
this.view = view;
this.model = model;
view.addActionListener(
new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
System.out.println("Clicked");
this.model.napis();
}
});
}
}
}
You should also be focused on the side effect that your View's constructor is creating, creating and showing the frame probably isn't the right choice here
I would also encourage you to do some research into the concept of "code to interface not implementation" as it's one of the key aspects of MVC
I've tried to apply the Observable/Observer pattern but there is something wrong with my code when I try to change a the textfield of a JTextPane.
I've got 3 classes, Play, Controller and SecondWindow here are a sample of their code.
public class Play() {
Controller c = new Controller();
SecondWindow sw = new SecondWindow();
c.addObserver(sw)
c.setText("blabla");
}
My class Controller:
public class Controller extends Observable(){
private String text ="";
private static Controller getInstance() {
if (instance == null) {
instance = new Controller();
}
return instance;
}
public void setText(String s) {
text = s;
setChanged();
notifyObservers();
}
}
and SecondWindow:
public class SecondWindow extends JFrame implements Observer{
private JPanel contentPane;
private Controller c;
private JTextPane txt = new JTextPane();
public SecondWindow () {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
SecondWindow frame = new SecondWindow();
frame.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
public SecondWindow() {
initComponents();
createEvents();
c = Controller.getInstance();
}
public void initComponents() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBounds(1000, 0, 300,500);
contentPane = new JPanel();
contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
setContentPane(contentPane);
contentPane.setLayout(null);
txt.setBounds(0, 0, 280, 460);
txt.enable(false);
contentPane.add(txt);
}
public void update(Observable arg0 , Object arg1){
// Things to change here
}
I can't manage to put the variable c in the textField (like a txt.setText(c.getText) instruction). I'm sure that it reads the method update, but I don't know how to make sure it works.
Hint: Per the Observerable API the notifyObservers method has an overload that accepts any object as a parameter:
public void notifyObservers(Object arg)
This can even be a String. And as per the Observer API, this object is then passed into the update method in the observer, and you can use it there.
void update(Observable o,
Object arg)
arg - an argument passed to the notifyObservers method.
Separate side issue here:
contentPane.setLayout(null);
For most Swing aficionados, seeing this is like hearing nails on a chalkboard -- it's painful. While null layouts and setBounds() might seem to Swing newbies like the easiest and best way to create complex GUI's, the more Swing GUI'S you create the more serious difficulties you will run into when using them. They won't resize your components when the GUI resizes, they are a royal witch to enhance or maintain, they fail completely when placed in scrollpanes, they look gawd-awful when viewed on all platforms or screen resolutions that are different from the original one. Instead you will want to study and learn the layout managers and then nest JPanels, each using its own layout manager to create pleasing and complex GUI's that look good on all OS's.
Side issue number two: your code is not Swing thread safe, since the Swing GUI could very well be notified by the observable off of the Swing event dispatch thread or EDT. While it is not likely to cause frequent or serious problems with this simple program, in general it would be better to use a SwingPropertyChangeSupport and PropertyChangeListeners rather than Observer / Observable if you can.
Next Side Issue
This:
public class Controller extends Observable(){
isn't compilable / kosher Java. Same for the duplicate parameter-less constructors for the SecondWindow class. Yes, we know what you're trying to do, but it's hard enough trying to understand someone else's code, you really don't want to make it harder by posting kind-of sort-of uncompilable code, trust me.
For example, something simple could be implemented in Swing using PropertyChangeListeners, like so:
import java.util.concurrent.TimeUnit;
public class Play2 {
public static void main(String[] args) {
Model2 model2 = new Model2();
View2 view2 = new View2();
new Controller2(model2, view2);
view2.show();
for (int i = 0; i < 10; i++) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
// one of the few times it's OK to ignore an exception
}
String text = String.format("Counter Value: %d", i);
model2.setText(text);
}
}
}
import java.beans.PropertyChangeListener;
import javax.swing.event.SwingPropertyChangeSupport;
public class Model2 {
private SwingPropertyChangeSupport pcSupport = new SwingPropertyChangeSupport(this);
public static final String TEXT = "text"; // name of our "bound" property
private String text = "";
public String getText() {
return text;
}
public void setText(String text) {
String oldValue = this.text;
String newValue = text;
this.text = text;
pcSupport.firePropertyChange(TEXT, oldValue, newValue);
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
pcSupport.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
pcSupport.removePropertyChangeListener(listener);
}
public void addPropertyChangeListener(String name, PropertyChangeListener listener) {
pcSupport.addPropertyChangeListener(name, listener);
}
public void removePropertyChangeListener(String name, PropertyChangeListener listener) {
pcSupport.removePropertyChangeListener(name, listener);
}
}
import javax.swing.*;
public class View2 {
private JPanel mainPanel = new JPanel();
private JTextField textField = new JTextField(10);
public View2() {
textField.setFocusable(false);
mainPanel.add(new JLabel("Text:"));
mainPanel.add(textField);
}
public JPanel getMainPanel() {
return mainPanel;
}
public void setText(String text) {
textField.setText(text);
}
public void show() {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("View");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(getMainPanel());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
public class Controller2 {
private Model2 model2;
private View2 view2;
public Controller2(Model2 model2, View2 view2) {
this.model2 = model2;
this.view2 = view2;
model2.addPropertyChangeListener(Model2.TEXT, new ModelListener());
}
private class ModelListener implements PropertyChangeListener {
#Override
public void propertyChange(PropertyChangeEvent pcEvt) {
view2.setText((String) pcEvt.getNewValue());
}
}
}
I'm trying to write a really simple Java example to learn MVC. It's a JButton that when clicked increments a counter and displays the number of clicks so far.
I broke out the Model, View and Controller into separate classes, and thought I was on the right path, but when I click the button the JLabel that displays the counter continues to stay at 0.
Can someone take a quick look and see why the JLabel that should display the number of clicks always stays at 0?
Thanks
View
package mvc;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
public class View extends javax.swing.JFrame {
private JButton jButton1;
private JLabel jLabel1;
private Controller c;
private Model m;
/**
* Auto-generated main method to display this JFrame
*/
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
Controller c = new Controller();
Model m = new Model();
View inst = new View(c,m);
inst.setLocationRelativeTo(null);
inst.setVisible(true);
}
});
}
public View(Controller c, Model m) {
super();
this.c = c;
this.m = m;
initGUI();
}
private void initGUI() {
try {
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
getContentPane().setLayout(null);
{
jButton1 = new JButton();
getContentPane().add(jButton1, "Center");
jButton1.setText("Click");
jButton1.setBounds(314, 180, 101, 34);
jButton1.addActionListener(c);
}
{
jLabel1 = new JLabel();
getContentPane().add(getJLabel1());
jLabel1.setText("Click Count = " + c.getClickCount());
jLabel1.setBounds(439, 183, 91, 27);
}
pack();
this.setSize(818, 414);
} catch (Exception e) {
//add your error handling code here
e.printStackTrace();
}
}
public JLabel getJLabel1() {
return jLabel1;
}
}
End View
Controller class
package mvc;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class Controller implements ActionListener
{
Model m;
View v;
public Controller()
{
m = new Model();
v = new View(this, m);
}
#Override
public void actionPerformed(ActionEvent arg0)
{
if (arg0.getSource() == "Click")
{
m.addClick();
v.getJLabel1().setText("Click count = " + getClickCount());
}
}
public int getClickCount()
{
return m.getClicks();
}
}
End Controller class
Model class
package mvc;
public class Model
{
private int clicks;
public Model()
{
clicks = 0;
}
public void addClick()
{
clicks++;
}
public int getClicks()
{
return clicks;
}
}
End Model class
I see why now. You have two different Model objects created.
One in controller and one in Main() - Which one is it?
Another advice.. create a MainController class.
This should have your Main method.
Your main method creates another Controller responsible for creating your View and Model.
Use this Controller as the bridge.
There are couple of problems:
The actual data should that view displays generally comes from the model not from the controller.
So your code in the view
jLabel1.setText("Click Count = " + c.getClickCount());
should change to
jLabel1.setText("Click Count = " + m.getClickCount());
Inside the Controller, you create a new instance of the mode and view and inside the main() method you again create new instance of controller and view. So essentially the Controller class is working on a different view and model objects.
In general, but its not a stone-graved standard:
The View has a model, it doesent refer to the controller
The model is independent and doesent refer to view or controller
The controller has both view and model.
if (arg0.getSource() == "Click")
== is not meant for string (or object) equality comparison. equals method is what you should use instead.
Moreover, I think you are interested in public String getActionCommand() method rather than public Object getSource().
A little test
JButton btn = new JButton();
btn.setText("Click");
btn.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
System.out.println(e.getSource());
System.out.println(e.getActionCommand());
}
});
btn.doClick();
and the output
javax.swing.JButton[,0,0,0x0,invalid,alignmentX=0.0,alignmentY=0.5,border=javax.swing.plaf.BorderUIResource$CompoundBorderUIResource#1f5b0afd,flags=296,maximumSize=,minimumSize=,preferredSize=,defaultIcon=,disabledIcon=,disabledSelectedIcon=,margin=javax.swing.plaf.InsetsUIResource[top=2,left=14,bottom=2,right=14],paintBorder=true,paintFocus=true,pressedIcon=,rolloverEnabled=true,rolloverIcon=,rolloverSelectedIcon=,selectedIcon=,text=Click,defaultCapable=true]
Click
should illustrate why.
Update
Try
if (arg0.getActionCommand().equals("Click"))
The number always stays at zero because you are updating the wrong instance of View. In your Controller class you create another instance which is not displayed.
v = new View(this, m);
You could instead pass in your main instance by adding a setter:
class Controller implements ActionListener {
Model m;
View v;
public Controller() {
m = new Model();
}
public void setView(View v) {
this.v = v;
}
...
ActionEvent.getSource() returns a component reference but jButton1 is not publicly visible. To fix you can either add a getter for the button or use the action command:
if (arg0.getActionCommand().equals("Click")) {
you could maybe try to make accessor to your button on the view class:
public JButton getButton(){
return jbutton1;
}
public void setButton(JButton button){
this.jbutton1 = button ;
}
and in your controler class accessing to your button like this:
if(arg0.getSource() == v.getButton()){
...
}
I always did like that and i never had any error...
Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 4 years ago.
Improve this question
I'm quite new in desktop applications development and I have a pretty big project do deliver this summer. The thing is that the code has to be very clear, so I won't go in (much) trouble when I will update it.
As a result, I want a good "separation of concerns". And the most difficult part to me is the View-Controller separation.
Now, I have read lots of tutorials, discussions etc. And I have designed a mini-app in 3 different ways. The app is simple : click on a button that transform a label to a "Hello world".
What do you think of those 3 designs ?
Is there a better design to meet my expectations ?
Design 1
View1.java :
public View1() {
initComponents();
this.controller = new Controller1(this);
}
private Controller1 controller;
public void updateLabel(String message){
this.jLabel1.setText(message);
}
private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {
this.controller.doSomething();
}
private void initComponents() {
...
jButton1.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
jButton1ActionPerformed(evt);
}
});
...}
Controller1.java :
public class Controller1 {
public Controller1(View1 v){
this.view = v;
}
public void doSomething(){
this.view.updateLabel("Hello world");
}
private View1 view;
}
Design 2
View2.java :
public View2() {
initComponents();
this.controller = new Controller2(this);
jButton1.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
controller.doSomething();
}
});
}
public void updateLabel(String message){
this.jLabel1.setText(message);
}
private Controller2 controller;
...
}
Controller2.java :
public class Controller2 {
public Controller2(View2 v){
this.view = v;
}
public void doSomething(){
this.view.updateLabel("Hello world");
}
private View2 view;
}
Design 3
View3.java :
public View3() {
initComponents();
this.controller = new Controller3(this);
this.jButton1.addActionListener(this.controller.listener);
}
private Controller3 controller;
public void updateLabel(String message){
this.jLabel1.setText(message);
}
...}
Controller3.java :
public class Controller3 {
public Controller3(View3 v){
this.view = v;
this.listener = new MyListener(v);
}
private View3 view;
public MyListener listener;
}
MyListener.java :
public class MyListener implements ActionListener{
private View3 view;
public MyListener(View3 v){
this.view = v;
}
public void actionPerformed(java.awt.event.ActionEvent evt) {
this.view.updateLabel("Hello world");
}
}
I don't like any of these designs. You are coupling the controller to the view to tightly. Let's say you wanted to change controller implementation in the future so you would have to go in all your classes and change the class. Instead you should make it injected. There are a lot of libs that can do this for you via annotations like Guice or Spring but I won't go in to those. Here is a better design.
public class View{
private Controller controller;
public View(Controller controller) {
this.controller = controller;
}
}
This a much cleaner design because the view doesn't have to know what the implementation of the controller is. You can later create a subclass and pass that instead.
So now with the above design I think you can see that you shouldn't pass the View to the controller. This is again coupling which is not good. Instead you can pass an onCallback class that will get executed when it is done. Here is code to undersand it
jButton1.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
controller.doSomething(new Runnable(){
public void run(){
updateLabel("Hello world");
}
});
}
});
Then in your controller do
public void doSomething(Runnable callback){
// do work
SwingUtilties.invokeLater(callback);
}
If you look exactly what I have suggested is removing any kind of coupling. The view should not ask for a Controller, it should be given on. The Controller should not know about the view it should just execute a call back. This is important because if you decided to not using Swing, then you wouldn't have all these dependencies on Swing package in your controller.
Hope this all helps!!
Deciding which pattern is best depends a lot on the problem you are solving. Swing is already an MVC framework, so you'll have to consider whether adding another layer of indirection on top of it is worth the effort.
Since you are new to UI programming, I suggest you throw together a walking skeleton of your system first, then based on what you learned from that, decide on your architecture. A well-designed architecture makes it easy to test and reuse components. MVP and MVVM are two well-known ways design patterns for UIs.
For your toy problem you could implement either MVP or MVVM as I do below. Keep in mind you also will typically use interfaces between each and will have observers on Model if that can change.
MVP
public class Model {
public String getWhatIWantToSay() {
return "Hello World";
}
}
public class Presenter implements ActionListener {
private final View view;
private final Model model;
public Presenter(Model model, View view) {
this.model = model;
this.view = view;
view.addButtonListener(this);
}
public void actionPerformed(ActionEvent e) {
view.setText(model.getWhatIWantToSay());
}
}
public class View {
private JButton button = new JButton();
private JLabel label = new JLabel();
public void addButtonListener(ActionListener listener) {
button.addActionListener(listener);
}
public void setText(String text) {
label.setText(text);
}
}
MVVP
public class ModelView extends Observable {
private final Model model;
private String text = "";
public ModelView(Model model) {
this.model = model;
}
public void buttonClicked() {
text = model.getWhatIWantToSay();
notifyObservers();
}
}
public class View implements Observer {
private JButton button = new JButton();
private JLabel label = new JLabel();
private final ModelView modelView;
public View(final ModelView modelView) {
this.modelView = modelView;
modelView.addObserver(this);
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
modelView.buttonClicked();
}
});
}
public void update(Observable o, Object arg) {
label.setText(modelView.text);
}
}
I think Design 2 is your best option to meet your criteria.
Problems with Design 1: It is too complex on the view side. The extra methods make it look almost like it has a controller inside of it. Simple changes would become complex to implement.
Problems with Design 3: This pushes too much onto the controller. The controller should not know what Swing events are happening. In that design if you want an action to happen based on a JList instead of a JButton you have to change the view and the controller which is bad.
Other comments about your code:
Use import statements so you don't have to include the package of a class in code as in: java.awt.event.ActionListener().
You use this. in several places were it is not necessary and that just adds noise.
As Amir points out, you have very tight coupling between your view and controller that is not necessary.
Another design approach can be something like this:
Model
package biz.tugay.toypro.model;
public interface LabelService {
String getDateInRandomLocale();
}
package biz.tugay.toypro.model;
import java.text.DateFormat;
import java.util.Calendar;
import java.util.Locale;
import java.util.concurrent.ThreadLocalRandom;
public class LabelServiceImpl implements LabelService {
private final Locale availableLocalesJava[];
public LabelServiceImpl() {
this.availableLocalesJava = DateFormat.getAvailableLocales();
}
#Override
public String getDateInRandomLocale() {
final int randomIndex = ThreadLocalRandom.current().nextInt(0, availableLocalesJava.length);
final Locale locale = availableLocalesJava[randomIndex];
final Calendar calendar = Calendar.getInstance();
final DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.LONG, locale);
return dateFormat.format(calendar.getTime());
}
}
View
package biz.tugay.toypro.view;
import biz.tugay.toypro.model.LabelService;
import javax.swing.*;
public class DateInRandomLocaleLabel extends JLabel {
private final LabelService labelService;
public DateInRandomLocaleLabel(final LabelService labelService) {
this.labelService = labelService;
}
public void showDateInRandomLocale() {
final String dateInRandomLocale = labelService.getDateInRandomLocale();
setText(dateInRandomLocale);
}
}
package biz.tugay.toypro.view;
import javax.swing.*;
public class RandomizeDateButton extends JButton {
public RandomizeDateButton() {
super("Hit Me!");
}
}
package biz.tugay.toypro.view;
import javax.swing.*;
import java.awt.*;
public class DateInRandomLocalePanel extends JPanel {
public DateInRandomLocalePanel(final JLabel dateInRandomLocaleLabel, final JButton randomizeDateButton) {
final GridLayout gridLayout = new GridLayout(1, 2);
setLayout(gridLayout);
add(dateInRandomLocaleLabel);
add(randomizeDateButton);
}
}
package biz.tugay.toypro.view;
import javax.swing.*;
public class MainFrame extends JFrame {
public void init() {
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
setSize(400, 50);
setVisible(true);
}
}
Controller
package biz.tugay.toypro.controller;
import biz.tugay.toypro.view.DateInRandomLocaleLabel;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class RandomizeDateButtonActionListener implements ActionListener {
private DateInRandomLocaleLabel dateInRandomLocaleLabel;
#Override
public void actionPerformed(final ActionEvent e) {
dateInRandomLocaleLabel.showDateInRandomLocale();
}
public void setDateInRandomLocaleLabel(final DateInRandomLocaleLabel dateInRandomLocaleLabel) {
this.dateInRandomLocaleLabel = dateInRandomLocaleLabel;
}
}
and finally how I start the Application:
package biz.tugay.toypro;
import biz.tugay.toypro.controller.RandomizeDateButtonActionListener;
import biz.tugay.toypro.model.LabelService;
import biz.tugay.toypro.model.LabelServiceImpl;
import biz.tugay.toypro.view.DateInRandomLocaleLabel;
import biz.tugay.toypro.view.DateInRandomLocalePanel;
import biz.tugay.toypro.view.MainFrame;
import biz.tugay.toypro.view.RandomizeDateButton;
import javax.swing.*;
public class App {
public static void main(String[] args) {
final LabelService labelService = new LabelServiceImpl();
// View
final DateInRandomLocaleLabel dateInRandomLocaleLabel = new DateInRandomLocaleLabel(labelService);
final RandomizeDateButton randomizeDateButton = new RandomizeDateButton();
final DateInRandomLocalePanel dateInRandomLocalePanel = new DateInRandomLocalePanel(dateInRandomLocaleLabel, randomizeDateButton);
final MainFrame mainFrame = new MainFrame();
mainFrame.getContentPane().add(dateInRandomLocalePanel);
// Controller
final RandomizeDateButtonActionListener randomizeDateButtonActionListener = new RandomizeDateButtonActionListener();
// Bind Controller to the View..
randomizeDateButton.addActionListener(randomizeDateButtonActionListener);
// Bind View to the Controller..
randomizeDateButtonActionListener.setDateInRandomLocaleLabel(dateInRandomLocaleLabel);
// Show the main frame..
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
mainFrame.init();
}
});
}
}
And this is what the application looks like:
I think there is no just 1 right answer, it depends on how you would like to tie the components you have..
You might find the following useful as well:
http://www.tugay.biz/2017/05/trying-to-understand-model-view.html
http://www.tugay.biz/2017/05/swingynotes-design-modified.html
http://www.tugay.biz/2017/05/refactoring-spring-swing-mvc.html
Been sitting here for hours now trying to figure this out, so a bit sympathy for this large question. :)
The Goal: I simply want to divide my done code into MVC (Model View Controller) parts. I have the game logics done and text based - the code works fine.
The Problem: Well, I want to implement this code into MVC, but where do explain for the MODEL that it should use text-based? Because the VIEW is only for the layout (graphically) correct? I am having a REALLY hard time figuring out where to begin at all. Any pointers would be so nice!
Here is my game logics code:
import mind.*;
import javax.swing.*;
import java.util.*;
import java.lang.*;
import java.awt.*;
public class Drive {
String[] mellan;
boolean gameEnd, checkempty, checkempty2, enemy, enemy2;
String gr,rd,tom;
int digits;
public Drive() {
// Gamepieces in textform
gr="G"; rd="R"; tom=" ";
mellan = new String[7];
String[] begin = {gr,gr,gr,tom,rd,rd,rd};
String[] end = {rd,rd,rd,tom,gr,gr,gr};
//input
Scanner in = new Scanner(System.in);
mellan=begin;
gameEnd=false;
while (gameEnd == false) {
for(int i=0; i<mellan.length; i++) {
System.out.print(mellan[i]);
}
System.out.print(" Choose 0-6: ");
digits = in.nextInt();
move();
checkWin();
}
}
void move() {
//BOOLEAN for gameruls!!!
checkempty = digits<6 && mellan[digits+1]==tom;
checkempty2 = digits>0 && mellan[digits-1]==tom;
enemy = (mellan[digits]==gr && mellan[digits+1]==rd && mellan[digits+2]==tom);
enemy2 = (mellan[digits]==rd && mellan[digits-1]==gr && mellan[digits-2]==tom);
if(checkempty) {
mellan[digits+1]=mellan[digits];
mellan[digits]=tom;
} else if (checkempty2) {
mellan[digits-1]=mellan[digits];
mellan[digits]=tom;
} else if (enemy) {
mellan[digits+2]=mellan[digits];
mellan[digits]=tom;
} else if (enemy2) {
mellan[digits-2]=mellan[digits];
mellan[digits]=tom;
}
}
void checkWin() {
String[] end = {rd,rd,rd,tom,gr,gr,gr};
for (int i=0; i<mellan.length; i++){
}
if (Arrays.equals(mellan,end)) {
for (int j=0; j<mellan.length; j++) {
System.out.print(mellan[j]);
}
displayWin();
}
}
void displayWin() {
gameEnd = true;
System.out.println("\nNicely Done!");
return;
}
// Kör Drive!
public static void main(String args[]) {
new Drive();
}
}
Here is how I defined my DriveView thus far: (just trying to make one button to work)
import mind.*;
import javax.swing.*;
import java.util.*;
import java.lang.*;
import java.awt.*;
import java.awt.event.*;
public class DriveView extends JFrame {
JButton ruta1 = new JButton("Green");
JButton ruta2 = new JButton("Green");
JButton rutatom = new JButton("");
JButton ruta6 = new JButton("Red");
private DriveModel m_model;
public DriveView(DriveModel model) {
m_model = model;
//Layout for View
JPanel myPanel = new JPanel();
myPanel.setLayout(new FlowLayout());
myPanel.add(ruta1);
myPanel.add(ruta2);
myPanel.add(rutatom);
myPanel.add(ruta6);
this.setContentPane(myPanel);
this.pack();
this.setTitle("Drive");
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
void addMouseListener(ActionListener mol) {
ruta2.addActionListener(mol);
}
}
And DriveController which gives me error at compile
import mind.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.lang.*;
public class DriveController {
private DriveModel m_model;
private DriveView m_view;
public DriveController(DriveModel model, DriveView view) {
m_model = model;
m_view = view;
view.addMouseListener(new MouseListener());
}
class MouseListener implements ActionListener {
public void actionPerformed(ActionEvent e) {
String mening;
mening = e.getActionCommand();
if (mening.equals("Green")) {
setForeground(Color.red);
}
}
}
}
Your game model can have more than one view: a GUI view, a console view, a status view, etc. Typically each view arranges to listen for changes in the model, and it then queries the model for the information it needs to render it's particular view. This simple game was designed specifically to illustrate the concepts. The section named "Design" elaborates in more detail.
Addendum: This outline corresponds roughly to this architecture, symbolized below.
public class MVCOutline {
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
//#Override
public void run() {
new MVCOutline().create();
}
});
}
private void create() {
JFrame f = new JFrame("MVC Outline");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(new MainPanel());
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
}
}
class MainPanel extends JPanel {
public MainPanel() {
super(new BorderLayout());
Model model = new Model();
View view = new View(model);
Control control = new Control(model, view);
this.add(view, BorderLayout.CENTER);
this.add(control, BorderLayout.WEST);
}
}
class Control extends JPanel implements ... {
private Model model;
private View view;
public Control(Model model, View view) {
this.model = model;
this.view = view;
}
}
class View extends JPanel implements Observer {
private Model model;
public View(Model model) {
this.model = model;
model.addObserver(this);
}
public void update(Observable o, Object arg) {
// update GUI based on model
}
}
class Model extends Observable {
public void next() {
this.notifyObservers(...);
}
}
To take a stab (and this is kind of overkill), I would make a game state bean that would represent the state that the game is currently in; that would be a "model object". Looking at your code, it would probably contain String [] mellan. Then I would have a data access object that contains a reference to the game state bean and it would have methods for updating the game state.
The game logic for different actions would be in a service object that has a reference to the data access object and the controller would have a reference to the service object. It would call the different action methods depending on what interaction was received from the interface, the view.
Like I said, this is kind of overkill.