Java e GUI – Onde os ActionListeners pertencem de acordo com o padrão MVC?

Atualmente estou escrevendo um modelo de aplicativo Java e de alguma forma, não tenho certeza sobre onde os ActionListeners pertencem se eu quiser seguir de forma limpa o padrão MVC.

O exemplo é baseado em Swing, mas não é sobre o framework, mas sim sobre o conceito básico do MVC em Java, usando qualquer framework para criar GUI.

Comecei com um aplicativo absolutamente simples contendo um JFrame e um JButton (para dispor o quadro, feche o aplicativo). O código por trás deste post. Nada realmente especial, apenas para esclarecer o que estamos falando. Eu não comecei com o Model ainda porque esta questão estava me incomodando demais.

Já houve mais de uma pergunta similar, como estas:
Padrão MVC com muitos ActionListeners
Java swing – Para onde o ActionListener deve ir?

Mas nenhum deles foi realmente satisfatório, pois gostaria de saber duas coisas:

  • É razoável ter todos os ActionListeners em um pacote separado?
    • Eu gostaria de fazê-lo por uma questão de legibilidade do View e Controller, esp. se há muitos ouvintes
  • Como eu executaria uma function de controle de dentro de um ActionListener, se o ouvinte não fosse uma subclass dentro do Controlador? (questão a seguir)

Espero que isso não seja muito geral ou vago, estou perguntando aqui, mas isso me faz pensar por um tempo agora. Eu sempre usei um pouco do meu jeito, deixando o ActionHandler saber sobre o Controlador , mas isso não parece certo, então eu finalmente gostaria de saber como isso é feito corretamente.

Atenciosamente,
jaySon


Controlador:

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(); } } } 

Visão:

 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); } } 

    Essa é uma pergunta muito difícil de responder com o Swing, já que o Swing não é uma implementação MVC pura, a view e o controller são mistos.

    Tecnicamente, um modelo e um controlador devem ser capazes de interagir e o controlador e a visualização devem interagir, mas a visão e o modelo nunca devem interagir, o que claramente não é como o Swing funciona, mas isso é outro debate …

    Outra questão é, você realmente não quer expor componentes da interface do usuário a ninguém, o controlador não deve se importar como certas ações ocorrem, apenas que eles podem.

    Isso sugeriria que os ActionListener anexados aos seus controles de interface do usuário devem ser mantidos pela exibição. A visualização deve então alertar o controlador de que algum tipo de ação ocorreu. Para isso, você poderia usar outro ActionListener , gerenciado pela view, ao qual o controller se inscreve.

    Melhor ainda, eu teria um ouvinte de exibição dedicado, que descrevia as ações que essa exibição poderia produzir, por exemplo …

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

    O controlador então didPerformClose a visualização por meio desse ouvinte e a exibição chamaria didPerformClose quando (neste caso) o botão Fechar for pressionado.

    Mesmo neste exemplo, eu ficaria tentado a fazer uma interface de “visão principal”, que descrevia as propriedades (setters e getters) e ações (ouvintes / retornos de chamada) que qualquer implementação é garantida para fornecer, então você não se importa como estes ações ocorrem, só que quando o fazem, você é esperado para fazer alguma coisa …

    Em cada nível que você quer se perguntar, como seria fácil mudar qualquer elemento (mudar o modelo ou o controlador ou a visão) para outra instância? Se você está tendo que desacoplar o código, então você tem um problema. Comunique-se por meio de interfaces e tente reduzir a quantidade de acoplamento entre as camadas e a quantidade que cada camada conhece sobre as outras, até o ponto em que elas simplesmente mantêm contratos.

    Atualizada…

    Vamos pegar isso como exemplo …

    Entrar

    Na verdade, existem duas visualizações (descontando a checkbox de diálogo real), há a exibição de credenciais e a visualização de login, sim, elas são diferentes, como você verá.

    Credenciais

    A exibição de credenciais é responsável por coletar os detalhes que devem ser autenticados, o nome de usuário e a senha. Ele fornecerá informações ao controlador para que ele saiba quando essas credenciais foram alteradas, pois o controlador pode querer executar alguma ação, como ativar o botão “login” …

    A visão também vai querer saber quando a autenticação está prestes a ocorrer, já que ela vai querer desabilitar seus campos, então o usuário não pode atualizar a view enquanto a autenticação estiver ocorrendo, da mesma forma, ela precisará saber quando a autenticação falhar ou ter sucesso, pois precisará tomar medidas para essas eventualidades.

     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

    O CredentialsPane é a implementação física de um CredentialsView , ele implementa o contrato, mas gerencia seu próprio estado interno. Como o contrato é gerenciado é irrelevante para o controlador, só se preocupa com o contrato foi confirmado …

     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

    O LoginView é responsável por gerenciar um CredentialsView , mas também por notificar o LoginViewController quando a autenticação deve ocorrer ou se o processo foi cancelado pelo usuário, por algum meio …

    Da mesma forma, o LoginViewController informará a exibição quando a autenticação estiver prestes a ocorrer e se a autenticação falhar ou tiver êxito.

     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

    O LoginPane é um pouco especial, está atuando como a visualização do LoginViewController , mas também está atuando como o controlador para o CredentialsView . Isso é importante, já que não há nada dizendo que uma visão não pode ser um controlador, mas eu seria cuidadoso sobre como você implementa essas coisas, pois pode nem sempre fazer sentido fazê-lo dessa maneira, mas porque as duas visões são trabalhando juntos para coletar informações e gerenciar events, fazia sentido nesse caso.

    Como o LoginPane precisará alterar seu próprio estado com base nas alterações no CredentialsView , faz sentido permitir que o LoginPane atue como o controlador nesse caso; caso contrário, você precisaria fornecer mais methods que controlassem esse estado do sistema. botões, mas isso começa a sangrar lógica da interface do usuário para o controlador …

     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(); } } 

    Exemplo de trabalho

     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; } } } 

    Eles estão associados ao controle, mas não precisam ser uma parte direta do controle. Por exemplo, por favor, veja o código postado abaixo que eu estava preparando para outra pergunta, um sobre classs internas anônimas e acoplamento, aqui eu dou todos os meus botões anônimos Ações internas (que são ActionListeners, é claro), e então uso as Ações para mudar o estado da GUI. Todos os ouvintes da GUI (o controle) serão notificados dessa alteração e poderão agir de acordo.

     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); } } 

    Note em aviso embora: Eu não sou um codificador profissional ou mesmo um programador treinado pela universidade, então por favor, tome isso apenas como minha opinião.

    Atualmente estou aprendendo Java na escola. Os professores nos disseram que os ouvintes sempre precisam ser declarados dentro da class Controller . A maneira que eu faço isso é implementar um método, por exemplo listeners () . Dentro estão todas as declarações de ouvintes usando classs anônimas. É assim que meus professores querem ver, mas francamente, eu não tenho certeza se eles acertaram tudo.