Skip to content

Adding a chapter with a dynamic example #39

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
377 changes: 377 additions & 0 deletions SpecDynamicExample.pier
Original file line number Diff line number Diff line change
@@ -0,0 +1,377 @@
!!Parties, a dynamic example
In an address book we find addresses of both people and organizations.
The generalization of both is called a party ('Analysis Patterns', Martin Fowler).
In this example we'll make an address book where we can add both persons and companies.
They will have different attributes.
We show how to do add them with a dynamic user interface, and minimize duplication.

!!! The party model
In the party model, party is a superclass of person and company.
Create the abstract superclass ==Party==
[[[label=createParty|caption=Create the party class|language=Smalltalk
Object subclass: #DOParty
instanceVariableNames: ''
classVariableNames: ''
category: 'Domain-Parties'
]]]

We should be able to select parties from a list.
A party responds to the ==fullName== message.

[[[label=partyName|caption=Select a party by its full name|language=Smalltalk
DOParty>fullName
^'Full name'
]]]

The subclasses are going to override this message.
Create a subclass for persons:

[[[label=createPerson|caption=Create person as a subclass of party|language=Smalltalk
DOParty subclass: #DOPerson
instanceVariableNames: 'firstName lastName'
classVariableNames: ''
category: 'Domain-Parties'
]]]

Create accessors for ==firstName== and ==lastName==. We don't want to need to handle ==nil==
as a special case, so return the empty string if the instVars are nil.

[[[label=personAccessors|caption=Accessors for person|language=Smalltalk
DOPerson>firstName
^firstName ifNil: [ '' ]

DOPerson>firstName: aString
firstName := aString

DOPerson>lastName
^lastName ifNil: [ '' ]

DOPerson>lastName: aString
lastName := aString
]]]

We can now override the ==fullName==.

[[[label=overrideFullname|caption=Override the full name|language=Smalltalk
DOPerson>fullName
^self firstName, ' ', self lastName
]]]

Create a subclass for companies

[[[label=createCompany|caption=Create company as subclass of party|language=Smalltalk
DOParty subclass: #DOCompany
instanceVariableNames: 'companyName'
classVariableNames: ''
category: 'Domain-Parties'
]]]

And its accessors and the overridden method

[[[label=companyAccessors|caption=Accessors and override for company|language=Smalltalk
DOCompany>companyName
^ companyName ifNil: ['']

DOCompany>companyName: anObject
companyName := anObject

DOCompany>fullName
^ self companyName
]]]

In this example we will simply keep all parties in the image.
We create a class to hold parties

[[[label=partyHolder|caption=Domain model for parties|language=Smalltalk
Object subclass: #DOPartiesModel
instanceVariableNames: 'parties'
classVariableNames: ''
category: 'Domain-Parties'
]]]

And lazily initialize with a collection

[[[label=lazyInstantiation|caption=Accessors, lazily initialized|language=Smalltalk
DOPartiesModel>parties
^parties ifNil: [ parties := OrderedCollection new ]

DOPartiesModel>parties: aCollection
parties := aCollection
]]]

On the class side we add an instanceVariable ==default== as the singleton
and two methods to access and reset it.

[[[label=partiesSingleton|caption=A singleton to hold all parties|language=Smalltalk
DOPartiesModel class
instanceVariableNames: 'default'

DOPartiesModel>>default
^default ifNil: [ default := self new ]

DOPartiesModel>>reset
default := nil
]]]

!!! A dynamic editor
To edit a single party, we create a subclass of ==DynamicComposableModel==

[[[label=partyEditor|caption=A class to edit one party|language=Smalltalk
DynamicComposableModel subclass: #DOPartyEditor
instanceVariableNames: 'partyClass'
classVariableNames: ''
category: 'Domain-Parties'
]]]
When we instantiate this editor, we'll tell it on what kind of party it operates and store that in the
==partyClass==. On the instance side we add accessors and on the class side we use that in a constructor.
A ==DynamicComposableModel== has a complex initialization proces, so we use a separate ==basicNew==
and ==initialize== to set the ==partyClass== early enough.
[[[label=kindOfParty|caption=The class needs to know what kind of party to edit|language=Smalltalk
DOPartyEditor>partyClass: aPartyClass
partyClass := aPartyClass

DOPartyEditor>partyClass
^partyClass

DOPartyEditor>>on: aPartyClass
^self basicNew
partyClass: aPartyClass;
initialize;
yourself
]]]
This class has no ==defaultSpec==, as it is only created with a dynamic spec.
The editor is going to be a separate window. The window title is dependent of the class.
Party and subclasses define it at the class side.

[[[label=kindOfParty|caption=Party editor wants to show the kind of party|language=Smalltalk
DOParty>>title
"override in subclasses"
^'Party'

DOCompany>>title
^'Company'

DOPerson>>title
^'Person'

DOPartyEditor>title
^ partyClass title
]]]

The editor needs to know what fields need to be created. On the class side of the Party subclasses
we return an array of symbols representing the fields. This will do for the example, for a real
application with differnt kinds of fields Magritte descriptions are much more suitable.

[[[label=partyFields|caption=Array of feilds|language=Smalltalk
DOParty>>fields
^self subclassResponsibility

DOCompany>>fields
^#(#companyName)

DOPerson>>fields
^#(#firstName #lastName)
]]]

Now we can initialize the widgets. ==instantiateModels== expects pairs of field names and field types
and adds them to the widgets dictionary.
They are then laid out in one column, given some default values and added in focus order.

[[[label=editorWidgets|caption=Use the fields to build the widgets needed|language=Smalltalk
DOPartyEditor>initializeWidgets
|models|
models := OrderedCollection new.
partyClass fields do: [ :field | models add: field; add: #TextInputFieldModel ].
self instantiateModels: models.

layout := SpecLayout composed
newColumn: [ :col |
partyClass fields do: [ :field |
col add: field height: self class inputTextHeight]];
yourself .

self widgets keysAndValuesDo: [ :key :value |
value autoAccept: true;
entryCompletion:nil;
ghostText: key.
self focusOrder add: value] .

]]]

The last thing needed is to calculate how large the window should be. It is going to be used
as a dialog with ok and cancel that take up a height of about three input fields.

[[[label=editorExtent|caption=Different kinds of party have different number of fieldse|language=Smalltalk
DOPartyEditor>initialExtent
^ 300@(self class inputTextHeight*(3+partyClass fields size))
]]]

Now we can test the editor with ==(DOPartyEditor on: DOCompany) openDialogWithSpec== and
==(DOPartyEditor on: DOPerson) openDialogWithSpec==

!!! The address book

We can now make the address book with a search field and buttons to add persons and companies.
Add a class

[[[label=partiesList|caption=Create a class to show th elist of parties|language=Smalltalk
ComposableModel subclass: #DOPartiesList
instanceVariableNames: 'search addPerson addCompany list'
classVariableNames: ''
category: 'Domain-Parties'
]]]

As soon as something is typed in the search field, the list should show the list of parties
having a fullName containing the search term, ignoring case.

[[[label=refreshList|caption=Show only the items matching the search|language=Smalltalk
DOPartiesList>refreshItems
|searchString|
searchString := search text asLowercase.
list
items: (DOPartiesModel default parties select: [: each |
searchString isEmpty or: [each fullName asLowercase includesSubstring: searchString]]);
displayBlock: [ :each | each fullName].
]]]

We can now create the widgets and the default layout (class side)

[[[label=partiesListWidgets|caption=The widgets for the parties list|language=Smalltalk
DOPartiesList>initializeWidgets
search := self newTextInput.
search autoAccept: true;
entryCompletion:nil;
ghostText: 'Search'.
addPerson := self newButton.
addPerson label: '+Person'.
addCompany := self newButton.
addCompany label: '+Company'.
list := self newList.

self refreshItems.
self focusOrder
add: search;
add: addPerson;
add: addCompany;
add: list.

DOPartiesList>>defaultSpec
<spec: #default>

^SpecLayout composed
newColumn: [ :col |
col newRow: [:row |
row add: #search;
add: #addPerson;
add: #addCompany]
height: ComposableModel toolbarHeight;
add: #list];
yourself
]]]

When the user clicks on the ok button of the party editor, we need to create an instance of the right
subclass, read the field values out of the editor and assign them to the attributes of the new instance.
We do that using meta-programming (==perform:== and ==perform:with:==).
Then we add the instance to the model and need to refresh the list. Add a method setting the okAction
block of the editor.

[[[label=addAParty|caption=Adding a party|language=Smalltalk
DOPartyList>addPartyBlockIn: anEditor
anEditor okAction: [ |party|
party := anEditor model partyClass new.
anEditor model partyClass fields do: [ :field |
party perform: (field asMutator) with: (anEditor model perform: field) text ].
DOPartiesModel default parties add: party.
self refreshItems ]
]]]

Now we can initialize the presenter

[[[label=partiesListPresenter|caption=Behaviour of the parties list|language=Smalltalk
DOPartyList>initializePresenter
search whenTextChanged: [ :class | self refreshItems ].

addPerson action: [ |edit|
edit := (DOPartyEditor on:DOPerson) openDialogWithSpec.
self addPartyBlockIn: edit].
addCompany action: [ |edit|
edit := (DOPartyEditor on: DOCompany) openDialogWithSpec.
self addPartyBlockIn: edit ].
]]]
Don't forget the accessors
[[[label=partiesListAccessors|caption=Accessors|language=Smalltalk
DOPartyList>addCompany
^addCompany

DOPartyList>addPerson
^addPerson

DOPartyList>items: aCollection
list items: aCollection

DOPartyList>list
^list

DOPartyList>search
^search
]]]
protocol
[[[label=partiesListProtocol|caption=Protocol|language=Smalltalk
DOPartyList>resetSelection
list resetSelection

DOPartyList>title
^ 'Parties'
]]]
and protocol-events
[[[label=partiesListProtocolEvents|caption=Protocol events|language=Smalltalk
DOPartyList>whenAddCompanyClicked: aBlock
addCompany whenActionPerformedDo: aBlock

DOPartyList>whenAddPersonClicked: aBlock
addPerson whenActionPerformedDo: aBlock

DOPartyList>whenSelectedItemChanged: aBlock
list whenSelectedItemChanged: aBlock

]]]
This can be tested with ==DOPartiesList new openWithSpec==

!!! Editing Parties
A next step is the editing of existing instances.
In the DOPartiesList, we need to use a NewListModel instead of the ListModel,
as that understands doubleClick actions.

[[[label=editingChanges|caption=Replace list by newlist|language=Smalltalk
DOPartiesList>initializeWidgets
- list := self newList.
+ list := self instantiate: NewListModel.
]]]

To edit a party, we modify addPartyBlockIn: anEditor to create editParty:in:.
We set the data values and the title. In the okAction we don't have to add the party.

[[[label=editAParty|caption=Edit instead of add|language=Smalltalk
DOPartiesList>editParty: aParty in: anEditor
aParty class fields do: [ :field |
(anEditor model perform: field) text: (aParty perform: field) ].

anEditor title: 'Edit ',aParty fullName.

anEditor okAction: [
anEditor model partyClass fields do: [ :field |
aParty perform: (field asMutator) with: (anEditor model perform: field) text ].
self refreshItems ].
]]]

In initializePresenter, we can then add an edit action. The list currently needs
to know that it should handle doubleClicks, and then call a partyeditor

[[[label=editingBehaviour|caption=Add a doubleclick action |language=Smalltalk
DOPartiesList>initializePresenter
+ list handlesDoubleClick: true.
+ list doubleClickAction: [ |party edit|
+ party := list selectedItem.
+ edit := (DOPartyEditor on: party class) openDialogWithSpec.
+ self editParty: party in: edit]
]]]
1 change: 1 addition & 0 deletions book.conf
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"TheHeartOfSpec.pier",
"WhereToFindWhatIWant.pier",
"SpecTheDynamic.pier",
"SpecDynamicExample.pier",
"WritingMyOwnModel.pier",
"SpecInterpreter.pier"
],
Expand Down
1 change: 1 addition & 0 deletions chapters.conf
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"TheHeartOfSpec.pier",
"WhereToFindWhatIWant.pier",
"SpecTheDynamic.pier",
"SpecDynamicExample.pier",
"WritingMyOwnModel.pier",
"SpecInterpreter.pier"
],
Expand Down