Java和GUI – 根据MVC模式,ActionListeners属于哪里?

我目前正在编写一个模板的Java应用程序,不知何故,如果我想干净地遵循MVC模式,我不知道ActionListeners属于哪里。

这个例子是基于Swing的,但不是关于框架,而是Java中MVC的基本概念,使用任何框架来创buildGUI。

我从一个包含JFrame和JButton的绝对简单的应用程序开始(为了处理框架而closures了应用程序)。 这个post尾随的代码。 没有什么特别的,只是为了澄清我们在说什么。 我没有从模型开始,因为这个问题太多了。

已经有不止一个类似的问题,像这样:
MVC模式与许多ActionListeners
Java swing – ActionListener应该去哪里?

但是,他们不是真的令人满意,因为我想知道两件事情:

  • 把所有的ActionListener放在一个单独的包里是合理的吗?
    • 我想这样做是为了View和Controller的可读性, 如果有很多听众
  • 如果侦听器不是Controller内部的子类,如何从ActionListener中执行Controller函数? (后续问题)

我希望这里不是太笼统或模糊,但是现在让我想一下。 我总是用我自己的方式,让ActionHandler知道Controller ,但这看起来不正确,所以我最终想知道如何做到这一点。

亲切的问候,
杰森


控制器:

package controller; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import view.MainView; public class MainController { MainView mainView = new MainView(); public MainController() { this.initViewActionListeners(); } private void initViewActionListeners() { mainView.initButtons(new CloseListener()); } public class CloseListener implements ActionListener { @Override public void actionPerformed(ActionEvent e) { mainView.dispose(); } } } 

视图:

 package view; import java.awt.Dimension; import java.awt.event.ActionListener; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JPanel; public class MainView extends JFrame { JButton button_close = new JButton(); JPanel panel_mainPanel = new JPanel(); private static final long serialVersionUID = 5791734712409634055L; public MainView() { setDefaultCloseOperation(DISPOSE_ON_CLOSE); this.setSize(500, 500); this.add(panel_mainPanel); setVisible(true); } public void initButtons(ActionListener actionListener) { this.button_close = new JButton("Close"); this.button_close.setSize(new Dimension(100, 20)); this.button_close.addActionListener(actionListener); this.panel_mainPanel.add(button_close); } } 

这是一个很难回答的问题,因为Swing不是纯粹的MVC实现,视图和控制器是混合的。

从技术上讲,模型和控制器应该能够交互,控制器和视图应该能够交互,但视图和模型不应该相互作用,这显然不是Swing的工作方式,但这是另一个争论。

另一个问题是,你真的不想公开UI组件给任何人,控制器不应该在乎如何发生某些行动,只有他们可以。

这将build议附加到您的UI控件的ActionListener应由视图维护。 然后该视图应该提醒控制器已经发生某种行为。 为此,可以使用控制器订阅的视图pipe理的另一个ActionListener

更好的是,我会有一个专门的视图监听器,描述这个视图可能产生的行为,例如…

 public interface MainViewListener { public void didPerformClose(MainView mainView); } 

然后控制器会通过这个监听器来订阅视图,当(在这种情况下)closuresbutton被按下时,视图会调用didPerformClose

即使在这个例子中,我也会试图创build一个“主视图”接口,它描述了任何实现保证提供的属性(setter和getters)和动作(listeners / callbacks),然后你不关心这些行动发生,只有当他们这样做,你需要做的事情…

在你想问自己的每一个级别,改变任何元素(改变模型或者控制器或者视图)为另一个实例是多么的容易? 如果你发现自己不得不分离代码,那么你有一个问题。 通过接口进行通信,尝试减less层间耦合量和每层对其他层的了解,直到他们仅仅维护合同

更新…

我们以此为例。

登录

实际上有两个视图(折扣实际的对话框),有凭据视图和login视图,是的,他们是不同的,你会看到。

CredentialsView

凭据视图负责收集要authentication的细节,用户名和密码。 它会提供信息给控制器,让它知道这些凭证何时被更改,因为控制器可能想采取一些行动,如启用“login”button…

该视图还将要知道何时进行身份validation,因为它将要禁用它的字段,所以用户无法在身份validation发生时更新视图,同样,需要知道何时进行身份validation失败或成功,因为它将需要为这些可能性采取行动。

 public interface CredentialsView { public String getUserName(); public char[] getPassword(); public void willAuthenticate(); public void authenticationFailed(); public void authenticationSucceeded(); public void setCredentialsViewController(CredentialsViewController listener); } public interface CredentialsViewController { public void credientialsDidChange(CredentialsView view); } 

CredentialsPane

CredentialsPaneCredentialsPane的物理实现,它实现合同,但pipe理它自己的内部状态。 如何pipe理合同对控制者是无关紧要的,只关心合同得到维护。

 public class CredentialsPane extends JPanel implements CredentialsView { private CredentialsViewController controller; private JTextField userNameField; private JPasswordField passwordField; public CredentialsPane(CredentialsViewController controller) { setCredentialsViewController(controller); setLayout(new GridBagLayout()); userNameField = new JTextField(20); passwordField = new JPasswordField(20); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; gbc.insets = new Insets(2, 2, 2, 2); gbc.anchor = GridBagConstraints.EAST; add(new JLabel("Username: "), gbc); gbc.gridy++; add(new JLabel("Password: "), gbc); gbc.gridx = 1; gbc.gridy = 0; gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.HORIZONTAL; add(userNameField, gbc); gbc.gridy++; add(passwordField, gbc); DocumentListener listener = new DocumentListener() { @Override public void insertUpdate(DocumentEvent e) { getCredentialsViewController().credientialsDidChange(CredentialsPane.this); } @Override public void removeUpdate(DocumentEvent e) { getCredentialsViewController().credientialsDidChange(CredentialsPane.this); } @Override public void changedUpdate(DocumentEvent e) { getCredentialsViewController().credientialsDidChange(CredentialsPane.this); } }; userNameField.getDocument().addDocumentListener(listener); passwordField.getDocument().addDocumentListener(listener); } @Override public CredentialsViewController getCredentialsViewController() { return controller; } @Override public String getUserName() { return userNameField.getText(); } @Override public char[] getPassword() { return passwordField.getPassword(); } @Override public void willAuthenticate() { userNameField.setEnabled(false); passwordField.setEnabled(false); } @Override public void authenticationFailed() { userNameField.setEnabled(true); passwordField.setEnabled(true); userNameField.requestFocusInWindow(); userNameField.selectAll(); JOptionPane.showMessageDialog(this, "Authentication has failed", "Error", JOptionPane.ERROR_MESSAGE); } @Override public void authenticationSucceeded() { // Really don't care, but you might want to stop animation, for example... } public void setCredentialsViewController(CredentialsViewController controller){ this.controller = controller; } } 

LoginView

LoginView负责pipe理一个CredentialsView ,但也用于通知LoginViewController进行身份validation或者如果该进程被用户取消,通过某种方式…

同样, LoginViewController会在authentication即将发生时以及authentication失败或成功时通知视图。

 public interface LoginView { public CredentialsView getCredentialsView(); public void willAuthenticate(); public void authenticationFailed(); public void authenticationSucceeded(); public void dismissView(); public LoginViewController getLoginViewController(); } public interface LoginViewController { public void authenticationWasRequested(LoginView view); public void loginWasCancelled(LoginView view); } 

LoginPane

LoginPane是一种特殊的,它作为LoginViewController的视图,但它也充当CredentialsView的控制器。 这一点很重要,因为没有什么可以说一个观点不能成为一个控制者,但是我会小心你如何实现这样的事情,因为这样做可能并不总是有意义的,但是因为这两个观点是共同收集信息和pipe理事件,在这种情况下是有道理的。

因为LoginPane将需要根据CredentialsView的更改来更改它自己的状态,所以在这种情况下允许LoginPane充当控制器是有意义的,否则,您需要提供更多控制该状态的方法button,但是这开始stream失UI逻辑到控制器…

 public static class LoginPane extends JPanel implements LoginView, CredentialsViewController { private LoginViewController controller; private CredentialsPane credientialsView; private JButton btnAuthenticate; private JButton btnCancel; private boolean wasAuthenticated; public LoginPane(LoginViewController controller) { setLoginViewController(controller); setLayout(new BorderLayout()); setBorder(new EmptyBorder(8, 8, 8, 8)); btnAuthenticate = new JButton("Login"); btnCancel = new JButton("Cancel"); JPanel buttons = new JPanel(); buttons.add(btnAuthenticate); buttons.add(btnCancel); add(buttons, BorderLayout.SOUTH); credientialsView = new CredentialsPane(this); add(credientialsView); btnAuthenticate.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { getLoginViewController().authenticationWasRequested(LoginPane.this); } }); btnCancel.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { getLoginViewController().loginWasCancelled(LoginPane.this); // I did think about calling dispose here, // but's not really the the job of the cancel button to decide what should happen here... } }); validateCreientials(); } public static boolean showLoginDialog(LoginViewController controller) { final LoginPane pane = new LoginPane(controller); JDialog dialog = new JDialog(); dialog.setTitle("Login"); dialog.setModal(true); dialog.add(pane); dialog.pack(); dialog.setLocationRelativeTo(null); dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); dialog.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { pane.getLoginViewController().loginWasCancelled(pane); } }); dialog.setVisible(true); return pane.wasAuthenticated(); } public boolean wasAuthenticated() { return wasAuthenticated; } public void validateCreientials() { CredentialsView view = getCredentialsView(); String userName = view.getUserName(); char[] password = view.getPassword(); if ((userName != null && userName.trim().length() > 0) && (password != null && password.length > 0)) { btnAuthenticate.setEnabled(true); } else { btnAuthenticate.setEnabled(false); } } @Override public void dismissView() { SwingUtilities.windowForComponent(this).dispose(); } @Override public CredentialsView getCredentialsView() { return credientialsView; } @Override public void willAuthenticate() { getCredentialsView().willAuthenticate(); btnAuthenticate.setEnabled(false); } @Override public void authenticationFailed() { getCredentialsView().authenticationFailed(); validateCreientials(); wasAuthenticated = false; } @Override public void authenticationSucceeded() { getCredentialsView().authenticationSucceeded(); validateCreientials(); wasAuthenticated = true; } public LoginViewController getLoginViewController() { return controller; } public void setLoginViewController(LoginViewController controller) { this.controller = controller; } @Override public void credientialsDidChange(CredentialsView view) { validateCreientials(); } } 

工作示例

 import java.awt.BorderLayout; import java.awt.EventQueue; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.util.Random; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPasswordField; import javax.swing.JTextField; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.UnsupportedLookAndFeelException; import javax.swing.border.EmptyBorder; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import sun.net.www.protocol.http.HttpURLConnection; public class Test { protected static final Random AUTHENTICATION_ORACLE = new Random(); public static void main(String[] args) { new Test(); } public interface CredentialsView { public String getUserName(); public char[] getPassword(); public void willAuthenticate(); public void authenticationFailed(); public void authenticationSucceeded(); public CredentialsViewController getCredentialsViewController(); } public interface CredentialsViewController { public void credientialsDidChange(CredentialsView view); } public interface LoginView { public CredentialsView getCredentialsView(); public void willAuthenticate(); public void authenticationFailed(); public void authenticationSucceeded(); public void dismissView(); public LoginViewController getLoginViewController(); } public interface LoginViewController { public void authenticationWasRequested(LoginView view); public void loginWasCancelled(LoginView view); } public Test() { EventQueue.invokeLater(new Runnable() { @Override public void run() { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) { ex.printStackTrace(); } LoginViewController controller = new LoginViewController() { @Override public void authenticationWasRequested(LoginView view) { view.willAuthenticate(); LoginAuthenticator authenticator = new LoginAuthenticator(view); authenticator.authenticate(); } @Override public void loginWasCancelled(LoginView view) { view.dismissView(); } }; if (LoginPane.showLoginDialog(controller)) { System.out.println("You shell pass"); } else { System.out.println("You shell not pass"); } System.exit(0); } }); } public class LoginAuthenticator { private LoginView view; public LoginAuthenticator(LoginView view) { this.view = view; } public void authenticate() { Thread t = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(2000); } catch (InterruptedException ex) { Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex); } SwingUtilities.invokeLater(new Runnable() { @Override public void run() { if (AUTHENTICATION_ORACLE.nextBoolean()) { view.authenticationSucceeded(); view.dismissView(); } else { view.authenticationFailed(); } } }); } }); t.start(); } } public static class LoginPane extends JPanel implements LoginView, CredentialsViewController { private LoginViewController controller; private CredentialsPane credientialsView; private JButton btnAuthenticate; private JButton btnCancel; private boolean wasAuthenticated; public LoginPane(LoginViewController controller) { setLoginViewController(controller); setLayout(new BorderLayout()); setBorder(new EmptyBorder(8, 8, 8, 8)); btnAuthenticate = new JButton("Login"); btnCancel = new JButton("Cancel"); JPanel buttons = new JPanel(); buttons.add(btnAuthenticate); buttons.add(btnCancel); add(buttons, BorderLayout.SOUTH); credientialsView = new CredentialsPane(this); add(credientialsView); btnAuthenticate.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { getLoginViewController().authenticationWasRequested(LoginPane.this); } }); btnCancel.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { getLoginViewController().loginWasCancelled(LoginPane.this); // I did think about calling dispose here, // but's not really the the job of the cancel button to decide what should happen here... } }); validateCreientials(); } public static boolean showLoginDialog(LoginViewController controller) { final LoginPane pane = new LoginPane(controller); JDialog dialog = new JDialog(); dialog.setTitle("Login"); dialog.setModal(true); dialog.add(pane); dialog.pack(); dialog.setLocationRelativeTo(null); dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); dialog.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { pane.getLoginViewController().loginWasCancelled(pane); } }); dialog.setVisible(true); return pane.wasAuthenticated(); } public boolean wasAuthenticated() { return wasAuthenticated; } public void validateCreientials() { CredentialsView view = getCredentialsView(); String userName = view.getUserName(); char[] password = view.getPassword(); if ((userName != null && userName.trim().length() > 0) && (password != null && password.length > 0)) { btnAuthenticate.setEnabled(true); } else { btnAuthenticate.setEnabled(false); } } @Override public void dismissView() { SwingUtilities.windowForComponent(this).dispose(); } @Override public CredentialsView getCredentialsView() { return credientialsView; } @Override public void willAuthenticate() { getCredentialsView().willAuthenticate(); btnAuthenticate.setEnabled(false); } @Override public void authenticationFailed() { getCredentialsView().authenticationFailed(); validateCreientials(); wasAuthenticated = false; } @Override public void authenticationSucceeded() { getCredentialsView().authenticationSucceeded(); validateCreientials(); wasAuthenticated = true; } public LoginViewController getLoginViewController() { return controller; } public void setLoginViewController(LoginViewController controller) { this.controller = controller; } @Override public void credientialsDidChange(CredentialsView view) { validateCreientials(); } } public static class CredentialsPane extends JPanel implements CredentialsView { private CredentialsViewController controller; private JTextField userNameField; private JPasswordField passwordField; public CredentialsPane(CredentialsViewController controller) { setCredentialsViewController(controller); setLayout(new GridBagLayout()); userNameField = new JTextField(20); passwordField = new JPasswordField(20); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; gbc.insets = new Insets(2, 2, 2, 2); gbc.anchor = GridBagConstraints.EAST; add(new JLabel("Username: "), gbc); gbc.gridy++; add(new JLabel("Password: "), gbc); gbc.gridx = 1; gbc.gridy = 0; gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.HORIZONTAL; add(userNameField, gbc); gbc.gridy++; add(passwordField, gbc); DocumentListener listener = new DocumentListener() { @Override public void insertUpdate(DocumentEvent e) { getCredentialsViewController().credientialsDidChange(CredentialsPane.this); } @Override public void removeUpdate(DocumentEvent e) { getCredentialsViewController().credientialsDidChange(CredentialsPane.this); } @Override public void changedUpdate(DocumentEvent e) { getCredentialsViewController().credientialsDidChange(CredentialsPane.this); } }; userNameField.getDocument().addDocumentListener(listener); passwordField.getDocument().addDocumentListener(listener); } @Override public CredentialsViewController getCredentialsViewController() { return controller; } @Override public String getUserName() { return userNameField.getText(); } @Override public char[] getPassword() { return passwordField.getPassword(); } @Override public void willAuthenticate() { userNameField.setEnabled(false); passwordField.setEnabled(false); } @Override public void authenticationFailed() { userNameField.setEnabled(true); passwordField.setEnabled(true); userNameField.requestFocusInWindow(); userNameField.selectAll(); JOptionPane.showMessageDialog(this, "Authentication has failed", "Error", JOptionPane.ERROR_MESSAGE); } @Override public void authenticationSucceeded() { // Really don't care, but you might want to stop animation, for example... } public void setCredentialsViewController(CredentialsViewController controller) { this.controller = controller; } } } 

他们与控制有关,但他们不一定是控制的直接部分。 例如,请参阅下面的代码,我正在准备另一个问题,一个关于匿名内部类和耦合,在这里我给我的所有button匿名内部动作(当然是ActionListeners),然后使用动作来改变GUI状态。 GUI(控件)的任何监听器都会收到这个改变的通知,然后可以采取相应的行动。

 import java.awt.*; import java.awt.event.*; java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import javax.swing.*; import javax.swing.event.SwingPropertyChangeSupport; public class AnonymousInnerEg2 { private static void createAndShowUI() { GuiModel2 model = new GuiModel2(); GuiPanel2 guiPanel = new GuiPanel2(); GuiControl2 guiControl = new GuiControl2(); guiControl.setGuiPanel(guiPanel); guiControl.setGuiModel(model); try { guiControl.init(); } catch (GuiException2 e) { e.printStackTrace(); System.exit(-1); } JFrame frame = new JFrame("AnonymousInnerEg"); frame.getContentPane().add(guiPanel); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.pack(); frame.setLocationRelativeTo(null); frame.setVisible(true); } public static void main(String[] args) { java.awt.EventQueue.invokeLater(new Runnable() { public void run() { createAndShowUI(); } }); } } enum GuiState { BASE("Base"), START("Start"), END("End"); private String name; private GuiState(String name) { this.name = name; } public String getName() { return name; } } class GuiModel2 { public static final String STATE = "state"; private SwingPropertyChangeSupport support = new SwingPropertyChangeSupport(this); private GuiState state = GuiState.BASE; public GuiState getState() { return state; } public void setState(GuiState state) { GuiState oldValue = this.state; GuiState newValue = state; this.state = state; support.firePropertyChange(STATE, oldValue, newValue); } public void addPropertyChangeListener(PropertyChangeListener l) { support.addPropertyChangeListener(l); } public void removePropertyChangeListener(PropertyChangeListener l) { support.removePropertyChangeListener(l); } } @SuppressWarnings("serial") class GuiPanel2 extends JPanel { public static final String STATE = "state"; private String state = GuiState.BASE.getName(); private JLabel stateField = new JLabel("", SwingConstants.CENTER); public GuiPanel2() { JPanel btnPanel = new JPanel(new GridLayout(1, 0, 5, 0)); for (final GuiState guiState : GuiState.values()) { btnPanel.add(new JButton(new AbstractAction(guiState.getName()) { { int mnemonic = (int) getValue(NAME).toString().charAt(0); putValue(MNEMONIC_KEY, mnemonic); } @Override public void actionPerformed(ActionEvent e) { String name = getValue(NAME).toString(); setState(name); } })); } setLayout(new BorderLayout()); add(stateField, BorderLayout.PAGE_START); add(btnPanel, BorderLayout.CENTER); } public String getState() { return state; } public void setState(String state) { String oldValue = this.state; String newValue = state; this.state = state; firePropertyChange(STATE, oldValue, newValue); } public void setStateField(String name) { stateField.setText(name); } } class GuiControl2 { private GuiPanel2 guiPanel; private GuiModel2 model; private boolean allOK = false; public void setGuiPanel(GuiPanel2 guiPanel) { this.guiPanel = guiPanel; guiPanel.addPropertyChangeListener(GuiPanel2.STATE, new GuiPanelStateListener()); } public void init() throws GuiException2 { if (model == null) { throw new GuiException2("Model is null"); } if (guiPanel == null) { throw new GuiException2("GuiPanel is null"); } allOK = true; guiPanel.setStateField(model.getState().getName()); } public void setGuiModel(GuiModel2 model) { this.model = model; model.addPropertyChangeListener(new ModelListener()); } private class GuiPanelStateListener implements PropertyChangeListener { @Override public void propertyChange(PropertyChangeEvent evt) { if (!allOK) { return; } if (GuiPanel2.STATE.equals(evt.getPropertyName())) { String text = guiPanel.getState(); model.setState(GuiState.valueOf(text.toUpperCase())); } } } private class ModelListener implements PropertyChangeListener { @Override public void propertyChange(PropertyChangeEvent evt) { if (!allOK) { return; } if (GuiModel2.STATE.equals(evt.getPropertyName())) { GuiState state = (GuiState) evt.getNewValue(); guiPanel.setStateField(state.getName()); } } } } @SuppressWarnings("serial") class GuiException2 extends Exception { public GuiException2() { super(); } public GuiException2(String message) { super(message); } } 

请注意,虽然我不是一个专业的编码员,甚至不是一个大学训练有素的编码员,所以请把这只作为我的意见而已。

目前我在学校学习Java。 老师告诉我们,听众总是必须在Controller类中声明。 我这样做的方法是实现一个方法,例如listeners() 。 里面都是使用匿名类的监听器声明。 这就是我的老师希望看到的方式,但坦率地说,我不确定他们是否完全正确。