Up  Next

Extending the PersonalMoney Application

 

Part 5

 

 

Objectives and Overview

 

Somewhere down the line we'll get to the point where PersonalMoney is sooooo wonderful that everyone wants it.  As a service to the multitudes using Quicken, we'll offer the means for them to import their Quicken data into PersonalMoney.  Naturally, we'll also provide the means to export PersonalMoney data into Quicken format.  In this exercise we look at the Quicken Interchange Format (QIF) and implement a subset of it, streaming data in and out of files.  Before we get there, though, we need to add some more functionality to the application model.  We'll add categories to transactions, since that is one big deficiency in the current model.

 

Along the way we look at several classes, among them File, Stream, and FileStream.  We implement an editable combo box.  We look at class SmalltalkSystem and ClassBrowserShell and implement some helper methods.  We also revisit binary filing to put in versioning that should have gone in before.  While we're at it, we also make account types subclasses of PersonalAccount, implement some behavior that differentiates them from each other, and also change the display to reflect different types of accounts.

 

 

Adding categories to accounts and transactions

 

The first change to the model is to add categories, that is, assigning each transaction to a category (or is it assigning a category to each transaction).  Examples of categories are 'Taxes', 'Utilities', and 'Salary'.  We implement this just like we implemented account types before (in Part 1), this time adding a 'categories' instance variable to the account and a 'category' instance variable to the transaction.  Along with these we add accessor and add/remove methods.  The intent is that the account maintain a list of transaction categories.  We start by adding 'category' and accessor methods to the transaction class:

 

In PersonalAccountTransaction:

Model subclass: #PersonalAccountTransaction

        instanceVariableNames: 'date description amount isDebit category'

        classVariableNames: ''

        poolDictionaries: ''

 

category

        "Answer the category of the receiver"

 

        ^category

 

category: aCategory

        "Set the category of the receiver to aCategory"

 

        category := (aCategory isNil or: [aCategory trimBlanks isEmpty])

                 ifTrue: [nil]

                 ifFalse: [aCategory]

 

The test in #category: ensures that category will be nil or a non-blank string.  We now add 'categories' to the account class, initialize it, and add accessor and collection-type add/remove methods.:

 

In PersonalAccount:

Model subclass: #PersonalAccount

        instanceVariableNames: 'name accountNumber accountType initialBalance transactions currentBalance categories'

        classVariableNames: ''

        poolDictionaries: ''

 

initialize

        "Private - Initialize the receiver"

 

        name := 'New account'.

        initialBalance := currentBalance := 0.0.

        transactions := ListModel with: (SortedCollection sortBlock: [:x :y | x date <= y date]).

        categories := ListModel with: SortedCollection new searchPolicy: SearchPolicy equality

 

categories

        "Answer the receiver's collection of transaction categories."

 

        ^categories

 

addCategory: aCategory

        "Add aCategory to the receiver's collection of transaction categories.

        Answer aCategory or nil (if the category is nil, is empty or is already there)."

 

        ((aCategory isNil or: [aCategory trimBlanks isEmpty]) or:

                 [self categories includes: aCategory]) ifTrue: [^nil].

        ^self categories add: aCategory

 

removeCategory: aCategory

        "Remove aCategory from the receiver's collection of transaction categories.

        Answer aCategory or nil (if aCategory can not be removed)"

 

        "Answer nil if there are existing transactions using aCategory."

        | col |

        col := self transactions select: [:trans | trans category = aCategory].

        col size > 0 ifTrue: [MessageBox notify: aCategory, ' not removed since ',

                         col size printString, ' transactions are using it.'.

                 ^nil].

 

        ^self categories remove: aCategory

 

These are patterned much like accountTypes in class PersonalMoney.  Note that we do not need to add a line to #stbSaveOn: to save categories as an OrderedCollection as we had to do before with transactions.  I suspect this is because categories is a list of strings whose sort block is the default sort block.  In any event, there is no problem in the binary filer when writing out and reading in categories.  Note too that in #initialize we could now change transactions' listmodel from a SortedCollection to an OrderedCollection since we know that it'll be displayed in a listview which has its own sorting criteria.  However, if it ain't broke, don't fix it -- and so I for one leave transactions just as it was.

 

 

Changing the views

 

Again, as we did with accountType in PersonalAccount, we move things around in PersonalAccountTransactionDialog's default view and add a ComboBox.  I'm not going to detail the way to make changes in View Composer --  I assume by now you're an expert (or close to one) on that.  After the changes, it looks something like this:

 

 

The combobox's name aspect is "category" and will be tied to its presenter (later on).  The date, a DateTimePicker, now shows a short date (above).  This is done by setting its hasLongDateFormat aspect to false.  You can, if you wish, override this by setting its displayFormat aspect.  For example, a displayFormat of 'dd-MMM-yyyy' will display a date like 15-Oct-2000.

 

The default view for PersonalAccountShell also needs to be changed.  A new column, "Category", is added to the listview.  Its getContentsBlock is set to [:account | account category] and its sortBlock is set to [:a :b | a isNil or: [b notNil and: [a <= b]]].  You may also want to change the hasFullRowSelect aspect of the listview to true so that you can double-click anywhere in a row to edit it, rather than having to double-click on column one.  Give the window some more height and width and it looks something like this:

 

 

 

Managing categories with the presenters

 

And still again, as we did with PersonalAccountShell when we added the combobox, we add a ChoicePresenter ('categoryPresenter') to PersonalAccountTransactionDialog, initialize it in #createComponents and #model:, and set its list of choices in #categoryChoices (though this time I chose not to assign a nilChoice):

 

In PersonalAccountTransactionDialog:

Dialog subclass: #PersonalAccountTransactionDialog

        instanceVariableNames: 'datePresenter amountPresenter descriptionPresenter isDebitPresenter categoryPresenter'

        classVariableNames: ''

        poolDictionaries: ''

 

createComponents

        "Create the presenters contained by the receiver"

 

        super createComponents.

        datePresenter := self add: DatePresenter new name: 'date'.

        amountPresenter := self add: NumberPresenter new name: 'amount'.

        descriptionPresenter := self add: TextPresenter new name: 'description'.

        isDebitPresenter := self add: BooleanPresenter new name: 'isDebit'.

        categoryPresenter := self add: ChoicePresenter new name: 'category'

 

model: aPersonalAccountTransaction

        "Set the model associated with the receiver."

 

        | aspectBuffer |

        super model: aPersonalAccountTransaction.

 

        aspectBuffer := self model.

        datePresenter model: (aspectBuffer aspectValue: #date).

        amountPresenter model: (aspectBuffer aspectValue: #amount).

        descriptionPresenter model: (aspectBuffer aspectValue: #description).

        isDebitPresenter model: (aspectBuffer aspectValue: #isDebit).

        categoryPresenter model: (aspectBuffer aspectValue: #category)

 

categoryChoices: aListOfChoices

        "Set the choices for the transaction categories to aListOfChoices."

 

        categoryPresenter choices: aListOfChoices

 

The category choices need to be sent to the combo box when the transactions dialog is created.  This is done in two places in PersonalAccountShell -- when an account is created and when an account is edited.  As we saw earlier in this tutorial series, instead of just showing the dialog, we need to create it, supply the categories list, and then show it.  One way to do this is to send #categoryChoices: from within #newTransaction and #editTransaction (in PersonalAccountShell).

 

In PersonalAccountShell:

newTransaction

        "Prompt for a new transaction and add it to the receiver's model"

 

        | newTransaction |

        newTransaction := PersonalAccountTransactionDialog create

                 categoryChoices: self model categories;

                 showModal.

        newTransaction notNil ifTrue: [

                 self model addTransaction: newTransaction.

                 self selectedTransactionOrNil: newTransaction]

 

editTransaction

        "Edit the selected transaction"

 

        | transaction |

        (transaction := self selectedTransactionOrNil) notNil ifTrue: [

                 self model removeTransaction: transaction.

                 (PersonalAccountTransactionDialog createOn: transaction)

                         categoryChoices: self model categories;

                         showModal.

                 self model addTransaction: transaction.

                 self selectedTransactionOrNil: transaction]

 

At this point, the parallel with account types differs a bit.  In the case of account types, we hard-coded the list of account types into the PersonalMoney class.  While it's true that we have add and remove methods, we didn't provide a way to add or remove account types through the user interface.  This is because the list of account types is really static -- it makes more sense when you think of account type as a subclass of PersonalAccount.  For instance, PersonalAccount as it stands now is really a cash account.  CheckingAccount is a type of PersonalAccount that has a check number.  CreditCardAccount is a type of PersonalAccount that doesn't have a check number but it does have a credit limit.  InvestmentAccount is another type of PersonalAccount which is different from both of the others.  However, all three do share some common characteristics, so they are all PersonalAccounts.  The list of account types then is static in that it reflects the list of subclasses of PersonalAccount (or should reflect, since we haven't actually gotten to subclassing PersonalAccount).

 

A transaction category, on the other hand, is just a string -- a name -- that's associated with a transaction, something that helps to group transactions.  Now having said that, I'll repeat something more or less from Part 1, and that's that it could all change.  If we find that category does indeed have characteristics and behavior that sets it as a class apart from a string, and we need that in our application model, then category becomes a class.  For now we implement category as a string.

 

 

Using an editable combo box to add categories

 

Since we expect users to add and remove categories, we need to provide them with the means to do so through the GUI.  We've already set up a combo box in the transaction view.  We expect that the user will either choose an existing item from the dropdown list or will type a new name in the combobox's edit control.  In the latter case, we need to make the combobox editable, i.e., watch for when a new category is typed in and then add it to the categories list.

 

Your first take on this might be like mine, that is, we need to monitor changes to the combo box's edit control, and so you start looking for a keyPressed or similar event to trap.  In fact, what we'd really like the combo box to do is when the user types in a character, have it move the selection to an item in its list that begins with that letter.  You've probably seen that behavior in other programs.  And your users have probably seen it and used it as well.  When the user finishes typing or is satisfied with the item showing in the combo box, then that is the category.

 

The first part, monitoring the combo box, seems easy.  Install an observer, through categoryPresenter (a ChoicePresenter), that tells the view or itself to inform us when a letter is entered.  The second part doesn't seem so easy, since there doesn't seem to be any made-to-fit method in ChoicePresenter or ComboBox to move the selection to what's in the combo box.  And the third part ("When the user finishes typing or is satisfied ...") is incredibly vague, but that's why we have users specifying things for us -- to keep us on our toes.

 

There are a few ways to look for what events a class triggers.  One way is to look at the class side method #publishedEventsOfInstances.  Try the following in a workspace, building up to a method:

 

"Inspect"

ComboBox publishedEventsOfInstances.

ComboBox publishedEventsOfInstances asSortedCollection.

"Evaluate"

ChoicePrompter choices:ComboBox publishedEventsOfInstances asSortedCollection.

 

The first line returns a Set of event names, the second line returns a sorted list.  The two inspectors also show the number of items as 28 (tally for a set, firstIndex throught lastIndex for a SortedCollection).  The third pops up a listbox with the sorted names -- this is what you see, along with a nicer caption, when you click on menu Class/Browse/Published Events in a class browser.  So that's one way to look for events, via the menu.

 

Version 4 update:  Dolphin version 4 shows 27 events names.  The lists for both versions are the same except for #viewActivated, which is in version 3 but not in version 4.

 

 

SmalltalkSystem:  Browsing method references in a class and its superclasses

 

Here we go on a little side trip, looking at ClassBrowserShell, the presenter for SmalltalkSystem, the model for the base development tools.  We'll be building a 'system method' -- and the building of it gives us reason to explore the many methods in SmalltalkSystem and ClassBrowserShell.  The new method uses some of the methods in the SmalltalkSystem model, and shows how easy it is to use them and extend the base system tools.

 

Although #publishedEventsOfInstances is an easy and almost surefire way of uncovering events, I say almost surefire because it is hand-coded, not system-generated, so it's not guaranteed to return all events for a class, though there seem to be very few that are missed.  Another way to find events is to look at all the methods in the system that reference #trigger:, #trigger:with:, #trigger:with:with: and #trigger:withArguments:.  This can be quite laborious, but it will show it all.  Go to a workspace and press Shift-F12 four times.  Each time it will pop up a prompter, asking for a method name: enter trigger:, then trigger:with:, then trigger:with:with:, and then trigger:withArguments.  You'll end up with 4 method browsers, showing all methods that generate events.

 

On my system, browsing all references to the four trigger variants returns well over a hundred methods.  Of course then you still need to look at each method to see what it is it's triggering.  In this case, it seems we really want to look at references to the trigger messages sent in ComboBox and its superclasses (BasicListAbstract, ControlView, View, and Object).  The event symbols in these messages are, by definition, the events generated by ComboBox.

 

Unfortunately, there's currently no simple way to do that, and by simple I mean a menu selection from the GUI or even a simple expression executable from a workspace.  It's possible to go the other way, i.e., down, but not up in the hierarchy.  For example, select View in the CHB and then select method #onActionPerformed, which happens to reference #trigger.  Then select menu item Method/Browse/Local References to/trigger:.  This will bring up a method browser with all references to #trigger: in View and all of its subclasses.  Some of these subclasses, e.g., ShellView, we're not interested in, but the fact that it can go down the hierarchy shows potential for its being able to go up the hierarchy as well.

 

Bring up View Composer on ClassBrowserShell's default view.  Select menu Modify/Set Menu Bar.  This will bring up the Menu Composer.  From there select the various menu items and doubleclick on them to see the commands they execute.  Exit VC and go back to ClassBrowserShell in the CHB.  In the middle pane (Categories) select 'commands' and you'll see most of the commands you saw in the view's menus.  (You might try clicking on the second column of the view in the right pane: this will order the public methods first, private methods last, which makes it easier to scan through them.)  The next thing to do is to scan through the commands -- you'll see most of them make reference to 'self model'.  The model for ClassBrowserShell (the CHB) is the singleton instance of SmalltalkSystem -- SmalltalkSystem current.

 

Go now and take a look at SmalltalkSystem.  For our purposes here, scan through categories 'browsing' and 'enquiries'.  In 'browsing', you'll find a method #browseReferencesTo: which brings up a method browser on a selector.  This is what's called when you select menu Methods/References to.  For example, in a workspace, evaluate the following:

 

SmalltalkSystem current browseReferencesTo: #trigger:.

SmalltalkSystem current browseReferencesTo: #trigger:with:.

SmalltalkSystem current browseReferencesTo: #trigger:with:with:.

SmalltalkSystem current browseReferencesTo: #trigger:withArguments:.

 

As you saw previously, four method browsers pop up with all the methods in the system that generate events.  There are two other methods of immediate interest: #browseMethods: (in category 'browsing'), which brings up a method browser on a collection of selectors, and #referencesTo: (in category 'enquiries'), which returns a set of methods referencing a specified selector.  We can combine these to form a single expression, rather than four, that returns all references to #trigger: et al.  Evaluate the following in a workspace:

 

SmalltalkSystem current browseMethods:

         ((SmalltalkSystem current referencesTo: #trigger:)

                 addAll: (SmalltalkSystem current referencesTo: #trigger:with:);

                 addAll: (SmalltalkSystem current referencesTo: #trigger:with:with:);

                 addAll: (SmalltalkSystem current referencesTo: #trigger:withArguments:);

                 yourself) asOrderedCollection

 

In my image, this brought up a method browser on 136 methods (164 in Dolphin version 4), which is still kind of overwhelming.  So we go back and look for ways to narrow down the selected methods.  In 'browsing', the method #browseReferencesTo:inAndBelow: opens a method browser on all methods "in the local hierarchy of aBehavior [the selected class]".  This is close to what we want, i.e., we'd like a #browseReferencesTo:inAndAbove: method.  (Whereas 'inAndBelow' means in a class and all of its subclasses, we want an 'inAndAbove' that means in a class and all of its superclasses.)  The first line of code in #browseReferencesTo:inAndBelow: is:

 

        refs := self referencesTo: selector inAndBelow: aBehavior.

 

The method first finds all references to the specified selector (in and below the specified class) and then, if there are any, it creates a method browser on the methods found.  This too seems promising, if we can come up with a #referencesTo:inAndAbove: method.  The next thing is to backtrack and find the definition of #referencesTo:inAndBelow:, which is in category 'enquiries', and which calls #selectMethods:inAndBelow:.  Keep going and find the definition for #selectMethods:inAndBelow:, which is also in category 'enquiries'.  The key line in this method starts with 'aBehavior withAllSubclassesDo:' -- at this point we've reached the end of the chain if Behavior has a method #withAllSuperClassesDo:, which it does have.


The next step is to go back to the three methods in SmalltalkSystem -- #browseReferencesTo:inAndBelow:, #referencesTo:inAndBelow:, and #selectMethods:inAndBelow: -- and change all occurances of 'inAndBelow' to 'inAndAbove' to create the following new methods:


In SmalltalkSystem:

browseReferencesTo: selector inAndAbove: aBehavior

        "Opens a MethodBrowser on all the methods which reference the specified

        selector in the local hierarchy of aBehavior."

 

        | browser refs suffix |

        refs := self referencesTo: selector inAndAbove: aBehavior.

        suffix := ' from the local hierarchy of ', aBehavior displayString.

        refs isEmpty

                 ifTrue: [MessageBox notify: selector printString, ' has no references', suffix]

                 ifFalse: [self

                         browseMethods: refs

                         caption: 'References to ', selector printString, suffix

                         findString: selector

                         filter: (self referenceFilterFor: selector)]

 

referencesTo: anObject inAndAbove: aBehavior

        "Answer a Set of all methods that reference anObject from their literal frame."

 

        ^self selectMethods: (self referenceFilterFor: anObject)

                 inAndAbove: aBehavior

 

selectMethods: discriminator inAndAbove: aBehavior

        "Private - Answer a Set of all methods for which the monadic value, discriminator, answers

        true in the local hierarchy of aBehavior.."

 

        | answer |

        answer := Set new.

        aBehavior withAllSuperclassesDo: [:behavior |

                 behavior methodDictionary do: [:m | (discriminator value: m) ifTrue: [answer add: m]]].

        ^answer

 

After creating these new methods, you can evaluate the following in a workspace:

 

SmalltalkSystem current browseReferencesTo: #trigger: inAndAbove: ComboBox.

SmalltalkSystem current browseReferencesTo: #trigger:with: inAndAbove: ComboBox.

SmalltalkSystem current browseReferencesTo: #trigger:with:with: inAndAbove: ComboBox.

SmalltalkSystem current browseReferencesTo: #trigger:withArguments: inAndAbove: ComboBox.

 

The first two lines bring up two method browsers on the methods in ComboBox's hierarchy that reference #trigger: and #trigger:with:.  The third and fourth lines reply that there are no methods referencing #trigger:with:with: and #trigger:withArguments:.  Better yet, you can evaluate the following expression, which is the analog to the one we saw earlier:

 

SmalltalkSystem current browseMethods:

         ((SmalltalkSystem current referencesTo: #trigger: inAndAbove: ComboBox)

                 addAll: (SmalltalkSystem current referencesTo: #trigger:with: inAndAbove: ComboBox);

                 addAll: (SmalltalkSystem current referencesTo: #trigger:with:with: inAndAbove: ComboBox);

                 addAll: (SmalltalkSystem current referencesTo: #trigger:withArguments: inAndAbove: ComboBox);

                 yourself) asOrderedCollection

 

This brings up a single method browser on all the methods that ComboBox has (or inherits) that trigger events.  The caption shows that there are 29 methods.  We saw earlier that #publishedEvents of ComboBox shows 28 events -- apparently, #positionChanging: is an event that is generated by ComboBox (in #onPositionChanging:) but that is not published.  Though finding #onPositionChanging isn't particularly earth-shattering, the new methods might prove to be useful in other cases.  With that in mind, we add the following method to SmalltalkSystem:

 

Version 4 update:  Dolphin version 4 shows 28 methods.  As was noted above, the lists for both versions are the same except for #onViewActivated, which is in version 3 but not in version 4.

 

In SmalltalkSystem:

browseTriggerReferencesIn: aBehavior

        "Open a method browser on methods in aBehavior that reference the

        various #trigger: methods in the local class hierarchy."

 

        self browseMethods:

                 ((self referencesTo: #trigger: inAndAbove: aBehavior)

                         addAll: (self referencesTo: #trigger:with: inAndAbove: aBehavior);

                         addAll: (self referencesTo: #trigger:with:with: inAndAbove: aBehavior);

                         addAll: (self referencesTo: #trigger:withArguments: inAndAbove: aBehavior);

                         yourself) asOrderedCollection

 

For the sake of completeness I brought up the View Composer for ClassBrowserShell's default view and added two menu items using the Menu Composer.

 

 

Menu item 'Class/Browse/Method Hierarchy...' calls #browseHierarchyReferences and 'Class/Browse/Trigger et al References' calls #browseTriggerReferences.  This way they can be easily called from the CHB GUI.  The two new commands (methods) in ClassBrowserShell are:

 

In ClassBrowserShell:

browseHierarchyReferences

        "Open a method browser displaying the references to the

        prompted-for selector in the local class hierarchy."

 

        | method class |

        (class := self selectedClass) notNil ifTrue: [

                 (method := Prompter prompt: 'Enter a method name:') notNil ifTrue: [

                         self model browseReferencesTo: method asSymbol inAndAbove: class]]

 

browseTriggerReferences

        "Open a method browser on methods that reference the

        various #trigger: methods in the local class hierarchy."

 

        self selectedClass notNil ifTrue: [

                 self model browseTriggerReferencesIn: self selectedClass]

 

 

Back to editing the combo box

 

If you browse through all 29 methods, you'll see they all send #trigger: or #trigger:with: to the view's presenter.  While this is not always the case for generated events, it does mean that for these, at least, we can install an observer on either the view or the presenter.  As for trapping the events when a character is entered in the combo box, it seems that #keyPressed:, #keyReleased:, or #keyTyped: will do the job.  To test this, install an observer on these events and an event handler, like so:

 

In PersonalAccountTransactionDialog:

createSchematicWiring

        "Create the trigger wiring for the receiver"

       

        super createSchematicWiring.

        categoryPresenter when: #keyPressed: send: #onKeyEvent: to: self.

        categoryPresenter when: #keyReleased: send: #onKeyEvent: to: self.

        categoryPresenter when: #keyTyped: send: #onKeyEvent: to: self

 

onKeyEvent: anEventSymbol

 

        Transcript nextPut: 'onKeyEvent'; cr

 

Bring up a transcript and run the dialog (PersonalAccountTransactionDialog show).  Type some characters into the combo box and see what's displayed in the transcript.  Nothing is displayed.  Huh?  For some reason, none of the three events are generated.

 

Let's take a different approach to this -- we'll return to character-gobbling another time.  The minimum requirement for the editable combo box is that the user can type a new entry and that entry is added to the categories list.  For this we don't really need to track each character entered.  We just need to track when the combo box loses focus or the dialog box is closed -- if the user had typed a new entry in the combo box, then at that point add the entry to the categories list and assign the new category to the transaction.

 

The first thing is to ensure the transaction category is whatever shows in the edit portion of the combo box.  Install an observer on #focusLost and a corresponding event handler:

 

In PersonalAccountTransactionDialog:

createSchematicWiring

        "Create the trigger wiring for the receiver"

       

        super createSchematicWiring.

        categoryPresenter when: #focusLost send: #onCategoryFocusLost to: self

 

onCategoryFocusLost

        "Category combo box has lost focus.  If user typed in a new category, assign it to the transaction."

 

        self model category: categoryPresenter view text

 

When focus changes, the transaction category is set to the contents of the combo box.  If the user had selected an item from the list, transaction would already be set to the selected category, but no harm is done by this duplicate code.  The next thing to do is to add the new category to the model list (categories).  This is already done in the model (PersonalAccount) when adding a new transaction.  Both #newTransaction and #editTransaction, in class PersonalAccountShell, send #addTransaction: to the account -- the account is responsible for maintaining a consistent state, i.e., that the category is in its categories list.

 

Now test it -- evaluate 'PersonalAccountShell show' and then create new transactions, typing new categories in the transaction dialog box's combo box and then see if they show up in PersonalAccountShell's listview category column.  They should show up there -- for the most part.  If you type in a new category, then tab to a new field and close the dialog box, the new category is added.  If you type in a new category and then click on the 'Ok' button, the new category is added.  But if you type in a new category and then press Alt-O (which should be the same as clicking on the 'Ok' button), the new category is not added.  This does not happen, by the way, with the other controls.  For example, type a description in the textedit control and press Alt-O -- the description appears in the parent listview.  This is one more example of ComboBox being such a pain in the ass -- no wonder many people give up and use a textedit control coupled with a listbox, instead of a ComboBox.

 

Bitching and moaning won't solve this problem.  My first thoughts on this were that when you tab from the combobox, focusLost is triggered, as expected.  Likewise, if you click on 'Ok', focusLost is also triggered.  But if you just press Alt-O, focusLost is not triggered.  So the solution should be to force a focusLost in the method that's called on the 'OK' button being invoked.  In class Dialog you'll find a method, #ok, that's inherited by PersonalAccountTransactionDialog.  It does a 'self apply' (which copies the cached changes from the dialog box to the model) and then closes the view.  We override this as follows:

 

In PersonalAccountTransactionDialog:

ok

        "Close the receiver and apply the changes cached in the receiver back to the model"

 

        self onCategoryFocusLost.

        super ok

 

Try it again now.  This time it works.

 

 

Using ChoicePrompter to remove categories

 

Removing a category should also be done at the account level, i.e., to PersonalAccount, through PersonalAccountShell.  A quick (and not all that dirty) way to remove categories is to use a multi-selection ChoicePrompter.  You can write a single short method, called by a menu selection, that prompts the user with a list of current categories, lets the user choose one or more categories to delete, then removes the selected categories from the categories list.  You've already seen ChoicePrompter used -- in Part 3, there was the line for that helper method:

 

        aspects := ChoicePrompter on: aspects multipleChoices: aspects caption: 'Choose model aspects'.

 

For this, we'll use a different class side method (ChoicePrompter>>#multipleChoices:caption:) since we won't be pre-selecting any of the categories when the prompter pops up.  This then will bring up a prompter on the categories list and return the items the user chooses:

 

        removedItems := ChoicePrompter multipleChoices: self model categories caption: 'Choose categories to remove'.

 

We then actually remove the selected items from the categories list by sending the model #removeCategory: with the category name:

 

        removedItems do: [:item | self model removeCategory: item].

 

Actually, we try to remove the selected items from the categories list.  If the category is being used, then #removeCategory will not remove it.  Our method then is this:

 

In PersonalAccountShell:

removeSelectedCategories

        "Prompts the user with the model's categories list, then tries to remove each item from the list."

 

        | choices |

        choices := ChoicePrompter multipleChoices: self model categories

                 caption: 'Choose categories to remove'.

        choices notNil ifTrue: [choices do: [:item | self model removeCategory: item]]

 

The final change is to add a menu item to PersonalAccountShell's default view that invokes the command #removeSelectedCategories.  As you can see below, I added a menu Categories.  This contains one menu item 'Remove..." which calls #removeSelectedCategories.

 

 

 

Next Steps

 

We didn't get anywhere close to covering all the things I'd hoped for, but I expect to get to these in the next chapter or two.  I try to keep each 'chapter' to a reasonable length, so it can be downloaded fairly quickly and read and reviewed in a reasonable amount of time.

 

In the meantime, if you want to read all about Windows Combo Boxes, you can read the API and descriptions in MSDN.  This can be found online at http://msdn.microsoft.com/library/default.asp.  You can drill down on the left to Platform SDK / User Interface Services / Windows User Interface / Controls / Combo Boxes.  Go even further down, to About Combo Boxes, and take a look at two of the pages beneath that: Combo Box Notifications and Default Combo Box Behavior.  These show the events that Windows combo boxes generate and the messages that they respond to.

 

 

If you've found this exercise helpful, let me know -- I welcome comments, criticism and feedback.

 

Written by Louis Sumberg

Last updated December 2000

 

 

Back to Part 4              Onwards to Part 6