OK. I've been whining about not understanding MVP for a few weeks now, and
last night I thought 'This is daft, you've successfully designed and coded [x number of complex sytems], and you can't work out a few little classes?'. So I sat down to suss it out. The following is a very full description of what I tried to do, and the compromises I had to make to get it working. If anyone who knows Dolphin MVP can read it and let me know where I am going wrong, I would be immeasurably grateful. Obvious thing: Pick a simple domain, and implement a tiny app to just try out the MVP stuff. A library. People can add books, check books out and return them. A book has a name. I want to know what books are in the library, and which are currently available (i.e. not on loan). NOTE: The following code was transcribed by hand. My machine with Dolphin is my works laptop, and my machine with ng access is my personal box. The two will not speak to each other yet, so I am writing this with my laptop beside my desktop, and copying the code by hand. Any syntax errors are a result of this process. I have the code running, honest. If you want a package, I can send it. I always start with domain classes, so I did this: Object sublass: #Library instanceVariableNames: 'books' classVariableNames: '' poolDictionaries: '' classInstanceVariableNames: '' Library(class)>>#new ^super new initialize Library>>#initialize books := ListModel with: (SortedCollection sortBlock: [:x :y | x title <= y title]) I chose ListModel because I had it in mind that I would be using a ListPresenter later to view the books. As I understand, the ListModel does things with events, so we don't have to, and I like the sound of that. I chose a SortedCollection because its as easy as anything else in Smalltalk. Then we need to be able to add books to the Library, and check books out. Library>>#addBook: aBook books add: aBook Library>>#checkOut: aBook to: aPerson aBook onLoanTo: aPerson I figured the easiest way to approach this is to tell a book that it is on loan. Book will come later (I'm trying to program by intention just a little bit). Returning books then also becomes easy: Library>>#return: aBook aBook beAvailable The only other thing the Library needs to be able to do is tell us about all its books, and about its currently available books: Library>>#books ^books Library>>#availableBooks ^books select: [:each | each isAvailable] All we need now is a book that knows how to be on loan, and be availble. Object sublass: #Book instanceVariableNames: 'title borrower' classVariableNames: '' poolDicationaries: '' classInstanceVariableNames: '' Book needs some basic things: Book(Class)>>#title: aString ^super new title: aString Book>>#title: aString title := aString Book>>#title ^title Book>>#printOn: aStream (also #displayOn the same) aStream nextPutAll: title Now it needs its specific behaviour. According to the Library, we need it to be able to be on loan to a person, make itself available, and answer if it is available: Book>>#onLoanTo: aPerson borrower := aPerson Book>>#isAvailable ^borrower isNil Book>>#beAvailable borrower := nil This was the simplest way I could imagine to fulfil the requirements. Using a workspace to create a few books, add them to the library check a couple out, and view availableBooks gives correct results. There is a caveat. This depends on a book being returned as the same object that was checked out. Its not prime time code, for sure. I'm not looking for robustness, just a simple working model. A book is always checked to 'Object new' as its person. (I was hoping get as far as adding people, but, well...) Now we are going to need a Presenter. As I am unsure about MVP, I decided to play (what I think is) safe, and I dragged my two classes onto Model, so that they become subclasses of model. Run the test again. Everything cool. Define a Presenter. I am going to start with one listbox just for the collection of all books: Shell subclass: #LibraryShell instancevariableNames: 'booksPresenter' classVariableNames: '' poolDictionaries: '' classinstanceVariableNames: '' I did a defaultModel method, eventhough I won't use it (bad boy). LibraryShell(Class)>>#defaultModel ^Library new Then the guess work starts. LibraryShell>>#createComponents super createComponents. booksPresenter := self add: ListPresenter new name: 'books' LibraryShell>>#createSchematicWiring "What the hell is this???" super createSchematicWiring LibraryShell>>model: aLibrary super model: aLibrary booksPresenter model: (aLibrary books) I can figure what createComponents is doing (binding an element from the view to an instance variable in the presenter?) and model: appears to be setting the model of the view element to a particular attribute from the model. But createSchematicWiring? From the docs suggested by Andy, I know what it was intended for, but what do I do with it? Oh, well, press on... Next step, create a view with a list box for all the books in the library. I fired up View Composer, and built a ShellView with a single ListPresenter called 'books'. Save it as the default view of LibraryShell. In the workspace do: LibraryShell showOn: lb where lb is the instance of Library I have been using for test. It shows with the list of books displayed. Hooray. Add a book to the model in the workspace: it shows in the view. Fantastic. Excited, I close the view, and reopen it in the View Composer. Add another list presenter and call it availableBooks. Go back to the presenter and make the following changes: Shell subclass: #LibraryShell instancevariableNames: 'booksPresenter availableBooksPresenter' classVariableNames: '' poolDictionaries: '' classinstanceVariableNames: '' LibraryShell>>#createComponents super createComponents. booksPresenter := self add: ListPresenter new name: 'books' availableBooksPresenter := self add: ListPresenter new name: 'availableBooks' LibraryShell>>#createSchematicWiring "What the hell is this???" super createSchematicWiring LibraryShell>>model: aLibrary super model: aLibrary booksPresenter model: (aLibrary books) availableBooksPresenter model: (aLibrary availableBooks) I didn't expect this to work because this attribute is not a ListModel. In fact, it is a derived attribute...But what the heck, run the test... The shell shows, with two lists, correctly showing all books and available books. Check a book out in the workspace, though, and the view does not update. This is what I expected. Now we are really deep into my ignorance, and the only lead I can find is PersonalMoney. None of the other examples really make use of MVP. Personal MoneyShell has: currentBalancePresenter model: (aPersonalAccount aspectValue: #currentBalance) aspectTriggers: #currentBalanceChanged My understanding is that the method that results in a model value changing should trigger an event like: PersonalAccount>>currentBalance: aNumber ... self triggers: currentBalanceChanged Now, in my model, the method that triggers the change in the model is checkOut:to: But the change is actually in the value of availableBooks. The only way I can see around this (and I have tried and it works) is to have an instance variable to represent availableBooks, with getters and setters for it. But I don't want that. Surely I don't have to compromise my design to the extent that every value I want to have in a view must be an instance variable in the model? I suspect not, but I just cannot figure out where to go next. I have tried all kinds of combinations using model:aspectTriggers:, but everything I tried gives a walkback. Eventually I realized the walkback was saying that model:aspectTriggers is not understood. And true enough, it is not there in any class in the heirarchy. Intrigued, I do: PersonalMoneyShell show Clicking New (for new account) gives exactly the same error! I really need some info about MVP now. I have read all the references given by Andy Bower in his first 'night school' post, but the problem there is that all the writing is either abstract, or it only covers basic easy uses. The examples are all really simple, except for personal money, and that doesn't work. I know that Dolphins MVP is not broken, because it is being used by the tool itself! I have tried to read some of the tool source, but it is all very complex, and I can't begin to figure it in the short time I have. If someone can show me just how to get a working view on the model I have given here (i.e. with the derived attribute, not with all instance variables) I think that may give me the breakthrough I need. If I could get a working copy of PersonalMoney, that would also be a great help. Thanks Phil |
Phil,
I'd say your example is perfectly sound, and your approach is certainly reasonable. I hope it gives good info to those in the know about what could be made clearer in the introduction to MVP. I'd give you the answers if I had them, but I don't (I haven't done any GUI stuff in Dolphin). And I'm sure curious to hear what they are. Thanks, Peter van Rooijen "Phil Lewis" <[hidden email]> wrote in message news:91qvub$qa$[hidden email]... > OK. I've been whining about not understanding MVP for a few weeks now, and > last night I thought 'This is daft, you've successfully designed and coded > [x number of complex sytems], and you can't work out a few little classes?'. > So I sat down to suss it out. > > The following is a very full description of what I tried to do, and the > compromises I had to make to get it working. > > If anyone who knows Dolphin MVP can read it and let me know where I am going > wrong, I would be immeasurably grateful. > > Obvious thing: Pick a simple domain, and implement a tiny app to just try > out the MVP stuff. > > A library. People can add books, check books out and return them. A book has > a name. I want to know what books are in the library, and which are > currently available (i.e. not on loan). > > NOTE: The following code was transcribed by hand. My machine with Dolphin is > my works laptop, and my machine with ng access is my personal box. The two > will not speak to each other yet, so I am writing this with my laptop beside > my desktop, and copying the code by hand. Any syntax errors are a result of > this process. I have the code running, honest. If you want a package, I can > send it. > > I always start with domain classes, so I did this: > > Object sublass: #Library > instanceVariableNames: 'books' > classVariableNames: '' > poolDictionaries: '' > classInstanceVariableNames: '' > > Library(class)>>#new > ^super new initialize > > Library>>#initialize > books := ListModel with: > (SortedCollection sortBlock: [:x :y | x title <= y > title]) > > I chose ListModel because I had it in mind that I would be using a > ListPresenter later to view the books. As I understand, the ListModel does > things with events, so we don't have to, and I like the sound of that. I > chose a SortedCollection because its as easy as anything else in > > Then we need to be able to add books to the Library, and check books out. > > Library>>#addBook: aBook > books add: aBook > > Library>>#checkOut: aBook to: aPerson > aBook onLoanTo: aPerson > > I figured the easiest way to approach this is to tell a book that it is on > loan. Book will come later (I'm trying to program by intention just a > bit). > Returning books then also becomes easy: > > Library>>#return: aBook > aBook beAvailable > > The only other thing the Library needs to be able to do is tell us about all > its books, and about its currently available books: > > Library>>#books > ^books > > Library>>#availableBooks > ^books select: [:each | each isAvailable] > > All we need now is a book that knows how to be on loan, and be availble. > > Object sublass: #Book > instanceVariableNames: 'title borrower' > classVariableNames: '' > poolDicationaries: '' > classInstanceVariableNames: '' > > Book needs some basic things: > > Book(Class)>>#title: aString > ^super new title: aString > > Book>>#title: aString > title := aString > > Book>>#title > ^title > > Book>>#printOn: aStream (also #displayOn the same) > aStream nextPutAll: title > > Now it needs its specific behaviour. According to the Library, we need it > be able to be on loan to a person, make itself available, and answer if it > is available: > > Book>>#onLoanTo: aPerson > borrower := aPerson > > Book>>#isAvailable > ^borrower isNil > > Book>>#beAvailable > borrower := nil > > This was the simplest way I could imagine to fulfil the requirements. > > Using a workspace to create a few books, add them to the library check a > couple out, and view availableBooks gives correct results. There is a > caveat. This depends on a book being returned as the same object that was > checked out. Its not prime time code, for sure. I'm not looking for > robustness, just a simple working model. A book is always checked to > new' as its person. (I was hoping get as far as adding people, but, well...) > > Now we are going to need a Presenter. As I am unsure about MVP, I decided to > play (what I think is) safe, and I dragged my two classes onto Model, so > that they become subclasses of model. Run the test again. Everything cool. > > Define a Presenter. I am going to start with one listbox just for the > collection of all books: > > Shell subclass: #LibraryShell > instancevariableNames: 'booksPresenter' > classVariableNames: '' > poolDictionaries: '' > classinstanceVariableNames: '' > > I did a defaultModel method, eventhough I won't use it (bad boy). > > LibraryShell(Class)>>#defaultModel > ^Library new > > Then the guess work starts. > > LibraryShell>>#createComponents > super createComponents. > booksPresenter := self add: ListPresenter new name: 'books' > > LibraryShell>>#createSchematicWiring > "What the hell is this???" > super createSchematicWiring > > LibraryShell>>model: aLibrary > super model: aLibrary > booksPresenter model: (aLibrary books) > > I can figure what createComponents is doing (binding an element from the > view to an instance variable in the presenter?) and model: appears to be > setting the model of the view element to a particular attribute from the > model. But createSchematicWiring? From the docs suggested by Andy, I know > what it was intended for, but what do I do with it? Oh, well, press on... > > Next step, create a view with a list box for all the books in the library. > fired up View Composer, and built a ShellView with a single ListPresenter > called 'books'. Save it as the default view of LibraryShell. > > In the workspace do: > > LibraryShell showOn: lb > > where lb is the instance of Library I have been using for test. It shows > with the list of books displayed. Hooray. Add a book to the model in the > workspace: it shows in the view. Fantastic. > > Excited, I close the view, and reopen it in the View Composer. Add another > list presenter and call it availableBooks. > > Go back to the presenter and make the following changes: > > Shell subclass: #LibraryShell > instancevariableNames: 'booksPresenter availableBooksPresenter' > classVariableNames: '' > poolDictionaries: '' > classinstanceVariableNames: '' > > LibraryShell>>#createComponents > super createComponents. > booksPresenter := self add: ListPresenter new name: 'books' > availableBooksPresenter := self add: ListPresenter new name: > 'availableBooks' > > LibraryShell>>#createSchematicWiring > "What the hell is this???" > super createSchematicWiring > > LibraryShell>>model: aLibrary > super model: aLibrary > booksPresenter model: (aLibrary books) > availableBooksPresenter model: (aLibrary availableBooks) > > I didn't expect this to work because this attribute is not a ListModel. In > fact, it is a derived attribute...But what the heck, run the test... > > The shell shows, with two lists, correctly showing all books and available > books. Check a book out in the workspace, though, and the view does not > update. This is what I expected. > > Now we are really deep into my ignorance, and the only lead I can find is > PersonalMoney. None of the other examples really make use of MVP. Personal > MoneyShell has: > > currentBalancePresenter model: (aPersonalAccount aspectValue: > #currentBalance) aspectTriggers: #currentBalanceChanged > > My understanding is that the method that results in a model value changing > should trigger an event like: > > PersonalAccount>>currentBalance: aNumber > ... > self triggers: currentBalanceChanged > > Now, in my model, the method that triggers the change in the model is > checkOut:to: But the change is actually in the value of availableBooks. > > The only way I can see around this (and I have tried and it works) is to > have an instance variable to represent availableBooks, with getters and > setters for it. But I don't want that. Surely I don't have to compromise > design to the extent that every value I want to have in a view must be an > instance variable in the model? I suspect not, but I just cannot figure out > where to go next. I have tried all kinds of combinations using > model:aspectTriggers:, but everything I tried gives a walkback. > > Eventually I realized the walkback was saying that model:aspectTriggers is > not understood. And true enough, it is not there in any class in the > heirarchy. Intrigued, I do: > > PersonalMoneyShell show > > Clicking New (for new account) gives exactly the same error! > > I really need some info about MVP now. I have read all the references > by Andy Bower in his first 'night school' post, but the problem there is > that all the writing is either abstract, or it only covers basic easy uses. > The examples are all really simple, except for personal money, and that > doesn't work. > > I know that Dolphins MVP is not broken, because it is being used by the tool > itself! I have tried to read some of the tool source, but it is all very > complex, and I can't begin to figure it in the short time I have. > > If someone can show me just how to get a working view on the model I have > given here (i.e. with the derived attribute, not with all instance > variables) I think that may give me the breakthrough I need. > > If I could get a working copy of PersonalMoney, that would also be a great > help. > > Thanks > > Phil > > > |
In reply to this post by Phil Lewis-2
Phil,
If nobody beats me to it I'll reply to the rest of your post tomorrow. > currentBalancePresenter model: (aPersonalAccount aspectValue: > #currentBalance) aspectTriggers: #currentBalanceChanged > If I could get a working copy of PersonalMoney, that would also be a great > help. The package has got a set of brackets missing. It should read currentBalancePresenter model: ((aPersonalAccount aspectValue: #currentBalance) aspectTriggers: #currentBalanceChanged) Ian |
In reply to this post by Phil Lewis-2
Phil,
OK. Let's have a go then. The following uses your original Book class and a slightly modified Library class. We'll come to the LibraryShell later. I think the best way to proceed is to take a step back and return to a basic MVP set-up. Your original version shared a ListModel between your Library and the LibraryShell's bookPresenter. While this is fine, and we will return to that technique later, for explanation purposes it somewhat obscures what is going on. I should also point out that by choosing Lists, rather than simple Strings or Integers, for your example you have probably made it a bit more difficult to understand. Anyway.... The Book class is the same as your original ==== Book class title: aString ^super new title: aString ==== Book isAvailable ^borrower isNil onLoanTo: aPerson borrower := aPerson beAvailable borrower := nil printOn: aStream aStream nextPutAll: title title ^title title: anObject title := anObject The Library classes has some changes - detailed after each method ==== Library books ^books getBook: aString ^self books detect: [:each | each title = aString] ifNone: [self error: 'Missing book'] This is an addition to get over the problem you had of needing to specify the actual book object in the list. Now, when you borrow or return a book you can just specify the title and the correct Book object is located from the library. initialize super initialize. books := SortedCollection sortBlock: [:a :b | a title <= b title] Major change. We make no assumptions about who will be using the Library so rather than using a ListModel we store the Books in a SortedCollection Small change - I find it advisable that all initialize methods supersend first to prevent surprises. checkOut: aString to: aPerson (self getBook: aString) onLoanTo: aPerson. self trigger: #libraryStatusChanged When a book is loaned out we locate the book, signal it is on loan and trigger a #libraryStatusChanged event (see below) addBook: aBook books add: aBook. self trigger: #libraryChanged When the full library is changed we trigger a #libraryChanged event availableBooks ^books select: [:each | each isAvailable] return: aString (self getBook: aString) beAvailable. self trigger: #libraryStatusChanged When a book is returned out we trigger a #libraryStatusChanged event Why the triggers?. The Library is now a stand alone module (model). It has no knowledge about who owns it or what they need to know. By using the triggered events the Library is just letting anybody who is interested know that it has changed in some way. It is up to the observers to register an interest in an event that the library triggers and then be prepared to deal with that event in an appropriate way. ==== LibraryShell class defaultModel ^Library new As you did I've used a default model but, because we will pass a fully initialized Library to the LibraryShell when it is opened this default model is not used. It would probably be better to have a singleton Library object in the image and use that here but that can wait for later. ==== LibraryShell createSchematicWiring super createSchematicWiring. self model when: #libraryChanged send: #onLibraryChanged to: self; when: #libraryStatusChanged send: #onLibraryStatusChanged to: self So what the hell does this do? This connects our model, an instance of the Library class, to the receiver, an instance of LibraryShell - it connects the Model and the Presenter parts of the MVP triad. The LibraryShell knows about the Library, it has to be able to register for the events that the Library triggers and request information from it, but the Library has no knowledge about the LibraryShell - it just triggers the appropriate events and returns the current information when asked. The above is simply saying that when the LibraryShell triggers an #libraryChanged event (see Library>>addBook:) then the LibraryShell>>onLibraryChanged method must be evaluated createComponents super createComponents. booksPresenter := self add: ListPresenter new name: 'books'. availableBooksPresenter := self add: ListPresenter new name: 'availableBooks'. onLibraryStatusChanged availableBooksPresenter list: self model availableBooks When the Library made a change to the borrowed status of one of the books it triggered a #libraryStatusChanged event. The LibraryShell>>connectSchematicWiring method registered the LibraryShell for that event and set the resulting action to be the evaluation of this method. We know the list of available books has changed (because the Library triggered the event) so we just ask the Library for the new list and pass that on to the availableBooksPresenter. _It is up to the availableBooks MVP triad (ListModel/ListView/ListPresenter) to arrange for the display to be updated_ onViewOpened super onViewOpened. self onLibraryChanged. self onLibraryStatusChanged This is an addition. We have to arrange it so that when the LibraryShell is opened that the lists held by the booksPresenter and availableBooksPresenter are initialised with the current state of the Library. We could do that by sending #list: to the presenter but, complying with the "do it once" principle, we will just simulate the triggering of the #libraryChanged and #libraryStatusChanged events which we know will update the list. onLibraryChanged booksPresenter list: self model availableBooks. self onLibraryStatusChanged See the explanation for #onLibraryStatusChanged. The only difference is that the Library has triggered the #libraryChanged event rather than the #libraryStatusChanged event We also recognise here that as we have two presenters displaying the list of books we need to update both when the Library changes. The Library has no knowledge of this, it just knows that the books it contains has changed and triggered the #libraryChanged event. It is up to us, by simulating a #libraryStatusChanged event to ensure the other list is also updated. Testing. Evaluate is a workspace lib := Library new. lib addBook: (Book title: 'Jude The Obscure'); addBook: (Book title: 'Far From The Madding Crowd'); addBook: (Book title: 'The Return Of The Native'); addBook: (Book title: 'Under The Greenwood Tree'). LibraryShell showOn: lib. Add a new book to the Library lib addBook: (Book title: 'The Trumpet Major') Borrow a book and it should disappear from the appropriate list lib checkOut: 'The Return Of The Native' to: 'Me' Return it and it should reappear lib return: 'The Return Of The Native' What I was trying to illustrate here is the way that events are used to communicate between a Model and a Presenter, informing the Presenter that a change of state has occurred and that it should perform any actions in feels necessary. The next episode will show how you can replace the SortedCollection in Library with a ListModel shared between Library and the booksPresenter MVP triads. This will mean that the #libraryChanged event and associated methods in Library can be removed. Ian |
In reply to this post by Phil Lewis-2
In comp.lang.smalltalk.dolphin, "Phil Lewis" <[hidden email]> wrote:
[...stuff...] I think changes something like the following should do the job. I don't guarantee it's good style tho', and I haven't tested it _at all_... >Library>>#checkOut: aBook to: aPerson > aBook onLoanTo: aPerson. self trigger: #availableChanged. >Library>>#return: aBook > aBook beAvailable. self trigger: #availableChanged. >LibraryShell>>#createSchematicWiring > "What the hell is this???" "As I understand it it's for specifying where triggered events are routed to for handling" > super createSchematicWiring "we're going to handle any #availableChanged events triggered on our model (which should be a Library) ourselves" self model when: #availableChanged send: #onAvailableChanged to: self. LibraryShell>>#onAvailableChanged "The available books have been changed so the relevant sub-presenter needs refreshing" "Simple brute force and ignorance approach, we just refetch the availableBooks and plug them in again." availableBooksPresenter model: (self model availableBooks). -- Kapusniak, Stefan e |
In reply to this post by Phil Lewis-2
Hi Phil,
Looks good so far, let me try to give some hints. > But createSchematicWiring? From the docs suggested by Andy, I know > what it was intended for, but what do I do with it? Oh, well, press on... > The #createSchematicWiring can be seen as a guard on watch, waiting to see if anything happens. If something happens in the environment (really, everything, keyboard events, to mouse events, window size changes, window focus changes, data coming in from an external source, model changes and so on), he (the guard) checks his list of things (triggers) he has to react to. If the event is on the list, he will fire off the specified method. > > LibraryShell>>model: aLibrary > super model: aLibrary > booksPresenter model: (aLibrary books) > availableBooksPresenter model: (aLibrary availableBooks) The #model: method is run once when you open the view, therefore the Library>>availableBooks method is run and returns the available books at the time of opening the view. > > currentBalancePresenter model: (aPersonalAccount aspectValue: > #currentBalance) aspectTriggers: #currentBalanceChanged > Like Ian said, this should have extra brackets, otherwise the message #model:aspectTriggers: is sent to the presenter. The intention of this line of code is: Connect the currentBalancePresenter to the result of: ((aPersonalAccount aspectValue: #currentBalance) aspectTriggers: #currentBalanceChanged) And this line of code can be taken apart as: 1. (aPersonalAccount aspectValue: #currentBalance) and 2. (anAspect) aspectTriggers: #currentBalanceChanged In line 1. the currentBalance instance variable is wrapped into an Aspect. The advantage of using aspects is that they have a common language, independent of what they contain, they can be set with the message #value: and can be read with the message #value. They basically convert the accessors in the model (like #currentBalance and #currentBalance:) to #value:/#value. On top of that they automatically trigger any changes to the ir contents. The limitations of the Aspects are that they can only wrap around 'simple' objects, like strings, numbers, dates. Line 2 tells the aspect that when the specified trigger is broadcasted, the value wrapped in the aspect is changed. This is basically a shortcut to what happens automatically anyway, but as we know exactly when the balance changes (namely when we create a transaction), we can explicitly inform the aspect, which saves the system to have to go through all the aspects and see if they map to the one that has changed. > My understanding is that the method that results in a model value changing > should trigger an event like: > > PersonalAccount>>currentBalance: aNumber > ... > self triggers: currentBalanceChanged > > Now, in my model, the method that triggers the change in the model is > checkOut:to: But the change is actually in the value of availableBooks. > > The only way I can see around this (and I have tried and it works) is to > have an instance variable to represent availableBooks, with getters and > setters for it. But I don't want that. Surely I don't have to compromise > design to the extent that every value I want to have in a view must be an > instance variable in the model? I suspect not, but I just cannot figure out > where to go next. You certainly don't have to have an instance variable for it. You just want the #availableBooks method to be run whenever something happens with any of the books in the library. You could for example add a method #updateAvailableBooks to the presenter: LibraryShell>>updateAvailableBooks availableBooksPresenter model: (aLibrary availableBooks) This would run the #availableBooks again and reconnect it to the presenter. Then you need to tell the shell when to run this code. We can use our guard for this: LibraryShell>>createSchematicWiring super createSchematicWiring. self model when: #availableBooksChanged send: #updateAvailableBooks to: self. The last thing you then need to do is fire off the trigger whenever the available books might have changed, like with adding books, borrowing and returning. They all need to have the following line: self trigger: #availableBooksChanged This is about how it should work, I haven't tested it, there might be some details missing, but it should give you some idea. Hope this helps, Ted |
In reply to this post by Ian Bartholomew-3
Stage 2 - replace the SortedCollection in the Library class with a ListModel
and share the ListModel with the LibraryShell's bookPresenter. This will mean that we can wash our hands of all responsibility for ensuring the Library and bookPresenter stay in sync. Changes to Library addBook: aBook books add: aBook. self trigger: #libraryStatusChanged Originally this triggered a #libraryChanged event but we are not now using that, see later. We still need to know when the library has changed though as LibraryShell has to update the available list as well. NB This is a temporary workaround and will be removed in the next version initialize super initialize. books := ListModel with: (SortedCollection sortBlock: [:a :b | a title <= b title]) We now wrap the books SortedCollection in a ListModel. A side effect of this, naturally, is that the unchanged #books method now answers a ListModel rather than a SortedCollection LibraryShell has a few more changes. createSchematicWiring super createSchematicWiring. self model when: #libraryStatusChanged send: #onLibraryStatusChanged to: self We have just removed references to the #libraryChanged event that is no longer triggered by Library model: aLibrary super model: aLibrary. booksPresenter model: aLibrary books When we created out booksPresenter, in the createComponents method, it used the ListPresenter>>defaultModel method to create the Model for the MVP triad. What we are doing here is replacing _that_ ListModel with the one from our Library - remember #books now answers a ListModel. _Important_ When we send #model: to the booksPresenter it, via a few methods starting at Presenter>>model:) eventually arrives at IconicListAbstract>>connectModel. This registers itself as an interested party for the events triggered by the ListModel. When, for example, our Library adds a book to the Library the ListModel (books) will trigger an #item:addedAtIndex: event. Because the ListPresenter (booksPresenter) has now registered for this event it will be informed and can arrange for the associated ListView (the ListModel/ListView/ListPresenter MVP triad it is part of) to be updated. We need to do nothing as this will happen automatically due to the link between the ListPresenter and Library ListModel that we set up above. onLibraryChanged Removed as Library no longer triggers this event onViewOpened super onViewOpened. self onLibraryStatusChanged We still need to initialise the availableBooksPresenter So what we have is three MVP triads working together #1 A ListModel/Shell/ShellView. This is comprised of the ListModel held in the Library instance and the Shell/ShellView that we created for LibraryShell #2 A ListModel/ListPresenter/ListView. This is comprised of the ListModel held in the Library instance and the ListPresenter/ListView created in the LibraryShell>>createComponents method and referenced by the booksPresenter instVar. Because it shares a model with #1 then we need have no further dealings with it - it will sort out all the event triggering itself. #3 A ListModel/ListPresenter/ListView. This is comprised of a separate MVP, the ListModel is the default created by the ListPresenter we created in the LibraryShell>>createComponents method and referenced by the availableBooksPresenter instVar. Because there is NO connection between the Library and this MVP we have to do all the triggering and updating (of the ListModel's list) ourselves. However the MVP triad sorts out the triggers between _it's_ ListModel/ListPresenter/ListView by itself. By sharing a ListModel between Library and #2 we have removed the need for any intervention by us. However the Library still has some knowledge that it does not need. It is not the job of the Library to notify the LibraryShell, via the trigger in Library>>addBook:, that LibraryShell needs to update it's availableBooksPresenter. That linkage will be removed in episode three. Ian |
Ian, This is Phil - original asker - at a diggrent machine again!
What you have said so far is brilliant. Thanks. It is beginning to sink in. I haven't tied any of it yet (I'm reading this in my 10 minute lunch break :-( XMas soon :-) I have one question so far, and this is a bit I have never managed to get an insight into. The model is raising events, and createSchematicWiring is basically sort of mutating those into other events with when:send:to:, eg: self model when: #libraryStatusChanged send: #onLibraryStatusChanged to: self So, the Library raises #libraryStatusChanged, and this is caught somewhere up the chain by cSW, which then sends #onLibraryStatusChanged to Self, where self is the LibraryShell. So what does the LibraryShell do with #onLibraryStatusChanged? Do we write some kind of message handler? Thisbit is really unclear to me... Really, thanks for this. Phil Sent via Deja.com http://www.deja.com/ |
In reply to this post by Ian Bartholomew
Stage 3 - Remove the #libraryStatusEvent trigger from Library and replace
it's function with events triggered from the ListModel. A bit of explanation first. The ListModel that Library holds, containing the collection of Books, can have two main operations performed on it. #1 The collection of books is changed - a book is either added or a book is removed from the ListModel's collection #2 A change is made to one of the Books contained within the ListModel - a book is loaned and it's internal state changes to reflect the borrowing. #1 is easy for the ListModel to cope with. The only way to change the ListModel's collection (without changing the complete list) is to go through it's #addXXX or #removeXXX methods. The ListModel can then trigger an appropriate event for anybody who is interested. #2 is not so easy for ListModel as a Book can be changed without going through the ListModel itself. e.g. x := ListModel with: aCollectionOfBooks. (aCollectionOfBooks at: 1) beAvailable Because of this ListModel provides a method that can be called to tell it that a Book's state has been changed and the ListModel should trigger an appropriate event. x := ListModel with: aCollectionOfBooks. (aCollectionOfBooks at: 1) beAvailable. x updateAtIndex: 1 Changes to the package - Book - as before Library addBook: aBook books add: aBook Removed the #libraryStatusEvent trigger checkOut: aString to: aPerson | book | book := self getBook: aString. book onLoanTo: aPerson. books updateAtIndex: (books indexOf: book) See the comments above, We are performing the same operation on the Book as before, setting it's loan state, but we now also inform the ListModel that an item in it's list has changed internally. We don't care what it does with that information or whether it needs to know - we just tell it that it has occured. return: aString | book | book := self getBook: aString. book beAvailable. books updateAtIndex: (books indexOf: book) As above LibraryShell is now left with just these three instance methods createComponents super createComponents. booksPresenter := self add: ListPresenter new name: 'books'. availableBookPresenter := self add: ListPresenter new name: 'availableBooks' Just as before model: aLibrary super model: aLibrary. booksPresenter model: aLibrary books. self libraryChanged. booksPresenter model when: #item:addedAtIndex: send: #libraryChanged to: self; when: #itemUpdatedAtIndex: send: #libraryChanged to: self I've moved the code that does the initial update of the availableBooksPresenter's list from the #onViewOpened method to here. This is the first place it can be done (we need to know the booksPresenter model to be able to get its list) so we might as well do it here. The other two lines connect this Presenter to events triggered from the ListModel. When either an item is updated or an item is changed we want to ensure that the availableBooksPresenter is updated as well. Note that other interested parties may already be monitoring the ListModel's events, the booksPresenter's associated ListView for one. Adding event receptors doesn't affect them, we are now notified of the event in _addition to anyone else. libraryChanged availableBooksPresenter list: self model availableBooks The previous #onViewOpened method, renamed for clarity And that is about it. When the Library adds or changes an item in the list it, either automatically (add/remove) or by request (update) causes the ListModel to trigger an event. The LibraryShell receives notification of these events and updates the availableBooksPresenter. The final step will add a couple of simple TextViews to the Library, showing the last book borrowed and the last book returned, to illustrate the use of #triggersEvent Ian Out of interest - is anone reading these? |
In reply to this post by plewis66
In comp.lang.smalltalk.dolphin, [hidden email] wrote:
>The model is raising events, and createSchematicWiring is basically >sort of mutating those into other events with when:send:to:, eg: > >self model > when: #libraryStatusChanged > send: #onLibraryStatusChanged > to: self Not _exactly_. Bear in mind that although #createSchematicWiring sets up the routing tables, it leaves the _actual execution_ of that routing (at the point you use #trigger:) to the object to which you send #when:send:to. An interesting method to look at is Object>>#when:perform: which #when:send:to uses. If you look at the two arguments of that method, you'll notice that they're called 'anEventSymbol' and 'aMessageSend' respectively. This means that yes, #libraryStatusChanged in the example above is an 'event', but #onLibraryStatusChanged is instead a completely normal message selector which if you trace your way through Object>>#notify: all the way down into Message>>#value: you'll see eventually gets #perform-ed. Therefore in the example above Ian's commited himself to implementing a method called 'onLibraryStatusChanged' in 'LibraryShell'. If instead Ian had written: self model when: #libraryStatusChanged send: #onPigsFly to: self ...he'd have to implement an 'onPigsFly' method instead. There's also nothing to stop you from specifying a message to a method that already exists if that method does what you want. -- Kapusniak, Stefan e |
In reply to this post by plewis66
Phil,
> The model is raising events, and createSchematicWiring is basically > sort of mutating those into other events with when:send:to:, eg: > > self model > when: #libraryStatusChanged > send: #onLibraryStatusChanged > to: self > > So, the Library raises #libraryStatusChanged, and this is caught > somewhere up the chain by cSW, Nope. cSW is an _initialisation_ method. It is only called once, when the LibraryShell is created. What it does is add an item to a collection held in an instVar called 'events' maintained by the receiver - self model (a Library instance) in this case. This events collection stores the trigger (#libraryStatusChanged), the method selector to be used (#onLibraryStatusChanged) and the object that the selector should be sent to. Aside - add a self halt to the above and when LibraryShell halts evaluate the following, in the debugger source view, to inspect the 'events' instVar self model events inspect At some later stage the Library will evaluate one of the trigger statements. This will be evaluated by Object>>trigger which, after some contortions, will locate all the items in the events collection _associated with the triggerer's instance_ that relate to the trigger that is being fired. It will then go through all of the class/selector pairs associated with the event and, for each pair, perform the selector on it's associated receiver class - in the above it will send #onLibraryStatusChanged to self (which will have been stored as the instance of LibraryShell in which the cSW method above was evaluated). So every object that has registered an interest in an event trigger will be notified as part of the evaluation of the #trigger statement by the object triggering the event. _Any statements following the trigger statement will not be evaluated until all the parties interested in the triggered event have been notified_ There's a bit more to it, there are other classes involved in storing the events, you can add arguments and it has to cope with objects that have disappeared but the above is a rough idea. > which then sends #onLibraryStatusChanged > to Self, where self is the LibraryShell. So what does the LibraryShell > do with #onLibraryStatusChanged? Do we write some kind of message > handler? Thisbit is really unclear to me... Yes. A bit later on in the message is a definition for a LibraryShell>>onLibraryStatusChanged method which just updates the list belonging to the availableBooksPresenter. NB: It is pretty standard in Dolphin to try to name events and the methods evaluated when the event occurs in a reasonably consistent way. So, as an example, a #modelChanged event will evaluate a method named #onModelChanged. It's not compulsory, and there are a few reasons for occasionally not following it, but it is a useful rule of thumb. Ian |
In reply to this post by Ian Bartholomew-3
Hi Ian,
> > Out of interest - is anone reading these? > > Maybe wrap it all up and put it on your web site as a MVP tutorial? Ted |
In reply to this post by Ian Bartholomew-3
Stage 4 - Add a couple of TextPresenters and views that will display the
last book checked out and the last book returned. The main purpose is to show how non-list objects are connected. Changes to the package Book - as before Library Add two instance variables to the class 'lastCheckOut' and 'lastReturn' checkOut: aString to: aPerson | book | book := self getBook: aString. book onLoanTo: aPerson. books updateAtIndex: (books indexOf: book). self lastCheckOut: book title As before except for the last line - see below initialize super initialize. books :=- ListModel with: (SortedCollection sortBlock: [:a :b | a title <= b title]). lastCheckOut := String new. lastReturn := String new Just adds the initial state for the two lastXXX values. NB Use 'String new' and not '' lastCheckOut ^lastCheckOut lastCheckOut: aString lastCheckOut := aString. self trigger: #lastCheckOutChanged lastReturn ^lastReturn lastReturn: aString lastreturn := aString. self trigger: #lastReturnChanged The four accessors for the two new instVars. The setters trigger events, in this case the instVar name followed by 'Changed'. This is a pretty standard naming convention but you can use anything you like. As before, the methods don't care if nobody responds to the triggered event, their job is just to indicate a change has occurred. return: aString | book | book := self getBook: aString. book beAvailable. books updateAtIndex: (books indexOf: book). self lastReturn: book title Added a last line to set the lastReturn value LibraryShell - two new instVars named #lastCheckOutPresenter and #lastReturnPresenter. Edit the defaultView and add two TextPresenter.Static text views and name them 'lastCheckOut' and 'lastReturn' createComponents super createComponents. booksPresenter := self add: ListPresenter new name: 'books'. availableBooksPresenter := self add: ListPresenter new name: 'availableBooks'. lastCheckOutPresenter := self add: TextPresenter new name: 'lastCheckOut'. lastReturnPresenter := self add: TextPresenter new name: 'lastReturn'. Just added the creation of the two TextPresenters to the end onBooksUpdated availableBooksPresenter list: self model availableBooks No change model: aLibrary super model: aLibrary. booksPresenter model: aLibrary books. self onBooksUpdated. booksPresenter model when: #item:addedAtIndex: send: #onBooksUpdated to: self; when: #itemUpdatedAtIndex: send: #onBooksUpdated to: self. lastCheckOutPresenter model: ((self model aspectValue: #lastCheckOut) aspectTriggers: #lastCheckOutChanged). lastReturnPresenter model: ((self model aspectValue: #lastReturn) aspectTriggers: #lastReturnChanged). Added the last two lines. To break them down bit by bit - self model aspectValue: #lastCheckOut This answers an instance of the class ValueAspectAdaptor. It has one main purpose in life - to provide an adaptor between the receiver - self model, a Library instance - and something that is expecting to be able to access this object using the #value/#value: accessors. To explain that further. In the Red corner (figuratively speaking) we have a Library object that, for it's lastCheckOut aspect, expects to be sent the #lastCheckOut: message to set it's value and the #lastCheckOut message to get it's value. In the Blue corner (the lastCheckOutPresenter whose model we are trying to set) is an object that expects to be able to set it's associated model object by sending it the #value: selector and retrieve the current setting by sending the #value message. It needs to have this fixed interface because it wants to be usable, without any extra wiring, by instances of any class that needs it. These obviously won't fit together so what we need is another object that can sit between the two and convert sends to #value: from the right hand side into calls to #lastCheckOut: to the left hand side and calls to: #value into calls to #lastCheckOut. That's exactly what a ValueAspectAdaptor does. Have a browse round the class and you can see where it receives the name of the aspect (the #lastCheckOut argument in the line above) and constructs a put and get selector from it. What about the other direction I hear you cry. How does an instanceVariable indicate that it has been changed and what the new value is. The answer to the first part is aspectTriggers: #lastCheckOutChanged This just states that when the object whose value we want to adapt changes it will trigger a #lastCheckOutChanged event. The adaptor registers for this event with the receivers class and when the instVar does change the Adaptor is notified. All the Adaptor then does is (see the ValueModel>>notifyChanged method) trigger an event of it's own - #valueChanged which will itself have been the target of registrations by interested parties. The answer to the second part of the question is that, in this case, it doesn't. It is up to any interested parties to wait for a notification that the adaptee has changed and then come and fetch the value (by sending #value to the adaptor which converts it into #lastCheckOut to the adaptee of course) for itself. It _is_ possible for the triggered event to pass the new state of the adaptee with the trigger but we don't use that here. To summarise - A ValueAspectAdaptor Converts message sends to #value: sent to it into the appropriate message to set the adaptee (and then triggers a #valueChanged event). Converts message sends to #value into the appropriate getter message for the adaptee Optionally, is alert for a specified event being triggered from the adaptee to say that it's value has changed and then triggering a #valueChanged event itself. So, finally, what does the following actually do lastCheckOutPresenter model: ((self model aspectValue: #lastCheckOut) aspectTriggers: #lastCheckOutChanged). It sets the model for lastCheckOutPresenter to a ValueAspectAdaptor which is wrapping around the Library instance held in the LibraryShells's model. The TextPresenter is expecting to converse with it's model using #value/#value: and be informed when the model changes by way of the #valueChanged event. The Adaptor provides this interface towards the TextPresnter but knows that to actually converse with the Library it has to convert those interface words into #lastCheckOut:/#lastCheckOut and #lastCheckOutChanged. That's enough for now. Questions will, of course, be welcome but will have to wait until I've been shopping and wrapping presents - which is what I _really_ should have been doing today instead of pontificating so much. Requests to shut the hell up will also be considered very carefully <g> Ian |
In reply to this post by plewis66
> I have one question so far, and this is a bit I have never managed to
> get an insight into. > > The model is raising events, and createSchematicWiring is basically > sort of mutating those into other events with when:send:to:, eg: > > self model > when: #libraryStatusChanged > send: #onLibraryStatusChanged > to: self > > So, the Library raises #libraryStatusChanged, and this is caught > somewhere up the chain by cSW, which then sends #onLibraryStatusChanged > to Self, where self is the LibraryShell. So what does the LibraryShell > do with #onLibraryStatusChanged? Do we write some kind of message > handler? Thisbit is really unclear to me... Perhaps I can help by explaining some things that often aren't being explained when events are introduced: 1. The event system (i.e. the mechanism that makes event observation work) is completely implemented with Smalltalk code. So, there is no magic, and you could look at all the code that makes it work if you want to. A corrollary of this is that the event system is completely synchronous, i.e., events are being handled (i.e., event handlers - methods that are called when an event is triggered - are being executed) in the same thread of execution as the code that that triggered the event. 2. #createSchematicWiring is a name that would mean nothing to me if I didn't already know the essential constructs of programming with events. Perhaps a more intention revealing name would be #setUpModelEventHandlers, but I'm quite open to the possibility that that wouldn't actually be correct (perhaps, because it is too specific), not having done any Dolphin GUI coding. 3. a) Events are most intuitively seen as objects that represent the fact that a certain type of thing (for lack of a better word) has happened (at a certain point in time). An EventType would then represent the type of 'thing' that has happened. And an Event could be seen as a sort of instance of an EventType. b) But it never works that way. It could work that way, but a more lightweight (no need for Event and EventType classes) system does the job as well (in Smalltalk). The idea is exactly the same, though. c) How it is done is that <event type>s are represented by Symbols (representing the 'name' attribute an EventType object might have), and <event>s aren't represented by regular objects at all. Instead, they only 'live' on the execution stack (starting at #triggerEventOfType: anEventTypeSymbol - or some such, such as #trigger:, signalEvent:), while their event handlers are being executed. d) When all the event handlers have completed execution, the execution stack frame representing the execution of #triggerEventOfType: anEventTypeSymbol is dropped from the execution stack, and there is no trace left in the system of the <event>. Only the effects of the invoked event handler code remind us that the event has occurred (has been triggered). e) So, a way of looking at this might be to think that <event>s are anonymous sub-objects. 4. a) Question: Why would you want to program with events? Answer: There are situations where you are aware that something might need to be done at a specific point in code you are writing, but you can't know in advance who (which object(s)) is/are going to do it (or even if any object is going to do anything). b) The only option available to you is (coding by intention) to simply state that some type of 'thing' has occurred. You can't do anything else. You'll have to rely on other code (than you are writing at the moment), to make sure all the code that must be executed is in fact executed. c) So, all you can do is write self trigger: #myThingHasHappened. And it has to be the method #trigger:, which is inherited, that has to make sure everything that needs to be done, is done. 5. a) Question: So how does #trigger: do that? Answer: Every object that understands #trigger: has access to its own list of messages it needs to send, for each <event type>. Since <event type>s are represented by Symbols, this can be done easily by having an instance variable called (say) 'eventHandlers', in which a Dictionary is kept with Symbol keys (for the <event type>s), and values which are (ordered) collections of <evaluable object>s, often <message>s (say instances of DirectedMessage, which know at least the receiver and the selector - a selector is a Symbol which is also used as the name of a method). b) There is often a class (like Model) in your image, that has just such an instance variable, and its implementation of #trigger: looks up the collection of <message>s associated with the Symbol key that is passed as the argument of #trigger:, and sends #evaluate, or some such, to every element in the collection. When an instance of DirectedMessage receives #evaluate, it sends its selector to its receiver. c) This sending of the selector to the receiver is done using the #perform: mechanism, which is built into all Smalltalks. It is easiest explained with examples: - '3 class' yields the same result as '3 perform: #class' - '3 + 4' yields the same result as '3 perform: #+ with: 4' - '3 between: 2 and: 4' yields the same result as '3 perform: #between:and: withArguments: #(2 4)' As you might expect, #perform: is executed by the virtual machine directly. All objects understand it. d) Other objects can usually also trigger events, but their list of messages to send is not stored in one of their instance variables, but in a (global) IdentityDictionary, kept somewhere in the system (e.g., in a class variable of Object). Of course, this takes extra indirection to find the correct list of things to do. So, this is slower, but saves space, because objects that don't trigger any events, don't require any storage for their list(s) of things to do. 6. a) Question: But how does the triggering object know what messages need to be sent? Answer: It is informed of messages to be sent in the time period between its creation (when its lists of messages to send are empty), and the time (each time) it executes #trigger:. That time period may be significant; it could be a microsecond, but it could also be ten days. b) Question: So how is the triggering object informed? Answer: Any object (can be many, and it can do it to itself) can send it messages of the form #when: eventTypeSymbol send: selector to: receiver. By doing that, they ask the (observed) object to make sure the specified <message> is sent when the specified <event type> is triggered. Adding a proper DirectedMessage object to the proper list of things to do is the typical reaction. In jargon, the proper event handler is registered. I guess this is about all there is to it. To summarize: - All objects can trigger events, by sending self #trigger: eventTypeSymbol. - Objects that trigger events, allow other objects to tell them to evaluate an evaluable object if and when they trigger an event, specifying the type of event. - If an object actually triggers an event, it first evaluates all the evaluable objects it has been asked to evaluate, then continues on its merry way. If anything is still unclear, let me know. Final point: The event system is different from the exception system, but also similar in some ways. Exception types and Exception instances are typically first class objects. With exceptions the issue is not that you don't know what should be done when a particular type of situation occurs, but you don't know *if* it will occur (and/or at what specific point in the code). Exception handling can easily be combined with event handling by triggering a event in the exception handler code, and in that way you can also deal with uncertainty regarding who is going to need to do something. This final note just so you won't confuse the two. Hope this helps. Regards, Peter van Rooijen |
In comp.lang.smalltalk.dolphin,
"Peter van Rooijen" <[hidden email]> wrote: >2. #createSchematicWiring is a name that would mean nothing to me if I >didn't already know the essential constructs of programming with events. A quite horrible name. I agree. I seem to recall OA called it that, back in the mists of time when they were part of Intuitive, in order to match a once-planned GUI tool for wiring up events, that has never actually existed. Deprecating it, and bringing in a better named replacement, would certainly get my vote. But then again I don't personally have reams and reams of GUI code I'd need to update... >Perhaps a more intention revealing name would be #setUpModelEventHandlers, >but I'm quite open to the possibility that that wouldn't actually be correct >(perhaps, because it is too specific), not having done any Dolphin GUI >coding. Yes, a little too specific I think. One also tends to set up handlers for events triggered on either the Presenter or the sub-Presenters in #cSW, not just models. -- Kapusniak, Stefan e |
In reply to this post by Ian Bartholomew-3
<HUGE SNIP>
> Ian: > > Out of interest - is anone reading these? Oh, yes, I'm readsing them OK. Earlier, I just overlooked the method for OnLibraryStatusChanged. I was reading in a hurry. Now I am at home, I have hard copy, and I going to read them toroughly. It would be really valuable to newcomers to have something like this availabe easily.... Thanks for you efforts Ian. I'm pretty sure I'm beginning to see it now, but its things like the explanation of the event mechanism that are vital. One of the problems with tutorials is that they assuse that the reader is a new programmer, and that the the tricky bits can be explained as magic. I'm not comfortable with this. I want to know eactly what is going on. However your exaplanations look clear, and I think penny isDropping Thanks loads Phil |
In reply to this post by Ian Bartholomew-3
YES I DO !!!!
IT'S A BRILLIANT EXPLANATION OF MVP. THANKS "Ian Bartholomew" <[hidden email]> wrote in message news:91t11p$o5m$[hidden email]... > Stage 3 - Remove the #libraryStatusEvent trigger from Library and replace > it's function with events triggered from the ListModel. > > A bit of explanation first. The ListModel that Library holds, containing the > collection of Books, can have two main operations performed on it. > > #1 The collection of books is changed - a book is either added or a book is > removed from the ListModel's collection > > #2 A change is made to one of the Books contained within the ListModel - a > book is loaned and it's internal state changes to reflect the borrowing. > > #1 is easy for the ListModel to cope with. The only way to change the > ListModel's collection (without changing the complete list) is to go through > it's #addXXX or #removeXXX methods. The ListModel can then trigger an > appropriate event for anybody who is interested. > > #2 is not so easy for ListModel as a Book can be changed without going > through the ListModel itself. e.g. > > x := ListModel with: aCollectionOfBooks. > (aCollectionOfBooks at: 1) beAvailable > > Because of this ListModel provides a method that can be called to tell it > that a Book's state has been changed and the ListModel should trigger an > appropriate event. > > x := ListModel with: aCollectionOfBooks. > (aCollectionOfBooks at: 1) beAvailable. > x updateAtIndex: 1 > > Changes to the package - > > Book - as before > > Library > > addBook: aBook > books add: aBook > > Removed the #libraryStatusEvent trigger > > checkOut: aString to: aPerson > | book | > book := self getBook: aString. > book onLoanTo: aPerson. > books updateAtIndex: (books indexOf: book) > > See the comments above, We are performing the same operation on the Book > before, setting it's loan state, but we now also inform the ListModel that > an item in it's list has changed internally. We don't care what it does with > that information or whether it needs to know - we just tell it that it has > occured. > > return: aString > | book | > book := self getBook: aString. > book beAvailable. > books updateAtIndex: (books indexOf: book) > > As above > > LibraryShell is now left with just these three instance methods > > createComponents > super createComponents. > booksPresenter := self add: ListPresenter new name: 'books'. > availableBookPresenter := self add: ListPresenter new name: > 'availableBooks' > > Just as before > > model: aLibrary > super model: aLibrary. > > booksPresenter model: aLibrary books. > self libraryChanged. > > booksPresenter model > when: #item:addedAtIndex: send: #libraryChanged to: self; > when: #itemUpdatedAtIndex: send: #libraryChanged to: self > > I've moved the code that does the initial update of the > availableBooksPresenter's list from the #onViewOpened method to here. This > is the first place it can be done (we need to know the booksPresenter > to be able to get its list) so we might as well do it here. > > The other two lines connect this Presenter to events triggered from the > ListModel. When either an item is updated or an item is changed we want to > ensure that the availableBooksPresenter is updated as well. > > Note that other interested parties may already be monitoring the ListModel's > events, the booksPresenter's associated ListView for one. Adding event > receptors doesn't affect them, we are now notified of the event in _addition > to anyone else. > > libraryChanged > availableBooksPresenter list: self model availableBooks > > The previous #onViewOpened method, renamed for clarity > > And that is about it. When the Library adds or changes an item in the list > it, either automatically (add/remove) or by request (update) causes the > ListModel to trigger an event. The LibraryShell receives notification of > these events and updates the availableBooksPresenter. > > The final step will add a couple of simple TextViews to the Library, > the last book borrowed and the last book returned, to illustrate the use of > #triggersEvent > > Ian > > Out of interest - is anone reading these? > > > > > > > |
In reply to this post by Ian Bartholomew-3
On Thu, 21 Dec 2000 13:34:44 -0000, "Ian Bartholomew"
<[hidden email]> wrote: >Stage 3 - Remove the #libraryStatusEvent trigger from Library and replace >it's function with events triggered from the ListModel. [snip] >Out of interest - is anone reading these? With great interest. I'll have to go through the motions of actually adding the code and running it to get the information to stick in my brain. Thanks. -- Richard A. Harmon "The only good zombie is a dead zombie" [hidden email] E. G. McCarthy |
In reply to this post by Ian Bartholomew-3
Ian Bartholomew wrote:
> > Stage 3 - Remove the #libraryStatusEvent trigger from Library and [snip] > > Ian > > Out of interest - is anone reading these? Certainly! I have found it helpful! Thank you and Merry Christmas! Cheers, John |
In reply to this post by Ian Bartholomew-3
> Out of interest - is anone reading these?
Bravo! Bravo!! Bravo!!! Ian, Thanks so much for putting the time and effort into this -- a great explanation of MVP and application building. I'm reading it alright, though I might be reading (and digesting) a lot slower than you were writing *s*. -- Louis |
Free forum by Nabble | Edit this page |