From 42373cc8ba3883b701f9d99b080e0fcd18e6b180 Mon Sep 17 00:00:00 2001 From: Stephan Eggermont Date: Thu, 24 Apr 2014 20:37:40 +0200 Subject: [PATCH 1/8] Create a more elaborate dynamic example --- SpecDynamicExample | 113 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 SpecDynamicExample diff --git a/SpecDynamicExample b/SpecDynamicExample new file mode 100644 index 0000000..8824ca6 --- /dev/null +++ b/SpecDynamicExample @@ -0,0 +1,113 @@ +!!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 +Create the abstract superclass ==Party== +[[[ +Object subclass: #DOParty + instanceVariableNames: '' + classVariableNames: '' + category: 'Domain-Parties' +]] + +A party responds to the ==fullName== message. + +[[[ +fullName + ^'Full name' +]]] + +The subclasses are going to override this message. +Create a subclass for persons: + +[[[ +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. + +[[[ +firstName + ^firstName ifNil: [ '' ] + +firstName: aString + firstName := aString + +lastName + ^lastName ifNil: [ '' ] + +lastName: aString + lastName := aString +]]] + +We can now override the ==fullName==. + +[[[ +fullName + ^self firstName, ' ', self lastName +]]] + +Create a subclass for companies + +[[[ +DOParty subclass: #DOCompany + instanceVariableNames: 'companyName' + classVariableNames: '' + category: 'Domain-Parties' +]]] + +And its accessors and the overridden method + +[[[ +companyName + ^ companyName ifNil: [''] + +companyName: anObject + companyName := anObject + +fullName + ^ self companyName +]]] + +In this example we will simply keep all parties in the image. +We create a class to hold parties + +[[[ +Object subclass: #DOPartiesModel + instanceVariableNames: 'parties' + classVariableNames: '' + category: 'Domain-Parties' +]]] + +And lazily initialize with a collection + +[[[ +parties + ^parties ifNil: [ parties := OrderedCollection new ] + +parties: aCollection + parties := aCollection +]]] + +On the class side we add an instanceVariable ==default== as the singleton +and two methods to access and reset it. + +[[[ +DOPartiesModel class + instanceVariableNames: 'default' + +default + ^default ifNil: [ default := self new ] + +reset + default := nil +]]] + From 102c5447cc4aa53cc1c84e849fe2c3d305663529 Mon Sep 17 00:00:00 2001 From: Stephan Eggermont Date: Thu, 24 Apr 2014 21:34:01 +0200 Subject: [PATCH 2/8] Rename, added contents --- SpecDynamicExample | 113 ---------------------- SpecDynamicExample.pier | 201 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+), 113 deletions(-) delete mode 100644 SpecDynamicExample create mode 100644 SpecDynamicExample.pier diff --git a/SpecDynamicExample b/SpecDynamicExample deleted file mode 100644 index 8824ca6..0000000 --- a/SpecDynamicExample +++ /dev/null @@ -1,113 +0,0 @@ -!!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 -Create the abstract superclass ==Party== -[[[ -Object subclass: #DOParty - instanceVariableNames: '' - classVariableNames: '' - category: 'Domain-Parties' -]] - -A party responds to the ==fullName== message. - -[[[ -fullName - ^'Full name' -]]] - -The subclasses are going to override this message. -Create a subclass for persons: - -[[[ -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. - -[[[ -firstName - ^firstName ifNil: [ '' ] - -firstName: aString - firstName := aString - -lastName - ^lastName ifNil: [ '' ] - -lastName: aString - lastName := aString -]]] - -We can now override the ==fullName==. - -[[[ -fullName - ^self firstName, ' ', self lastName -]]] - -Create a subclass for companies - -[[[ -DOParty subclass: #DOCompany - instanceVariableNames: 'companyName' - classVariableNames: '' - category: 'Domain-Parties' -]]] - -And its accessors and the overridden method - -[[[ -companyName - ^ companyName ifNil: [''] - -companyName: anObject - companyName := anObject - -fullName - ^ self companyName -]]] - -In this example we will simply keep all parties in the image. -We create a class to hold parties - -[[[ -Object subclass: #DOPartiesModel - instanceVariableNames: 'parties' - classVariableNames: '' - category: 'Domain-Parties' -]]] - -And lazily initialize with a collection - -[[[ -parties - ^parties ifNil: [ parties := OrderedCollection new ] - -parties: aCollection - parties := aCollection -]]] - -On the class side we add an instanceVariable ==default== as the singleton -and two methods to access and reset it. - -[[[ -DOPartiesModel class - instanceVariableNames: 'default' - -default - ^default ifNil: [ default := self new ] - -reset - default := nil -]]] - diff --git a/SpecDynamicExample.pier b/SpecDynamicExample.pier new file mode 100644 index 0000000..78af5ba --- /dev/null +++ b/SpecDynamicExample.pier @@ -0,0 +1,201 @@ +!!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 +Create the abstract superclass ==Party== +[[[ +Object subclass: #DOParty + instanceVariableNames: '' + classVariableNames: '' + category: 'Domain-Parties' +]] + +A party responds to the ==fullName== message. + +[[[ +fullName + ^'Full name' +]]] + +The subclasses are going to override this message. +Create a subclass for persons: + +[[[ +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. + +[[[ +firstName + ^firstName ifNil: [ '' ] + +firstName: aString + firstName := aString + +lastName + ^lastName ifNil: [ '' ] + +lastName: aString + lastName := aString +]]] + +We can now override the ==fullName==. + +[[[ +fullName + ^self firstName, ' ', self lastName +]]] + +Create a subclass for companies + +[[[ +DOParty subclass: #DOCompany + instanceVariableNames: 'companyName' + classVariableNames: '' + category: 'Domain-Parties' +]]] + +And its accessors and the overridden method + +[[[ +companyName + ^ companyName ifNil: [''] + +companyName: anObject + companyName := anObject + +fullName + ^ self companyName +]]] + +In this example we will simply keep all parties in the image. +We create a class to hold parties + +[[[ +Object subclass: #DOPartiesModel + instanceVariableNames: 'parties' + classVariableNames: '' + category: 'Domain-Parties' +]]] + +And lazily initialize with a collection + +[[[ +parties + ^parties ifNil: [ parties := OrderedCollection new ] + +parties: aCollection + parties := aCollection +]]] + +On the class side we add an instanceVariable ==default== as the singleton +and two methods to access and reset it. + +[[[ +DOPartiesModel class + instanceVariableNames: 'default' + +default + ^default ifNil: [ default := self new ] + +reset + default := nil +]]] + +!!! A dynamic editor +To edit a single party, we create a subclass of ==DynamicComposableModel== + +[[[ +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 a setter 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. +[[[ +partyClass: aPartyClass + partyClass := aPartyClass + +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. + +[[[ +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. + +[[[ +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. + +[[[ +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 + +[[[ +DOPartyEditor>initialExtent + ^ 300@(self class inputTextHeight*(3+partyClass fields size)) +]]] From f6a59c3f405bf1a26894f4ee637258e2a6e0cdbf Mon Sep 17 00:00:00 2001 From: Stephan Eggermont Date: Thu, 24 Apr 2014 22:46:00 +0200 Subject: [PATCH 3/8] Added party list with search --- SpecDynamicExample.pier | 135 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 134 insertions(+), 1 deletion(-) diff --git a/SpecDynamicExample.pier b/SpecDynamicExample.pier index 78af5ba..edc0bda 100644 --- a/SpecDynamicExample.pier +++ b/SpecDynamicExample.pier @@ -193,9 +193,142 @@ DOPartyEditor>initializeWidgets ]]] -The last thing needed is to calculate how large the window should be +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. [[[ 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 adres book + +We can now make the address book with a search field and buttons to add persons and companies. +Add a class + +[[[ +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. + +[[[ +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) + +[[[ +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 + + + ^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. + +[[[ +DOPartyList>addPartyBlockIn: anEditor + anEditor okAction: [ |party| + party := anEditor partyClass new. + anEditor 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 + +[[[ +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 +[[[ +DOPartyList>addCompany + ^addCompany + +DOPartyList>addPerson + ^addPerson + +DOPartyList>items: aCollection + list items: aCollection + +DOPartyList>list + ^list + +DOPartyList>search + ^search +]]] +protocol +[[[ +DOPartyListresetSelection + list resetSelection + +DOPartyListtitle + ^ 'Parties' +]]] +and protocol-events +[[[ +DOPartyList>whenAddCompanyClicked: aBlock + addCompany whenActionPerformedDo: aBlock + +DOPartyList>whenAddPersonClicked: aBlock + addPerson whenActionPerformedDo: aBlock + +DOPartyList>whenSelectedItemChanged: aBlock + list whenSelectedItemChanged: aBlock + +]]] From 3563c3b05acc274dedac0350fa824c05633548e0 Mon Sep 17 00:00:00 2001 From: Stephan Eggermont Date: Thu, 24 Apr 2014 22:51:43 +0200 Subject: [PATCH 4/8] Make classes explicit --- SpecDynamicExample.pier | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/SpecDynamicExample.pier b/SpecDynamicExample.pier index edc0bda..b7c4e6a 100644 --- a/SpecDynamicExample.pier +++ b/SpecDynamicExample.pier @@ -17,7 +17,7 @@ Object subclass: #DOParty A party responds to the ==fullName== message. [[[ -fullName +DOParty>fullName ^'Full name' ]]] @@ -35,23 +35,23 @@ Create accessors for ==firstName== and ==lastName==. We don't want to need to ha as a special case, so return the empty string if the instVars are nil. [[[ -firstName +DOPerson>firstName ^firstName ifNil: [ '' ] -firstName: aString +DOPerson>firstName: aString firstName := aString -lastName +DOPerson>lastName ^lastName ifNil: [ '' ] -lastName: aString +DOPerson>lastName: aString lastName := aString ]]] We can now override the ==fullName==. [[[ -fullName +DOPerson>fullName ^self firstName, ' ', self lastName ]]] @@ -67,13 +67,13 @@ DOParty subclass: #DOCompany And its accessors and the overridden method [[[ -companyName +DOCompany>companyName ^ companyName ifNil: [''] -companyName: anObject +DOCompany>companyName: anObject companyName := anObject -fullName +DOCompany>fullName ^ self companyName ]]] @@ -90,10 +90,10 @@ Object subclass: #DOPartiesModel And lazily initialize with a collection [[[ -parties +DOPartiesModel>parties ^parties ifNil: [ parties := OrderedCollection new ] -parties: aCollection +DOPartiesModel>parties: aCollection parties := aCollection ]]] @@ -104,10 +104,10 @@ and two methods to access and reset it. DOPartiesModel class instanceVariableNames: 'default' -default +DOPartiesModel>>default ^default ifNil: [ default := self new ] -reset +DOPartiesModel>>reset default := nil ]]] @@ -125,10 +125,10 @@ When we instantiate this editor, we'll tell it on what kind of party it operates A ==DynamicComposableModel== has a complex initialization proces, so we use a separate ==basicNew== and ==initialize== to set the ==partyClass== early enough. [[[ -partyClass: aPartyClass +DOPartyEditor>partyClass: aPartyClass partyClass := aPartyClass -on: aPartyClass +DOPartyEditor>>on: aPartyClass ^self basicNew partyClass: aPartyClass; initialize; @@ -314,10 +314,10 @@ DOPartyList>search ]]] protocol [[[ -DOPartyListresetSelection +DOPartyList>resetSelection list resetSelection -DOPartyListtitle +DOPartyList>title ^ 'Parties' ]]] and protocol-events @@ -332,3 +332,4 @@ DOPartyList>whenSelectedItemChanged: aBlock list whenSelectedItemChanged: aBlock ]]] +This can be tested with ==DOPartiesList new openWithSpec== From 831f4c8eb461f6abffa2efed98b75f74fbe8d04a Mon Sep 17 00:00:00 2001 From: Stephan Eggermont Date: Thu, 24 Apr 2014 22:53:57 +0200 Subject: [PATCH 5/8] added dynamic example --- chapters.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/chapters.conf b/chapters.conf index dac2da3..93394ca 100644 --- a/chapters.conf +++ b/chapters.conf @@ -7,6 +7,7 @@ "TheHeartOfSpec.pier", "WhereToFindWhatIWant.pier", "SpecTheDynamic.pier", + "SpecDynamicExample.pier", "WritingMyOwnModel.pier", "SpecInterpreter.pier" ], From 21b40752914bae8d476cd9302d0a41ff5e5ce078 Mon Sep 17 00:00:00 2001 From: Stephan Eggermont Date: Thu, 24 Apr 2014 22:54:47 +0200 Subject: [PATCH 6/8] added dynamic example --- book.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/book.conf b/book.conf index 0f0badd..98a132c 100644 --- a/book.conf +++ b/book.conf @@ -6,6 +6,7 @@ "TheHeartOfSpec.pier", "WhereToFindWhatIWant.pier", "SpecTheDynamic.pier", + "SpecDynamicExample.pier", "WritingMyOwnModel.pier", "SpecInterpreter.pier" ], From b0d666c2f38494da9224372cc40b66c5307c8147 Mon Sep 17 00:00:00 2001 From: Stephan Eggermont Date: Thu, 24 Apr 2014 23:13:03 +0200 Subject: [PATCH 7/8] missing accessor for partyClass, missing model --- SpecDynamicExample.pier | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/SpecDynamicExample.pier b/SpecDynamicExample.pier index b7c4e6a..323a1cb 100644 --- a/SpecDynamicExample.pier +++ b/SpecDynamicExample.pier @@ -121,13 +121,16 @@ DynamicComposableModel subclass: #DOPartyEditor 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 a setter and on the class side we use that in a constructor. +==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. [[[ DOPartyEditor>partyClass: aPartyClass partyClass := aPartyClass +DOPartyEditor>partyClass + ^partyClass + DOPartyEditor>>on: aPartyClass ^self basicNew partyClass: aPartyClass; @@ -275,11 +278,11 @@ block of the editor. [[[ DOPartyList>addPartyBlockIn: anEditor anEditor okAction: [ |party| - party := anEditor partyClass new. - anEditor partyClass fields do: [ :field | + 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 ]. + self refreshItems ] ]]] Now we can initialize the presenter From 5eb8fb6c065912eee1bd6190a9d40c500fd234ef Mon Sep 17 00:00:00 2001 From: Stephan Eggermont Date: Fri, 9 May 2014 18:02:23 +0200 Subject: [PATCH 8/8] Added editing of parties, cleanups --- SpecDynamicExample.pier | 95 +++++++++++++++++++++++++++++------------ 1 file changed, 67 insertions(+), 28 deletions(-) diff --git a/SpecDynamicExample.pier b/SpecDynamicExample.pier index 323a1cb..cf385da 100644 --- a/SpecDynamicExample.pier +++ b/SpecDynamicExample.pier @@ -6,17 +6,19 @@ 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' ]]] @@ -24,7 +26,7 @@ DOParty>fullName 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: '' @@ -34,7 +36,7 @@ DOParty subclass: #DOPerson 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: [ '' ] @@ -50,14 +52,14 @@ DOPerson>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: '' @@ -66,7 +68,7 @@ DOParty subclass: #DOCompany And its accessors and the overridden method -[[[ +[[[label=companyAccessors|caption=Accessors and override for company|language=Smalltalk DOCompany>companyName ^ companyName ifNil: [''] @@ -80,7 +82,7 @@ DOCompany>fullName 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: '' @@ -89,7 +91,7 @@ Object subclass: #DOPartiesModel And lazily initialize with a collection -[[[ +[[[label=lazyInstantiation|caption=Accessors, lazily initialized|language=Smalltalk DOPartiesModel>parties ^parties ifNil: [ parties := OrderedCollection new ] @@ -100,7 +102,7 @@ DOPartiesModel>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' @@ -114,7 +116,7 @@ DOPartiesModel>>reset !!! 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: '' @@ -124,7 +126,7 @@ When we instantiate this editor, we'll tell it on what kind of party it operates ==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 @@ -141,7 +143,7 @@ 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' @@ -160,7 +162,7 @@ The editor needs to know what fields need to be created. On the class side of th 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 @@ -175,7 +177,7 @@ Now we can initialize the widgets. ==instantiateModels== expects pairs of field 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. @@ -199,7 +201,7 @@ DOPartyEditor>initializeWidgets 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)) ]]] @@ -207,12 +209,12 @@ DOPartyEditor>initialExtent Now we can test the editor with ==(DOPartyEditor on: DOCompany) openDialogWithSpec== and ==(DOPartyEditor on: DOPerson) openDialogWithSpec== -!!! The adres book +!!! 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: '' @@ -222,7 +224,7 @@ ComposableModel subclass: #DOPartiesList 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. @@ -234,7 +236,7 @@ DOPartiesList>refreshItems 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; @@ -266,8 +268,6 @@ DOPartiesList>>defaultSpec 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. @@ -275,7 +275,7 @@ 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. @@ -287,7 +287,7 @@ DOPartyList>addPartyBlockIn: anEditor Now we can initialize the presenter -[[[ +[[[label=partiesListPresenter|caption=Behaviour of the parties list|language=Smalltalk DOPartyList>initializePresenter search whenTextChanged: [ :class | self refreshItems ]. @@ -299,7 +299,7 @@ DOPartyList>initializePresenter self addPartyBlockIn: edit ]. ]]] Don't forget the accessors -[[[ +[[[label=partiesListAccessors|caption=Accessors|language=Smalltalk DOPartyList>addCompany ^addCompany @@ -316,7 +316,7 @@ DOPartyList>search ^search ]]] protocol -[[[ +[[[label=partiesListProtocol|caption=Protocol|language=Smalltalk DOPartyList>resetSelection list resetSelection @@ -324,7 +324,7 @@ DOPartyList>title ^ 'Parties' ]]] and protocol-events -[[[ +[[[label=partiesListProtocolEvents|caption=Protocol events|language=Smalltalk DOPartyList>whenAddCompanyClicked: aBlock addCompany whenActionPerformedDo: aBlock @@ -336,3 +336,42 @@ DOPartyList>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] +]]]