听众放置坚持传统(非中介)MVC模式

时间:2015-07-24 04:18:41

标签: java swing model-view-controller

我正在Swing中实现一个程序,我读过Nirmal's implementation of this pattern in Swing,它似乎表现出对整个“责任分离”概念的优雅处理。

然而,由于我正在开发一个比Nirml发布的更复杂的程序,它由一个JFrame容器组成,我寻找指导如何正确实现MVC。

我的程序将由子容器等组成。我很好奇Controller应该如何实现定义和分配View的所有侦听器背后的逻辑..或者如果为每个View组件定义侦听器的控制器是否实用?

似乎我需要在View的顶级容器中使用一个方法来允许Controller调用视图来向相关组件添加一个Listener?所以我需要一个方法链,每个方法都将侦听器从顶层容器传递到持有组件的直接容器。最后用容器调用addActionListener()就可以了。

这是在MVC中处理侦听器的正确方法吗?

在MVC中强制控制View中每个组件的所有侦听器,还是一个有用的练习?这也意味着我在顶级容器(View)中创建方法,以便为Controller提供一种方法,将侦听器分配给子容器中的每个组件?

1 个答案:

答案 0 :(得分:6)

好的,首先,Swing已经实现了MVC的一种形式,尽管是以VC-M的形式。这意味着你不应该试图直接将Swing限制为纯粹的MVC,因为你会非常失望并且花费大量的时间来制作他们不应该做的黑客。

相反,您可以围绕Swing包装MVC,允许它围绕API进行操作。

在我看来,一个控制器不需要知道,也不应该关心视图或模型是如何实现的,但它应该只关心它如何与它们一起工作(我也有)许多开发人员掌握了UI组件并与他们一起做了他们不应该做的事情,并在我们改变了实现时破坏了API。最好隐藏那种细节)

在这种情况下,您可以将视图视为自包含实体 - 它具有控件并且它可以独立于控制器执行操作。控制器并不关心实现细节。它所关心的是获取信息,并在合同描述的某些事件发生时被告知。它不应该关心它是如何产生的。

例如,假设您有登录视图。控制器只想知道用户输入的用户名和密码以及何时应该验证该信息。

我们假设您实现了视图/控制器以公开JTextFieldJPasswordField s,但稍后,您的用户希望将用户名选择限制为特定列表(可能由模型提供)。现在,您的控制器中存在实施细节,这些细节已不再适用,您必须手动更改或为此新用例创建新的MVC。

相反,如果你只是声明视图中有一个用户名和密码的getter以及某种事件监听器会告诉控制器用户何时需要验证凭据,该怎么办?那么现在,您只需提供一个新视图,无需修改控制器。控制器不关心如何生成这些值。

关于你问题的更大方面。

  

我的程序将由子容器等组成。我好奇   至于控制器应该如何实现背后的逻辑   定义和分配视图的所有侦听器..或者如果是   控制器为每个View组件定义监听器   甚至实用?

     

似乎我需要在View的顶级方法中使用一种方法   容器允许Controller调用视图来添加监听器   到有问题的组件?所以我需要一系列方法,   每个从顶级容器向下传递给侦听器   直接容器拿着组件..最终与   容器调用addActionListener()就可以了。

     

这是在MVC中处理侦听器的正确方法吗?

一般的答案是,不,这不是正确的方法。

每个子视图都将成为自己的MVC,并专注于自己的需求。父MVC可以使用由子MVC提供的事件或其他功能来进行更新,甚至修改其他子MVC的状态。

这里要记住的重要一点是,视图可以充当其他视图的控制器,但是,您可以选择使用一系列允许视图管理的控制器。

想象一下像#"向导"。它有一系列步骤,可以收集用户的各种信息,每一步都需要有效才能进入下一步。

现在,您可能想直接将导航集成到此,但更好的想法是将导航细节分离为自己的MVC。

当被询问时,向导将向用户呈现步骤,用户将填写信息,可能触发事件。然后,这些事件将允许导航MVC决定用户是否可以移动到下一步或上一步。

两个MVC将由第三个" master" MVC,它将帮助管理状态(从向导中侦听事件并更新导航状态)

让我们尝试一个问题,这个问题可以在这里被问到很多,这是一个测验!

测验有问题,每个问题都有提示,正确答案,一系列可能的答案,我们也希望存储用户得到的答案。

测验API

所以,下面我们有测验MVC的基本概要,我们有一个问题,它由模型管理,有一个控制器和一个视图和一系列观察者(听众)

合同(接口)

public interface Question {
    public String getPrompt();
    public String getCorrectAnswer();
    public String getUserAnswer();
    public String[] getOptions();
    public boolean isCorrect();

}

/**
* This is a deliberate choice to separate the update functionality
* No one but the model should ever actually -apply- the answer to the
* question
*/
public interface MutableQuestion extends Question {
    public void setUserAnswer(String userAnswer);
}

public interface QuizModel {
    public void addQuizObserver(QuizModelObserver observer);
    public void removeQuizObserver(QuizModelObserver observer);
    public Question getNextQuestion();
    public Question getCurrentQuestion();
    public int size();
    public int getScore();
    public void setUserAnswerFor(Question question, String answer);
}

public interface QuizModelObserver {
    public void didStartQuiz(QuizModel quiz);
    public void didCompleteQuiz(QuizModel quiz);
    public void questionWasAnswered(QuizModel model, Question question);
}

public interface QuizView extends View {
    public void setQuestion(Question question);
    public boolean hasAnswer();
    public String getUserAnswer();
    public void addQuizObserver(QuizViewObserver observer);
    public void removeQuizObserver(QuizViewObserver observer);
}

public interface QuizViewObserver {
    public void userDidChangeAnswer(QuizView view);
}

public interface QuizController {
    public QuizModel getModel(); // This is the model
    public QuizView getView();
    public void askNextQuestion();
}

我个人而言,遵循"代码接口(而非实施)的原则",我也故意过分强调这个想法以证明这一点。

如果仔细观察,您会注意到视图或模型实际上彼此之间没有任何关系。这全部由控制器控制

我在这里做的一件事就是为控制器提供一个askNextQuestion,因为控制器不知道何时应该发生(你可能会考虑使用{{1}但是,这意味着用户只能尝试回答一个问题,有点意思)

实施

现在,通常情况下,我希望有一些userDidChangeAnswer实施,以填补"普通"功能,我已经放弃了大部分内容并直接进入默认实现,这主要是为了演示目的。

abstract

这里真的没什么特别的。关于唯一重要的事情是控制器如何管理模型和视图之间的事件

导航API

导航API非常基础。它允许您控制用户是否可以实际导航到下一个或上一个元素(如果操作应该对用户可用)以及随时禁用任何一个操作

(同样,我已经专注于一个简单的设计,实际上,对修改模型的状态进行一些控制以改变导航可以在哪个方向工作会很好,但是我已经故意将其留下来以保持简单)

合同(接口)

public class DefaultQuestion implements MutableQuestion {

    private final String prompt;
    private final String correctAnswer;
    private String userAnswer;
    private final String[] options;

    public DefaultQuestion(String prompt, String correctAnswer, String... options) {
        this.prompt = prompt;
        this.correctAnswer = correctAnswer;
        this.options = options;
    }

    @Override
    public String getPrompt() {
        return prompt;
    }

    @Override
    public String getCorrectAnswer() {
        return correctAnswer;
    }

    @Override
    public String getUserAnswer() {
        return userAnswer;
    }

    @Override
    public String[] getOptions() {
        List<String> list = new ArrayList<>(Arrays.asList(options));
        Collections.shuffle(list);
        return list.toArray(new String[list.size()]);
    }

    public void setUserAnswer(String userAnswer) {
        this.userAnswer = userAnswer;
    }

    @Override
    public boolean isCorrect() {
        return getCorrectAnswer().equals(getUserAnswer());
    }

}

public abstract class AbstractQuizModel implements QuizModel {

    private List<QuizModelObserver> observers;

    public AbstractQuizModel() {
        observers = new ArrayList<>(25);
    }

    @Override
    public void addQuizObserver(QuizModelObserver observer) {
        observers.add(observer);
    }

    @Override
    public void removeQuizObserver(QuizModelObserver observer) {
        observers.remove(observer);
    }

    protected void fireDidStartQuiz() {
        for (QuizModelObserver observer : observers) {
            observer.didStartQuiz(this);
        }
    }

    protected void fireDidCompleteQuiz() {
        for (QuizModelObserver observer : observers) {
            observer.didCompleteQuiz(this);
        }
    }

    protected void fireQuestionWasAnswered(Question question) {
        for (QuizModelObserver observer : observers) {
            observer.questionWasAnswered(this, question);
        }
    }

}

public class DefaultQuizModel extends AbstractQuizModel {

    private List<MutableQuestion> questions;
    private Iterator<MutableQuestion> iterator;

    private MutableQuestion currentQuestion;
    private boolean completed;

    private int score;

    public DefaultQuizModel() {
        questions = new ArrayList<>(50);
    }

    public void add(MutableQuestion question) {
        questions.add(question);
    }

    public void remove(MutableQuestion question) {
        questions.remove(question);
    }

    @Override
    public Question getNextQuestion() {
        if (!completed && iterator == null) {
            iterator = questions.iterator();
            fireDidStartQuiz();
        }
        if (iterator.hasNext()) {
            currentQuestion = iterator.next();
        } else {
            completed = true;
            iterator = null;
            currentQuestion = null;
            fireDidCompleteQuiz();
        }
        return currentQuestion;
    }

    @Override
    public Question getCurrentQuestion() {
        return currentQuestion;
    }

    @Override
    public int size() {
        return questions.size();
    }

    @Override
    public int getScore() {
        return score;
    }

    @Override
    public void setUserAnswerFor(Question question, String answer) {
        if (question instanceof MutableQuestion) {
            ((MutableQuestion) question).setUserAnswer(answer);
            if (question.isCorrect()) {
                score++;
            }
            fireQuestionWasAnswered(question);
        }
    }

}

public class DefaultQuizController implements QuizController {

    private QuizModel model;
    private QuizView view;

    public DefaultQuizController(QuizModel model, QuizView view) {
        this.model = model;
        this.view = view;
    }

    @Override
    public QuizModel getModel() {
        return model;
    }

    @Override
    public QuizView getView() {
        return view;
    }

    @Override
    public void askNextQuestion() {
        Question question = getModel().getCurrentQuestion();
        if (question != null) {
            String answer = getView().getUserAnswer();
            getModel().setUserAnswerFor(question, answer);
        }
        question = getModel().getNextQuestion();
        getView().setQuestion(question);
    }

}

public class DefaultQuizViewPane extends JPanel implements QuizView {

    private final JLabel question;
    private final JPanel optionsPane;
    private final ButtonGroup bg;

    private final List<JRadioButton> options;
    private String userAnswer;

    private final List<QuizViewObserver> observers;

    private final AnswerActionListener answerActionListener;

    private final GridBagConstraints optionsGbc;

    protected DefaultQuizViewPane() {

        setBorder(new EmptyBorder(4, 4, 4, 4));

        question = new JLabel();
        optionsPane = new JPanel(new GridBagLayout());
        optionsPane.setBorder(new EmptyBorder(4, 4, 4, 4));

        answerActionListener = new AnswerActionListener();

        optionsGbc = new GridBagConstraints();
        optionsGbc.gridwidth = GridBagConstraints.REMAINDER;
        optionsGbc.weightx = 1;
        optionsGbc.anchor = GridBagConstraints.WEST;

        options = new ArrayList<>(25);

        bg = new ButtonGroup();

        observers = new ArrayList<>(25);

        setLayout(new BorderLayout());

        add(question, BorderLayout.NORTH);
        add(optionsPane);

    }

    protected void reset() {
        question.setText(null);
        for (JRadioButton rb : options) {
            rb.removeActionListener(answerActionListener);
            bg.remove(rb);
            optionsPane.remove(rb);
        }
        options.clear();
    }

    @Override
    public void setQuestion(Question question) {
        reset();
        if (question != null) {
            this.question.setText(question.getPrompt());

            for (String option : question.getOptions()) {
                JRadioButton rb = makeRadioButtonFor(option);
                options.add(rb);
                optionsPane.add(rb, optionsGbc);
            }
            optionsPane.revalidate();
            revalidate();
            repaint();
        }
    }

    @Override
    public void addQuizObserver(QuizViewObserver observer) {
        observers.add(observer);
    }

    @Override
    public void removeQuizObserver(QuizViewObserver observer) {
        observers.remove(observer);
    }

    protected void fireUserDidChangeAnswer() {
        for (QuizViewObserver observer : observers) {
            observer.userDidChangeAnswer(this);
        }
    }

    protected JRadioButton makeRadioButtonFor(String option) {
        JRadioButton btn = new JRadioButton(option);
        btn.addActionListener(answerActionListener);
        bg.add(btn);

        return btn;
    }

    @Override
    public boolean hasAnswer() {
        return userAnswer != null;
    }

    @Override
    public String getUserAnswer() {
        return userAnswer;
    }

    @Override
    public JComponent getViewComponent() {
        return this;
    }

    protected class AnswerActionListener implements ActionListener {

        @Override
        public void actionPerformed(ActionEvent e) {
            userAnswer = e.getActionCommand();
            fireUserDidChangeAnswer();
        }

    }

}

实施

public enum NavigationDirection {
    NEXT, PREVIOUS;
}

public interface NavigationModel {
    public boolean canNavigate(NavigationDirection direction);

    public void addObserver(NavigationModelObserver observer);
    public void removeObserver(NavigationModelObserver observer);

    public void next();
    public void previous();

}

public interface NavigationModelObserver {
    public void next(NavigationModel view);
    public void previous(NavigationModel view);
}

public interface NavigationController {
    public NavigationView getView();
    public NavigationModel getModel();

    public void setDirectionEnabled(NavigationDirection navigationDirection, boolean b);
}

public interface NavigationView extends View {

    public void setNavigatable(NavigationDirection direction, boolean navigtable);
    public void setDirectionEnabled(NavigationDirection direction, boolean enabled);

    public void addObserver(NavigationViewObserver observer);
    public void removeObserver(NavigationViewObserver observer);
}

public interface NavigationViewObserver {
    public void next(NavigationView view);
    public void previous(NavigationView view);
}

测验大师

现在,这些是两个不同的API,它们没有任何共同点,因此,我们需要某种控制器来桥接它们

合同(接口)

public static class DefaultNavigationModel implements NavigationModel {

    private Set<NavigationDirection> navigatableDirections;
    private List<NavigationModelObserver> observers;

    public DefaultNavigationModel() {
        this(true, true);
    }

    public DefaultNavigationModel(boolean canNavigateNext, boolean canNavigatePrevious) {
        navigatableDirections = new HashSet<>(2);
        observers = new ArrayList<>(25);
        setCanNavigate(NavigationDirection.NEXT, canNavigateNext);
        setCanNavigate(NavigationDirection.PREVIOUS, canNavigatePrevious);
    }

    public void setCanNavigate(NavigationDirection direction, boolean canNavigate) {
        if (canNavigate) {
            navigatableDirections.add(direction);
        } else {
            navigatableDirections.remove(direction);
        }
    }

    @Override
    public boolean canNavigate(NavigationDirection direction) {
        return navigatableDirections.contains(direction);
    }

    @Override
    public void addObserver(NavigationModelObserver observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(NavigationModelObserver observer) {
        observers.remove(observer);
    }

    protected   void fireMoveNext() {
        for (NavigationModelObserver observer : observers) {
            observer.next(this);
        }
    }

    protected   void fireMovePrevious() {
        for (NavigationModelObserver observer : observers) {
            observer.previous(this);
        }
    }

    @Override
    public void next() {
        fireMoveNext();
    }

    @Override
    public void previous() {
        fireMovePrevious();
    }

}

public static class DefaultNavigationController implements NavigationController {

    private final NavigationModel model;
    private final NavigationView view;

    public DefaultNavigationController(NavigationModel model, NavigationView view) {
        this.model = model;
        this.view = view;

        view.setNavigatable(NavigationDirection.NEXT, model.canNavigate(NavigationDirection.NEXT));
        view.setNavigatable(NavigationDirection.PREVIOUS, model.canNavigate(NavigationDirection.PREVIOUS));

        view.addObserver(new NavigationViewObserver() {
            @Override
            public void next(NavigationView view) {
                if (getModel().canNavigate(NavigationDirection.NEXT)) {
                    getModel().next();
                }
            }

            @Override
            public void previous(NavigationView view) {
                if (getModel().canNavigate(NavigationDirection.PREVIOUS)) {
                    getModel().previous();
                }
            }
        });
    }

    @Override
    public NavigationView getView() {
        return view;
    }

    @Override
    public NavigationModel getModel() {
        return model;
    }

    @Override
    public void setDirectionEnabled(NavigationDirection navigationDirection, boolean enabled) {
        getView().setDirectionEnabled(navigationDirection, enabled);
    }

}

public static class DefaultNavigationViewPane extends JPanel implements NavigationView {

    private final List<NavigationViewObserver> observers;

    private final JButton btnNext;
    private final JButton btnPrevious;

    public DefaultNavigationViewPane() {
        btnNext = new JButton("Next >");
        btnNext.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                fireMoveNext();
            }
        });
        btnPrevious = new JButton("< Previous");
        btnPrevious.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                fireMovePrevious();
            }
        });
        setLayout(new FlowLayout(FlowLayout.RIGHT));

        add(btnPrevious);
        add(btnNext);

        observers = new ArrayList<>();
    }

    @Override
    public void addObserver(NavigationViewObserver observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(NavigationViewObserver observer) {
        observers.remove(observer);
    }

    protected void fireMoveNext() {
        for (NavigationViewObserver observer : observers) {
            observer.next(this);
        }
    }

    protected void fireMovePrevious() {
        for (NavigationViewObserver observer : observers) {
            observer.previous(this);
        }
    }

    @Override
    public JComponent getViewComponent() {
        return this;
    }

    @Override
    public void setNavigatable(NavigationDirection direction, boolean navigtable) {
        switch (direction) {
            case NEXT:
                btnNext.setVisible(navigtable);
                break;
            case PREVIOUS:
                btnPrevious.setVisible(navigtable);
                break;
        }
    }

    @Override
    public void setDirectionEnabled(NavigationDirection direction, boolean enabled) {
        switch (direction) {
            case NEXT:
                btnNext.setEnabled(enabled);
                break;
            case PREVIOUS:
                btnPrevious.setEnabled(enabled);
                break;
        }
    }

}

好的,所以你可能会问自己一个明显的问题,模特在哪里?嗯,它不需要一个,它只是导航和测验API之间的桥梁,它不会管理它自己的任何数据...

实现

public interface QuizMasterController {
    public QuizController getQuizController();
    public NavigationController getNavigationController();
    public QuizMasterView getView();
}

public interface QuizMasterView extends View {
    public NavigationController getNavigationController();
    public QuizController getQuizController();
    public void showScoreView(int score, int size);
    public void showQuestionAndAnswerView();
}

现在,实施很有意思,它实际上有两个状态&#34;或&#34;观点&#34;,&#34;问答#&34;查看和&#34;得分视图&#34;。这也是故意的,因为我真的不想要另一个MVC。 Q&amp; A视图已经以任何方式管理两个MVC:P

基本上,它的作用是监视测验API,以便当用户更改问题的答案时,它会告诉导航API它可以移动到下一个问题。它还监视开始和已完成的事件,为这些状态提供所需的视图。

它还监视导航事件的导航API。在此示例中,我们只能向单一方向移动,即使导航API配置为其他方式,测验API也不提供该功能

把它放在一起

现在,我已选择单独刻意构建每个部分,可以想象,您可以让public class DefaultQuizMasterController implements QuizMasterController { private QuizController quizController; private NavigationController navController; private QuizMasterView view; public DefaultQuizMasterController(QuizController quizController, NavigationController navController) { this.quizController = quizController; this.navController = navController; view = new DefaultQuizMasterViewPane(quizController, navController); // Setup the initial state quizController.askNextQuestion(); navController.getModel().addObserver(new NavigationModelObserver() { @Override public void next(NavigationModel view) { getQuizController().askNextQuestion(); getNavigationController().setDirectionEnabled(NavigationDirection.NEXT, false); } @Override public void previous(NavigationModel view) { // NOOP } }); quizController.getView().addQuizObserver(new QuizViewObserver() { @Override public void userDidChangeAnswer(WizeQuiz.QuizView view) { getNavigationController().setDirectionEnabled(NavigationDirection.NEXT, true); } }); quizController.getModel().addQuizObserver(new QuizModelObserver() { @Override public void didStartQuiz(QuizModel quiz) { getView().showQuestionAndAnswerView(); } @Override public void didCompleteQuiz(QuizModel quiz) { getView().showScoreView(quiz.getScore(), quiz.size()); getNavigationController().setDirectionEnabled(NavigationDirection.NEXT, false); } @Override public void questionWasAnswered(QuizModel model, Question question) { } }); navController.setDirectionEnabled(NavigationDirection.NEXT, false); } @Override public QuizController getQuizController() { return quizController; } @Override public NavigationController getNavigationController() { return navController; } @Override public QuizMasterView getView() { return view; } } public class DefaultQuizMasterViewPane extends JPanel implements QuizMasterView { private QuizController quizController; private NavigationController navController; private QuestionAndAnswerView qaView; private ScoreView scoreView; private CardLayout cardLayout; public DefaultQuizMasterViewPane(QuizController quizController, NavigationController navController) { this.quizController = quizController; this.navController = navController; quizController.getModel().addQuizObserver(new QuizModelObserver() { @Override public void didStartQuiz(QuizModel quiz) { } @Override public void didCompleteQuiz(QuizModel quiz) { } @Override public void questionWasAnswered(QuizModel model, Question question) { qaView.updateScore(); } }); scoreView = new ScoreView(); qaView = new QuestionAndAnswerView(); qaView.updateScore(); cardLayout = new CardLayout(); setLayout(cardLayout); add(qaView, "view.qa"); add(scoreView, "view.score"); } @Override public JComponent getViewComponent() { return this; } @Override public NavigationController getNavigationController() { return navController; } @Override public QuizController getQuizController() { return quizController; } @Override public void showScoreView(int score, int size) { scoreView.updateScore(); cardLayout.show(this, "view.score"); } @Override public void showQuestionAndAnswerView() { cardLayout.show(this, "view.qa"); } protected class QuestionAndAnswerView extends JPanel { private JLabel score; public QuestionAndAnswerView() { setLayout(new BorderLayout()); add(getQuizController().getView().getViewComponent()); JPanel south = new JPanel(new BorderLayout()); south.add(getNavigationController().getView().getViewComponent(), BorderLayout.SOUTH); score = new JLabel(); score.setHorizontalAlignment(JLabel.RIGHT); south.add(score, BorderLayout.NORTH); add(south, BorderLayout.SOUTH); } protected void updateScore() { score.setText(getQuizController().getModel().getScore() + "/" + getQuizController().getModel().size()); } } protected class ScoreView extends JPanel { private JLabel score; public ScoreView() { setLayout(new GridBagLayout()); score = new JLabel("You scored:"); add(score); } protected void updateScore() { score.setText("You scored: " + getQuizController().getModel().getScore() + "/" + getQuizController().getModel().size()); } } } 构建QuizMasterController API本身,因为它知道测验API只允许转发导航,同样我们可以改变导航API以允许通过模型修改这些状态​​或改变模型,这些都是可行的解决方案,我只是为了直接的例子。

Navigation

最后我们最终得到像......

Quiz Quiz Quiz

如果不是粗略的话,这不算什么,但旨在提供一些关于如何完成复杂的复合MVC的想法