6

It is often necessary to change the behaviour of other GUI objects depending on the state of another GUI object. E.g. when a button is pressed, a label shall change its name. However, when I use an AbstractAction object like JButton myButton = new JButton(myButtonAction); I need a reference to the GUI objects in the object that inherits from AbstractAction. Should I just create the AbstractAction objects in the GUI and then pass all the necessary GUI references to the AbstractAction objects or could that be considered bad style?

To make it more concrete:

// AbstractAction
   public class MyAction extends AbstractAction {
        public  MyAction(String name, 
                            String description, Integer mnemonic, JLabel) {
            super(name);
            putValue(SHORT_DESCRIPTION, description);
            putValue(MNEMONIC_KEY, mnemonic);
        }
        public void actionPerformed(ActionEvent e) {

                // do something     
            }
        }
    }

public class GUI{
   public Action myAction = null;

   public GUI(){     
        JLabel label = new JLabel("text");
        //This is not a good idea:
         myAction = new MyAction("some text" , desc, new Integer(KeyEvent.VK_Q), label);

        JButton myButton = new JButton(myAction);
   }
}
mKorbel
  • 109,525
  • 20
  • 134
  • 319
user1812379
  • 827
  • 3
  • 11
  • 22

2 Answers2

6

You want to loosen coupling as much as possible, not tighten it as your question suggests, and to do this, I think that you should do further abstraction, by separating portions even further into a full-fledged MVC program. Then the listener (the Action) can change the model, and the view, which is your GUI, can listen for model's changes and respond accordingly.

For example:

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import javax.swing.*;
import javax.swing.event.SwingPropertyChangeSupport;

public class MvcEg {

   private static void createAndShowGui() {
      View view = new MvcEgView();
      Model model = new MvcEgModel();
      new MvcEgControl(model, view);

      JFrame frame = new JFrame("MvcEg");
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.getContentPane().add(view.getMainPanel());
      frame.pack();
      frame.setLocationByPlatform(true);
      frame.setVisible(true);
   }

   public static void main(String[] args) {
      SwingUtilities.invokeLater(new Runnable() {
         public void run() {
            createAndShowGui();
         }
      });
   }
}

interface View {

   void setMyButtonAction(Action action);

   Component getMainPanel();

   void setStatusLabelText(String text);

}

@SuppressWarnings("serial")
class MvcEgView implements View {
   private static final int PREF_W = 500;
   private static final int PREF_H = 400;
   private static final String STATUS_TEXT = "Status: ";
   private JPanel mainPanel = new JPanel() {
      @Override
      public Dimension getPreferredSize() {
         return new Dimension(PREF_W, PREF_H);
      }
   };
   private JLabel statusLabel = new JLabel(STATUS_TEXT, SwingConstants.CENTER);
   private JButton myButton = new JButton();

   public MvcEgView() {
      JPanel btnPanel = new JPanel(new GridBagLayout());
      btnPanel.add(myButton);

      mainPanel.setLayout(new BorderLayout());
      mainPanel.add(btnPanel, BorderLayout.CENTER);
      mainPanel.add(statusLabel, BorderLayout.SOUTH);
   }

   @Override
   public void setMyButtonAction(Action action) {
      myButton.setAction(action);
   }

   @Override
   public void setStatusLabelText(String text) {
      statusLabel.setText(STATUS_TEXT + text);
   }

   @Override
   public Component getMainPanel() {
      return mainPanel;
   }
}

interface Model {
   public static final String MOD_FIVE_STATUS = "mod five status";

   void incrementStatus();

   ModFiveStatus getModFiveStatus();

   void removePropertyChangeListener(PropertyChangeListener listener);

   void addPropertyChangeListener(PropertyChangeListener listener);

   void setModFiveStatus(ModFiveStatus modFiveStatus);

}

class MvcEgModel implements Model {
   private ModFiveStatus modFiveStatus = ModFiveStatus.ZERO;   
   private SwingPropertyChangeSupport pcSupport = new SwingPropertyChangeSupport(
         this);

   @Override
   public void incrementStatus() {
      int value = modFiveStatus.getValue();
      value++;
      value %= ModFiveStatus.values().length;
      setModFiveStatus(ModFiveStatus.getValuesStatus(value));
   }

   @Override
   public void setModFiveStatus(ModFiveStatus modFiveStatus) {
      ModFiveStatus oldValue = this.modFiveStatus;
      ModFiveStatus newValue = modFiveStatus;
      this.modFiveStatus = modFiveStatus;
      pcSupport.firePropertyChange(MOD_FIVE_STATUS, oldValue, newValue);
   }

   @Override
   public ModFiveStatus getModFiveStatus() {
      return modFiveStatus;
   }

   @Override
   public void addPropertyChangeListener(PropertyChangeListener listener) {
      pcSupport.addPropertyChangeListener(listener);
   }

   @Override
   public void removePropertyChangeListener(PropertyChangeListener listener) {
      pcSupport.removePropertyChangeListener(listener);
   }

}

enum ModFiveStatus {
   ZERO(0, "Zero"), ONE(1, "One"), TWO(2, "Two"), THREE(3, "Three"), FOUR(4, "Four");
   private int value;
   private String text;

   private ModFiveStatus(int value, String text) {
      this.value = value;
      this.text = text;
   }

   public int getValue() {
      return value;
   }

   public String getText() {
      return text;
   }

   public static ModFiveStatus getValuesStatus(int value) {
      if (value < 0 || value >= values().length) {
         throw new ArrayIndexOutOfBoundsException(value);
      }

      for (ModFiveStatus modFiveStatus : ModFiveStatus.values()) {
         if (modFiveStatus.getValue() == value) {
            return modFiveStatus;
         }
      }
      // default that should never happen
      return null;
   }

}

@SuppressWarnings("serial")
class MvcEgControl {
   private Model model;
   private View view;

   public MvcEgControl(final Model model, final View view) {
      this.model = model;
      this.view = view;

      view.setMyButtonAction(new MyButtonAction("My Button", KeyEvent.VK_B));
      view.setStatusLabelText(model.getModFiveStatus().getText());
      System.out.println("model's status: " + model.getModFiveStatus());
      System.out.println("model's status text: " + model.getModFiveStatus().getText());

      model.addPropertyChangeListener(new ModelListener());
   }

   private class MyButtonAction extends AbstractAction {


      public MyButtonAction(String text, int mnemonic) {
         super(text);
         putValue(MNEMONIC_KEY, mnemonic);
      }

      @Override
      public void actionPerformed(ActionEvent e) {
         model.incrementStatus();
         System.out.println("button pressed");
      }
   }

   private class ModelListener implements PropertyChangeListener {

      @Override
      public void propertyChange(PropertyChangeEvent evt) {
         if (evt.getPropertyName().equals(Model.MOD_FIVE_STATUS)) {
            String status = model.getModFiveStatus().getText();
            view.setStatusLabelText(status);
            System.out.println("status is: " + status);
         }
      }

   }

}

The key in my mind is that the Model knows nothing of the view, and the view knows little (here nothing) about the model.

Hovercraft Full Of Eels
  • 283,665
  • 25
  • 256
  • 373
  • Thanks :-) However, I can't evaluate the pros and cons of the Observer and the PropertyChangeEvent version. – user1812379 Jul 06 '13 at 14:57
  • I see that `MyButtonAction` has access to `model.incrementStatus();` because `MyButtonAction` is declared inside `MvcEgControl`. But how would you do it if `MyButtonAction` was declared in a separate file? How then would you give it access to the model? – trusktr Oct 27 '13 at 02:41
  • 1
    @trusktr: you could simply give MyButtonAction's constructor another parameter, a Model parameter, and use it to set a Model class field, and thereby allow its actionPerformed method a Model reference with which to call public Model methods. – Hovercraft Full Of Eels Oct 27 '13 at 03:08
5

Amplifying on @Hovercraft's suggested approach, let your button and label access a common model. The button's Action updates the model, and the model notifies the listening label, perhaps using a PropertyChangeListener as outlined here. A more elaborate example is seen in the concrete implementations of javax.swing.text.EditorKit, which operate on a common Document model used by swing text components.

Community
  • 1
  • 1
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • Thanks. I know the MVC, but it's an abstract concept whose implementations vary strongly. There are even people who say that MVC is an anti pattern and that MVP should be preferred. The bad thing about that is that there is not definite answer to that. In my example, a PropertyChangeListener would be the easiest way to implement that? Some also use observers. – user1812379 Jul 06 '13 at 14:23
  • 1
    @user1812379: the key I think is not which precise pattern you use, but that you use interfaces, and that you try to maximize cohesion while minimizing coupling. – Hovercraft Full Of Eels Jul 06 '13 at 14:24
  • 1
    Thank you guys. My problem here is to adapt this abstract concept to a concrete case. Maybe I should start with this standard observer approach that trashgod showed me. – user1812379 Jul 06 '13 at 14:32
  • Also compare @HovercraftFullOfEels' [example](http://stackoverflow.com/a/5533581/230513) and [mine](http://stackoverflow.com/a/10523401/230513). – trashgod Jul 06 '13 at 14:35