ste*_*key 0 java model-view-controller swing
我正在Swing中实现一个程序,我在Swing中阅读了Nirmal对这种模式的实现,这似乎表明了对整个"职责分离"概念的相当优雅的处理.
但是,由于我正在开发一个比Nirml发布的更复杂的程序,它由一个JFrame容器组成,我寻求指导如何正确实现MVC.
我的程序将由子容器等组成.我很好奇Controller应该如何实现定义和分配View的所有侦听器的逻辑?或者如果为每个View组件定义侦听器的控制器是否实用?
看来我在View的顶级容器中需要一个方法来允许Controller调用视图来向相关组件添加一个Listener?所以我需要一个方法链,每个方法都将侦听器从顶层容器传递到持有组件的直接容器.最后用容器调用addActionListener()就可以了.
这是在MVC中处理侦听器的正确方法吗?
是否在MVC中强制控制View中每个组件的所有侦听器,或者是一种有用的实践?这也意味着我在顶级容器(View)中创建方法,以便为Controller提供一种方法,将侦听器分配给子容器中的每个组件?
好的,首先,Swing已经实现了MVC的一种形式,尽管是以VC-M的形式.这意味着你不应该试着直接将Swing限制在一个纯粹的MVC中,因为你会非常失望并花费大量的时间来制作他们不应该做的黑客.
相反,您可以围绕Swing包装MVC,允许它围绕API工作.
在我看来,控制器不需要知道,也不应该关心视图或模型是如何实现的,但是它应该只关心它如何与它们一起工作(我有太多的开发人员掌握了UI组件)和他们一起做他们不应该做的事情,并在我们改变实施时打破API.最好隐藏那种细节)
在这种情况下,您可以将视图视为自包含实体 - 它具有控件,并且它可以独立于控制器执行操作.控制器不关心实现细节.它所关心的是获取信息,并在合同描述的某些事件发生时被告知.它不应该关心它是如何产生的.
例如,假设您有登录视图.控制器只想知道用户输入的用户名和密码以及何时应该验证该信息.
假设您实现了视图/控制器以公开JTextField和JPasswordField开始,但稍后,您的用户希望将用户名选择限制为特定列表(可能由模型提供).现在,您的控制器中存在实施细节,这些细节已不再适用,您必须手动更改或为此新用例创建新的MVC.
相反,如果您只是声明该视图具有用户名和密码的getter以及某种类型的事件侦听器,当用户想要验证凭据时会告诉控制器,该怎么办?那么现在,您只需提供一个新视图,无需修改控制器.控制器不会关心如何生成这些值.
至于你问题的更大方面.
我的程序将由子容器等组成.我很好奇Controller应该如何实现定义和分配View的所有侦听器背后的逻辑..或者如果定义每个View组件的侦听器的控制器甚至是实用的?
看来我在View的顶级容器中需要一个方法来允许Controller调用视图来向相关组件添加一个Listener?所以我需要一个方法链,每个方法都将侦听器从顶层容器传递到持有组件的直接容器.最后用容器调用addActionListener()就可以了.
这是在MVC中处理侦听器的正确方法吗?
一般的答案是,不,这不是正确的方法.
每个子视图都将成为自己的MVC,它专注于自己的需求.父MVC可以使用由子MVC提供的事件或其他功能来进行更新或甚至修改其他子MVC的状态.
这里需要记住的重要一点是,视图可以充当其他视图的控制器,但是,您可以选择使用一系列允许视图管理的控制器.
想象一下像"巫师".它有一系列步骤,从用户收集各种信息,每个步骤需要有效才能进入下一步.
现在,您可能想要直接将导航集成到此,但更好的想法是将导航细节分离为自己的MVC.
当被询问时,向导将向用户呈现步骤,用户将填写信息,可能触发事件.然后,这些事件将允许导航MVC决定用户是否可以移动到下一步骤或前一步骤.
两个MVC将由第三个"主"MVC控制,它将帮助管理状态(从向导中侦听事件并更新导航状态)
让我们试试一个问题,这个问题可以在这里被问到很多,一个测验!
测验有问题,每个问题都有提示,正确答案,一系列可能的答案,我们也希望存储用户得到的答案.
所以,下面我们有测验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();
}
Run Code Online (Sandbox Code Playgroud)
我个人致力于"代码与接口(不是实现)"的原则,我也故意用这个想法来证明这一点.
如果仔细观察,您会注意到视图或模型实际上彼此之间没有任何关系.这全部由控制器控制
我在这里做的一件事就是为控制器提供一个askNextQuestion,因为控制器不知道何时应该发生(你可能会想到使用它userDidChangeAnswer,但这意味着用户只能尝试回答一次)问题,有点意思)
Now, normally, I like to have some abstract implementations laying around to fill out the "common" functionality, I've forgone that for the most part and gone directly to the default implementation, this done mostly for demonstration purposes.
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();
}
}
}
Run Code Online (Sandbox Code Playgroud)
Really nothing fancy here. About the only thing of significant interest here is how the controller is managing the events between the model and the view
The navigation API is pretty basic. It allows you to control if the user can actually navigate to the next or previous element (if the actions should be made available to the user) as well as disable either of the actions at any time
(Again, I've focused on a simple design, realistically, it would be nice to have some control over modifying the state of the model to change in which directions the navigation can work, but I've left this out on purpose to keep it simple)
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);
}
Run Code Online (Sandbox Code Playgroud)
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;
}
}
}
Run Code Online (Sandbox Code Playgroud)
Now, these are two distinct APIs, they have nothing in common, so, we need some kind of controller to bridge them
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();
}
Run Code Online (Sandbox Code Playgroud)
Okay, so you're probably asking yourself the obvious question, where's the model? Well, it doesn't need one, it's just a bridge between the navigation and quiz APIs, it doesn't manage any data of it's own...
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());
}
}
}
Run Code Online (Sandbox Code Playgroud)
Now, the implementation is interesting, it actually has two "states" or "views", the "question and answer" view and the "score view". This, again, is deliberate, because I really didn't want ANOTHER MVC. The Q&A view is already managing two MVCs any way :P
Basically, what this does is monitors the quiz API for when the user changes the answer to a question, it then tells the navigation API that it can move to the next question. It monitors the start and completed events as well, presenting the required view for those states.
It is also monitoring the navigation API for navigation events. In this example, we can only move in a single direction and even if the navigation API was configured to do otherwise, the quiz API does not provide that functionality
Now, I've chosen to deliberately build each section separately, conceivably, you could have the QuizMasterController build the Navigation API itself, as it knows that the quiz API only allows for forward navigation, equally we could change the navigation API to allow those states to be modified via the model or the model changed, these are all viable solutions, I've just gone for a direct example.
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);
Run Code Online (Sandbox Code Playgroud)
And finally we end up with something like...
This is nothing if not rough, but is designed to provide some ideas into how you might accomplish complex, compound MVCs
| 归档时间: |
|
| 查看次数: |
302 次 |
| 最近记录: |