Java和GUI—根据MVC模式,ActionListener属于哪里?

Java和GUI—根据MVC模式,ActionListener属于哪里?,java,swing,user-interface,model-view-controller,awt,Java,Swing,User Interface,Model View Controller,Awt,我目前正在编写一个模板Java应用程序,不知何故,如果我想完全遵循MVC模式,我不确定ActionListeners属于哪里 这个例子是基于Swing的,但它不是关于框架,而是关于Java中MVC的基本概念,使用任何框架来创建GUI 我从一个包含JFrame和JButton的绝对简单的应用程序开始(处理框架,然后关闭应用程序)。跟踪此帖子的代码。没什么特别的,只是想弄清楚我们在说什么。我还没有从模型开始,因为这个问题太困扰我了 类似的问题已经不止一个了,比如: 但他们中没有一个是真正令人满意

我目前正在编写一个模板Java应用程序,不知何故,如果我想完全遵循MVC模式,我不确定ActionListeners属于哪里

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

我从一个包含JFrame和JButton的绝对简单的应用程序开始(处理框架,然后关闭应用程序)。跟踪此帖子的代码。没什么特别的,只是想弄清楚我们在说什么。我还没有从模型开始,因为这个问题太困扰我了

类似的问题已经不止一个了,比如:

但他们中没有一个是真正令人满意的,因为我想知道两件事:

  • 将所有ActionListener放在一个单独的包中是否合理?
    • 我希望这样做是为了视图和控制器的可读性,特别是当有很多侦听器时
  • 如果监听器不是控制器中的子类,我将如何从ActionListener中执行控制器函数?(跟进问题)
我希望我在这里问的不是太笼统或含糊,但这让我思考了一会儿。我总是用我自己的方式,让ActionHandler知道控制器的情况,但这似乎不对,所以我想知道这是如何正确完成的

亲切的问候,
杰森


控制器:

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

视图:


它们与控件关联,但不必是控件的直接部分。例如,请参阅下面发布的代码,我正准备回答另一个问题,一个关于匿名内部类和耦合的问题,这里我给出了所有按钮的匿名内部操作(当然是ActionListeners),然后使用这些操作更改GUI状态。GUI(控件)的任何侦听器都将收到此更改的通知,然后可以相应地进行操作

import java.awt.*;
import java.awt.event.*; 
import 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);
   }
}

但请注意:我不是专业的程序员,甚至不是受过大学培训的程序员,所以请仅将此作为我的观点。

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

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

另一个问题是,您真的不想向任何人公开UI组件,控制器不应该关心某些操作是如何发生的,只是它们可以

这将建议视图维护附加到UI控件的
ActionListener
s。然后,视图应提醒控制器发生了某种操作。为此,您可以使用另一个由视图管理的
ActionListener
,控制器订阅该视图

更好的是,我会有一个专用的视图侦听器,它描述这个视图可能产生的操作,例如

public interface MainViewListener {
    public void didPerformClose(MainView mainView);
}
然后,控制器将通过此侦听器订阅视图,当(在本例中)按下关闭按钮时,视图将调用
didPerformClose

即使在这个示例中,我也会尝试创建一个“主视图”接口,它描述了任何实现都保证提供的属性(setter和getter)和操作(侦听器/回调),然后您不关心这些操作是如何发生的,只关心当它们发生时,您应该做些什么

在每个级别上,您都想问问自己,为另一个实例更改任何元素(更改模型、控制器或视图)有多容易?如果您发现自己必须解耦代码,那么您就有问题了。通过接口进行通信,并尝试减少各层之间的耦合量以及各层对其他层的了解量,使其仅维持契约

已更新…

让我们以这个为例

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

实际上有两个视图(不考虑实际对话框),即凭据视图和登录视图,是的,正如您将看到的,它们是不同的

认证视图 credentials视图负责收集要进行身份验证的详细信息、用户名和密码。它将向控制器提供信息,让其知道这些凭据何时已更改,因为控制器可能希望采取一些操作,例如启用“登录”按钮

视图还希望知道何时将要进行身份验证,因为它希望禁用其字段,因此用户无法在进行身份验证时更新视图,同样,它还需要知道身份验证何时失败或成功,因为它需要针对这些可能发生的情况采取措施

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
CredentialsView
的物理实现,它实现合同,但管理其自身的内部状态。合同的管理方式与控制人无关,它只关心合同是否得到维护

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
负责管理
CredentialsView
,但也负责通知
LoginView控制器
何时应该进行身份验证,或者如果用户通过某种方式取消了该过程

同样地,
LoginViewController
将告诉视图何时进行身份验证,以及身份验证是否失败或成功

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
有点特殊,它充当
LoginViewController
的视图,但也充当
credentials视图的控制器
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;
        }

    }

}
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class Ripples implements Runnable {

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

    private Animation animation;

    private DrawingPanel drawingPanel;

    private RipplesModel model;

    public Ripples() {
        model = new RipplesModel();
    }

    @Override
    public void run() {
        JFrame frame = new JFrame("Ripples");
        frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
        frame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent event) {
                stopAnimation();
                frame.dispose();
                System.exit(0);
            }
        });

        drawingPanel = new DrawingPanel(model);
        frame.add(drawingPanel, BorderLayout.CENTER);

        frame.pack();
        frame.setLocationByPlatform(true);
        frame.setVisible(true);

        animation = new Animation(this, model);
        new Thread(animation).start();
    }

    public void repaint() {
        drawingPanel.repaint();
    }

    private void stopAnimation() {
        if (animation != null) {
            animation.setRunning(false);
        }
    }

    public class DrawingPanel extends JPanel {

        private static final long serialVersionUID = 1L;

        private RipplesModel model;

        public DrawingPanel(RipplesModel model) {
            this.model = model;
            setBackground(Color.BLACK);
            setPreferredSize(new Dimension(500, 500));
            addMouseListener(new RipplesListener(model));
        }

        @Override
        public void paintComponent(Graphics g) {
            super.paintComponent(g);

            Graphics2D g2 = (Graphics2D) g;
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_ON);
            g2.setStroke(new BasicStroke(5f));

            List<Circle> circles = model.getCircles();
            for (Circle circle : circles) {
                Point p = circle.getCenter();
                int radius = circle.getRadius();
                g2.setColor(circle.getColor());
                g2.drawOval(p.x - radius, p.y - radius,
                        2 * radius, 2 * radius);
            }
        }

    }

    public class RipplesListener extends MouseAdapter {

        private Random random;

        private RipplesModel model;

        public RipplesListener(RipplesModel model) {
            this.model = model;
            this.random = new Random();
        }

        @Override
        public void mousePressed(MouseEvent event) {
            model.addCircle(new Circle(event.getPoint(),
                    createColor()));
        }

        private Color createColor() {
            int r = random.nextInt(128) + 128;
            int g = random.nextInt(128) + 128;
            int b = random.nextInt(128) + 128;
            return new Color(r, g, b);
        }
    }

    public class Animation implements Runnable {

        private volatile boolean running;

        private Ripples frame;

        private RipplesModel model;

        public Animation(Ripples frame, RipplesModel model) {
            this.frame = frame;
            this.model = model;
            this.running = true;
        }

        @Override
        public void run() {
            while (running) {
                sleep(20L);
                incrementRadius();
                repaint();
            }
        }

        private void incrementRadius() {
            List<Circle> circles = model.getCircles();
            for (Circle circle : circles) {
                circle.incrementRadius();
            }
        }

        private void sleep(long delay) {
            try {
                Thread.sleep(delay);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        private void repaint() {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    frame.repaint();
                }
            });
        }

        public synchronized void setRunning(boolean running) {
            this.running = running;
        }

    }

    public class RipplesModel {

        private List<Circle> circles;

        public RipplesModel() {
            this.circles = new ArrayList<>();
        }

        public void addCircle(Circle circle) {
            this.circles.add(circle);
        }

        public List<Circle> getCircles() {
            return circles;
        }

    }

    public class Circle {

        private int radius;

        private final Color color;

        private final Point center;

        public Circle(Point center, Color color) {
            this.center = center;
            this.color = color;
            this.radius = 10;
        }

        public void incrementRadius() {
            radius = (++radius > 200) ? 10 : radius;
        }

        public Color getColor() {
            return color;
        }

        public int getRadius() {
            return radius;
        }

        public Point getCenter() {
            return center;
        }

    }

}