-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathSpecDynamicExample.pier
More file actions
377 lines (299 loc) · 11.7 KB
/
SpecDynamicExample.pier
File metadata and controls
377 lines (299 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
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]
]]]