一尘不染

Java和GUI-根据MVC模式,ActionListeners在哪里?

java

我目前正在编写模板Java应用程序,并且如果我想完全遵循MVC模式,就不确定ActionListener的位置。

该示例基于Swing,但与框架无关,而是Java中使用任何框架创建GUI的MVC的基本概念。

我从一个绝对简单的应用程序开始,该应用程序包含一个JFrame和一个JButton(以放置框架,从而关闭该应用程序)。此帖子后面的代码。没什么特别的,只是为了澄清我们在说什么。我还没有开始使用Model,因为这个问题困扰了我太多。

已经有一个以上类似的问题,例如:

但是我想知道两件事,但没有一个让我真正满意:

  • 将所有ActionListeners放在单独的程序包中是否合理?
  • 我这样做是出于View和Controller尤其是可读性的考虑。如果有很多听众
  • 如果侦听器不是Controller内部的子类,则如何从ActionListener中执行Controller函数?(后续问题)
    我希望我在这里问的不是太笼统或含糊,但这让我想了一会儿。我总是使用自己的方式,让ActionHandler知道Controller,但这似乎不对,所以我最后想知道如何正确完成。

此致,
jaySon

控制器:

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

阅读 419

收藏
2020-03-03

共1个答案

一尘不染

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

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

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

这建议ActionListener附加到UI控件的s应该由视图维护。然后,视图应警告控制器已发生某种动作。为此,你可以使用ActionListener控制器订阅的视图(由视图管理)。

更好的是,我将拥有一个专用的视图侦听器,该监听器描述了该视图可能产生的动作,例如…

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

然后,控制器将通过此侦听器订阅该视图,并且该视图将didPerformClose在(在这种情况下)按下关闭按钮时调用。

即使在此示例中,我也很想创建一个“主视图”界面,该界面描述了任何实现都可以保证提供的属性(设置程序和获取器)和操作(侦听器/回调),那么你无需关心这些会发生一些动作,只有当他们这样做时,你才有望做某事…

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

更新…

让我们以这个为例…

实际上有两个视图(为实际对话框打折),有凭据视图和登录视图,是的,你将看到它们是不同的。

CredentialsView

凭证视图负责收集要验证的详细信息,用户名和密码。它将向控制器提供信息,以便在更改那些凭据时通知它,因为控制器可能要采取一些措施,例如启用“登录”按钮…

该视图还希望知道何时进行认证,因为它将禁用其字段,因此用户在进行认证时无法更新视图,同样,它需要知道何时进行认证失败或成功,因为它将需要针对这些突发事件采取行动。

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

该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,同时也为通知LoginViewController时应该存放着认证,或者整个过程由用户取消,通过一些手段…

同样,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,但也充当的控制器CredentialsView。这很重要,因为没有任何说法说视图不能成为控制器,但是我会谨慎考虑如何实现这样的事情,因为这样做并非总是有意义,但是因为这两个视图是一起收集信息和管理事件,在这种情况下很有意义。

由于LoginPane将需要根据的更改来更改自己的状态CredentialsView,因此LoginPane在这种情况下允许充当控制器是有意义的,否则,你需要提供更多方法来控制按钮的状态,但是这开始将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();
    }
2020-03-03