听众安置坚持传统的(非中介)MVC模式
我正在Swing中实现一个程序,并且我已经读过了Nirmal在Swing中实现这个模式 ,这似乎对整个“职责分离”概念有一个相当优雅的处理。
但是,由于我正在开发比由Nirml发布的更复杂的程序,而Nirml由单个JFrame容器组成,所以我正在寻找如何正确实现MVC的指导。
我的程序将由子容器等组成。 我很好奇,如何控制器应该如何实现的逻辑背后的定义和assigining所有的监听器的视图..或者如果控制器定义每个单一的视图组件的监听器是实际的?
看起来我需要在View的顶级容器中使用一个方法来允许Controller调用视图来向所讨论的组件添加一个Listener? 所以我需要一个方法链,每个方法将顶层容器中的侦听器传递给持有组件的直接容器。最后,容器调用addActionListener()。
这是正确的方式来处理MVC中的监听器?
在MVC中必须定义Controller中View的每个组件的所有监听器,还是一个有用的练习? 这也意味着我在顶级容器(View)中创build了方法,使Controller可以将侦听器分配给子容器中的每个组件。
好的,首先,Swing已经实现了MVC的一种forms,尽pipe是VC-M的forms。 这意味着你不应该试图将Swing直接限制在一个纯粹的MVC中,因为你会感到非常失望,并且花费大量的时间尝试在不应该的地方进行黑客攻击。
相反,你可以在Swing中包装一个MVC,允许它绕过API来代替。
在我看来,一个控制器不需要知道,也不应该关心如何实现视图或模型,但它应该只关心它如何与它们一起工作(我已经有太多的开发人员掌握了UI组件和他们不应该做的事情,当我们改变了实现时,打破了API,最好隐藏这种细节)
在这种情况下,您可以将视图看作自包含的实体 – 它具有控件,并且独立于控制器。 控制器不关心实现细节。 它所关心的是获取信息,并在合同描述的事件发生时被告知。 它不应该关心它是如何产生的。
例如,假设你有一个login视图。 控制器只想知道用户input的用户名和密码以及何时应validation该信息。
假设你实现了视图/控制器来公开JTextField
和JPasswordField
,但是稍后用户希望用户名select被限制在一个特定的列表中(可能由模型提供)。 现在,您的控制器中的实现细节已停滞不前,您必须手动为此新用例更改或创build新的MVC。
那么如果你只是简单地说视图有一个getter的用户名和密码,以及某种types的事件监听器,它会告诉控制器用户何时需要validation凭据? 那么现在,你只需要提供一个新的视图,不需要修改控制器。 控制器不会在乎这些值是如何生成的。
至于你的问题的更大的方面。
我的程序将由子容器等组成。 我很好奇控制器应该如何实现定义和分配View的所有监听器的逻辑,或者如果为每个View组件定义监听器的控制器更实用?
看起来我需要在View的顶级容器中使用一个方法来允许Controller调用视图来向所讨论的组件添加一个Listener? 所以我需要一个方法链,每个方法将顶层容器中的侦听器传递给持有组件的直接容器。最后,容器调用addActionListener()。
这是正确的方式来处理MVC中的监听器?
一般的答案是,不,这不是正确的方法。
每个子视图都将成为自己的MVC,重点关注自己的需求。 父MVC可以使用由子MVC提供的事件或其他function来更新甚至修改其他子MVC的状态。
这里要记住的重要一点是,一个视图可以充当其他视图的控制器,但是,您也可以select拥有一系列允许视图进行pipe理的控制器。
想象一下像“巫师”的东西。 它有一大堆从用户收集各种信息的步骤,每一步都需要有效,然后才能进入下一步。
现在,您可能会试图将导航直接集成到这个中,但更好的办法是将导航细节作为自己的MVC分开。
当被问及时,向导会向用户提出一个步骤,用户将填写信息,可能触发事件。 这些事件将允许导航MVC决定用户是否可以移动到下一步或上一步。
这两个MVC将由第三个“主”MVC控制,这将有助于pipe理状态(监听向导中的事件并更新导航状态)
让我们尝试一个问题,在这里得到问题的方式,一个测验!
一个测验有问题,每个问题都有一个提示,一个正确的答案,一系列可能的答案,我们也想存储用户得到的答案。
测验API
那么下面我们就有了问答MVC的基本轮廓,我们有一个问题,它由一个模型来pipe理,有一个控制器和一个视图以及一系列的观察者(听众)
合同(界面)
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(); }
我个人是按照“代码接口(不实现)”的原则工作的,我也故意忽略了这个想法来certificate这一点。
如果仔细观察,你会注意到这个视图或模型实际上没有任何关系。 这全部通过控制器进行控制
我在这里完成的一件事是为控制器提供一个askNextQuestion
,因为控制器不知道什么时候应该发生(你可能会考虑使用userDidChangeAnswer
,但这意味着用户只能获得一次尝试回答这个问题,有点意思)
执行
现在,通常情况下,我喜欢有一些abstract
实现来填充“通用”function,我已经放弃了这个function,直接进入了默认实现,主要是为了演示目的。
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(); } } }
这里真的没什么特别的。 关于这里唯一重要的兴趣是控制器如何pipe理模型和视图之间的事件
导航API
导航API是非常基本的。 它允许您控制用户是否可以实际导航到下一个或上一个元素(如果这些操作应该提供给用户)以及随时禁用任何操作
(再次,我已经把注意力集中在一个简单的devise上,实际上,对于修改模型的状态来改变导航可以在哪个方向上工作是很好的,但是我留下了这个意图来保持简单)
合同(界面)
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); }
执行
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(); }
好吧,你可能问自己一个明显的问题,模型在哪里? 那么,它不需要一个,它只是导航和测验API之间的桥梁,它不pipe理它自己的任何数据…
实现
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()); } } }
现在这个实现很有意思,实际上有两个“状态”或“视图”,“问答”视图和“分数视图”。 这也是故意的,因为我真的不想要另一个MVC。 问答视图已经以任何方式pipe理两个MVC:P
基本上,这样做是监视测验API,当用户改变问题的答案,然后告诉导航API,它可以移动到下一个问题。 它还监视开始和已完成的事件,并为这些状态提供所需的视图。
它也在监视导航事件的导航API。 在这个例子中,我们只能向一个方向移动,即使导航API被configuration为另外的方式,测验API也不提供这个function
把它放在一起
现在,我已经分别故意构build每个部分,可以想象,您可以让QuizMasterController
本身构buildNavigation
API,因为它知道测验API只允许向前导航,同样我们可以更改导航API以允许这些状态通过模型或模型进行修改,这些都是可行的解决scheme,我刚刚去了一个直接的例子。
NavigationModel navigationModel = new DefaultNavigationModel(true, false); NavigationView navigationView = new DefaultNavigationViewPane(); NavigationController navigationController = new NavWiz.DefaultNavigationController(navigationModel, navigationView); DefaultQuizModel quizModel = new DefaultQuizModel(); quizModel.add(new DefaultQuestion( "Which pop duo was the first western band to play in The Peoples Republic of China?", "Wham", "Wham", "Simon and Garfunkel", "Chas and Dave", "Right Said Fred")); quizModel.add(new DefaultQuestion( "Timber selected from how many fully grown oak trees were needed to build a large 3 decker Royal Navy battle ship in the 18th century?", "3,500", "50", "500", "1,500", "3,500")); quizModel.add(new DefaultQuestion( "Speed skating originated in which country?", "Netherlands", "Russia", "Netherlands", "Canada", "Norway")); quizModel.add(new DefaultQuestion( "Off the coast of which country did the Amoco Cadiz sink?", "France", "South Africa", "France", "USA", "Spain")); quizModel.add(new DefaultQuestion( "The song 'An Englishman in New York' was about which man?", "Quentin Crisp", "Quentin Crisp", "Sting", "John Lennon", "Gordon Sumner")); QuizView quizView = new DefaultQuizViewPane(); QuizController quizController = new DefaultQuizController(quizModel, quizView); QuizMasterController quizMasterController = new DefaultQuizMasterController(quizController, navigationController); JFrame frame = new JFrame("Testing"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.add(quizMasterController.getView().getViewComponent()); frame.pack(); frame.setLocationRelativeTo(null); frame.setVisible(true);
最后,我们结束了像…
如果不是粗糙的话,这是没有什么意义的,但它的目的是为了提供一些关于如何完成复杂的复合MVC的想法