Previous
Contents

Reworking the Temperature Calculator with Value Holders


You find the code for this example in the change set ValueHolderExample.cs.

This example demonstrates the use of two ValueHolders. One ValueHolder displays a temperature in centigrades, the other ValueHolder displays the same temperature in Fahrenheit. Two buttons are provided to increment and to decrement the centigrade temperature value.

A View that uses two ValueHolders

Evaluate

TemperatureCalculator open

to open the view.

To display the contents of a value holder, we need a view. For this example, the view is very simple: In principle, we need three instance methods:

For a better visual result, we add an instance variable that is used to store the unit name, and an instance method that assigns a value to that variable.

The class TemperatureView is a direct subclass of View:

View subclass: #TemperatureView
     instanceVariableNames: 'unitName'
     classVariableNames: ''
     poolDictionaries: ''
     category: 'MVC-Tutorial'

This view will use an instance of ValueHolder as its model. That model class does not implement support for input processing. It is therefore a good choice to create instances of TemperatureView with a NoController:

defaultControllerClass
    " answer the class of a typically useful controller"
   ^NoController.

The method displayView is a bit technical, but straightforward: It creates a DisplayText with the string to be displayed, and displays it centered in the view.

displayView
     | box string displayText boundingBoxOfText displayRect |
 
   self model isNil
     ifTrue: [^self].

   string := self model contents printShowingDecimalPlaces: 1.
   displayText := DisplayText
                     text: (Text string: string, unitName 
                                 attribute: TextEmphasis normal)
                     textStyle: (TextStyle default copy defaultFontIndex: 5).
  
   displayText foregroundColor: Color black
               backgroundColor: Color white.

   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.

A ValueHolder sends the message update: #contents to all its dependents when the value was changed. To react to a value change, class TemperatureView has to implement the instance method update:. This method checks the value of the message argument and redisplays the entire view when the message argument is #contents.

update: aParameter
   aParameter = #contents
     ifTrue: [self display].

The method that assigns the unit name is very simple:

unitName: aString
  unitName := aString

The application itself is implemented by a subclass of Model.

Model subclass: #TemperatureCalculator
	instanceVariableNames: 'celsius fahrenheit'
	classVariableNames: ''
	poolDictionaries: ''
	category: 'MVC-Tutorial'

Use of Instance Variables:

Instance Initialization:

The instance method initialize creates two initialized ValueHolders. Both value holders are initialized to the same temperature: the freezing point of water.

initialize

  celsius := ValueHolder new.
  celsius contents: 0.0.
  fahrenheit := ValueHolder new.
  fahrenheit contents: 32.0.

Accessors:

celsius
  " answer the ValueHolder that is used to store the centigrade temperature "
  ^celsius
fahrenheit
  " answer the ValueHolder that is used to store the Fahrenheit temperature "
  ^fahrenheit

These two accessors are only needed by the class method open, which builds the application window. It is entirely possible to use an instance method to build the application window and when this approach is choosen, these accessors are not needed at all.

window geometry:

initialExtent
  ^280 @ 80

Button Support

The messages decrement and increment are sent by the switches to inform the application about a button event. The methods adjust both temperature values.

decrement
   | v newValue |
  v := celsius contents.
  newValue := v - 1.
  celsius contents: newValue.
  fahrenheit contents: 1.8*newValue + 32. 
increment
   | v newValue |
  v := celsius contents.
  newValue := v + 1.
  celsius contents: newValue.
  fahrenheit contents: 1.8*newValue + 32. 

Class Protocol:

The entire view is assembled in class method open. This method creates the top view and four subviews.

open
   " TemperatureCalculator open "
  
   | topView model incrButton decrButton
     incrSwitchView decrSwitchView
     celsiusView fahrenheitView dt textStyle |

     " create the top window "
   model := self new.
   topView := ColorSystemView new
                 model: model;
                 label: 'Temperature'.
   topView minimumSize: 200 @ 60;
           maximumSize: 260 @ 220.

     " create the model for the increment button
       and the button itself "
   incrButton := Button newOff.
   incrButton onAction: [model increment].
   incrSwitchView := PluggableButtonView on: incrButton
			getState: #isOn
			action: #turnOn.
   textStyle := (TextStyle default) copy.
   dt := ('+' asText addAttribute: TextFontChange font4) allBold asDisplayText.
   dt foregroundColor: Color black
      backgroundColor: Color lightGreen.
   dt textStyle: textStyle.
   incrSwitchView label: dt.
   incrSwitchView borderWidthLeft: 2 right: 1 top: 2 bottom: 1;
                  insideColor: Color lightGreen.
   incrSwitchView window: (0 @ 0 extent: 40 @ 50).

     " create the model for the decrement button
       and the button itself "

   decrButton := Button newOff.
   decrButton onAction: [model decrement].
   decrSwitchView := PluggableButtonView on: decrButton
			getState: #isOn
			action: #turnOn.
   dt := ('-' asText addAttribute: TextFontChange font4) allBold asDisplayText.
   dt foregroundColor: Color black
      backgroundColor: Color lightRed.
   dt textStyle: textStyle.

   decrSwitchView label: dt.
   decrSwitchView borderWidthLeft: 2 right: 1 top: 1 bottom: 2;
                  insideColor: Color lightRed.
   decrSwitchView window: (0 @ 0 extent: 40 @ 50).

     " create the view for the value holder 'celsius' "

   celsiusView := TemperatureView new.
   celsiusView model: model celsius;
               unitName: ' °C'.
   celsiusView insideColor: Color white;
               borderWidthLeft: 1 right: 1 top: 2 bottom: 2.
   celsiusView window: (0 @ 0 extent: 60@100).

     " create the view for the value holder fahrenheit "

   fahrenheitView := TemperatureView new.
   fahrenheitView model: model fahrenheit;
                  unitName: ' °F'.
   fahrenheitView insideColor: Color white;
                  borderWidthLeft: 1 right: 2 top: 2 bottom: 2.
   fahrenheitView window: (0 @ 0 extent: 60@100).

     " install all views as subviews of the top view "

   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:

Upon closer examination of this example, we find that TemperatureCalculator itself does almost nothing to support its views: It provides an initial extent for the top view, that's all. All subviews have their own models. TemperatureCalculator references two of these submodels: The value holders. To display a value, the TemperatureCalculator communicates it to the value holders, which have the responsibility to support their views. Value holders are very simple components that can be used to structure an application model.


Previous
Contents