Размещение слушателя Придерживаясь традиционного шаблона MVC (без посредника)

Я реализую программу в Swing, и я прочитал реализацию Nirmal этого шаблона в Swing , который, кажется, демонстрирует довольно элегантную обработку концепции «разделения обязанностей».

Тем не менее, поскольку я разрабатываю более сложную программу, чем ту, которая была отправлена ​​Nirml, которая состоит из одного контейнера JFrame, я ищу руководство как правильно реализовать MVC.

Моя программа будет состоять из субконтейнеров и т. Д. Мне любопытно, как Контроллер должен реализовать логику определения и усвоения всех слушателей View .. или если controller, определяющий слушателей для каждого отдельного компонента View, даже практичен?

Казалось бы, мне нужен метод в контейнере верхнего уровня View, чтобы позволить controllerу вызывать представление для добавления Listener к рассматриваемому компоненту? и поэтому мне понадобится цепочка методов, каждая из которых передаёт слушателя из контейнера верхнего уровня, в непосредственный контейнер, содержащий компонент. Кульминацией этого является контейнер, вызывающий addActionListener () на нем.

Это правильный способ обработки слушателей в MVC?

Является ли определение всех слушателей каждого компонента в представлении controllerом обязательным в MVC или полезной практикой? Это также подразумевает, что я создаю методы в контейнере верхнего уровня (View), чтобы дать controllerу возможность назначить слушателей каждому отдельному компоненту в подконтейнерах?

Итак, во-первых, Swing уже использует форму MVC, хотя и в виде VC-M. Это означает, что вы не должны пытаться сдерживать Swing до чистого MVC напрямую, так как вы будете очень разочарованы и потратите много времени, пытаясь сделать хаки там, где их не должно быть.

Вместо этого вы можете обернуть MVC вокруг Swing, позволяя ему работать с API.

На мой взгляд, controller не должен знать и не должен заботиться о том, как реализуется представление или модель, но нужно только заботиться о том, как с ними можно работать (у меня было слишком много разработчиков получить компоненты пользовательского интерфейса и делать с ними что-то, что им не нужно, и нарушать API, когда мы изменили реализацию. Лучше всего скрыть такие детали)

В этом ключе вы можете думать о представлении как самостоятельной сущности – он имеет элементы управления, и он не зависит от controllerа. Контроллер не заботится о специфике реализации. Он заботится о получении информации и рассказал, когда произошло какое-то событие, описанное в контракте. Он не должен заботиться о том, КАК он был создан.

Например, скажем, у вас есть вход в систему. Контроллер хочет знать только имя пользователя и пароль, которые вводил пользователь, и когда он должен проверить эту информацию.

Предположим, вы реализуете представление / controller, чтобы открыть JTextField и JPasswordField s для начала, но позже ваши пользователи хотят, чтобы выбор имени пользователя был ограничен определенным списком (возможно, предоставленным моделью). Теперь у вас есть детали реализации, застрявшие в controllerе, которые больше не применимы, и вам нужно вручную изменить или создать новый MVC для этого нового варианта использования.

Что, если вместо этого вы просто заявили, что в представлении есть геттер для имени пользователя и пароля и какого-то прослушивателя событий, который будет указывать controllerу, когда пользователь хочет проверить учетные данные? Ну, теперь вам нужно только предоставить новый вид, не нужно изменять controller. Контроллеру все равно, КАК будут созданы эти значения.

Что касается большего аспекта вашего вопроса.

Моя программа будет состоять из субконтейнеров и т. Д. Мне любопытно, как controller должен реализовать логику определения и назначения всех слушателей View .. или если controller, определяющий слушателей для каждого отдельного компонента View, даже практичен?

Казалось бы, мне нужен метод в контейнере верхнего уровня View, чтобы позволить controllerу вызывать представление для добавления Listener к рассматриваемому компоненту? и поэтому мне понадобится цепочка методов, каждая из которых передаёт слушателя из контейнера верхнего уровня, в непосредственный контейнер, содержащий компонент. Кульминацией этого является контейнер, вызывающий addActionListener () на нем.

Это правильный способ обработки слушателей в MVC?

Общий ответ: нет, это не так.

Каждое подвое представление станет его собственным MVC, при этом основное внимание будет уделяться его собственным требованиям. Родительский MVC может использовать события или другие функции, предоставляемые дочерним MVC, чтобы делать обновления или даже изменять состояния других дочерних MVC.

Здесь важно помнить, что представление может выступать в качестве controllerа для других представлений, хотя вы можете выбрать серию controllerов, которым разрешено управлять представлением.

Представьте себе нечто вроде «волшебника». Он имеет множество шагов, которые собирают различную информацию от пользователя, каждый шаг должен быть действительным, прежде чем он сможет перейти к следующему шагу.

Теперь у вас может возникнуть соблазн интегрировать навигацию в это прямо, но лучше было бы разделить навигационные детали как свои собственные MVC.

Мастер, когда его спросят, представит шаг пользователю, пользователь заполнит эту информацию, возможно, инициируя события. Эти события затем позволят навигационной MVC решить, может ли пользователь перейти к следующему шагу или предыдущему шагу.

Два MVC будут контролироваться третьим «master» MVC, который поможет управлять состояниями (прослушивание событий из мастера и обновление состояния навигации)

Давайте попробуем пример с вопросом, который часто задают вопрос вокруг, викторину!

У викторины есть вопросы, у каждого вопроса есть подсказка, правильный ответ, серия возможных ответов, и мы также хотим сохранить полученный ответ от пользователя.

API Quiz

Итак, ниже у нас есть основная схема викторины MVC, у нас есть вопрос, который управляется моделью, есть controller и вид и серия наблюдателей (слушателей)

Контракты (интерфейсы)

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

Я лично работаю над принципом «код для интерфейса (а не для реализации)», я также сознательно перешел за борт с идеей продемонстрировать суть.

Если вы посмотрите внимательно, вы заметите, что ни вид, ни модель на самом деле не имеют отношения друг к другу. Все это контролируется через controller

Одна из вещей, которые я здесь сделал, – предоставить controllerу askNextQuestion , потому что controller не знает, когда это произойдет (вы можете подумать об использовании userDidChangeAnswer , но это будет означать, что пользователь получает только одну попытку ответить на вопрос, вид среднего)

Реализация

Теперь, как правило, мне нравится создавать некоторые abstract реализации, чтобы заполнить «общие» функциональные возможности, я пропустил это по большей части и перешел непосредственно к реализации по умолчанию, это сделано в основном для демонстрационных целей.

 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 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 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 questions; private Iterator 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 options; private String userAnswer; private final List 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(); } } } 

На самом деле ничего необычного здесь нет. О единственном, что представляет значительный интерес, заключается в том, как controller управляет событиями между моделью и представлением

API навигации

API навигации довольно простой. Он позволяет вам контролировать, может ли пользователь фактически перейти к следующему или предыдущему элементу (если действия должны быть доступны пользователю), а также отключить любое из действий в любое время

(Опять же, я сосредоточился на простом дизайне, реалистично, было бы неплохо иметь некоторый контроль над изменением состояния модели, чтобы изменить, в каких направлениях может работать навигация, но я оставил это намеренно, чтобы сохранить это просто)

Контракты (интерфейсы)

 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 navigatableDirections; private List 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 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, у них нет ничего общего, поэтому нам нужен какой-то controller для их объединения

Контракты (интерфейсы)

 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 навигации и викторины, он не управляет никакими данными из своего собственного …

Реализации

 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. Представление Q & A уже управляет двумя MVC любым способом: P

В основном, это то, что он контролирует API викторины, когда пользователь меняет ответ на вопрос, затем он сообщает API навигации, что он может перейти к следующему вопросу. Он также отслеживает начальные и завершенные события, представляя требуемое представление для этих государств.

Он также контролирует API навигации для событий навигации. В этом примере мы можем двигаться только в одном направлении, и даже если API-интерфейс навигации был настроен на выполнение иначе, API викторины не обеспечивает эту функциональность

Поместите это вместе

Теперь я решил умышленно строить каждый раздел отдельно, возможно, вы могли бы QuizMasterController построить сам API-интерфейс Navigation , поскольку он знает, что API-интерфейс викторины позволяет только пересылать навигацию, в равной степени мы могли бы изменить API-интерфейс навигации, чтобы эти состояния могли чтобы быть измененными с помощью модели или модели, это все жизнеспособные решения, я просто пошел на прямой пример.

 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

Interesting Posts
Давайте будем гением компьютера.