So far we have used pluggable list views, pluggable text views and some individually programmed paint views. In this section we will take a first look on pluggable button views. We will learn how to use action buttons to change the state of the application.
The objective of this section is to program this application:
On the left hand side of this view we have two action buttons, one labelled "+", the other labelled "-". Next to these action buttons we have two value display views, one showing a temperature in centigrades, the other showing the same temperature in fahrenheit.
The user can press the green action button "+" to increment the temperature by one centigrade. To decrement the temperature by 1 °C, the user presses the red action button.
You find the code for this example in the change set ButtonExample1.cs.
Once you have filed in this change set, you can evaluate
ButtonExample1 open
to see the example.
The example uses two new classes:
ButtonExample1
which serves a a model for the top view as well as for the
two value display views. An instance stores the temperature value
in celsius and fahrenheit (it would be sufficient to store one
of these and to compute the other as needed). Additionally,
it implements the actions that the action buttons trigger.
PluggablePassiveValueView
which displays a value that is stored in its model.
To obtain that value, a PluggablePassiveValueView sends
a configurable message to the model.
Additionally, it uses the following existing classes:
PluggableButtonView
a view that renders various types of buttons.
Button
an object that configures a PluggableButtonView to render an
action button. A Button instance acts as a model for a
PluggableButtonView and it provides a facility to trigger
actions in other instances.
Class ButtonExample1 is subclassed to Model and defines a kind of a model with two additional instance variables:
Model subclass: #ButtonExample1 instanceVariableNames: 'celsius fahrenheit' classVariableNames: '' poolDictionaries: '' category: 'MVC-Tutorial'
celsius - Number
This variable stores the temperature value in centigrades.
fahrenheit - Number
This variable stores the temperature value in fahrenheit.
An instance initializer is needed to assign suitable initial values to the instance variables:
initialize celsius := 0.0. fahrenheit := 32.0.
This is a well-known method to specify the initial size of the top window:
initialExtent ^280 @ 80
Two accessors are provided for use by the value display views:
temperatureInCelsius ^celsius.
temperatureInFahrenheit ^fahrenheit.
The following two methods are provided for use by the buttons.
The decrement button send this method to trigger the reduction of the temperature by one centigrade:
decrement celsius := celsius - 1. fahrenheit := 1.8*celsius + 32. self changed.
The increment button send this method to increment the temperature by one centigrade:
increment celsius := celsius + 1. fahrenheit := 1.8*celsius + 32. self changed.
All the interesting stuff is placed in class method open. Here we configure two action buttons and four views. Finally, we put the four views in a top view, which, in this case, is a ColoredSystemView. A ColoredSystemView allows the use of colored action buttons.
To keep this method short, the configuration of an action button is implemented as a separate method.
open " ButtonExample1 open" | topView model textStyle incrSwitchView decrSwitchView celsiusView fahrenheitView | model := self new. topView := ColorSystemView new model: model; borderWidth: 1; label: 'Temperature'; minimumSize: 200 @ 60; maximumSize: 260 @ 220. textStyle := (TextStyle default) copy. incrSwitchView := self buildActionButton: Color lightGreen label: '+' textStyle: textStyle. decrSwitchView := self buildActionButton: Color lightRed label: '-' textStyle: textStyle. incrSwitchView model onAction: [model increment]. decrSwitchView model onAction: [model decrement]. celsiusView := PluggablePassiveValueView on: model valueAccessor: #temperatureInCelsius unit: ' °C'. celsiusView insideColor: Color white; borderWidth: 1; window: (0 @ 0 extent: 60@100). fahrenheitView := PluggablePassiveValueView on: model valueAccessor: #temperatureInFahrenheit unit: ' °F'. fahrenheitView insideColor: Color white; borderWidth: 1; window: (0 @ 0 extent: 60@100). topView addSubView: incrSwitchView. topView addSubView: decrSwitchView below: incrSwitchView. topView addSubView: celsiusView toRightOf: incrSwitchView. topView addSubView: fahrenheitView toRightOf: celsiusView. topView controller open
Some details of this method are worth being commented:
It should be noted that the method buildActionButton:label:textStyle: only creates a PluggableButtonView and its model, an instance of Button. The auxiliary method does not completely configure the Button instance. To complete the configuration of the Button instances, we need the statements
incrSwitchView model onAction: [model increment]. decrSwitchView model onAction: [model decrement].
Each of these statements takes one view, accesses its model - a Button - and gives it a parameterless block as an on-action. That block is executed each time the button state changes to #turnOn. In our example the action blocks are kept as simple as possible: An action block sends a single message to model, an instance of BlockExample1.
To create and to configure an action button, we use this method:
buildActionButton: backColor label: aString textStyle: textStyle | button switchView dt | button := Button newOff. switchView := PluggableButtonView on: button getState: #isOn action: #turnOn. dt := (aString asText addAttribute: TextFontChange font4) allBold asDisplayText. dt foregroundColor: Color black backgroundColor: backColor. dt textStyle: textStyle. switchView label: dt; borderWidth: 1; insideColor: backColor; window: (0 @ 0 extent: 40 @ 50). ^switchView.
The first and the second assignment should be explained:
Button newOff creates a new button with initial state 'off'. That button is used as the model for the PluggableButtonView.
The PluggableButtonView is created with the message on:getState:action:. The first message argument is the view model, the other arguments are names of messages that a Button understands.
To display a value, we use a very simple view that implements one single feature: It displays a string that is composed from a temperature value and a unit name. The string is centered to obtain a pleasing resize behaviour.
Class PluggablePassiveValueView is subclassed to View and defines a kind of a view with two additional instance variables:
View subclass: #PluggablePassiveValueView instanceVariableNames: 'unitName valueAccessor' classVariableNames: '' poolDictionaries: '' category: 'MVC-Tutorial'
unitName - String
The variable stores a string that is appended to the display string.
For our example, this string is one of the physical units °C, °F.
valueAccessor - Symbol
This variable stores the name of a message. The message is sent to
the model to obtain the value to be displayed. In our example, the
message is one of #temperatureInCelsius, #temeratureInFahrenheit
The protocol of this view is very simple: We override these three instance methods:
Additionally, we define a class method to create an instance and a fourth instance method to initialize an instance. That's all.
To create a completely initialized instance of a PluggablePassiveValueView, we provide a class method that is called with all necessary parameters.
on: aModel valueAccessor: aSymbol unit: aString ^self new on: aModel valueAccessor: aSymbol unit: aString
The class method creates the instance and initializes it with the following instance method:
on: aModel valueAccessor: aSymbol unit: aString self model: aModel. valueAccessor := aSymbol. unitName := aString.
Note that we have to write
self model: aModel.
to make the view an observer of its model. It is not sufficient to simply write:
model := aModel.
For a passive view, we do not need a sophisticated controller. An instance of NoController is fully sufficient.
defaultControllerClass ^NoController.
The instance method displayView does all of the hard work.
displayView | box string displayText boundingBoxOfText displayRect | self model isNil ifTrue: [^self]. string := (self model perform: valueAccessor) printShowingDecimalPlaces: 1. displayText := DisplayText text: (Text string: string, unitName attribute: TextEmphasis normal) textStyle: (TextStyle default copy defaultFontIndex: 5). displayText foregroundColor: Color black backgroundColor: self backgroundColor. boundingBoxOfText := displayText boundingBox. box := self insetDisplayBox. displayRect := Rectangle center: box center extent: boundingBoxOfText extent. displayText displayOn: Display at: displayRect origin clippingBox: box rule: Form over fillColor: nil.
Here we see some things that we have used earlier:
Note that the method verifies the availability of a model before the model is sent the value access message. Error robust code should do that.
The update method is very simple: The view redisplays itself each time it receives an update request.
update: aParameter self display.
Upon closer examination of this example, we find that ButtonExample1 sends only one changed message to update two value display views. This is possible because both views share the same model and because each view uses its own accessor method to fetch its display value.