Navigation:  Tutorials > Creating a GUI Application >

PersonalAccountShell: a Presenter for PersonalAccount

Previous pageReturn to chapter overviewNext page

We want to create a presenter that displays a PersonalAccount as its model and allows for its various aspects to be edited. This should be a top level shell window so a suitable superclass for our new class will be Shell.

Shell subclass: #PersonalAccountShell

       instanceVariableNames: 'namePresenter accountNumberPresenter

       initialBalancePresenter transactionsPresenter

       currentBalancePresenter '

       classVariableNames: ''

       poolDictionaries: ''

 

The definition includes instance variables to hold separate presenters which will handle the editing of each of the relevant aspects of the model. These sub-presenters are initialized in the createComponents method.

createComponents

       "Private - 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'.

 

For each model aspect a suitable presenter is created and added to the shell composite. The sub-presenters are given names to identify them when the view is connected; each sub-view will be given an identical name so that presenter-view pairs can be matched and connected together. The choice of presenter to use for each aspect depends on the effective type of the aspect's value. Each of the aspects will be accessed using a ValueAspectAdaptor so that #value and #value: can be used to get and set its value.

The next task is to specify how the model is connected to the presenter.

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

       transactionsPresenter model: (aPersonalAccount transactions).

       currentBalancePresenter model: (aPersonalAccount aspectValue: #currentBalance).

 

       "Sometimes a model may trigger its own events when some aspects of it are changed. For these

       aspects we must explicitly inform the ValueAspectAdaptor that this is the case. This allows

       the adaptor to update its observers if an aspect is changed other than by going through the

       adaptor itself. In the case of a PersonalAccount, the only aspect that triggers a change in

       this way is #currentBalance. We inform our newly created aspect adaptor that its model

       triggers #currentBalanceChanged whenever the currentBalance is updated. See

       PersonalAccount>>currentBalance:."

       

       currentBalancePresenter model aspectTriggers: #currentBalanceChanged.

 

First of all the PersonalAccount is assigned as the actual model of our composite using the super message send. Then the models of all the sub-presenters are set to be ValueAspectAdaptors on the appropriate aspects of the account.

Tip: a shortcut to creating a ValueAspectAdaptor is to send #aspectValue: to the account object specifying the name of the aspect you want.

Take a look at the one special case here; creating the ValueAspectAdapator for #currentBalance. Once created the adaptor is sent #aspectTriggers:. This is important since it informs the adaptor that the aspect it's connected to will trigger its own update notifications whenever it is changed. Do you remember the #currentBalance method we wrote for PersonalAccount?

PersonalAccount>>currentBalance: aNumber

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

 

       currentBalance := aNumber.

       self trigger: #currentBalanceChanged

 

See that , it triggers a #currentBalanceChanged event when the balance is assigned to using #currentBalance: directly. If we didn't send #aspectTriggers: to the ValueAspectAdaptor we create for this aspect (see above)  then it wouldn't be able to inform its observers when the #currentBalance: method is called by some other object. This would result in some notifications being missed and the current balance not being updated correctly. .

The next step is to wire together the sub-presenters using the standard Smalltalk event notification mechanism. This wiring is implemented by overriding the createSchematicWiring method. It is only necessary to override this method if you wish to respond to events triggered by your sub-presenters. In this case we want to send an #editTransaction message to the PersonalAccountShell when a transaction in the transactions list is double-clicked.

createSchematicWiring

       "Private - Create the trigger wiring for the receiver"

 

       super createSchematicWiring.

       transactionsPresenter when: #actionRequested send: #editTransaction to: self.

       

The last essential task that must be considered when creating any presenter is to define class methods, defaultModel and defaultView. The former should answer an object which the presenter will use as its model in cases where this is not specified explicitly (which is the most likely situation). In this case we answer a default instance of PersonalAccount. The defaultView method must answer a resource name to use to load a default view. The defaultView method inherited from Presenter class specifies a view name, not surprisingly, of 'Default view'. This is suitable for most purposes so we just have to make sure that this matches the name under which the actual view is saved by the View Composer.

defaultModel

       "Answer a default model to be assigned to the receiver when it

       is initialized."

       

       ^PersonalAccount new

 

Tip: remember these are class methods.

Now add the remainder of the accessing methods for PersonalAccountShell.

selectedTransactionOrNil

       "Answer the currently selected transaction or nil if there is none"

       ^transactionsPresenter selectionOrNil

 

selectedTransactionOrNil: aPersonalAccountTransactionOrNil

       "Sets the currently selected transaction to

       aPersonalAccountTransactionOrNil. If nil if there

       will be no selection"

       ^transactionsPresenter selectionOrNil: aPersonalAccountTransactionOrNil

 

hasSelectedTransaction

       "Answer true it there is a currently selected transaction in the

       receiver"

       ^transactionsPresenter hasSelection

 

One of the fundamental ways in which a user can interact with a model is by issuing commands from a menu or toolbar. It is the presenter's role to intercept these commands and translate them into modifications to the model. Commands are parameter-less messages and we need to provide the appropriate methods for handling them.

editTransaction

       "Edit the selected transaction"

       #todo "Write this later".

 

newTransaction

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

       #todo "Write this later".

 

removeTransaction

       "Removes the current transaction from the receiver's model"

       | transaction |

       transaction := self selectedTransactionOrNil.

       transaction notNil ifTrue: [

               self model removeTransaction: transaction ].

 

These methods should go in the commands category. We will implement the #todos after we have created a suitable dialog for editing transactions.

Tip: you can use a #todo symbol to mark methods where something needs to be modified or implemented in future. You can then browse for references to the #todo symbols to find all the outstanding tasks. Remember the period following the comment, otherwise the statement will not be valid Smalltalk syntax.

By default any command that is implemented by a presenter will be enabled when the menu is pulled down. To change this default behavior and enable or disable commands directly, you must implement a queryCommand: method.

queryCommand: aCommandQuery

       "Enters details about a potential command for the receiver into

       aCommandQuery"

 

       super queryCommand: aCommandQuery.

       (#(editTransaction removeTransaction) includes: aCommandQuery command)

               ifTrue: [ aCommandQuery isEnabled: self hasSelectedTransaction ]