Interlude: Cleaning up PersonalInformationView

David Shaffer
Shaffer Consulting

A business model class

PersonalInformationView needs some work. If we're going to build an addressbook or some such thing, when its time to display someone's "information", we'd have to fill in each of the view's fields using data from our data source (presumably a database). Doesn't sound very object-oriented. Instead we should have a business (model) object which represents this information and the view should simply edit such an object. That is we should have a class to represent PersonalInformation (or even Person if you prefer):
Object subclass: #PersonalInformation
	instanceVariableNames: 'name address gender sendEmailUpdates birthdate'
	classVariableNames: ''
	poolDictionaries: ''
	category: 'SCSeasideTutorial'

address
	^address 

address: anObject
	address := anObject 

birthdate
	^birthdate 

birthdate: anObject
	birthdate := anObject 

doNotSendEmailUpdates
	sendEmailUpdates := false 

doSendEmailUpdates
	sendEmailUpdates := true 

female
	gender := #Female 

isFemale
	^self isMale not 

isMale
	^gender = #Male 

male
	gender := #Male 

name
	^name 

name: anObject
	name := anObject 

sendEmailUpdates
	^sendEmailUpdates

printOn: aStream
	aStream nextPutAll: self name
Picture our database as simply a list of these objects. We might want to show the list, edit the objects etc.

Notice the protocol for email updates. The model doesn't provide direct mutators but instead the methods #doSendEmailUpdates, #doNotSetEmailUpdates, #sendEmailUpdates. Also notice that the gender property follows a similar pattern. I wrote the model this way to illustrate a point: your model is what it is. Often the model and the (web or graphical) UI require some work to adapt to one another. For example, we won't be able to just do something like:

...inside render method
   html textInputOn: #gender of: model
because the textInputOn:of: relies on accessors and mutators for the property. In this case we can either use textInputWithValue:callback: or use a "go-between" or adapter method in our view class. I show both methods below. Before we move on to the UI lets make a class side method which gives us a sample person to play with:
sample1
	^PersonalInformation new
		name: 'SpongeBob SquarePants';
		address: 'a Pineapple, Bikini Bottom';
		male;
		doNotSendEmailUpdates;
		birthdate: ('05/12/1999' asDate);
		yourself. 

The new view

Now, we need to modify PersonalInformationView so that it can edit an instance of PersonalInformation. Let's just start fresh.

WAComponent subclass: #PersonalInformationView2
	instanceVariableNames: 'model'
	classVariableNames: ''
	poolDictionaries: ''
	category: 'SCSeasideTutorial'

model
	^model 

model: anObject
	model := anObject 


renderContentOn: html
	html form: 
		[html spanClass: 'label' with: 'Name:'.
		html spanClass: 'field' with: [html textInputOn: #name of: model].
		html spanClass: 'label' with: 'Address:'.
		html spanClass: 'field' with: [html textAreaOn: #address of: model].
		html spanClass: 'label' with: 'Gender:'.
		html spanClass: 'field' with:  [|group|
					group := html radioGroup.
					html radioButtonInGroup: group
						selected: model isMale
						callback: [model male].
					html text: 'Male'; break.
					html radioButtonInGroup: group
						selected: model isFemale
						callback: [model female].
					html text: 'Female'].
		html spanClass: 'label' with: 'Updates:'.
		html spanClass: 'field' with: [html checkboxOn: #sendEmailUpdates of: self].
		html spanClass: 'label' with: 'Birthdate:'.
		html spanClass: 'field'
			with: [html dateInputWithValue: model birthdate
					callback: [:v | model birthdate: v]].
		html spanClass: 'button'
			with: [html submitButtonWithAction: [self save] text: 'Save']] 

sendEmailUpdates
	^model sendEmailUpdates 

sendEmailUpdates: aBoolean
	aBoolean ifTrue: [model doSendEmailUpdates] ifFalse: [model doNotSendEmailUpdates]

save
	self answer: true

style
	"Same as PersonalInformationView"
	...
I've tried to make the renderContentOn: method somewhat readable but its still a mess. We'll see later that further abstraction helps to avoid methods like this one. For now, walk through it one statement at a time. Compare it to the same method in the original view. Notice in places where the old code specified "self" as the target (#textInputOn:of:) we now specify the model. So, the view will retrieve its data and store its data right in our model objects. How would we use this? Well, to play with it add an anchor with a callback to your HelloWorldComponent and make the callback look like this:
editPersonalInformation
	| view |
	view := PersonalInformationView2 new.
	view model: PersonalInformation sample1.
	self call: view.
	self inform: 'Hello ' , view model name

Later in this tutorial we will see a view similar to this one written in far fewer lines of code. This is because Seaside includes support for building simple two-column editor dialogs. If you want to get ahead, browse the WAEditDialog class.

What did this get us?

Just in case it isn't already obvious why we needed to do this, lets build an address book application. We're going to store the data in the Smalltalk image, no "real" database at this point. First, make a fake database in the PersonalInformation class by adding a class variable Database and some class side methods:
Object subclass: #PersonalInformation
	instanceVariableNames: 'name address gender sendEmailUpdates birthdate'
	classVariableNames: 'Database'
	poolDictionaries: ''
	category: 'SCSeasideTutorial'

"These are class side methods!!!!!!!!!"

database
	^Database ifNil: [self resetSampleDatabase] 

resetSampleDatabase
	"self resetSampleDatabase"
	Database := OrderedCollection
				with: self sample1
				with: self sample2
				with: self sample3.
	^Database 

sample1
	^PersonalInformation new
		name: 'SpongeBob SquarePants';
		address: 'a Pineapple, Bikini Bottom';
		male;
		doNotSendEmailUpdates;
		birthdate: ('05/12/1999' asDate);
		yourself. 

sample2
	^PersonalInformation new
		name: 'Patrick Star';
		address: 'Under a rock';
		male;
		doSendEmailUpdates;
		birthdate: ('01/12/1998' asDate);
		yourself. 

sample3
	^PersonalInformation new
		name: 'Sandy Cheeks';
		address: 'A biodome';
		femal;
		doSendEmailUpdates;
		birthdate: ('01/01/1980' asDate);
		yourself. 

Now lets make a view which lists these and allows us to edit them:
WAComponent subclass: #AddressBook
	instanceVariableNames: ''
	classVariableNames: ''
	poolDictionaries: ''
	category: 'SCSeasideTutorial'

editPerson: person
	| view |
	view := PersonalInformationView2 new.
	view model: person.
	self call: view 

people
	^PersonalInformation database 

renderContentOn: html
	html heading: 'My friends' level: 1.
	html table: 
		[html tableRow:
			[html tableHeading: 'Name'.
			html tableHeading: 'Address'.
			html tableHeading: 'Birthdate'].
		self renderDatabaseRowsOn: html] 

renderDatabaseRowsOn: html
	self people do: [:person |
		html tableRow: [self renderPerson: person on: html]] 

renderPerson: person on: html
	html tableData: [html anchorWithAction: [self editPerson: person] text: person name].
	html tableData: person address.
	html tableData: person birthdate mmddyyyy. 

If all goes well you can visit this application and you should see:

Clicking a link raises the editor. Pressing the save button updates our "database". Pause for a moment to think about how the editor is doing its job: You give it a model, it displays the contents of the model, the user edits the fields and presses "save", the form gets submitted and the model is filled in automatically by all of the field callbacks, the editor answers true and so the list is displayed with the edited model object.

Exercises

  1. Add links to add and remove entries from the list
  2. Turn the table headings into links so that clicking on them sorts the list by name, address etc
  3. Add a #style method and make the list look better (right justify the date, maybe add borders etc).
  4. Try to think how you might make a "cancel" button. It is more subtle than you think. Simply self answer: false isn't enough since the model has already been filled in with the new values. This same problem occurs in GUI applications where a view directly edits a model. There are several simple solutions...try to find one.

C. David Shaffer
Last modified: Fri Jul 22 17:12:27 EDT 2005