Previous
Contents
Next

First Steps: Windows with Cooperating Views


So far, in our examples all the work was done by the view: The view

We will now examine a slightly different approach, where responsibilities are distributed between a window and its model. The window will contain a list view that is used to select the number of vertices of the polygon to be drawn and it will contain a paint view that paints the polygon. When the user selects a number in the list view, the model receives a message, stores the selected number and notifies the paint view that the vertex number has changed. The paint view will ask the model for the new vertex number and it will redraw itself.

The window itself shall have this design:

A DrawingView and a PluggableListView

The complete example can be loaded from GraphicalDemo2.cs.

To keep different things separated, we begin with entirely new classes. This time we can write our paint view very quickly:

View subclass: #PluggableDrawingView
     instanceVariableNames: ''
     classVariableNames: ''
     poolDictionaries: ''
     category: 'MVC-FirstSteps'

The display method is well-known with only one small modification:

displayView

 | points radius centralAngle n pen boxWidth center |

  boxWidth := self insetDisplayBox width min: self insetDisplayBox height.
  radius := boxWidth //2 - 16.
  center := self insetDisplayBox center.
  n := model numberOfVertices.
  centralAngle := 360.0/n.
  points := OrderedCollection new: n.
  0 to: n - 1 do:
     [:idx | | angle |
       angle := centralAngle * idx.
       points add: ((angle degreeSin @ angle degreeCos) * radius) rounded
                      + center
     ].
  Display fill: self insetDisplayBox
          fillColor: Color white.  
  pen := Pen new
       defaultNib: 2;
       color: (Color black);
       place: points last.
  points do: [:pt | pen goto: pt].
  pen roundNib: 9;
       color: (Color h: 0 s: 0.5 v: 0.9).
  1 to: n do:
      [:idx | | angle pt |
        angle := centralAngle * (idx - 1).
        pt := points at: idx.
        pen color: (Color h: angle s: 0.5 v: 0.9).
        pen place: pt; goto: pt]

The model is asked to tell the view the number of vertices. That number is stored in the model - we will not store it a second time in the view. The view will ask the model every time it needs this number.

Note that model is not mentioned as an instance variable in the class definition of PluggableDrawingView. This is correct; that instance variable is defined in View and hence inherited.

The model has to keep the number of vertices which it stores in an instance variable:

Model subclass: #GraphicalDemo2
      instanceVariableNames: 'numberOfVertices '
      classVariableNames: ''
      poolDictionaries: ''
      category: 'MVC-FirstSteps'

We want to ensure that an instance of this class always has a number value in instance variable numberOfVertices. To ensure that, we have to provide the instance method initialize:

initialize
      " this is called immediately after creation
        of an instance with class method new. "
   numberOfVertices := 5.

Whenever the class method new creates an instance, it sends it the message initialize. Instance initialization is simple, but some rules should be kept in mind.

As the view will ask for the value of instance variable numberOfVertices, we have to provide an access method:

numberOfVertices
  ^numberOfVertices

Next, we define a class method to create a window with a paint view and a list view. We add this method to the class protocol of GraphicalDemo2.

open
     "GraphicalDemo2 open"
   | topView model selectionView drawing |
  model := self new.
  topView := ColorSystemView new
                label: 'Drawing'.
  topView model: model.
  topView borderWidth: 1.
  selectionView := PluggableListView
                       on: model
                       list: #getVertexList
                       selected: #getVertexSelection
                       changeSelected: #notifyVertexSelection:
                       menu: nil
                       keystroke: nil.

  selectionView borderWidth: 1;
                window: (0 @ 0 extent: 20 @ 100).

  drawing := PluggableDrawingView new.
  drawing model: model;
          borderWidth: 1.
  drawing window: (0 @ 0 extent: 80 @ 100).

  topView addSubView: selectionView.
  topView addSubView: drawing toRightOf: selectionView.

  topView controller open.

The easy part of this method is that we create a top window and a PluggableDrawingView.

An instance of PluggableListView is added as a subview to the topview. A PluggableListView needs a model and the names of five methods that it will use to communicate with its model. The instance creation method
on: list: selected: changeSelected: menu: keyStroke:
provides the instance to be created with a model and with all needed method names. In our example, we provide only the model and names for the first three of the five method that are used to communicate with the model. The methods for a menu and for keystroke input are not needed at this moment.

You should pay some attention to the code elements that specify the view layout:

In the example code, the selection view is added first. In a second step, the drawing view is added to the right of the selection view. It is entirely possible to do it the other way round and to write:

  topView addSubView: drawing.
  topView addSubView: selectionView toLeftOf: drawing.

In the instance protocol of GraphicalDemo2 we have to implement the three methods whose names were given to the PluggableListView:

getVertexList
   " prepare a ollection of items for the list view " 
  ^(4 to: 72) collect: [:item | item printString].

This method, which is sent from the PluggableListView to obtain the data to display, returns a collection of strings; the list of items to be displayed in the list view. We display the integers from 4 to 72.

getVertexSelection
   " answer the index of
     the currently selected item "
  ^numberOfVertices - 3

This method answers the index of the currently selected item.

notifyVertexSelection: idx
    " this message is sent from the
      list view every time a selection
      was made. " 
  idx ~= 0
     ifTrue:
       [numberOfVertices := idx + 3. 
        self changed].

This method is sent by the list view when the user made a selection. The method stores the new value for the instance variable and sends the message changed to itself. A method named changed is defined in the instance protocol of Object. That method notifies the dependent objects of the sending object that a change happened. Views are dependents of their model and thus the receivers of a change notification. Change notifications are implemented as update methods.

To take notice of a change notification, a view has to implement the instance method update:. To complete our work, we have to add this method to the instance protocol of PluggableDrawingView:

update: anObject

   self displayView

We can now execute GraphicalDemo2 open to see the window and to play with it.

In this example the model and the view cooperate to make a change of state visible. That cooperation is best explained by a sequence diagram:

Sequence diagram, showing the cooperation between a model and a view to repaint a diagram

To add keystroke-selection with the keys 'up arrow' and 'down arrow', we add a further instance method:

keystroke: aCharacter

     " handle characters 31 (arrow up) and
          30 (arrow down).  " 
  (aCharacter asciiValue = 31 and: [numberOfVertices < 72])
    ifTrue: [numberOfVertices := numberOfVertices + 1.
              self changed: #getVertexSelection.
              self changed]
   ifFalse:
     [(aCharacter asciiValue = 30 and: [numberOfVertices > 4])
        ifTrue: [numberOfVertices := numberOfVertices - 1.
                  self changed: #getVertexSelection.
                  self changed]
     ].

The message self changed: #getVertexSelection. issues a change notification that will cause the list view to update its current selection.

In the open method we have to add the name of this method.

  <...>  
selectionView := PluggableListView on: model list: #getVertexList selected: #getVertexSelection changeSelected: #notifyVertexSelection: menu: nil keystroke: #keystroke:. <...>

In an earlier example, we put four graphical views into one window. This was possible because each of these views had its own state and managed the dialog with its user. Now, two views together with their common model form a visual component. To put two such components into one window, we have to create two models; one for each component:

open2Views
     "GraphicalDemo2 open2Views"
   | topView container2 container model selectionView drawing |

  model := self new.
  topView := ColorSystemView new
              label: 'Drawing'.
  topView model: model.
  topView borderWidth: 1.
      " create a container for the first component: "
  container := View new.
  container window: (0 @ 0 extent: 200 @ 100).
  topView addSubView: container.
     " add the views of the first component: "
  selectionView := PluggableListView
                         on: model
                         list: #getVertexList
                         selected: #getVertexSelection
                         changeSelected: #notifyVertexSelection:
                         menu: nil
                         keystroke: #keystroke:.

  selectionView borderWidth: 1;
             window: (0 @ 0 extent: 30 @ 100).
  container addSubView: selectionView.

  drawing := PluggableDrawingView new.
  drawing model: model;
            borderWidth: 1.
  drawing window: (0 @ 0 extent: 170 @ 100).
  container addSubView: drawing toRightOf: selectionView.

      " create a container for the second component: "
  container2 := View new.
  container2 window: (0 @ 0 extent: 200 @ 100).
  topView addSubView: container2 toRightOf: container.

      " add the views of the second component: "
  model := self new.   " this is the model for the views
                         of the second component. "
  selectionView := PluggableListView
                         on: model
                         list: #getVertexList
                         selected: #getVertexSelection
                         changeSelected: #notifyVertexSelection:
                         menu: nil
                         keystroke: #keystroke:.

  selectionView borderWidth: 1;
                window: (0 @ 0 extent: 30 @ 100).
  container2 addSubView: selectionView.
  drawing := PluggableDrawingView new.
  drawing model: model;
          borderWidth: 1.
  drawing window: (0 @ 0 extent: 170 @ 100).
  container2 addSubView: drawing toRightOf: selectionView.
  
  topView controller open.

This looks a bit complicated, but it works:

Two composite views, each with its own model

What we have learned


Previous
Contents
Next