parent previous next question (Smalltalk Textbook 06)

EngiDisplayModelViewController

I would like to make classes that can display any kind of object. There are 'EngiDisplayModel', 'EngiDisplayView', 'EngiDisplayController' classes in Appendix 04. These classes are useful to examine any kind of object as shown in Program 6-1.


Program-6-1: (EngiDisplayModel; openOn:)
---------------------------------------------------------------------- 
EngiDisplayModel openOn: anObject
---------------------------------------------------------------------- 

If 'anObject' understands messages for displaying, this object itself is displayed in a window. Otherwise, the 'printString' of this object is displayed in a window and you can inspect this object via the yellow button menu. Evaluate the examples in Appendix 04 to understand the behavior of 'EngiDisplayModel'. Use it to examine various kinds of objects.

'EngiDisplayModel' is a subclass of 'EngiVariable' and has 'dependents', 'value', 'displayBounds', 'displayOrigin', 'windowLabel' as instance variables. 'dependents' is inherited from 'Model' and 'value' is inherited from 'EngiVariable'. Dependent objects are stored in 'dependents'. 'value' contains the object to display and 'displayBounds' has the display space of the object that 'value' has. 'displayOrigin' has coordinates of the starting point to display. 'windowLabel' has a label for the window to display the object. If you 'do it' Program 6-2, two windows that contain 100 each and an inspector window are opened.


Program-6-2: (EngiDisplayModel; on:, timesRepeat:)
---------------------------------------------------------------------- 
| displayModel |
displayModel := EngiDisplayModel on: 100. 2 timesRepeat: [displayModel open].
displayModel inspect
---------------------------------------------------------------------- 

Evaluate Program 6-3 in the right window of the inspector. 'self' is the instance of 'EngiDisplayModel'. The contents of two windows that are displaying the object are changed to the image you select from the screen. Also the window labels are changed to 'Image'. You can scroll the image vertically and horizontally. The two views are dependent objects of the same model. They each receive broadcast messages from the model and display the latest status of the model automatically.


Program-6-3: (Image; value:, windowLabel:)
---------------------------------------------------------------------- 
self value: Image fromUser.
self windowLabel: 'Image'
---------------------------------------------------------------------- 

Let us trace Program 6-4 step by step to understand the mechanism of 'EngiDisplayModel'.


Program-6-4: (EngiDisplayModel)
---------------------------------------------------------------------- 
| displayModel |
displayModel := EngiDisplayModel on: 100.
displayModel open.
---------------------------------------------------------------------- 

First, an instance of 'EngiDisplayModel' named 'displayModel' is created by sending the class message 'on:' to 'EngiDisplayModel'. The value of instance variable 'value' of 'displayModel' is set to 100.

Then the message 'open' is sent to 'displayModel'. Program 6-5 shows the method for the message 'open'.


Program-6-5: (EngiDisplayModel, EngiTopView, LookPreferences; open)
---------------------------------------------------------------------- 
open
    | graphView edgeDecorator topView |
    graphView := self defaultViewClass model: self. 
    edgeDecorator := LookPreferences edgeDecorator on: graphView. edgeDecorator noMenuBar.
    edgeDecorator useVerticalScrollBar.
    edgeDecorator useHorizontalScrollBar.
    topView := EngiTopView
            model: nil
            label: self windowLabel
    minimumSize: 100 @ 100.
    topView add: edgeDecorator in: (topView frameFraction: (0 @ 0 corner: 1 @ 1)).
    topView open
---------------------------------------------------------------------- 

'self defaultViewClass' returns 'EngiDisplayView'. So this program creates an instance of a view with itself as the new view's model. Now add this view to a newly created 'EngiTopView' instance. Open the window. After opening the window, the 'displayOn:' message is sent to 'aScheduledWindow', 'anEngiTopView', 'aBoundedWrapper', 'aBorderDecorator', 'aBorderedWrapper', 'aScrollWrapper', 'anEngiDisplayView' in turn. When 'anEngiDisplayView' gets this message it delegates the 'displayOn:' message to its model. The model which gets the 'displayOn:' draws itself. First it draws the xy-axis, then the object which is stored in 'value'. Then the model checks whether the object is displayable by using the 'canBeDisplayed' message. Let's trace a change in value of this model. The 'value:' message, used to chage the value of the model is defined in Program 6.6.


Program-6-6: (EngiDisplayModel; setValue:, flushBounds, changed:)
---------------------------------------------------------------------- 
value: anObject
    self setValue: anObject.
    self flushBounds.
    self changed: #value
---------------------------------------------------------------------- 
  1. setValue: is a private message which changes the contents of the 'value' instance variable.
  2. flush 'displayBounds'
  3. and broadcast to all dependents that it has been changed. the 'changed:' message is defined in 'Object' which is the top class of the model.
Trace 6-1: (EngiDisplayModel; changed:, changed:with:, update:, 
update:with:, update:with:from:)
---------------------------------------------------------------------- 
"Object"
changed: anAspectSymbol
    self changed: anAspectSymbol with: nil

"Object"
changed: anAspectSymbol with: aParameter 
    self myDependents
        update: anAspectSymbol
        with: aParameter
        from: self

"Model"
myDependents
    ^dependents

"Object"
update: anAspectSymbol with: aParameter from: aSender 
    ^self update: anAspectSymbol with: aParameter 

"Object"
update: anAspectSymbol with: aParameter
    ^self update: anAspectSymbol

"EngiDisplayView"
update: aSymbol
    self positionTo: Point zero.
    self clearInside.
    self displayOn: self graphicsContext
---------------------------------------------------------------------- 

Trace 6-1 is an ordered list of messages. You see finally that 'EngiDisplayView' receives the 'update:' message, sets its scroll position, clears itself, and sends itself 'displayOn:'. The 'displayOn:' message reaches the model and the view now displays the current contents of the model. This action is done for every view of dependent-objects and keeps the various views in synch.

This way of using one Model and many Views is fundamental to Smalltalk programming. Learn to use and depend on it. You should always check your models by using multiple views to see that changes are properly propagated.

Next we examing the controller for 'EngiDisplayView', which is 'EngiDisplayController'. You define the controller in the view by overriding the following messages.


Program-6-7: (EngiDisplayView, EngiDisplayController)
---------------------------------------------------------------------- 
"EngiDisplayView"
defaultControllerClass
    ^EngiDisplayController
---------------------------------------------------------------------- 

The controller is started by 'startUp', sent by 'aStandardSystemController' which is a companion of 'aScheduledWindow' after 'aScheduledWindow' is opend and displayed.

Trace 6-2
---------------------------------------------------------------------- 
"StandardSystemController"
startUp
    ScheduledControllers class closedWindowSignal 
        handle: [:ex |
            self view == nil
                ifTrue: [ex return]
                ifFalse: [ex reject]]
        do: [super startUp]

"Controller"
startUp
    self controlInitialize.
    self controlLoop.
    self controlTerminate

"Controller"
controlInitialize
    ^self

"Controller"
controlLoop
    [self poll. "<- Ignore this for now"
    self isControlActive]
        whileTrue:
            [self controlActivity]

"Controller"
controlTerminate
    ^self

"StandardSystemController"
isControlActive
    ^self sensor isActive

"StandardSystemController"
controlActivity
    | choice |
    (self sensor blueButtonPressed and: [self viewHasCursor]) 
        ifFalse: [^self controlToNextLevel].
    choice := view menuDo: [ScheduledBlueButtonMenu startUp]. 
    choice = 0 ifTrue: [^self].
    self perform: choice

"StandardSystemController"
controlToNextLevel
    | aView |
    aView := view subViewWantingControl.
    aView == nil ifFalse: [aView startUp]
---------------------------------------------------------------------- 

'aStandardSystemController' uses 'startUp' to create his sub-view's controller unless the blue button is pressed in the display area of 'aScheduledWindow'. So 'startUp' is sent to 'anEngiDisplayController'. Then messages are traced similar to 'aStandardSystemController', finally reaching the 'controlActivity' message of 'ControllerWithMenu' which is a super class of 'EngiDisplayController'. Program 6-8 shows the 'controlActivity' message of 'ControllerWithMenu'.


Program-6-8: (ControllerWithMenu; controlActivity)
---------------------------------------------------------------------- 
"ControllerWithMenu"
controlActivity
    self sensor redButtonPressed & self viewHasCursor 
        ifTrue: [^self redButtonActivity].
    self sensor yellowButtonPressed & self viewHasCursor 
        ifTrue: [^self yellowButtonActivity].
    super controlActivity
---------------------------------------------------------------------- 

Program 6-8 detects a press of the red or yellow button and does 'redButtonActivity' or 'yellowButtonActivity' which are redefined in 'EngiDisplayController'. Program 6-9 shows the redefined 'redButtonActivity' and 'yellowButtonActivity' in 'EngiDisplayController'.


Program-6-9: (EngiDisplayController; redButtonActivity, yellowButtonActivity)
---------------------------------------------------------------------- 
"EngiDisplayController"
redButtonActivity
    (model notNil and: [model respondsTo: #redButtonActivity:]) 
        ifTrue: [model redButtonActivity: self]

"EngiDisplayController"
yellowButtonActivity
    (model notNil and: [model respondsTo: #yellowButtonActivity:]) 
        ifTrue: [model yellowButtonActivity: self] 
---------------------------------------------------------------------- 

Finally, these messages are delegated to the model as in Program 6-10.


Program-6-10: (EngiDisplayModel; redButtonActivity, yellowButtonActivity)
---------------------------------------------------------------------- 
"EngiDisplayModel"
redButtonActivity: aController
    | aPoint |
    aPoint := self convertToMyPoint: aController sensor cursorPoint.
    Transcript cr; show: aPoint printString

"EngiDisplayModel"
yellowButtonActivity: aController
    | aMenu aResult |
    aMenu := EngiMenuMaker fromCollection: (Array with: 'inspect' -> [self
    value inspect]).
    aResult := aMenu startUp.
    aResult = 0 ifFalse: [aResult value]
---------------------------------------------------------------------- 

Program 6-10 writes coordinates of the red button to the Transcript if the red button is pressed. If the yellow button is pressed it displays a menu with just one choice ('inspect') and sends the 'inspect' message to the 'value' object of the model.

This section is a little difficult to understand. But once you understand the MVC model well, Smalltalk programming will be much easier for you.


parent previous next question
Copyright (C) 1994-1996 by Atsushi Aoki
Translated by Kaoru Rin Hayashi & Brent N. Reeves