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
Related
A Controller creates a new JFrame with a button. Using the actionListener is getting the order correctly but the action is asked to perform after is not working. Is asked to change a button name literal on the view but it never happens. Otherwise with debugging the function seems tu run but there is no change on the view.
With Java are implemented the following classes:
public class Window extends JFrame {
JButton bGoFile;
public static final String FILE = "FILE";
public Window(ActionListener actionListener) {
this.actionListener = actionListener;
setupButtons();
setupView();
}
private void setupButtons() {
bGoFile = new JButton("Button");
bGoFile.addActionListener(actionListener);
bGoFile.setActionCommand(GO_FILE);
}
private void setupView() {
setTitle("Cover pdf to img");
setBounds(300, 90, 900, 600);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLocationRelativeTo(null);
setResizable(false);
setVisible(true);
ImageIcon icon = new ImageIcon("./src/resources/logo.jpg");
setIconImage(icon.getImage());
JPanel jp = new JPanel;
jp.add(bGoFile);
add(jp);
}
public void changeButtonName(String name) {
bGoFile.setText(name);
System.out.println("Name should be changed.");
// Here I already tried to user repaint() but with no result.
}
public class WindowController implements ActionListener {
Window window;
public WindowController() {
this.window = new Window(this);
}
#Override
public void actionPerformed(ActionEvent e) {
System.out.println("Action performed");
switch (e.getActionCommand()) {
case GO_FILE -> {
synchronized (this) {
window.changeButtonName("New name");
break;
}
}
}
}
The point is that the both prints on terminal are shown but the button name on the running Window is not changing.
You shouldn't be creating an instance of the view in the controller. This should be passed to the controller (AKA using dependency injection).
You should also be making use of interfaces, as the controller should not be bound to an implementation of a view (or model), but should be working through an established series of contracts and observers.
So, let's start with some basics...
public interface View {
public JComponent getView();
}
public interface Controller<V extends View> {
public V getView();
}
I did say basic. But, working with these interfaces directly will become tedious really fast, so let's add some helpers...
public abstract class AbstractController<V extends View> implements Controller<V> {
private V view;
public AbstractController(V view) {
this.view = view;
}
#Override
public V getView() {
return view;
}
}
public abstract class AbstractView extends JPanel implements View {
#Override
public JComponent getView() {
return this;
}
}
Nothing special, but this takes care of the a lot of boiler plating.
Next, we want to define the contract of our view...
public interface MainView extends View {
public interface Observer {
public void didPerformGoFile(MainView view);
}
public void addObserver(Observer observer);
public void removeObserver(Observer observer);
public void setDescription(String description);
}
That's pretty simple. Note though, this does not describe any kind of implementation detail. The contract does not care how didPerformGoFile might be generated, only that the action can be observed by interested parties
Next, we want to define or implementations for the MainView...
public class DefaultMainView extends AbstractView implements MainView {
private List<Observer> observers = new ArrayList<>(8);
private JButton goFileButton;
private JLabel descriptionLabel;
public DefaultMainView() {
goFileButton = new JButton("Make it so");
descriptionLabel = new JLabel("...");
descriptionLabel.setHorizontalAlignment(JLabel.CENTER);
setBorder(new EmptyBorder(32, 32, 32, 32));
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridwidth = gbc.REMAINDER;
add(goFileButton, gbc);
add(descriptionLabel, gbc);
goFileButton.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
fireDidPerformGoFile();
}
});
}
#Override
public void addObserver(Observer observer) {
observers.add(observer);
}
#Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
protected void fireDidPerformGoFile() {
for (Observer observer : observers) {
observer.didPerformGoFile(this);
}
}
#Override
public void setDescription(String description) {
descriptionLabel.setText(description);
}
}
And MainController....
public class MainViewController extends AbstractController<MainView> {
public MainViewController(MainView view) {
super(view);
view.addObserver(new MainView.Observer() {
#Override
public void didPerformGoFile(MainView view) {
view.setDescription("Go file!");
}
});
}
}
Now, we can put them together and run them...
JFrame frame = new JFrame();
Controller controller = new MainViewController(new DefaultMainView());
frame.add(controller.getView().getView());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
Now, you're probably sitting there thinking, "that's a lot of work for little gain" and you'd be ... wrong, actually.
Let's say you wanted to change how the controller responds to the goFileAction depending on some kind of state, like the user's credentials or something. You could put a lot of logic into the MainViewController to handle it, or, more easily, just create a different controller altogether (nb: This is where the model in "Model-View-Controller" would come in, but since there's no concept of model in your example, I've done it "differently")
public class OverlordController extends AbstractController<MainView> {
public OverlordController(MainView view) {
super(view);
view.addObserver(new MainView.Observer() {
#Override
public void didPerformGoFile(MainView view) {
view.setDescription("Your overload has spoken!");
}
});
}
}
Then, by simply changing...
Controller controller = new MainViewController(new DefaultMainView());
to
Controller controller = new OverlordController(new DefaultMainView());
you change the output!
"Model-View-Controller" is not as straight forward in Swing as it might be in other APIs/frameworks, this is because Swing is already based on MVC, so you're actually wrapping a MVC on a MVC. If you understand this, you can make it work more easily.
For example, above, I don't expose the ActionListener to the controller, instead I created my own observer which described the actual actions which might be triggered by implementations of the view. The actual action handling took place in the implementation of the view itself.
This is good in the fact that we've decoupled the workflow, it also means that the view is free to implement the triggers for these actions in any way it sees fit.
You might want to also take a look at:
Implementing the Controller part of MVC in Java Swing
How MVC work with java swing GUI
Java and GUI - Where do ActionListeners belong according to MVC pattern?
What is the correct way of Message Passing between Classes in MVC?
Listener Placement Adhering to the Traditional (non-mediator) MVC Pattern
JTextField input fails to update output in TextView in MVC
Multithreaded MVC to recreate plane dashboard with multiple independent gauges
Where to store model objects in MVC design?
Runnable example...
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;
public final class Main {
public static void main(String[] args) {
new Main();
}
public Main() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame();
Controller controller = new OverlordController(new DefaultMainView());
frame.add(controller.getView().getView());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public interface View {
public JComponent getView();
}
public interface Controller<V extends View> {
public V getView();
}
public abstract class AbstractController<V extends View> implements Controller<V> {
private V view;
public AbstractController(V view) {
this.view = view;
}
#Override
public V getView() {
return view;
}
}
public abstract class AbstractView extends JPanel implements View {
#Override
public JComponent getView() {
return this;
}
}
public interface MainView extends View {
public interface Observer {
public void didPerformGoFile(MainView view);
}
public void addObserver(Observer observer);
public void removeObserver(Observer observer);
public void setDescription(String description);
}
public class MainViewController extends AbstractController<MainView> {
public MainViewController(MainView view) {
super(view);
view.addObserver(new MainView.Observer() {
#Override
public void didPerformGoFile(MainView view) {
view.setDescription("Go file!");
}
});
}
}
public class OverlordController extends AbstractController<MainView> {
public OverlordController(MainView view) {
super(view);
view.addObserver(new MainView.Observer() {
#Override
public void didPerformGoFile(MainView view) {
view.setDescription("Your overload has spoken!");
}
});
}
}
public class DefaultMainView extends AbstractView implements MainView {
private List<Observer> observers = new ArrayList<>(8);
private JButton goFileButton;
private JLabel descriptionLabel;
public DefaultMainView() {
goFileButton = new JButton("Make it so");
descriptionLabel = new JLabel("...");
descriptionLabel.setHorizontalAlignment(JLabel.CENTER);
setBorder(new EmptyBorder(32, 32, 32, 32));
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridwidth = gbc.REMAINDER;
add(goFileButton, gbc);
add(descriptionLabel, gbc);
goFileButton.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
fireDidPerformGoFile();
}
});
}
#Override
public void addObserver(Observer observer) {
observers.add(observer);
}
#Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
protected void fireDidPerformGoFile() {
for (Observer observer : observers) {
observer.didPerformGoFile(this);
}
}
#Override
public void setDescription(String description) {
descriptionLabel.setText(description);
}
}
}
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...
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.