5.2 Model View Presenter

MVC8 and MVP9 designs are very similar. MVC, nowadays widely used in GUI and Web application development, was invented by the Smalltalk community in the late seventies and early eighties. MVP is a subtle variation where the presenter has more responsibilities than the controller and acts more as a middleman between the model and view objects.

Let’s review the responsibilities of each of the three objects.

Now, let’s see how to reshape our memory game to fit it into the MVP design.

5.2.1 Memory Game Model

The model is the isolated object, without knowledge of the presenter and the view11, so it is easier to start from there.

In the previous design of the game, we had two classes, MemoryGameWindow and MemoryCard, acting as view and model. Therefore, we need to extract what is model-related.

Our game involves the domain of a game with cards. We define two models:

5.2.2 Memory Game View

We have defined two model classes, so we might expect to define two view classes. Well, not necessarily. Here we just need to define one view class for the whole game, and we use an existing view of Cuis-Smalltalk for the card model, the PluggableButtonMorph.

We need to reshape MemoryGameWindow to contain only view-related business, first in its attributes then its behaviors. First of all, a view always knows about its presenter, it can even know about the model through the mediation of the presenter:

presenter: aPresenter
presenter := aPresenter. self
model: presenter model

It also knows about some other views needed for its internal organisation and regulation:

SystemWindow subclass: #MemoryGameWindow
   instanceVariableNames: 'presenter statusBar playground'
   ...

Again, the behavior is stripped down to only view considerations, and the initialize method is shortened.

Installing the toolbar slightly differs:

installToolbar
| toolbar button |
toolbar := LayoutMorph newRow separation: 2.
button := PluggableButtonMorph model: presenter action: #startGame :: 
   enableSelector: #isStopped;
...

The model of the buttons is not anymore the view but the presenter.

Indeed, we explained earlier that it is the presenter’s responsibility to handle user events. The actions remain the same, and we can anticipate the related methods will be transferred from the view to the presenter class.

Now, we should look at installing the card views:

installCards
| row size |
playground removeAllMorphs.
size := model size.
1 to: size y do: [:y |
   row := LayoutMorph newRow.
   1 to: size x do: [:x | | cardModel cardView |
      cardModel := model cards at: x@y.
      cardView := PluggableButtonMorph
         model: presenter
         action: #flip:
         actionArgument: x@y.
      ...
      cardView layoutSpec proportionalWidth: 1; proportionalHeight: 1.
      cardView color: cardModel backColor.
      row addMorph: cardView].
   playground addMorph: row ]

It relies on the already instantiated card models; we ask the game model for all the card models: model cards.

Observe how we just use a stock PluggableButtonMorph as a view for the card. Indeed, we don’t need to specialize its behavior, so we keep it simple. Again, the presenter handles the user’s click on the card. This should be understood as executing the statement presenter flip: x@y at the event12.

For clarity, we have presented above a shortened version of the installCards method, without the dependencies between the card models and the card views. The logic of installing the card models, then the card views, is handled by the presenter, the middleman. We discuss it in the next section.

5.2.3 Memory Game Presenter

We define a new class MemoryGame as our presenter:

Object subclass: #MemoryGame
   instanceVariableNames: 'model view playing'
   classVariableNames: ''
   poolDictionaries: ''
   category: 'MemoryGameV2'

It acts as the entry point of a GUI application, therefore its name is kept short with no Presenter fragment. A new game instance is then invoked by a simple:

MemoryGame new

As we explained earlier, the presenter instantiates both the model and the view:

initialize
model := MemoryGameModel new.
view := MemoryGameWindow presenter: self.
self startGame.
view openInWorld

Observe that the game model is attached neither to the view nor to the presenter.

From the initialization, the game is then started:

startGame
model installCardModels.
view installCards.
view message: 'Starting a new game' bold green.
view setLabel: 'P L A Y I N G'.
playing := true

By invoking card models and views installations, each object in charge of that business is asked to perform its task.

We already learned the flip: method, called when the user clicks on a card, is now defined in the presenter. The method is quite similar to the previous iteration, except now we only know about the card model. The associated card view is unknown:

flip: position
| flippedCards |
(model cards at: position) 
   flip;
   triggerEvent: #lock.	
flippedCards := model flippedCards.
...
   " Unflip and unlock the flipped cards "
   flippedCards do: [:aCard | 
      aCard flip; 
         triggerEvent: #flash;
         triggerEvent: #unlock].
   ^ self].
...
   " We found an n-tuple! "
   view message: 'Great!' bold, ' You found a ', model tupleSize asString, '-tuple!'.
   flippedCards do: [:aCard | aCard triggerEvent: #flash].
   flippedCards do: #setDone.
...

Therefore, to update the state of a card view, a card model triggers events which are propagated to any card view listening to the events. An event is coded as a symbol representing an aspect of the model that has changed. The symbol name is arbitrarily chosen to be meaningful. In the flip: method, there are three events:

When triggering an event, it is additionally possible to pass along a parameter. Observe this feature in the model’s flip method to inform about the card color changed:

MemoryCardModel>>flip
| newColor |
flipped := flipped not.
newColor := flipped ifTrue: [color ] ifFalse: [self backColor].
self triggerEvent: #color with: newColor 

All in all, there are four events triggered by a card model: lock, flash, unlock, and color. How a view can listen to a given event is discussed in the next section.


Footnotes

(8)

https://en.wikipedia.org/wiki/Model-view-controller

(9)

https://en.wikipedia.org/wiki/Model-view-presenter

(10)

This is an assumed variation from the view, which is used as the entry point of the traditional approach. Being a middleman, it makes sense that it is instantiated first.

(11)

Though several models may know about each other.

(12)

The message #model:action:actionArgument: to instantiate a button is extremely confusing in its model: keyword; it is not a model as we discussed earlier, but only the receiver of the user action, the controller in the sense of the MVC pattern.