5.2 Model View Presenter

MVC8 and MVP9 designs are very close. MVC, nowadays widely used in GUI and Web application developments, 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 middle-man between the model and view objects.

Let’s review the responsibilities of each three objects.

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

5.2.1 Memory Game Model

The model is the isolated object, without knowledge of the presenter and the view11, 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 may expect to define two view classes. Well, not necessary, 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 at 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 behaviors 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 any more the view but the presenter.

Indeed, we explained earlier it is the presenter 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 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 click on the card, it 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 middle-man, 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 the game model is attached neither to the view nor to the presenter.

From the initialization, the game then is 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, it is asked to each object in charge of that business.

We already learn the flip: method, called when the user click 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 a n-tuple! "
   view message: 'Great!' bold, ' You find 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 changed. The symbol name is arbitrary 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 used as the entry point of the traditional approach. Being a middle-man, it makes sense 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.