========================================= Notes for: 10 May 1996 ========================================= ***** This is a continuation of the notes on MVC that covers the rest of building the interface ***** ========================================= General notes on Model-View-Controller MVC is a fairly abstract, simple idea. Basically it just says that the data object (the "model") should be programmed without depending on what output interface ("view") and input interface ("controller") are slapped on top of it. In other words, you should be able to take your code for an Employee data base, build one GUI on top of it, then rip that interface out and slap a new one on, without having to change the database code at all. There is a supposed distinction between a view and a controller, but I've never seen that distinction as important. What is really important is separating the model from the interface. We started by looking at the model for a bank account class. That should have looked very familar, except maybe for three things: -- we built "setters" and "getters" for every instance variable, -- any place in the code that wanted to change an instance variable had to do so by calling a setter (rather than saying "variable := value" directly) -- each setter sent an abstract message indicating that its instance variable had changed state. These looked like balance: aValue balance := aValue self changed: #balance The changed: #balance message is sent to the interface, which realizes that instance variable balance has changed, and decides what part of the display interface should change as a result. That's all we need to say about the model. Next we turned to building the interface. ================================================ Interface design. 1. Prototyping the screen We started at the "widget" level, doing so by opening a "canvas builder" from the VisualWorks top menu. You see a palatte that allows you to place widgets on the canvas and move them around. Some widgets are totally passive: labels, lines, boxes. Others will correspond to output-only boxes (the account number and balance). Still others will be input/output text boxes (the name and address). And finally there will be some that have a non-textual representation of the data (the radio buttons). The first step then is to place all the widgets on the canvas and align them to your satisfaction. There are plenty of cool tools on the menu bar to do things like line widgets up and force them to be the same size. To change the appearance in other ways, click on the widget, and get its PROPERTIES (either from the tool bar or using the center mouse button) and you can do things like change the label text, change the color, make the field read only, disable tabbing through the field, and so on. Next it is a good idea to save this canvas, a process called "installing" it. The specification for a canvas is actually stored as an instance method in a class. You make up both the name of the class and the name of the method. But the strong convention is that if the model has class name Account, the interface will have the class name AccountInterface, and the method will be stored under the name windowSpec. The first time you install the canvas you will be creating the AccountInterface class, and you will be asked what category to save it in. Convention is to store it in the same category as your model, that way you can file them in and out at the same time. You also want to select the "application" check box, which is a just a note to the Resource Manager identifying this new interface. After installing your canvas you can look at the new class AccountInterface, and browse the class method windowSpec to see what the code associated with a canvas is. You will see an entry for every widget on the canvas. You can also go to the resource manager (a button on the main VisualWorks menu bar) and notice that AccountInterface->windowSpec is now in the resource directory. This will be useful when you want to edit the canvas again. At this point you have the canvas completely built and installed. This is a good time to file out your work just in case. Next we began connecting up the interface with the model. (Currently there is no connection whatsoever.) One thing you should realize is that this single canvas hooks up to a single *instance* of account. Likewise in the problem set you will hook up a canvas to a single *instance* of WagedEmployee. Therefore you will not deal with the employee Database again unless you want to do so for extra credit. The first thing we need to do is hook up an instance of the model (Account) with an instance of the view (AccountInterface). To do this we create an instance variable account within AccountInterface. We obviously need to initialize this variable, however. The way this is done is by defining an instance method initialize for AccountInterface. This method is *guaranteed* to be called every time an instance is created. (Think about how this is guaranteed.... remember that AccountInterface is an subclass of ApplicationModel.) In the code for initialize we just say account := Account new, or whatever the appropriate instance creation method for Account is. First note that certain fields in the interface need some "local space" to store information---name, address, and deluxe/basic, for example: when you type into these fields, you are not entering information directly into the model. Only when you push the "action buttons" marked CHANGE or RESTORE should data be passed from the interface to the model or vice versa. On the other hand, the "Balance" and the "ID Number" fields do not need any local storage since you can't write into them. Likewise you will not type directly into the list of transactions. These widgets can get their information directly from the model, and every time the model's instance variables "balance," and "number," and "transaction" change, the associated fields in widgets should change too. We begin by looking at the Name/Address/Basic/Deluxe fields, and the first thing to do is to associate an "aspect" with each one. An aspect corresponds to an instance variable in the class AccountInterface, where we will store the local information for each of those widgets. We assigned an aspect for Name and for Address, the conventional names are nameValue and addressValue. We assigned the same aspect name, typeValue, for the Basic and the Deluxe radio buttons, since they are associated with the same piece of information in the model (the instance variable "type"). Aspects are assigned in the Canvas Editor, again by selecting the widget and selecting its Properties. The next thing we did was assigned symbol values for the Basic and the Deluxe values: we gave the symbol #basic to the first and the symbol #deluxe to the second. Once again we do this by editing the widgets Properties. Recalling that "typeValue" is an instance variable in AccountInterface, what this means is that whenever the Basic radio button is selected, the instance variable typeValue will be bound to the symbol #basic, and whenever the Deluxe widget is selected, the variable typeValue will be bound to the symbol #deluxe. These are the same symbols that the Account class will use to bind to the instance variable "type." Finally we edited the properties of all the action buttons: Restore, Change, Deposit, Withdraw. In each case we made up a method name: doRestore, doChange, etc. The interface will be sent this method whenever the corresponding button is pushed. To update these changes in the AccountInterface class, we select *all* the widgets, go to the toolbar, and press DEFINE. This creates the appropriate instance variables and message stubs in the definition of AccountInterface itself. Open a browser on the class AccountInterface, and note that there are protocols called aspects and actions; in the former is all the aspects we defined for the widgets: nameValue, addressValue, typeValue, ... and in the latter are unary methods for all the action buttons: doChange, doRestore, etc. For the action buttons notice that all they do is return self at this point. For the nameValue, addressValue, and typeValue aspects, there is some interesting code. First of all, the accessor does "lazy initialization." Instead of having an initialize method set the value of the instance variable, this method instead counts on the fact that its value will first be nil. If it is, the first branch of the conditional is executed, which sets the instance variable to an object. After that first time, the ifFalse: branch of the conditional is executed, which acts just like a normal accessor, returning the value of the instance variable. The object that these aspects are being initialized to are things called "Value holders." The point of these things are just to enforce a consistent interface between the model and the interface. To set the value holder for name, for example, we say self nameValue value: 'New String' and to access the name, we say self nameValue value. (It's actually important to use self nameValue instead of just nameValue, since the actual instance variable might not be initialized yet, and you want the accessor to initialize it for you.) For all the fields that need to hold an internal value: the name, the address, and the amount deposited or withdrawn, this definition for the aspect is right, and need not be changed further. Two of the fields get their data directly from the model, however: Balance and AccountNumber. For these there is no intermediate value holder. Instead we use an object called an AspectAdaptor that just acts as a "channel" between an instance variable in the model and an aspect in the interface. The code for balanceValue still does lazy initialization, but instead of a value holder we put the code: (AspectAdaptor subject: account sendsUpdates: true) forAspect: #balance In this code, "account" is of course the instance variable in AccountInterface, sendsUpdates and #balance indicates that this adaptor will update itself when it gets a "changed" method from the model with aspect "#balance" and furthermore it will get its new value from the result of calling account balance which is what we want it to do. The account number field works exactly the same way. There is one more twist on the defining of aspects: we have the list of transactions, which is a "list" widget. Look at the code for that aspect. Again it does lazy initialization, but not to a ValueHolder or an AspectAdaptor, rather to an object SelectionInList. In this case you have to tell it *which* list it should be displaying---in our case this corresponds to an accessor "transactions" in the Account class. So we replace the line SelectionInList new. "Initialize to a null selection" to SelectionInList account transactions. ============= Finally we program the action buttons, and these are very easy: doChange: when this is pushed, we want to transfer information from the Interface to the Model. So we do things like this: account name: self nameValue value. account address: self addressValue value. account type: self typeValue value. doRestore: when this is pushed, we want to transfer information in the other direction: from the model to the local value holders. So we do self nameValue value: account name. self addressValue value: account address. self typeValue value: account type. doDeposit: the local value holder amountValue, the field the user types the dollar amount into, has a number which should be deposited. One twist here is that the deposit: method for account does not take a dollar amount, instead it takes a "transaction" object which is either a Deposit or a Check. Both of these have an "amount" and an "agent," but in this case we don't know who the agent is, so we say something like this: account deposit: (Deposit amount: self amountValue value agent: 'Anonymous') and we also might want to zero out the deposit/withdrawal field on the interface: self amountValue value: 0. ===================== There are two more parts of the interface you might want to know about, but these are better explained in the tutorial 1. Menu bars: each selection in the menu bar just sends a unary message to the Interface when it is selected. To associate a menu bar with a canvas, you have to do two things: -- go to the PROPERTIES window for the canvas itself. You do that by pressing PROPERTIES when there are no widgets selected. Notice that there is a "menu bar enable" check box, and also an aspect associated with the menu bar. It is convention to call this aspect menuBar -- invoke the menu editor by selecting the Tools item in the canvas menu bar. On each line you just type one menu entry, and you separate the displayed name of the menu from the message associated with it using a TAB. One note: the interface knows how to respond to the message "closeRequest" already: it closes itself. 2. Updating Lists. You have to be careful to update the interface on a scrolled list (like the list of transactions) whenever the interface does something to cause the list's contents to change (e.g. whenever it adds a deposit or withdrawal). See Page 164-165 and Page 182 for the details.