Up  Next

Extending the PersonalMoney Application

 

Part 2

 

 

Objectives and Overview

 

In Part 1 we added an account type to the PersonalMoney model and modified the display to include a combox box and a listview.  In this, Part 2, we start by addressing some of the weaknesses in the implementation with the aim towards making the application more robust as well as learning a little more about Object Arts' implementation of Dolphin Smalltalk.

 

Some areas where the application can use bolstering include:

 

Classes and other areas to look at include:

 

 

Dealing with existing instances

 

In Part 1, we opened the PersonalMoney application window ("PersonalMoneyShell show"), then created PersonalAccounts from within that.  Once we exited the main window, the accounts were gone.  In a full application you'd want to read and write application data from and to some external medium, perhaps a database or a file.  So now we look, just a bit, at handling persistent data.

 

There are two ways we can quickly test bringing in instances from outside the application.  The first is from the image itself.  We can create instances of PersonalAccounts in the workspace and then open a view on them.  The other way is to use the builtin read/write capability of PersonalMoneyShell -- remember that PersonalMoneyShell inherits this capability from DocumentShell.  We'll try both.

 

The first thing is to create some instances in the workspace.  We create an instance of PersonalMoney, a few instances of PersonalAccount, add the accounts to the PersonalMoney instance (our model), and then display the model.  You can do this by evaluating the following lines:

 

pm1 := PersonalMoney withDefaultTypes owner: 'Louis'.

pa1 := (PersonalAccount new name: 'Wells Fargo') accountType: 'Checking'; accountNumber: '1234567890'; initialBalance: 100.

pa2 := (PersonalAccount new name: 'BofA Visa') accountType: 'Credit Card'; accountNumber: '987-65-4321'; initialBalance: -472.

pm1 addAccount: pa1; addAccount: pa2.

PersonalMoneyShell showOn: pm1.

 

So far, so good -- the application window appears and the accounts from the image are there.  The next step, as long as we have this view up, is to save the model to a file.  Click on menu File/Save and you'll be prompted for a filename.  I entered "PersonalMoney1" and the data was saved to file "PersonalMoney1.pm".

 

Now let's see if you can read the data back in.  Exit the window and this time evaluate "PersonalMoneyShell show".  A "Personal Money Application" window will pop up with no accounts in it.  Select menu File/Open and choose the file you just saved the accounts to.  Voila!  The accounts show up -- note too the caption shows the filename.

 

 

Technically, we didn't do what we said we were doing, but we were close.  That is, we didn't read in just the two accounts to our application but rather we read in the entire application model -- a PersonalMoney instance and two PersonalAccount instances.  Keep that distinction in mind and you'll understand later why sometimes you can't read in old files after making changes to the application.  In any event, the next step gets a little more interesting.

 

Try editing one of the accounts -- uh oh, a walkback pops up.

 

 

You might look at this and think "Waddya mean, 'Checking' not found?  It's gotta be there".

 

Bring up the Debugger and take a closer look.

 

 

Here the second line of the debugger is highlighted.  In the upper right pane it shows that ComboBox's model is a ListModel that contains 'Checking', yet it insists that 'Checking' was not found.  Perhaps you've already identified what the problem is, but if not, let's take a little detour down to the bottom of the list (debugger left panel) to see where this is coming from.

 

At the bottom, you can see PersonalMoneyShell>>editAccount.  When we start to edit the account, we are sending #editAccount to our application presenter, an instance of PersonalMoneyShell.  It then creates a PersonalAccountShell and sends #accountTypeChoices: to initialize the choices list.  Then PersonalAccountShell routes the choices list to its accountTypePresenter (a ChoicePresenter) via #choices:.  This should be familiar territory so far -- it's what we wrote in Part 1 to initialize the combo box choices list.  Then accountTypePresenter connects its choicesModel to the new choices list (within #choices:) and sends #updateChoice to itself, intending to select the current choice given the new choices list.  Things now move over to the view (a ComboBox), where the view tries to select the current choice based on the new choices list, and we see a bunch of "selection" type messages and key/value lookups with a couple of stopovers at an IdentitySearchPolicy before it finally coughs and sputters.

 

The big clue is IdentitySearchPolicy.  Class SearchPolicy and its two subclasses (IdentitySearchPolicy and CaseInsensitiveSearchPolicy) specify the way a collection's elements should be compared when the collection is searched.  IdentitySearchPolicy uses identity (a == b) to compare, whereas SearchPolicy uses equality (a = b), and CaseInsensitiveSearchPolicy uses a case-insensitive string comparison (a asString sameAs: b asString)  It turns out that ListModel has a searchPolicy instance variable whose default is an identity comparison.  So even though the accountType is 'Checking' and there's a 'Checking' in accountTypes, these are not the same instance of String.  They are two different instances with the same value.  Clearly, equality, not identity, is needed here.

 

Why then did it ever work?  For example, go back to the PersonalMoney Application window and create a new account.  In the PersonalAccount view, assign it an account type.  Then return to the PersonalMoney Application window.  Edit the newly created account – it should bring you back to the PersonalAccount view.  But isn't this where it spit up and rolled over before?  It works this time because when you assign a new account type in the view, accountType links (points) directly to one of the strings in the choices list.  This means they are identical.  It's different when we load an account from a file or database -- in that case we are creating a string for the account type which is equal but not identical to the account type in the choices list.

 

So now we know we're using an identity search policy and we know we want to use an equality search policy.  The way to fix this problem is to change the search policy for accountTypes (a ListModel).  You can do this when the instance is created in PersonalMoney, using an instance creation method, #with:searchPolicy:, from class ListModel.

 

In PersonalMoney:

initialize

        "Private - Initialize the receiver"

 

        accounts := ListModel with: OrderedCollection new.

        accountTypes := ListModel with: OrderedCollection new searchPolicy: SearchPolicy equality

 

Having closed the debugger and the PersonalMoney Application window, go back and re-evaluate the last set of lines in the workspace:

 

pm1 := PersonalMoney withDefaultTypes owner: 'Louis'.

pa1 := (PersonalAccount new name: 'Wells Fargo') accountType: 'Checking'; accountNumber: '1234567890'; initialBalance: 100.

pa2 := (PersonalAccount new name: 'BofA Visa') accountType: 'Credit Card'; accountNumber: '987-65-4321'; initialBalance: -472.

pm1 addAccount: pa1; addAccount: pa2.

PersonalMoneyShell showOn: pm1.

 

This time you'll find you can edit the accounts.  When done, save to an external file again, under another name (e.g., PersonalMoney2.pm) and exit the application window.

 

One last check:  Open an empty application window again (evaluate "PersonalMoneyShell show"), then open up the file you just wrote to (PersonalMoney2.pm).  The accounts should show in the view.  Edit one of them.  This too should work.  Great!  Now just out of curiosity, open up the first file you wrote to (PersonalMoney1.pm) and try to edit an account.  The old walkback appears.  This is because the builtin serializing capability of PersonalMoneyShell is now reading the full state of the whole model at the time the file was written -- including accountTypes, with its old identity search policy.  Close the walkback or debugger and the application window.

 

Additional notes on searchPolicy:  In this example, we have access to the choices model, a ListModel, when it is created, so we can create it with an equality search policy.  If the listmodel already exists and you need to change the search policy, you can  use the #searchPolicy: method.  For example,

 

pm1 accountTypes searchPolicy: SearchPolicy equality.

 

Also note that if the listmodel contains numbers or symbols, you won't run into the identity/equality problem since there can't be two copies of any one number or symbol.  This means that we could've taken another approach, that is, left the identity search policy as is (no change to #initialize) and used symbols for account types.  This would require only two minor changes -- adding #asSymbol to the following two methods like so:

 

In PersonalMoney:

addAccountType: aType

        "Add aType to the receiver's collection of account types and answer aType."

 

        ^self accountTypes add: aType asSymbol

 

In PersonalAccount:

accountType: aType

        "Set the account type of the receiver to aType."

 

        accountType := aType asSymbol

 

Version 4 update:  The walkback in Dolphin version 4 is a little different but it ends up in the same place for essentially the same reason.  Version 4 adds a few more search policy classes, including EqualitySearchPolicy, which is the search policy for the combo box's choices listmodel.  Shown below is the debugger, with a method highlighted in class EqualitySearchPolicy.  The problem is that our model is a ListModel that still has a default search policy of identity in version 4 and ultimately it's this ListModel that fails the comparison test.  The solution for version 4 is the same as that of version 3 (above) -- change the search policy for the accountTypes listmodel from identity to equality.

 

 

 

Digging a little further in version 4 you can see that ComboBox now wraps the choices in a listmodel whose search policy is equality.  But our application model (in class PersonalMoney) has already wrapped the account types collection in a listmodel.  The problem is that even though ComboBox has a listmodel whose search policy is equality, it's the identity listmodel that is eventually compared.  Since ComboBox is now friendlier (to unwrapped string collections), it seems as though you can pass just the list portion (a collection) of the account types listmodel to the combo box when the account is edited.  This works but only up to a point -- you'll find later that you still need to change the accountTypes listmodel search policy to equality -- there are methods in the model that will expect accountTypes to be searchable on strings.  In general, if you have a collection of strings, wrap it in a listmodel with an equality search policy.

 

 

Help, I've got a zombie

 

Now that we've fixed the problem with equality it's time to look at and deal with your zombies.  "MY zombies?" you say.  Yes, yours.  Zombies are orphaned instances that float around in your image.  They may or may not be eventually picked up by the garbage collector, but one way they may cause problems is if you save your image with them in it, restart the image, and then run into problems when the image tries to recreate them.  Zombies can also tie up system resources.  During the course of the above testing, instances of PersonalAccountShell were created but never displayed or destroyed because of that identity/equality error.

 

You can see if there are any zombies in your image by displaying or inspecting "PersonalAccountShell allInstances".  When I do it, I see two instances -- these correspond to the two times a walkback appeared when trying to edit an account.  You might have even more than two in your image.  If you use the inspector, you'll see an Array containing the instances of PersonalAccountShell.  Select one of them and inspect it.  You can even evaluate "self show" (in the Inspector view) and a window will pop up on the account instance -- in this case, close all the inspector windows, close the account window, then inspect "PersonalAccountShell allInstances" again and you'll see one less instance in the Array being inspected.

 

I assume that by opening the window on the orphaned instance, then closing it, we allowed the normal dereferencing mechanisms to take over.  Fortunately, you don't have to go through this process with each instance -- inspecting, showing, closing, etc.  Evaluate the following:

 

"See Steve Waring's post (16 July 2000) for more information."

PersonalAccountShell allInstances do: [:inst | inst view destroy].

 

The remaining instance(s) of PersonalAccountShell should now be gone.  Another method that seems to work is:

 

PersonalAccountShell allInstances do: [:inst | inst oneWayBecome: nil].

 

There is also the alternative of using the "panic button",  on the toolbar, which destroys "all top-level views".  These top-level views include shells and tooltips, I'm not sure what else.  In our case, the panic button is a viable alternative to the above procedures.  The only drawback to the panic button is that since it closes all windows, you will lose your visible windows (e.g., browsers and workspaces) so you'll have to reopen them.  For this reason it's a good idea to save your workspace contents before hitting the panic button.  In general, if you think your image is cluttered with zombies of various classes, use the panic button.  If you know there are zombies in one or a few specific classes, then use the view destroy phrase above to keep the rest of your image as is.

 

 

Dealing with nil data

 

Now we look at how the extended application deals with nil values.  We start with the easy one -- the combo box -- easy because we've already seen a nil value handled there.  A new account starts with no account type -- its value is nil (uninitialized).  You've already seen then that the combo box shows a blank value for nil and then allows you to choose a value from the choices list.  You may remember earlier when the debugger went into a bunch of "selection" type messages -- well, the first of those was #selectionOrNil: in ComboBox.  This method looks at the new selection passed in -- if the new selection is nil, then it just resets the combo box text box, otherwise it looks to see if the new selection is in the choices list so it can select it.

 

Another method of interest in ChoicePresenter is #nilChoice: which lets you designate a choice for the combo box to show when the account type is nil.  While not absolutely necessary in this application, it does add something.  For example, when the PersonalAccount view displays, right now it shows a blank value for account type if accountType is nil.  It might draw more attention though, or be clearer in what a blank value means, if it displayed '[none]'.  To implement this, you need to do two things.  The first is to ensure that the nil choice value is in the list of choices for the combo box and the second is to send #nilChoice: to the ChoicePresenter.  Here then are the revised methods:

 

In PersonalMoney, class side:

withDefaultTypes

        "Answers an instance of the receiver with accountTypes set to a default collection."

 

        ^self new

                 addAccountType: '[none]';

                 addAccountType: 'Checking';

                 addAccountType: 'Credit Card';

                 addAccountType: 'Investment';

                 yourself

 

In PersonalAccountShell:

accountTypeChoices: aListOfChoices

        "Set the choices for the account type to aListOfChoices.

         Set the choice to represent nil to be '[none]' if it's in the list."

 

        "#nilChoice: must come before #choices: for this to work."

        (aListOfChoices includes: '[none]') ifTrue: [accountTypePresenter nilChoice: '[none]'].

        accountTypePresenter choices: aListOfChoices

 

Note that setting nilChoice is not the same as setting a default choice.  My thanks to Ian Bartholomew for clarifying this for me.  If accountType is nil, the combo box will show '[none]'.  If no choice is made, then when you return to the main window, the listview will show a blank for that value.  This is because the underlying model has not been changed.  If, however, you do change the value to something else (e.g., 'Checking') and then want to set the value back to nil, choosing '[none]' in the combo box will do just that.

 

You can test it by once again evaluating "PersonalMoneyShell show" or re-evaluating that block of lines (pm1, pa1, pa2, etc) in the workspace.  Note that the reason you need to re-evaluate the lines, rather than just evaluating "PersonalMoneyShell showOn: pm1" is to capture the changes we just made in class PersonalMoney.  Otherwise you'd just be opening the application on the old instances.

 

 

Sorting a listview

 

We now turn to a serious problem with the listview.  If you try to sort on a column where there's a nil value, the sort will fail.  Go ahead and try it.  Open the same application window as before with the two accounts, then create a new account.  Don't choose an account type in the account view but return to the application view and click the header for account type.  You should get a walkback saying "UndefinedObject does not understand #<=".  To do a sort, two operands are compared, and the default sort uses <= (less than or equal) to compare.  The message tells us that at least one of the operands is nil (UndefinedObject) and nil does not understand (DNU) the #<= message.

 

We need to fix the sorting criteria for each column where nil is an acceptable value.  Since each column represents a model aspect, we go to the model first and determine which fields can not be nil.  In this case, and focusing on just the model aspects shown in the listview, you can see that PersonalAccount initializes its name and currentBalance.  We can prevent them from ever returning to nil by adding a test in the set accessors for both:

 

In PersonalAccount:

currentBalance: aNumber

        "Set the current balance of the receiver to aNumber."

 

        aNumber notNil ifTrue: [

                 currentBalance := aNumber.

                 self trigger: #currentBalanceChanged]

 

name: aString

        "Set the account name of the receiver to aString."

 

        aString notNil ifTrue: [name := aString]

 

That still leaves account number and account type.  For the sake of this exercise, forego the argument whether or not either should ever be nil and assume that nil is a valid value.  Then that means the presenter or view needs to handle it.  It turns out that the listview is really in charge here.  ListView tells its ListPresenter to sort the model using the sortblock in the ListViewColumn.  The default sortblock is [:a :b | a <= b], which says that given two objects, a comes before b if a is less than or equal to b.  Otherwise b comes before a.  We need to add a test for nil to that.  The new sortblock will be:

 

[:a :b | a isNil or: [b notNil and: [a <= b]]]

 

This says that given two objects, a comes before b if a is nil.  If a is not nil and b is nil, b comes first.  If they're both not nil, the lesser of the two comes first.  To implement this, open up View Composer on PersonalMoneyShell 'Default view', select the listview, and then, for the two columns (account number and account type), change the sortblock to the above.  Save the view and open the application again and all columns are now sortable.

 

 

Validation -- dealing with unexpected data

 

We've seen that the combo box is somewhat fragile, generating an error whenever it encounters a choice that's not in its choices list, but we've now tightened up some of those loose ends.  There's still at least one way that this type of "bad data" can get in and that's through bypassing the PersonalAccount view.  For example, let's say you have personal accounts in a database and you write a data reader to bring in the values.  By now, it's clear that if an account has an account type (other than nil) that's not in the lookup table, the combo box will not like it one bit.  We can simulate this through the workspace.  For example:

 

"Evaluate"

pm1 addAccount: (PersonalAccount new accountType: 'Bad news').

PersonalMoneyShell showOn: pm1.

 

When you try to edit the new account, you'll see a walkback appear saying "Bad News not found" (ahh, if it were only so).  The walkback is exactly the same as the "Checking not found" walkback earlier.  This time, though, it's not a case of mistaken identity -- it's because the accountType is really not in the choices list.

 

Where then to fix this one.  Start with the model.  The model is responsible for its own state.  Presenters come and go, views change like fashions, platforms change, but the model, the Smalltalk model at least, stays pretty much the same.  The model should not allow itself to get in a "bad" state.  Think "healthy model".  Now this is not to say you shouldn't have a strategy for when things go wrong, because they will indeed go wrong.  We all know that.  But start at the model.

 

The best way to fix our 'Bad news' account type is to prevent it, and the place is in the model, in PersonalMoney>>#addAccount:, where we can ensure that the account's type, if any, is valid.

 

In PersonalMoney:

addAccount: aPersonalAccount

        "Add aPersonalAccount to the collection of accounts owned by the receiver if it is valid.

        Answer aPersonalAccount or nil (if an invalid account)"

 

        "Check account type.  If not nil and invalid, post a message and answer nil."

        | type |

        (type := aPersonalAccount accountType) notNil ifTrue: [

                 (self accountTypes includes: type) ifFalse: [

                         MessageBox notify: 'Account not added: ', aPersonalAccount displayString.

                         Transcript display: 'Account not added: ', aPersonalAccount displayString; cr.

                         ^nil]].

 

        ^self accounts add: aPersonalAccount

 

Here we check to see if the account type is in our list of valid account types.  If it isn't, we display a message box, post a line to the Transcript and return nil, otherwise we add the account as before.  You may want to remove (or comment out) one or both of the message lines after testing.

 

The complement to this is #removeAccountType: which will prevent us from deleting an account type if there are existing instances using it.  Without this test, if you remove an account type and then edit an account that has that removed account type, you'll get another "account type not found" error.

 

In PersonalMoney:

removeAccountType: aType

        "Remove aType from the receiver's collection of account types.

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

 

        "Answer nil if there are existing instances using aType."

        | accts |

        accts := PersonalAccount allSubinstances select: [:acct | acct accountType = aType].

        accts size > 0 ifTrue: [

                 MessageBox notify: aType, ' not removed since ', accts size printString, ' accounts are using it.'.

                 ^nil].

 

        ^self accountTypes remove: aType

 

After testing, don't forget to destroy those zombies, save your package, your image and your workspace.

 

 

Sorting has its ups and downs

 

Everyone expects a listview to be able to sort both ways -- ascending and descending -- one clicks on a column header and the list re-sorts itself according to the reverse order of the column just clicked on.  We currently have only an ascending sort, courtesy of the listview's default sortBlock on two of the columns and the custom sortBlocks we installed for the other two.  It seems like we need to trap some event, know what state we're in, change the sortBlock if necessary, and then re-sort.

 

Another way to look at it is what do we want to happen.  We know that the user initiates the sort through a click on a listview column header.  From using other Windows programs, we expect that the column will be sorted in ascending order, unless it's already in ascending order, in which case it'll now be sorted in descending order.  It'll already have been in ascending order if the column had just been clicked or if the model was already sorted that way.

 

In looking at the necessary sorts, they all seem to be the same, the only differences between them being up or down (<= or >) and the column's model aspect (i.e., name, number, etc., used as the basis of comparison).  That leads to the thought of what if we can tell the model to sort itself based on the column (i.e., using the model aspect that the column displays) and either an up sortBlock or a down sortBlock, and then just have the listview display the result.

 

With these thoughts in mind, turn to ListView, since the click is on listview, to see what events are possible.  Looking at #publishedEventsOfInstances (on the class side) shows some keyPressed, keyTyped events, and others, but they don't look promising.  You can right-click on the method to bring up the context menu, then choose Hierarchy to bring up a view of #publishedEventsOfInstances in all its parent classes, but that too doesn't seem promising for what we need.

 

Step back a second and ask what methods does ListView have that relate to sorting.  Then look at the categories and select 'sorting' -- you'll find #sortOnColumn:, which takes aListViewColumn as its argument.  Feels like you're getting warmer, doesn't it.  Insert a self halt message within #sortOnColumn: (see below) and then open the application again ("PersonalMoneyShell openOn: pm1") and click on one of the column headers.  In the debugger, look at the chain of messages before the self halt and then start tracing through -- this'll give you a good idea of what the sequence of processing is.  Remove the self halt when you're done.  Dismiss the debugger and close the application window.

 

In ListView:

sortOnColumn: aListViewColumn

        "Sorts the receiver according to the sort block in aListViewColumn"

self halt.

        Cursor wait showWhile: [

                 self presenter beSorted: aListViewColumn rowSortBlock]

 

In the last line of  #sortOnColumn: you can see it tells its presenter to sort itself using the selected column's sortBlock.  Its presenter, a ListPresenter, in its #beSorted: method, uses that sortBlock to replace its model's list (a ListModel) with a sorted version of the model.  ListModel, in turn, triggers a #listChanged event, which ends up sending #onListChanged to ListView, causing the listview to re-display itself with the newly sorted list model.

 

There are no doubt many ways to do what we want to do but at this point you may be thinking that just about everything you need is there, in ListView's #sortOncolumn:, if only you could tell the presenter to sort using the (yet-to-be-defined) upSortBlock or downSortBlock.  One way to do this is to change aListViewColumn's sortBlock before the call to #beSorted: -- and that brings us to subclassing ListView.

 

 

Subclassing ListView

 

If we can subclass ListView and override #sortOnColumn: to first change aListViewColumn's sortBlock to what we want, and only then call the parent #sortOnColumn:, all will be well and beautiful.  We want the new #sortOnColumn: to look something like this:

 

sortOnColumn: aListViewColumn

        "Changes aListViewColumn's sortBlock to a descending or ascending sort

        depending on if aListViewColumn was the last column clicked or not.

        Sorts the receiver according to the sort block in aListViewColumn."

 

        (lastColumnSelected = aListViewColumn) ifTrue: [aListViewColumn reverseSortBlock].

        lastColSelected := aListViewColumn.

        super sortOnColumn: aListViewColumn

 

Right now this is pseudo-code.  The first line says that if the same column is clicked again, reverse its sortBlock.  The second line keeps track of the last column clicked (selected) and the third line calls ListView's #sortOnColumn: which does the actual sorting.  This method tells us that the new ListView class will need an instance variable (lastColSelected) to keep track of the last column that was clicked.  Now notice the expression "aListViewColumn reverseSortBlock".  There is currently no #reverseSortBlock method in ListViewColumn.  We could subclass ListViewColumn and implement this, but for now, one subclass is enough.  So the revised pseudo-code for the first line looks like:

 

            (lastColumnSelected = aListViewColumn)

ifTrue: [aListViewColumn sortBlock = upBlock

            ifTrue: [aListViewColumn sortBlock: downBlock]

            ifFalse: [aListViewColumn sortBlock: upBlock]]

ifFalse: [aListViewColumn sortBlock: upBlock].

 

We're back in the ballpark.  We just need to define the up and down sortBlocks.  For that, we'll add another two variables that hold the sortBlocks.  Since they will always be the same for this simple subclass (and for another reason, which I won't get into now) we'll add them as class variables.  AcDcListView, an ascending/descending ListView, looks like this:

 

ListView subclass: #AcDcListView

        instanceVariableNames: 'lastColSelected'

        classVariableNames: 'AscSortBlock DescSortBlock'

        poolDictionaries: ''

 

sortOnColumn: aListViewColumn

        "Changes aListViewColumn's sortBlock to a descending or ascending sort

        depending on if aListViewColumn was the last column clicked or not.

        Sorts the receiver according to the sort block in aListViewColumn."

 

        (lastColSelected = aListViewColumn and: [aListViewColumn sortBlock = AscSortBlock])

                 ifTrue: [aListViewColumn sortBlock: DescSortBlock]

                 ifFalse: [aListViewColumn sortBlock: AscSortBlock].

        lastColSelected := aListViewColumn.

 

        super sortOnColumn: aListViewColumn

 

In AcDcListView, class side:

initialize

        "Private - initialize the sortBlocks."

 

        super initialize.

        AscSortBlock := [:a :b | a <= b].

        DescSortBlock := [:a :b | a > b]

 

To initialize the sort blocks, you'll need to evaluate the following in the workspace:

 

AcDcListView initialize.

 

Again, there are other ways to implement this, but I opted for simplicity for now.  AcDcListView now implements a ListView that supports simple ascending and descending sorting of its columns.  Note that any sortBlock that is in one of its columns (e.g., placed there in View Composer) will be replaced when the column is sorted.  Of course the up side to this is that you don't have to specifiy any sortBlocks for any of the columns.  Plug it in and it'll do the job.

 

The next thing is to plug in the new listview.  This is easy -- bring upView Composer on PersonalMoneyShell's view and mutate the accounts listview to an AcDcListView, then save and run the application again.  The first thing you'll see is that the icons are back in column 1, so go back into VC and evaluate "self imageManager: nil" as you did earlier.  Run the application and try sorting, up and down, each column in turn.  It should work fine for the first and last columns, but if you have any nil values in the second or third columns, you'll see a DNU (object does not understand) walkback.

 

The problem is that AcDcListView ignores the sortBlocks we put in earlier, so it coughs when it sees a nil value.  One way to fix this, and we could've done this to begin with, is to change the getContentsBlock aspect of the two columns (accountNumber and accountType) instead of changing the sortBlock aspect.  For example, for the third column (accountType), we could have left the sortBlock as SortedCollection and changed the getContentsBlock aspect to

 

[:acct | acct accountType isNil ifTrue: [''] ifFalse: [acct accountType]]

 

This would tell the listview to display a blank string wherever the underlying model was nil -- it would not change the underlying model.  The default sort would work fine, since there would now be blank strings, which it knows how to sort, instead of nil, which it doesn't.

 

For AcDcListView, though, we want something easy (for us or for some other user) to plug in, so we fix the problem in the sort blocks of this new class (similar to what we did before with the sort blocks in the View Composer).  The new class initialize method now looks like:

 

In AcDcListView, class side:

initialize

        "Private - initialize the receiver."

 

        super initialize.

        AscSortBlock := [:a :b | a isNil or: [b notNil and: [a <= b]]].

        DescSortBlock := [:a :b | b isNil or: [a notNil and: [a > b]]]

 

To re-initialize the sort blocks, you'll need to again evaluate the following in the workspace:

 

AcDcListView initialize.

 

The sort should now work on all columns, with or without nil data there.

 

Note: You might've noticed in the View Composer that SortedCollection is the default sortBlock for a column and wondered how could a class be a sortBlock.  The reason this works is that SortedCollection has a method #value:value:.  When sorting, what is needed is a "dyadic valuable" which is something that responds to #value:value:.  We typically use a two-parameter (dyadic) block for this, like each of the two blocks just above, and expect that two values will be passed in as arguments.  What happens behind the scene is that the block is sent a #value:value: message.  Look up the definitions for #value:value: and you'll see it's implemented in BlockClosure, SortedCollection, MessageSend, and a few other places.  What this means is that wherever a dyadic valuable is expected, you can use an expression that responds to #value:value:.  By the way, dyadic = 2, monadic = 1, and nyadic = none.

 

 

Next Steps

 

There's always more that can be done, of course -- enhancements as well as making the application more robust while demonstrating and learning Dolphin Smalltalk features.  Again, don't forget to destroy those zombies, save any new classes to your package, save your package, your image and your workspace.

 

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

 

 

Written by Louis Sumberg

Last updated December 2000

 

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

 

 

Back to Part 1              Onwards to Part 3