Up  Next

Extending the PersonalMoney Application

 

Part 1

 

 

Objectives and Overview

 

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.

 

 

Testing the models

 

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.

 

 

Extending the presenters

 

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.

 

 

Extending the view for PersonalAccountShell

 

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:

 

  1. Open View Composer (VC) on the existing view for PersonalAccount -- right-click on PersonalAccountShell, the presenter class for PersonalAccount, and choose "edit view".
  2. Make room for the new fields -- shorten the width of the initial balance field and move it, along with its (static text) label to the right.  You might want to right-align and shorten the label.
  3. Add the new fields -- bring up the Resource Toolbox if it's not already there (Ctl-T) and drag over a label (TextPresenter 'Static Text') and a combo box (ListPresenter 'Combo box').
  4. Set the label text -- select the label and in the Published Aspect Inspector (PAI), select the text aspect and give it a value of 'Account Type:'.
  5. Set the combo box name aspect - select the combo box and in the PAI, select the name aspect and give it a value of 'accountType'.  This will tie the combox box to the subpresenter named "accountType" in PersonalAccountShell.
  6. Fix the tab order - with the combo box still selected, click on the  button in the toolbar.  You will see the combo box move up in the View Hierarchy.  Move it so that it is just above initialBalance (the editbox, not "Initial Balance:", the label).
  7. Save the view -- File/Save.  Note:  In this exercise, we save over the existing PersonalAccountShell 'Default view' since the old view will be obsolete with the addition of the accountType field.

 

 

Testing the presenter and view

 

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.

 

 

Extending the view for PersonalMoneyShell

 

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.

 

  1. Open View Composer (VC) on the existing view for PersonalMoney -- right-click on PersonalMoneyShell, the presenter class for PersonalMoney, and choose "edit view".
  2. Change the listbox to a listview -- select the listbox and then choose "Mutate View" from the context menu or the VC's Modify menu.  A ChoicePrompter pops up from which you should choose ListView.  Where the listbox was, you'll now see a listview with one column.  Notice too the following in the bottom three columns of the VC, starting from right to left:  The Workspace shows that this is, indeed, now a ListView.  The middle column shows the name is still 'accounts', and the View Hierarchy shows that the tab order has been retained.  Tres kewl!
  3. Prevent icons from showing in column 1 (optional) - with the listview still selected and "a ListView" showing in the workspace (the third column), type and evaluate the following in the workspace: self imageManager: nil.  (A ListView, by virtue of being an IconicListAbstract, has an ImageManager).
  4. Add gridlines to the listview -- select the "hasGridlines" aspect in the PAI and click the checkbox so that its value is true.
  5. Add columns to the listview, for a total of four, corresponding to account name, account number, account type, and current balance (we don't really need to show the initial balance in the listview).  Select the listview's "columnsList" aspect and then click on the asterisk  three times in the PAI (right column).  Each time you click, a prompter will appear with a single choice - ListViewColumn new - click OK each time.  (In version 4 there's no prompt, it just adds the column.)  Each time you'll see the new column added in the view.  Click on the plus sign to the left of the "columnsList" aspect, to expand it, and you'll now see the four columns listed in the PAI.
  6. Link the columns to the model's aspects and set other properties.  For each ListViewColumn beneath columnsList (in the PAI), click on the plus sign to the left to expand it and see its aspects, then change the values as follows:

                                                               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

  1. Version 4 update -- before you save the view (step 8 below), ensure that the view will work, by pressing F5 in the VC (or menuitem File/Test).  If you get a walkback with the caption "Invalid arg 9: Cannot coerce a DeafObject to handle", this will require a code change before you save the view.  If there's no walkback, then just proceed with step 8.  The short explanation for this walkback is that in version 2.1 (where PersonalMoney was first created), the listbox aspect for getTextBlock contains a block and this block is carried over to the listview after the mutate.  Unfortunately (as we briefly see in Part 2) blocks retain a reference to the context they were created in and in this case it's trying to recreate the listbox which is quite dead.  To fix this problem you can do either of the following things listed just below.  Make sure you test the view (F5) after you make the change, otherwise you'll have saved a corrupted version of the view and you won't be able to edit it again:
    1. Select the listview in the VC and change the value for aspect getTextBlock from "[] in BasicListAbstract>>defaultGetTextBlock" to "BasicListAbstract"
                  OR
    2. Find method View>>state (in a browser) and change the phrase "self handle notNil ..." to "handle notNil ..."
  2. Save the view -- File/Save.  Note:  As we did with the view for PersonalAccountShell, we save over the existing PersonalMoneyShell 'Default view' since the old view will be obsolete with the addition of the listview control.

 

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.

 

 

Testing the presenter and view

 

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., "<=".

 

 

Save your work

 

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).

 

 

Next steps

 

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.

 

 

Written by Louis Sumberg

Last updated November 2000

 

To download the PersonalMoney package, revised to include this exercise, click here for version 3 or here for version 4.

 

 

Onwards to Part 2