GWT: Building widgets with separated mvp - java

I'm trying to build a gwt widget with separated model, view and presenter.
I'm using just one class for all of these components so far:
A compact example:
public class MyCellSelectableTable extends Composite {
private WhatEverRepresentation selectedCell;
public MyCellSelectableTable() {
Grid myTable = new Grid(2,2);
/*
* Some code to realize a table with cell selection
* ...
*/
initWidget(myTable);
}
}
In my appreciation the information "selectedCell" (and in my project many other data) should be stored in a separate model.
How can I implement this structurally correct, so it still is a widget but with an encapsulated mvp architecture?

In one of my projects I was asked to design a spinner item which is dressed to look good on a mobile web app. Then we realized we actually need another view for our spinner which is 'thinner' as well. So I have tried to aplly the MVP approach to my widget implementation which worked well for me. This one below is a very simple example not for production use just for the sake of demonstration
Define a Presenter and a View interface as in MVP pattern. The point here is to make the view implementations dumb so they can be switched without a hassle. Note that Spinner is not actually a Widget but it implements IsWidget which means it will be treated like a widget but in fact we will be passing the reference of our view implementation.
public class Spinner implements IsWidget, SpinnerPresenter{
interface View{
Widget asWidget();
void stepUp(int step);
void stepDown(int step);
void setValue(int value);
void setPixelSize(int width,int height);
}
View view;
int value;
public Spinner() {
view = new SpinnerImpl(this);
view.setValue(0);
}
public int getValue() {
return value;
}
public void setValue(int value) {
if (value == this.value)
return;
this.value = value;
view.setValue(value);
}
public void setPixelSize(int width, int height){
view.setPixelSize(width,height);
}
#Override
public void downButtonClicked() {
value--;
view.stepDown(1);
}
#Override
public void upButtonClicked() {
value++;
view.stepUp(1);
}
#Override
public Widget asWidget() {
return view.asWidget();
}
}
And the view Implementation itself which implements the view interface defined in Spinner class. Notice how we delegate user events to Spinner class to be handled over the SpinnerPresenter interface.
public class SpinnerImpl extends Composite implements Spinner.View{
private TextBox txtBox;
private Button upButton,downButton;
private HorizontalPanel panel;
private SpinnerPresenter presenter;
public SpinnerImpl(SpinnerPresenter presenter){
this.presenter = presenter;
upButton = new Button("up");
downButton = new Button("down");
txtBox = new TextBox();
txtBox.setEnabled(false);
panel = new HorizontalPanel();
panel.add(upButton);
panel.add(txtBox);
panel.add(downButton);
addHandlers();
initWidget(panel);
}
private void addHandlers() {
upButton.addClickHandler(new ClickHandler() {
#Override
public void onClick(ClickEvent event) {
presenter.upButtonClicked();
}
});
downButton.addClickHandler(new ClickHandler() {
#Override
public void onClick(ClickEvent event) {
presenter.downButtonClicked();
}
});
}
#Override
public void stepDown(int step) {
txtBox.setValue(Integer.parseInt(txtBox.getValue())-1+"");
}
#Override
public void stepUp(int step) {
txtBox.setValue(Integer.parseInt(txtBox.getValue())+1+"");
}
#Override
public void setValue(int value) {
txtBox.setValue(0+"");
}
}
SpinnerPresenter interface :
public interface SpinnerPresenter {
void upButtonClicked();
void downButtonClicked();
}
Finally to add my widget to rootpanel. Notice I can add spinner class as if it was a widget
Spinner s = new Spinner();
RootPanel.get().add(s);
Now if i wanted to change the way my spinner item looks, change orientation, maybe add a fancy animation for spinning etc, I need only to change my View Implementation. Last but not least when it comes to testing my widget this approach will help since I can easily mockup my view.

FYI, the cell widgets in GWT are built with MVP internally. A CellList (for instance) is the "view" part and instantiates an internal presenter.
Also, this session from Google I/O 2010 was eye opening for me: http://www.google.com/events/io/2010/sessions/gwt-continuous-build-testing.html

I recommend you read the following MVP article if you haven't already (and forgive me if you have): http://code.google.com/webtoolkit/articles/mvp-architecture.html http://www.gwtproject.org/articles/mvp-architecture.html
Believe me it's worth it to go ahead and separate out your model, view, and presenter. The problem always seems simple enough to have in one class, but when I've tried that I always end up separating things out. You'll appreciate having a better separation of concerns and the flexibility it offers.

Related

Can we change GWT Bootstrap 3 ButtonCell Icon Dynamically?

I have a GWT bootstrap 3 button as a ButtonCell created with IconType and ButtonType:
public abstract class ButtonColumn<T> extends Column<T, String> {
public ButtonColumn(IconType iconType, ButtonType buttonType) {
this(new ButtonCell(buttonType, iconType));
}
}
So when I create the button, I do
new ButtonColumn<Object>(IconType.PLAY, ButtonType.SUCCESS) {
#Override
public void onClick(Object obj) {
doStuff(obj);
}
};
I want to change my button IconType onClick. Is it possible to achieve it?
And can I create a custom IconType extending the GWT IconType Enum? I wanted to put an animated icon (like a loading icon).
Well, you can not change the button's icon in a row, especially when you create the whole column with an icon already specified. But you can redraw() a row and this could be a way to achieve what you want.
I use AbstractCell to render a button and onBrowserEvent:
first create an AbstractCell with ClickEvent in consumedEvents parameter
in the render() method render a button based on the clicked state
in the onBrowserEvent() method change the clicked state and re-render the row
The clicked state is best to be kept in the table's underlying data type so it is available for each row.
Here is a complete working example code:
final CellTable<TableType> table = new CellTable<TableType>();
AbstractCell<TableType> buttonCell = new AbstractCell<ButtonCellTest.TableType>(ClickEvent.getType().getName()) {
#Override
public void render(Context context, TableType value, SafeHtmlBuilder sb) {
Button button = new Button();
button.setType(ButtonType.SUCCESS);
button.setSize(ButtonSize.SMALL);
button.add(new Icon(value.isClicked() ? IconType.CHECK : IconType.TIMES));
sb.append(SafeHtmlUtils.fromTrustedString(button.toString()));
}
#Override
public void onBrowserEvent(Context context, Element parent, TableType value, NativeEvent event, ValueUpdater<TableType> valueUpdater) {
value.setClicked(!value.isClicked());
// ... do stuff...
table.redrawRow(context.getIndex());
}
};
table.addColumn(new Column<TableType, TableType>(buttonCell) {
#Override
public TableType getValue(TableType object) {
return object;
}
});
ArrayList<TableType> rowData = new ArrayList<TableType>();
rowData.add(new TableType("row 1"));
rowData.add(new TableType("row 2"));
...
table.setRowData(rowData);
And example table's data type keeping the clicked state:
public class TableType {
String text;
boolean clicked = false;
public TableType(String text) {
this.text = text;
}
public String getText() {
return text;
}
public boolean isClicked() {
return clicked;
}
public void setClicked(boolean clicked) {
this.clicked = clicked;
}
}
As for extending the IconType enum - no, you can not extend an enum in Java. See this question for example: Can enums be subclassed to add new elements?.
You could try to add your own CSS class but this should be asked as another question to get precise answers.

How to listen to selection changes on TableViewer?

I'm working on an Eclipse RCP application and I'm trying to update an expression value which is provided by MySourceProvider according to selection changes on a TableViewer in MyEditorPart.
MyEditorPart instance defines a TableViewer like this:
public class MyEditorPart extends EditorPart {
#Override
public void createPartControl(Composite parent) {
TableViewer tableviewer = new TableViewer(parent, SWT.CHECK);
tableviewer.setContentProvider(ArrayContentProvider.getInstance());
getSite().setSelectionProvider(tableViewer);
...
MySourceProvider have some expression values like this:
public class MySourceProvider extends AbstractSourceProvider {
public static final String EXPR = "org.xyz.isEntrySelected";
// other expressions
#Override
public String[] getProvidedSourceNames() {
return new String[] { EXPR,
// other expressions
};
}
#Override
public Map getCurrentState() {
HashMap<String, Object> map = new HashMap<String, Object>(1);
map.put(EXPR, expr_value); // expr_value calculated by the listener
// other expressions
return map;
}
I want to change expr_value according to selection changes on TableViewer.
I registered the listener like this:
window.getSelectionService().addPostSelectionListener(MyEditorPartId, selectionListener);
private final ISelectionListener selectionListener = new SelectionListener() {
#Override
public void selectionChanged(IWorkbenchPart part, ISelection selection) {
handleEvent();
}
};
The listener registers successfully but gets notified only once if I clicked somewhere on MyEditorPart (not just TableViewer but the whole editor). To get notified again, I have to click on some other view (or editor) part to lose focus and then click again on MyEditorPart.
1. Why does the listener gets notified only once when MyEditorPart re-gains focus?
2. How to listen only to selection changes to TableViewer rows?
What am I missing here? What is the proper way to listen to selection changes?
Thanks in advance.
What you need is not a SelectionListener, but a SelectionChangedListener.
With this you can write the following code:
viewer.addSelectionChangedListener(new ISelectionChangedListener() {
#Override
public void selectionChanged(SelectionChangedEvent event) {
IStructuredSelection selection = viewer.getStructuredSelection();
Object firstElement = selection.getFirstElement();
// do something with it
}
});
It does appear that this form of addPostSelectionListener only fires when the part becomes active. Use the:
addPostSelectionListener(ISelectionListener listener)
form of the listener which is called for every selection change.
You can then test the IWorkbenchPart id in the listener:
#Override
public void selectionChanged(final IWorkbenchPart part, final ISelection selection)
{
if (MyEditorPartId.equals(part.getSite().getId()))
{
// your code
}
}

Processing a buttonclick in Oracle forms programticly for an implementation class

I am having the following problem which is exclusive to the oracle forms framework and custom implementation classes provided in java. I modified a VButton to display a custom menue as soon as i am pressing the right mouse button.
The following was accomplished by inheriting from the VButton and providing a custom behavior.
The popup will be displayed by a JPopupMenu. This JPopupMenu will be filled dynamicly by a HashMap<String, JMenuItem> which represents each option in this JPopupMenu and it´s representing key. To accomplish this i wrote an abstract class which contains everything that needs to be done to create a custom right click menu, with two abstract methods. These methods will be overriden by subclasses of this class and will create two representing lists of Keys for the Map and theyr representing JMenuItem.
This is how the abstract class is designed.
public abstract class AbstractRightClickButton extends VButton{
// The PopupMenu for this item;
private JPopupMenu popup = new JPopupMenu();
// The HashMap that stores the Menuitems and it´s keys
private HashMap<String, JMenuItem> menuItems = new HashMap<String, JMenuItem>();
// Stores the current selected key.
private String choice= "";
public AbstractRightClickButton() {
this.menuItems = generateHashmap();
initMouseListener();
initPopUp();
}
// Abstract method to create an ArrayList of all JMenuItem
public abstract ArrayList<JMenuItem> generateJMenuItemSide();
// Abstract method to create an ArrayList for each key in the HashMap
public abstract ArrayList<String> generateStringSide();
private void initPopUp() {
ArrayList<String> list = generateStringSide();
for (String counter : list) {
popup.add(menuItems.get(counter));
}
}
private void initMouseListener() {
this.addMouseListener(new MouseListener() {
public void mousePressed(MouseEvent e) {
if(SwingUtilities.isRightMouseButton(e)) {
// If the width of the popup is 0 then it wasn´t displayed yet. Just show it once and make it invisible again to get the width and height of the popup.
if (popup.getWidth() == 0) {
popup.show(AbstractRightClickButton.this, AbstractRightClickButton.this.getWidth(), 0);
popup.setVisible(false);
}
popup.show(AbstractRightClickButton.this, AbstractRightClickButton.this.getWidth()-popup.getWidth(), 0);
}
}
public void mouseExited(MouseEvent e) {}
public void mouseEntered(MouseEvent e) {}
public void mouseClicked(MouseEvent e) {}
public void mouseReleased(MouseEvent e) {}
});
protected HashMap<String, JMenuItem> generateHashmap() {
HashMap<String, JMenuItem> map = new HashMap<String, JMenuItem>();
ArrayList<String> key = generateStringSide();
ArrayList<JMenuItem> value = generateJMenuItemSide();
for (int i = 0;i < key.size();++i) {
map.put(key.get(i), value.get(i));
}
return map;
};
// This method is a helper method that can be called by subclasses of AbstractRightClickButton
// It creates a JMenutButton based on the parameter
protected JMenuItem generateJMenuItem(final String key, final boolean click) {
JMenuItem item = new JMenuItem(new AbstractAction(key) {
#Override
public void actionPerformed(ActionEvent e) {
choice = key;
if (click) {
ActionEvent act = new ActionEvent((Object)AbstractRightClickButton.this, ActionEvent.ACTION_PERFORMED, ""); // Button Event erstellen
AbstractRightClickButton.this.processEvent(act); // Event Processen um den Buttondruck Formsseitig auch auszulösen
}
}
});
return item;
}
}
A concrete class would look like this
public class ConcreteRightClickButton extends AbstractRightClickButton{
#Override
public ArrayList<String> generateStringSide() {
ArrayList<String> basis = new ArrayList<String>(0);
basis.add("OPTION1");
basis.add("OPTION2");
return basis;
}
#Override
public ArrayList<JMenuItem> generateJMenuItemSide() {
ArrayList<JMenuItem> basis = new ArrayList<JMenuItem>(0);
basis.add(generateJMenuItem("OPTION1", true));
basis.add(generateJMenuItem("OPTION2", true));
return basis;
}
}
The problem i am facing depends on the type of button, espacially how this button is defined in the forms builder. If the button property iconic is defined as No, then the call of AbstractRightClickButton.this.processEvent(act) correctly processes this event and can be handelt by the When-Button-Pressed Event in Oracle Forms. But if the button property iconic is defined as Yes then the processing of the event somehow doesn´t work. While Debugging everything looks fine, there is no exception and it reaches the AbstractRightClickButton.this.processEvent(act) without any problem. But the representing When-Button-Pressed Trigger in Oracle Forms doesn´t react. Am i processing the Event incorrectly or is there something else that stops oracle forms from recieving this event?
I am using Oracle forms 11g to create a mask containing this specific button.
1 year 8 months later, maybe this isn't an "answer" but its an alternative way to do the same thing.
Create a class that extends VBean, and create a JButton.
Add the jar to the classpath, and call it within a Bean Area.
It's called java pluggable components.
This page is useful:
JPC
EDIT:
import java.awt.Color;
import javax.swing.JButton;
import oracle.forms.ui.VBean;
public class TryButton extends VBean {
JButton button = new JButton();
public TryButton() {
this.setSize(500, 500);
this.setVisible(true);
this.add(button);
button.setSize(500, 500);
button.setIcon(new javax.swing.ImageIcon(getClass().getResource("/images/imagen2.png")));
}
}
Then, add the jar to: oracle_home/forms/java
and add it to classpath
In oracle forms in a new canvas create a Bean Area
Go to Item Properties and add the class package and name to: implementation class.

Listener Placement Adhering to the Traditional (non-mediator) MVC Pattern

I'm implementing a program within Swing, and I've read Nirmal's implementation of this pattern in Swing, which seems to show a rather elegant handling of the whole "separation of responsibilities" concept.
However, as I'm developing a more complicated program than the one posted by Nirml, which consists of a single JFrame container, I look for guidance as how to implement MVC properly.
My program will be consisting of sub-containers and such. I am curious as to how the Controller is supposed to implement the logic behind defining and assigining all the listeners of the View.. or if the controller defining the listeners for every single View component is even practical?
It would seem that I would need a method in the View's top-level container to allow the Controller to invoke the view to add a Listener to the component in question? and so I would need a chain of methods, each passing down the listener from the top-level container, to the immediate container holding the component.. culminating with the container invoking addActionListener() on it.
Is this the proper way to handle listeners in MVC?
Is defining all listeners of every component in the View by the Controller mandatory in MVC, or a useful practice? This would also imply that I create methods in the top-level container(View) to give the Controller a way to assign listeners to every single component in sub-containers?
Okay, first things first, Swing implements a form of the MVC already, albeit in the form of VC-M. This means that you shouldn't try and constrain Swing to a pure MVC directly, as you'll be very disappointed and spend a lot of time trying to make hacks where they shouldn't be.
Instead, you can wrap a MVC around Swing, allowing it to work around the API instead.
To my mind, a controller doesn't need to know, nor should it care, how the view or model are implemented, but it should only care how it can work with them (I've had too many developers get hold of UI components and do things to/with them that they shouldn't have, and broken the API when we've changed the implementation. It's best to hide that kind of detail)
In this vein, you can think of a view as self contained entity - it has controls and it does stuff independent of the controller. The controller doesn't care about the implementation specifics. What it does care about is getting information and been told when some event, described by the contract, has occurred. It shouldn't care about HOW it was generated.
For example, say you have a login view. The controller only wants to know the user name and password that the user entered and when it should validate that information.
Let's say you implement the view/controller to expose the JTextField and JPasswordFields to start with, but later on, your users want the user name selection to be restricted to a specific list (possibly provided by the model). Now you have implementation details stuck in your controller which are no longer applicable and you have to manually change or create a new MVC for this new use case.
What if, instead, you simply stated that the view has a getter for the user name and password and some kind event listener which would tell the controller when the user wanted the credentials verified? Well now, you'd only need to provide a new view, no need to modify the controller. The controller won't care HOW these values are generated.
As to the greater aspect of your question.
My program will be consisting of sub-containers and such. I am curious
as to how the Controller is supposed to implement the logic behind
defining and assigning all the listeners of the View.. or if the
controller defining the listeners for every single View component is
even practical?
It would seem that I would need a method in the View's top-level
container to allow the Controller to invoke the view to add a Listener
to the component in question? and so I would need a chain of methods,
each passing down the listener from the top-level container, to the
immediate container holding the component.. culminating with the
container invoking addActionListener() on it.
Is this the proper way to handle listeners in MVC?
The general answer is, no, it's not the proper way.
Each sub view would become its own MVC, with it focusing on its own requirements. The parent MVC might use events or other functionality provided by the child MVC's to make updates or even modify the states of other child MVCs.
The important thing to remember here, is a view can act as a controller for other views, although, you might choose to have a series of controllers which the view would be allowed to manage.
Imagine something like a "wizard". It has a bunch of steps which collects various information from the user, each step needs to be valid before it can move on to the next step.
Now, you might be tempted to integrate navigation into this directly, but a better idea would be to separate the navigation details as its own MVC.
The wizard would, when asked, present a step to the user, the user would fill in the information, possibly triggering events. These events would then allow the navigation MVC to decide if the user can move to the next step or the previous step.
The two MVC's would be controlled by a third "master" MVC which would help manage the states (listening for events from the wizard and updating the state of the navigation)
Let's try an example with a question that gets asked way to much around here, a quiz!
A quiz has questions, each question has a prompt, a correct answer, a series of possible answers and we also want to store the resulting answer from the user.
The Quiz API
So, below we have the basic outline of the quiz MVC, we have a question, which is managed by a model, there is a controller and a view and series of observers (listeners)
The Contracts (interfaces)
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();
}
I, personally, work to the principle of "code to interface (not implementation)", I've also deliberately gone overboard with the idea to demonstrate the point.
If you look closely, you will note that neither the view or model actually have any relationship to each other. This is all controlled via, the controller
One of the things I've done here is to provide the controller with a askNextQuestion, because the controller doesn't know when that should occur (you might think about using the userDidChangeAnswer, but that would mean that the user only gets a single attempt to answer the question, kind of mean)
The implementation
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();
}
}
}
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
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)
The Contracts (interfaces)
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);
}
The implementation
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;
}
}
}
The Quiz Master
Now, these are two distinct APIs, they have nothing in common, so, we need some kind of controller to bridge them
The contracts (the interfaces)
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();
}
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...
The implementations
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());
}
}
}
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
Put it together
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);
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

Model-View-Presenter passive view: bootstraping - who displays the view initially?

In the Passive View Model View Presenter pattern, who has the responsibility for displaying the view? I have found related answers for other MVP versions, but they don't seem applicable to the passive view version.
I have a concrete example using Java Swing. It's pretty simple, but basically we have a SwingCustomersView which internally builds a JPanel with a table (list of customers) and a label displaying the currently selected customers age. When a customer is selected in the table, the presenter retrieves the selected customer age from the model. I think the example is a correct implementation of MVP Passive View, but correct me if I'm wrong.
The question is how do we bootstrap these classes? For example, if we wanted to display the SwingCustomersView in a JFrame. How would one do that? I imagine something along the lines of:
void launcher() {
CustomersModel model = new CustomersModel();
SwingCustomersView view = new SwingCustomersView();
CustomersPresenter presenter = new CustomersPresenter(view, model);
}
This is the initial wiring, but nothing is displayed yet. How do we actually display the view? Is it the responsibility of (1) launcher() , (2) SwingCustomersView or (3) CustomersPresenter to display the view? Unfortunately I don't believe any of those are very good as you can see from my thoughts below. Perhaps there's another way?
(1.a): launcher
Make SwingCustomersView extend JFrame and make it add it's internal JPanel to the content pane of itself. Then we can do this:
void launcher() {
CustomersModel model = new CustomersModel();
SwingCustomersView view = new SwingCustomersView();
CustomersPresenter presenter = new CustomersPresenter(view, model);
view.setVisible(true); // Displays the view
}
However in this case we don't use the presenter instance for anything. Isn't that strange? It's just there for wiring, we could just as well delete the variable and just do new CustomersPresenter(view, model).
(2): SwingCustomersView
Make SwingCustomersView take a Container in the constructor to which it should add it's internal JPanel:
void launcher() {
CustomersModel model = new CustomersModel();
JFrame frame = new JFrame("Some title");
SwingCustomersView view = new SwingCustomersView(frame.getContentPane());
CustomersPresenter presenter = new CustomersPresenter(view, model);
frame.pack();
frame.setVisible(true) // Displays the view
}
However, same problem as (1): the presenter instance does nothing. It seems strange. Furthermore with both (1) and (2) it is possible to display the view before the presenter is hooked up, which I imagine could cause strange results in some situations.
(3): CustomersPresenter
Make CustomersPresenter responsible for displaying the view somwhow. Then we could do this:
void launcher() {
CustomersModel model = new CustomersModel();
SwingCustomersView view = new SwingCustomersView();
CustomersPresenter presenter = new CustomersPresenter(view, model);
presenter.show() // Displays the view
}
This would solve the problem of not using it for anything after construction. But I don't see how do to this without either changing the CustomersView interface or making CustomersPresenter too dependent on the underlying GUI implementation. Furthermore, displaying a view doesn't sound like presentation logic and thus doesn't seem to belong in the presenter.
Example
public class CustomersModel {
private List<Customer> customers;
public CustomersModel() {
customers = new ArrayList<Customer>();
customers.add(new Customer("SomeCustomer", "31"));
customers.add(new Customer("SomeCustomer", "32"));
}
public List<Customer> getCustomers() {
return customers;
}
}
public class Customer {
public String name;
public String age;
public Customer(String name, String age) {
this.name = name;
this.age = age;
}
}
public interface CustomersView {
void addCustomerSelectionChangeListener(ItemListener listener);
void onNewActiveCustomer(String age);
void onNewCustomers(List<String> newCustomers);
}
public class SwingCustomersView implements CustomersView {
// Swing components here all put into a main JPanel
public void addCustomerSelectionChangeListener(ItemListener listener) {
// Add event listener to table
}
public void onNewActiveCustomer(String age) {
// Display age in label beneath table
}
public void onNewCustomers(List<String> newCustomers) {
// Display customers in table
}
}
public class CustomersPresenter {
private final CustomersView view;
private final CustomersModel model;
public CustomersPresenter(CustomersView view, CustomersModel model) {
this.view = view;
this.model = model;
initPresentationLogic();
populateView();
}
private void initPresentationLogic() {
view.addCustomerSelectionChangeListener(new ItemListener() {
#Override
public void itemStateChanged(ItemEvent e) {
String selectedName = (String)e.getItem();
List<Customer> customers = model.getCustomers();
for (Customer c : customers)
if (c.name.equals(selectedName))
view.onNewActiveCustomer(c.age);
}
});
}
private void populateView() {
List<Customer> customers = model.getCustomers();
List<String> names = new ArrayList<String>();
for (Customer c : customers)
names.add(c.name);
// View will now populate its table, which in turn will call customerSelectionChangeListener
// so the view 'automagically' updates the selected contact age too
view.onNewCustomers(names);
}
}
Option (3) all the way. It is the presenter's jobs for "controlling" the view, which includes making it visible. Yes, you'll need to add to the view's interface to allow this to happen, but that's not a big deal. Remember, you can make the view is as passive as possible. No logic whatsoever!
Working Example:
I stumbled upon this example of a simple Swing game using an MVC architecture. Since I write my Swing apps using MVP instead of MVC, I can't say with authority if this example is a true and pure example of MVC. It looks okay to me, and the author trashgod has more than proven himself here on SO using Swing, so I'll accept it as reasonable.
As an exercise, I decided to rewrite it using an MVP architecture.
The Driver:
As you can see in the code below, this is pretty simple. What should jump out at you are the separation of concerns (by inspecting the constructors):
The Model class is standalone and has no knowledge of Views or Presenters.
The View interface is implemented by a standalone GUI class, neither of which have any knowledge of Models or Presenters.
The Presenter class knows about both Models and Views.
Code:
import java.awt.*;
/**
* MVP version of https://stackoverflow.com/q/3066590/230513
*/
public class MVPGame implements Runnable
{
public static void main(String[] args)
{
EventQueue.invokeLater(new MVPGame());
}
#Override
public void run()
{
Model model = new Model();
View view = new Gui();
Presenter presenter = new Presenter(model, view);
presenter.start();
}
}
and the GamePiece that we'll be using for the game:
import java.awt.*;
public enum GamePiece
{
Red(Color.red), Green(Color.green), Blue(Color.blue);
public Color color;
private GamePiece(Color color)
{
this.color = color;
}
}
The Model: Primarily, the job of the Model is to:
Provide data for the UI (upon request)
Validation of data (upon request)
Long-term storage of data (upon request)
Code:
import java.util.*;
public class Model
{
private static final Random rnd = new Random();
private static final GamePiece[] pieces = GamePiece.values();
private GamePiece selection;
public Model()
{
reset();
}
public void reset()
{
selection = pieces[randomInt(0, pieces.length)];
}
public boolean check(GamePiece guess)
{
return selection.equals(guess);
}
public List<GamePiece> getAllPieces()
{
return Arrays.asList(GamePiece.values());
}
private static int randomInt(int min, int max)
{
return rnd.nextInt((max - min) + 1) + min;
}
}
The View: The idea here is to make it as "dumb" as possible by stripping out as much application logic as you can (the goal is to have none). Advantages:
The app now be 100% JUnit testable since no application logic is mixed in with Swing code
You can launch the GUI without launching the entire app, which makes prototyping much faster
Code:
import java.awt.*;
import java.awt.event.*;
import java.util.List;
public interface View
{
public void addPieceActionListener(GamePiece piece, ActionListener listener);
public void addResetActionListener(ActionListener listener);
public void setGamePieces(List<GamePiece> pieces);
public void setResult(Color color, String message);
}
and the GUI:
import java.awt.*;
import java.awt.event.*;
import java.util.List;
import javax.swing.*;
/**
* View is "dumb". It has no reference to Model or Presenter.
* No application code - Swing code only!
*/
public class Gui implements View
{
private JFrame frame;
private ColorIcon icon;
private JLabel resultLabel;
private JButton resetButton;
private JButton[] pieceButtons;
private List<GamePiece> pieceChoices;
public Gui()
{
frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
icon = new ColorIcon(80, Color.WHITE);
}
public void setGamePieces(List<GamePiece> pieces)
{
this.pieceChoices = pieces;
frame.add(getMainPanel());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public void setResult(Color color, String message)
{
icon.color = color;
resultLabel.setText(message);
resultLabel.repaint();
}
private JPanel getMainPanel()
{
JPanel panel = new JPanel(new BorderLayout());
panel.add(getInstructionPanel(), BorderLayout.NORTH);
panel.add(getGamePanel(), BorderLayout.CENTER);
panel.add(getResetPanel(), BorderLayout.SOUTH);
return panel;
}
private JPanel getInstructionPanel()
{
JPanel panel = new JPanel();
panel.add(new JLabel("Guess what color!", JLabel.CENTER));
return panel;
}
private JPanel getGamePanel()
{
resultLabel = new JLabel("No selection made", icon, JLabel.CENTER);
resultLabel.setVerticalTextPosition(JLabel.BOTTOM);
resultLabel.setHorizontalTextPosition(JLabel.CENTER);
JPanel piecePanel = new JPanel();
int pieceCount = pieceChoices.size();
pieceButtons = new JButton[pieceCount];
for (int i = 0; i < pieceCount; i++)
{
pieceButtons[i] = createPiece(pieceChoices.get(i));
piecePanel.add(pieceButtons[i]);
}
JPanel panel = new JPanel(new BorderLayout());
panel.add(resultLabel, BorderLayout.CENTER);
panel.add(piecePanel, BorderLayout.SOUTH);
return panel;
}
private JPanel getResetPanel()
{
resetButton = new JButton("Reset");
JPanel panel = new JPanel();
panel.add(resetButton);
return panel;
}
private JButton createPiece(GamePiece piece)
{
JButton btn = new JButton();
btn.setIcon(new ColorIcon(16, piece.color));
btn.setActionCommand(piece.name());
return btn;
}
public void addPieceActionListener(GamePiece piece, ActionListener listener)
{
for (JButton button : pieceButtons)
{
if (button.getActionCommand().equals(piece.name()))
{
button.addActionListener(listener);
break;
}
}
}
public void addResetActionListener(ActionListener listener)
{
resetButton.addActionListener(listener);
}
private class ColorIcon implements Icon
{
private int size;
private Color color;
public ColorIcon(int size, Color color)
{
this.size = size;
this.color = color;
}
#Override
public void paintIcon(Component c, Graphics g, int x, int y)
{
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setColor(color);
g2d.fillOval(x, y, size, size);
}
#Override
public int getIconWidth()
{
return size;
}
#Override
public int getIconHeight()
{
return size;
}
}
}
What might not be so obvious right away is how large the View interface can get. For each Swing component on the GUI, you may want to:
Add/Remove a listener to the component, of which there are many types (ActionListener, FocusListener, MouseListener, etc.)
Get/Set the data on the component
Set the "usability" state of the component (enabled, visible, editable, focusable, etc.)
This can get unwieldy really fast. As a solution (not shown in this example), a key is created for each field, and the GUI registers each component with it's key (a HashMap is used). Then, instead of the View defining methods such as:
public void addResetActionListener(ActionListener listener);
// and then repeat for every field that needs an ActionListener
you would have a single method:
public void addActionListener(SomeEnum someField, ActionListener listener);
where "SomeEnum" is an enum that defines all fields on a given UI. Then, when the GUI receives that call, it looks up the appropriate component to call that method on. All of this heavy lifting would get done in an abstract super class that implements View.
The Presenter: The responsibilities are:
Initialize the View with it's starting values
Respond to all user interactions on the View by attaching the appropriate listeners
Update the state of the View whenever necessary
Fetch all data from the View and pass to Model for saving (if necessary)
Code (note that there's no Swing in here):
import java.awt.*;
import java.awt.event.*;
public class Presenter
{
private Model model;
private View view;
public Presenter()
{
System.out.println("ctor");
}
public Presenter(Model model, View view)
{
this.model = model;
this.view = view;
}
public void start()
{
view.setGamePieces(model.getAllPieces());
reset();
view.addResetActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
reset();
}
});
for (int i = 0; i < GamePiece.values().length; i++)
{
final GamePiece aPiece = GamePiece.values()[i];
view.addPieceActionListener(aPiece, new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
pieceSelected(aPiece);
}
});
}
}
private void reset()
{
model.reset();
view.setResult(Color.GRAY, "Click a button.");
}
private void pieceSelected(GamePiece piece)
{
boolean valid = model.check(piece);
view.setResult(piece.color, valid ? "Win!" : "Keep trying.");
}
}
Keep in mind that each portion of the MVP architecture can/will be delegating to other classes (that are hidden to the other 2 portions) to perform many of its tasks. The Model, View, and Presenter classes are just the upper divisions in your code base heirarchy.

Categories