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:
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:
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: