Extending
the PersonalMoney Application
Part 1
The intent of this exercise is to learn a little about implementing combo boxes and list views. Starting where Object Arts' PersonalMoney tutorial application ends, we're going to extend the models by adding an "account type" field and change the views by adding a combox box and list views. In Part 2 we'll look at making these extensions a bit more robust (i.e., "bullet-proofing them") and will touch on some areas such as ListModels and SearchPolicy.
I'm using Dolphin Smalltalk version 3, which I highly recommend to those using version 2.1 -- it's got a lot of nice features that make life more pleasant. I tried to duplicate this exercise in version 2.1 but ran into problems with combo boxes working properly, not to mention the problems with maintaining two parallel exercises, so sorry, this is a version 3 targeted exercise.
To recap Personal Money, I thought I'd start by drawing a diagram of the existing application. Below is a quick sketch - and I do mean quick. It actually shows quite a bit, though by no means all of it.

In the left column are the models. I'm using my own notation and I'm only showing the data model here (not the behavior), but going from top to bottom this is what it shows:
In the middle column are the names of the presenters:
In the right column are the views, showing the caption for each view and showing they contain various textboxes and listboxes.
Extending
the models
In this exercise, I'd like to extend the application model by saying that each account has to be a certain type, for example, a checking account or credit card account. Furthermore, the types of accounts are predefined, i.e., there is a list of known account types. To implement this, we'll add an "accountType" aspect to PersonalAccount and a lookup table (a Collection) of account types to PersonalMoney. The two revised submodels now look like this:

You can see that PersonalMoney now has an accountTypes field, PersonalAccount has an accountType field, and there's a relation between the two.
Adding these to the models is pretty straightforward. We add an "accountTypes" instance variable to PersonalMoney, initialize it to an empty ListModel, and add collection-type accessors. For convenience, we also add an instance creation method to the class side of PersonalMoney that creates an instance with some account types already set up.
In PersonalMoney:
Model
subclass: #PersonalMoney
instanceVariableNames:
'owner accounts accountTypes'
classVariableNames: ''
poolDictionaries: ''
initialize
"Private - Initialize the
receiver"
accounts := ListModel with: OrderedCollection new.
accountTypes := ListModel with:
OrderedCollection new
accountTypes
"Answer the receiver's
collection of account types."
^accountTypes
addAccountType: aType
"Add aType to the receiver's
collection of account types and answer aType."
^self accountTypes add: aType
removeAccountType: aType
"Remove aType from the
receiver's collection of account types and answer aType"
^self accountTypes remove:
aType
In PersonalMoney, class side:
withDefaultTypes
"Answer
an instance of the receiver with some values added in to accountTypes."
^PersonalMoney new
addAccountType:
'Checking';
addAccountType:
'Credit Card';
addAccountType:
'Investment';
yourself
It's a good idea to change the class comment at this time. Something simple is better than nothing.
PersonalMoney
represents the entirety of the Personal Money system for a single user. It
holds owner details and a collection of accounts for this user.
Instance
Variables
owner <String> containing the owner's name
accounts <OrderedCollection> of PersonalAccounts.
accountTypes <OrderedCollection> of Strings, contains valid PersonalAccount types.
For PersonalAccount we add an "accountType" instance variable, add accessors and modify #displayOn: to show the account type:
In PersonalAccount:
Model
subclass: #PersonalAccount
instanceVariableNames: 'name
accountNumber accountType initialBalance transactions currentBalance'
classVariableNames: ''
poolDictionaries: ''
accountType
"Answer
the account type of the receiver."
^accountType
accountType: aType
"Set
the account type of the receiver to aType."
accountType := aType
displayOn: aStream
"Append
to aStream a description of the receiver as a user would want to see it."
self name
displayOn: aStream.
aStream nextPut:
$-.
self accountType
displayOn: aStream.
aStream nextPut:
$-.
self accountNumber
displayOn: aStream.
aStream nextPut:
$-.
self currentBalance
displayOn: aStream
You might also want to modify #printOn: in a similar manner, having it just wrap displayOn: in parentheses.
printOn: aStream
"Append
to aStream a description of the receiver as a developer would want to see
it."
self basicPrintOn:
aStream.
aStream nextPut:
$(.
self displayOn:
aStream.
aStream nextPut:
$)
As the comments indicate, one is for the user, the other for the developer -- you'll see the print string in the inspector and debugger and the display string in the view (e.g., PersonalAccountShell listbox).
Note on the model design: Andy Bower made a great point (Newsgroup, 4 Oct 2000) that account type should probably be implemented as a model subclass. That is, CheckingAccount, CreditCardAccount, and InvestmentAccount should be subclassed from PersonalAccount. A good rule of thumb is that when you see a "type", this should raise a red flag -- after all, when you say that something is a type of something-else, that's like saying something IS A something-else and that, of course, is what inheritence is all about. The assumption though, is that the subclass exhibits additional and/or different behavior from its parent. While that is a reasonable assumption in this application, at this point we are not interested in those differences and are not modeling them.
You can test these changes in the workspace by inspecting (Ctrl-I) each of the following:
"Inspect"
pm1 := PersonalMoney withDefaultTypes owner:
'Me'.
pa1 := PersonalAccount new accountType: 'Checking'.
pm1 addAccount:
pa1; yourself.
We've added an accountType aspect to the PersonalAccount model. Since PersonalAccountShell is the presenter for PersonalAccount, this is the logical place to add a subpresenter for accountType. The first thing we do is to modify the class definition to add an instance variable (accountTypePresenter) to PersonalAccountShell.
Shell
subclass: #PersonalAccountShell
instanceVariableNames:
'namePresenter accountNumberPresenter accountTypePresenter
initialBalancePresenter transactionsPresenter currentBalancePresenter'
classVariableNames: ''
poolDictionaries: ''
We've already decided that we want to use a combo box in the application, in which case accountTypePresenter will be a ChoicePresenter, a presenter that's been designed to work with a ComboBox. A ChoicePresenter essentially manages a list of items (the "choices" that the user can choose from in the combo box) and a single item (the "text", or selected item, of the combo box). Accordingly, we instantiate accountTypePresenter in #createComponents (see the last line below). Note that we give it the name 'accountType' -- this will tie it to the subview when we get to the View Composer.
In PersonalAccountShell:
createComponents
"Create the presenters
contained by the receiver"
super createComponents.
namePresenter := self add:
TextPresenter new name:
'name'.
accountNumberPresenter := self add:
TextPresenter new name:
'accountNumber'.
initialBalancePresenter := self add:
NumberPresenter new name: 'initialBalance'.
transactionsPresenter := self add:
ListPresenter new name:
'transactions'.
currentBalancePresenter := self add:
NumberPresenter new name: 'currentBalance'.
accountTypePresenter := self add:
ChoicePresenter new name: 'accountType'
The next step is to assign a model to accountTypePresenter. In this case the model is the accountType aspect of our PersonalAccount model. The revised #model looks like this (see the last line):
model: aPersonalAccount
"Set
the model associated with the receiver."
super model:
aPersonalAccount.
namePresenter model:
(aPersonalAccount aspectValue: #name).
accountNumberPresenter model: (aPersonalAccount aspectValue:
#accountNumber).
initialBalancePresenter model: (aPersonalAccount aspectValue:
#initialBalance).
currentBalancePresenter model: (aPersonalAccount aspectValue:
#currentBalance).
transactionsPresenter model: (aPersonalAccount transactions).
accountTypePresenter model: (aPersonalAccount aspectValue:
#accountType)
Note that #aspectTriggersUpdates, which used to be sent to currentBalancePresenter, is not needed in version 3 (it's been deprecated).
The final change for PersonalAccountShell is to specify the choices for the combo box. A ChoicePresenter does this through its #choices: method, so we add an #accountTypeChoices method to our presenter which delegates (routes) the choices to accountTypePresenter (a ChoicePresenter).
accountTypeChoices: aListOfChoices
"Set the choices for the
account type to aListOfChoices."
accountTypePresenter choices:
aListOfChoices
To summarize the changes for PersonalAccountShell, we've added a subpresenter that will mediate between the accountType aspect of our PersonalAccount model and the text value in the combo box view. It will also supply the choices for the combo box. But where does it get the choices from and how do we link them in? Looking at our data model, we see that the choices are in the PersonalMoney model, so somehow we've got to get them from there to PersonalAccountShell's #accountTypeChoices method (which feeds it to the combo box). Since we create and edit PersonalAccounts via PersonalMoneyShell's #editItem method, we look at that and send the choices list from there.
In PersonalMoneyShell:
editAccount
"Edit the selected
account"
| account |
self hasSelectedAccount ifTrue:
[
account := self
selectedAccountOrNil.
(PersonalAccountShell
createOn: account)
accountTypeChoices: self model accountTypes;
when:
#viewClosed send: #updateAccount: to: self with: account;
show]
We also modify the
class side #defaultModel in PersonalMoneyShell to return an instance of
PersonalMoney with some default types already set up.
In
PersonalMoneyShell, class side:
defaultModel
"Answer
a default model to be assigned to the receiver when it's initialized."
^PersonalMoney withDefaultTypes
That's all we need for now in the presenters. If you're wondering what that #createOn: method is doing in #editAccount (where it used to be #showOn:), then take a few minutes to look at the class side methods of Presenter (the great-great-granddaddy of PersonalAccountShell). You'll see a bunch of "show" methods (#show, #show:, #showOn: and #show:on), all of which essentially call some type of "create" method followed by #showShell. Now look at the "create" methods (#create, #create:, #createOn:, etc.) -- and here you can see they all create a presenter connected to some model with some kind of view. One of the reasons for using a create method rather than show is, as we find here, when we want to do some initialization or processing with the view, before it's displayed. In this case, we want to supply the account types, as part of the view initialization, and then send #show to the created instance. Having said that, I'll add that in this case, it doesn't make a difference -- we still could have used #showOn: and then sent #accountTypeChoices:, but that's because this thing that looks and walks like a dialog box is not a Dialog but is a modeless Shell. If we had used a modal dialog box, then #showOn: would create the presenter/view, display it with no choices in the combo box, and wait for the user to press OK or Cancel. Then #accountTypeChoices: would be sent to the dialog box which was no longer there, or at least not visible.
What we'd like to do is allow the user to set the account type in the view for PersonalAccount. Since the user will be choosing from a list of types, a combo box will work fine. If you look at the existing view you'll see that the initial balance field is pretty wide. We can narrow it a bit and add an account type combo box next to it, so it'll look something like this:

To add the combo box to the view, do the following:
Evaluate the following line in a workspace:
"Evaluate"
PersonalMoneyShell show.
You'll see the familiar PersonalMoneyShell 'Default view' show on the screen, with all values blank. Click on "New" to add a new account and our new PersonalAccountShell 'Default view' pops up. Fill in the values. Click on the combo box's down-arrow and choose from an account type in the list.

Now click on "Exit". You'll return to the PersonalMoney window and you'll see the new account, as well as the effects of the revised #displayOn: method. With another couple of accounts added it looks like this:

One more thing before we move on - notice the title bar (caption) in the above window. It says 'Untitled' even though in View Composer the caption aspect is 'Personal Money Application'. This doesn't happen for the other two views in PersonalMoney. The reason is that DocumentShell, which adds to Shell the ability to read from and write application data to a file, uses the filename to set the caption. If no file has been read from or written to, it sets the caption to 'Untitled'. Well, we'd like the default to be 'Personal Money Application' so we override #onViewOpened in PersonalMoneyShell to do just that.
In PersonalMoneyShell:
onViewOpened
"Set the default caption
for the view."
super onViewOpened.
self view caption: 'Personal Money Application'
Try evaluating "PersonalMoney show" again and now you'll see the application title is there.
Look at the view for PersonalMoneyShell and you'll see that the listbox showing accounts does a pretty good job of displaying the information for each account. Now a listbox is really suited for showing one item per line, but we've managed to squeeze account name, account number, and account type into each line of the listbox by overriding #displayOn: in PersonalAccount. However, a listview control is more suited for displaying tabular data such as this. It lines the data up nicely in columns, allows the user to resize each column, move columns around, and sort the display by column. The final step then, in this part of the exercise, is to swap the listbox in PersonalMoneyShell's view for a listview.
We take a look at the presenter first to see what changes, if any, may be needed. Then we breathe a sigh of relief, perhaps even letting a little jubilation show, when we realize that our existing subpresenter (accountTypesPresenter), a ListPresenter, is designed to manage the contents of a ListModel within a ListView, which is exactly what we want to do. Great! We now move on to the view.
We want to swap the listbox for a listview in the view for PersonalMoneyShell, so that it will look something like this:

One way to do this is to delete the existing listbox, add a listview, and then link the listview columns to the various model aspects. Instead of deleting the listbox and adding a listview, we can take a shortcut by "mutating" the listbox to a listview. (See #mutate and #mutateTo: in class ViewComposer.) Let's do it.
i. column 1:
1. text = 'Name'
2. getContentsBlock = [:acct | acct name]
ii. column 2:
1. text = 'Account #'
2. getContentsBlock = [:acct | acct accountNumber]
iii. column 3:
1. text = 'Type'
2. getContentsBlock = [:acct | acct accountType]
iv. column 4:
1. text = 'Balance'
2. getContentsBlock = [:acct | acct currentBalance]
3. alignment = #right
4. width = 70
The getContentsBlocks above all do roughly the same thing. Each assumes a PersonalAccount instance will be passed in as an argument and each returns the appropriate aspect (name, accountNumber, accountType, or currentBalance) of that instance. For this exercise, getContentsBlock works fine, but for more complex objects or more control over the display format of the returned object, getTextBlock should be used.
In the workspace, again evaluate "PersonalMoney show". You'll see the new listview. Create a few accounts and enter values for all fields in each account. Then in the PersonalMoney view, try sorting on each column. The default sort for each column is ascending order and since these are all simple objects (strings and numbers), they recognize comparison operators, e.g., "<=".
Assuming nasty things haven't occurred, it's a good time, if you haven't already just done so, to save the image. You might also want to save the revised PersonalMoney package, using the Package Manager, and you might want to save your workspace contents into a file (using File/Save in the workspace).
There are some enhancements that can be made, as well as some deficiencies and problems in the application, though you might not have noticed the latter if you followed the steps in this exercise and didn't wander off and experiment on your own *s*. The next steps then are to identify these issues and resolve them. In brief, they include:
These will be addressed in Part 2. In the meantime, I certainly welcome comments, criticism and feedback.
To download the PersonalMoney package, revised to include this exercise, click here for version 3 or here for version 4.