Extending the PersonalMoney Application
Part 5
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.
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.
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:

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.
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.
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]
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"