I have a problem with a Dialog which has to do with side effects that occur
before ok or cancel is selected. I think I can express it in terms of PersonalMoney. For instance, PersonalAccountTransaction has an #amount: method which says "amount := aNumber". Let's say you add a line after this that does some other processing, e.g. "amount > 40000 ifTrue: [self contactAntiTerrorismUnit]". If I run the application and popup the PersonalAccountTransactionsDialog, change the amount to 50000, it will immediately send the #contactAntiTerrorismUnit message, even if I decide to cancel the dialog. In reality, I have a different application that has clients, rates, and services. The rate method in Client looks like this: rate: aRate "Assign aRate as the receiver's rate. Reprice the receiver's services." rate := aRate. self setServicePrices If I change the rate in the dialog, it calls #setServicePrices which changes all services' prices. If I press cancel, the client's rate is unchanged, as one would expect, but in the meantime all the client's services have been repriced. Any ideas how to easily fix this? Thank you. -- Louis |
Sumberg Louis wrote:
> > I have a problem with a Dialog which has to do with side effects that occur > before ok or cancel is selected. I think I can express it in terms of > PersonalMoney. For instance, PersonalAccountTransaction has an #amount: > method which says "amount := aNumber". Let's say you add a line after this > that does some other processing, e.g. "amount > 40000 ifTrue: [self > contactAntiTerrorismUnit]". If I run the application and popup the > PersonalAccountTransactionsDialog, change the amount to 50000, it will > immediately send the #contactAntiTerrorismUnit message, even if I decide to > cancel the dialog. > > In reality, I have a different application that has clients, rates, and > services. The rate method in Client looks like this: > > rate: aRate > "Assign aRate as the receiver's rate. Reprice the receiver's > services." > > rate := aRate. > self setServicePrices > > If I change the rate in the dialog, it calls #setServicePrices which changes > all services' prices. If I press cancel, the client's rate is unchanged, as > one would expect, but in the meantime all the client's services have been > repriced. Any ideas how to easily fix this? [snip] Sumberg, It's always dangerous to try to give advice based upon 30 seconds of digesting what someone has said. I probably should ask for more context and specifics. But then, hey, if I did, this wouldn't be Usenet, would it? (;-)} Anyway, I'm brave because there'll be lots of folks out in the happy Dolphin sea who'll catch my fall should I misstep. What's happening is that you are placing application function in the display management or "View" mechanism. That is often how function gets placed in badly designed C++ systems and in Visual Basic systems, but it's a really terrible idea. It should be a quite separate thing, even if one is using design patterns other than Dolphin's cornerstone Model-View-Presenter (MVP) or Model-View-Controller (MVC) elsewhere. In these schemes such behavior is the responsibility of the Model instance and there are self-imposed and rather strict rules about what goes where and the way communications are managed. This is far more than simply intellectual masochism, it is very deliberate so if a massive reorganization is to be made of the View part or, for that matter, of the Presenter part (to use MVP terms exclusively for a minute), the part which handles inputs and gestures, then the Model object remains untouched and without change. There are project-related reasons why this is a good idea, too, not to mention business reasons: Encapsulating all the application mechanism in one or more models also lets folks go to a central place to determine what exactly the business logic is. This keeps the system under construction from degenerating into an expensive and glorified spreadsheet system where the "application logic" is scatterred about myriads of cells and everyone's differs just a tad from their neighbor's spreadsheet. There are fine structure issues, too, such as using a Value Holder pattern to provide a painless means of issuing the triggers needed to tell a View the Model has changed, or to tell the Model that its Presenter has noted a commitment to a value change. Check out the MVP stuff -- there's a lot of it -- both in the Dolphin Education Centre and on the Dolphin Wiki. Best of luck, and come back if I've not been clear for you. -- --------------------------------------------------------------------- Jan Theodore Galkowski [hidden email] The Smalltalk Idiom [hidden email] ********************************************************************* "Smalltalk? Yes, it's really that slick." --------------------------------------------------------------------- Want to know more? Check out http://www.dnsmith.com/SmallFAQ/ http://www.object-arts.com/DolphinWhitePaper.htm http://st-www.cs.uiuc.edu/users/johnson/smalltalk/ ********************************************************************* |
In reply to this post by Louis Sumberg-2
The general approach with a Dialog is that a copy is made of the 'subject'
and then the changes are made to the 'subjectCopy' object. When you click <OK> then the instance variables are updated from the subjectCopy to the subject. When you click <Cancel> then the instance variables are not updated from the copy to the original, but any changes made to the subjectCopy remain. This is fine if there are no side effects and no one references the subjectCopy (which is then garbage collected). The confusion is that the changes are applied IMMEDIATELY to the 'subjectCopy' whenever an edit is made. Thus, any side effects of making a change to any instance will be made even if you hit cancel. This is particularly an issue when an object holds collections. Now we get into the copy/shallowCopy/deepCopy issues. Maybe you need to override #postCopy to copy some other instance variables (e.g., collections). Your #rate: method is being called before the <OK> or <Cancel> takes place. The confusion is that it is being called on the subjectCopy, not on the subject. There are various work-arounds. You can override AspectBuffer and change the above behavior. You could keep a cache of 'original' objects, and ignore changes made to copies. The solution depends on a better understanding of the problem. James Foster Fargo, ND "Sumberg Louis" <[hidden email]> wrote in message news:9spij4$5dj$[hidden email]... > I have a problem with a Dialog which has to do with side effects that occur > before ok or cancel is selected. I think I can express it in terms of > PersonalMoney. For instance, PersonalAccountTransaction has an #amount: > method which says "amount := aNumber". Let's say you add a line after this > that does some other processing, e.g. "amount > 40000 ifTrue: [self > contactAntiTerrorismUnit]". If I run the application and popup the > PersonalAccountTransactionsDialog, change the amount to 50000, it will > immediately send the #contactAntiTerrorismUnit message, even if I decide to > cancel the dialog. > > In reality, I have a different application that has clients, rates, and > services. The rate method in Client looks like this: > > rate: aRate > "Assign aRate as the receiver's rate. Reprice the receiver's > services." > > rate := aRate. > self setServicePrices > > If I change the rate in the dialog, it calls #setServicePrices which > all services' prices. If I press cancel, the client's rate is unchanged, as > one would expect, but in the meantime all the client's services have been > repriced. Any ideas how to easily fix this? > > Thank you. > > -- Louis > > |
In reply to this post by Louis Sumberg-2
Sumberg Louis <[hidden email]> wrote in message
news:9spij4$5dj$[hidden email]... ... > If I change the rate in the dialog, it calls #setServicePrices which changes > all services' prices. If I press cancel, the client's rate is unchanged, as > one would expect, but in the meantime all the client's services have been > repriced. Any ideas how to easily fix this? ... There are probably a few different ways to work around this. I will offer my 2 cents. Three different ideas off the top of my head follow: 1. Dialogs normally make a copy of the model. Changes are directly applied to the copy, and then applied to the original via aspect adapters if the dialog is accepted. I suspect what you are seeing is a side effect of a shallow copy of your model. You could override copy in your model class to be a bit deeper. Depending upon the depth and size of your model object it may or may not be a simple fix. 2. Defer calling #setServicePrices until you accept the dialog by removing the call from #rate: and overriding #apply in your Dialog subclass. You could check to see if the rate is different between the subject and subjectCopy in the AspectBuffer and recalculate only if needed after it is set. If you want to be able to still set the rate outside of dialogs and want it to automatically recalculate the service prices you could add a new method #basicRate: and use that for the aspect in the dialog rather than #rate: to preserver the current functionality. 3. Always dynamically calculate service prices based on the parent rate. A variation on this would be to cache the service prices, and just clear them when the rate is changed to force them to recalculate. That way even though they would be cleared if a rate were changed and the dialog was canceled they would recalculate based on the current rate. I am by no means an MVP expert! I would be interested to hear other ideas, or comments about these. Hope this helps, Chris |
In reply to this post by Jan Theodore Galkowski-2
Jan and Chris,
Thanks for your responses. I think I now have enough to solve this particular issue, one way or the other. It's an interesting problem because it brings into play modeling issues that are affected by view/presenter constraints. Here are some things I've gone through and noticed. Anyone wanting to comment, please do. Keeping it simple, a client has a rate and it has services. A particular service for a particular client has a price, which is a function of that service's hours as well as that client's rate. How then does one access that price? If the service instance doesn't know its client, then it can't compute its own price. This is for the Client instance to compute, which knows which rate to apply. In this case, Client has a #priceFor: aService method that takes aService and returns a price (e.g., ^self rate * aService hours). The problem I ran into is when I wanted to display clients and services. Clients are displayed through a list presenter. The services for the selected client appear through another listpresenter. I want the listview showing each service to display the service's hours along with its price. While I can set the #getContentsBlock: for the hours column to [:service | service price], I can't show a column for price because there's no #price method in Service. At the application level, where the selected client rate is known, I might be able to wrap each service somehow, e.g., create a PricedService instance, and then buffer against a derived model, but this looks to be a nasty road. So it's back to the Service model and add a 'price' aspect, with standard accessors. The listview now can have its price column, with #getContentsBlock: [:service | service price]. The real price setter though is still in the client, in a new method #setPriceFor: aService which sends aService price: (self priceFor: aService). This is the point where I was at in my initial post. Looking at Chris' suggestions (damn good stuff coming off the top of your head, Chris), I didn't want to mess with copies, shallow or deep, so I started with #2, overriding #apply and adding a #basicRate: method. I got bogged down, though, and I won't bore anyone with the details, but that may well be one way to do it. However, I moved on to #3, dynamically calculating the service price. In order to make this work, a Service instance needs to know its client, so I added an instance variable and accessors for client, removed the price instance variable and #price: accessor, and changed #price to return self client priceFor: self. I still need to add some more event handling but it looks like this will work. The one oddity I'm left with is that a client has services and each service knows its client, which seems somewhat circular to me. Perhaps I'm still stuck in the relational data modeling world, I'm not sure, but it seems to work and there aren't any zombies (particularly client and service instances) left after I run and exit the application. That, to me, is a good sign. Thanks again for your help. -- Louis |
In reply to this post by Christopher J. Demers
Chris,
> > If I change the rate in the dialog, it calls #setServicePrices which > changes > > all services' prices. If I press cancel, the client's rate is unchanged, > as > > one would expect, but in the meantime all the client's services have been > > repriced. Any ideas how to easily fix this? > ... > > There are probably a few different ways to work around this. I will offer > my 2 cents. Three different ideas off the top of my head follow: > > 1. Dialogs normally make a copy of the model. Changes are directly applied > to the copy, and then applied to the original via aspect adapters if the > dialog is accepted. I suspect what you are seeing is a side effect of a > shallow copy of your model. You could override copy in your model class to > be a bit deeper. Depending upon the depth and size of your model object it > may or may not be a simple fix. <snip> Without engaging too much brain (because I'm using it for something else at the minute) this seems likely to be the problem to me. The dialog framework assumes that a copy of the model be made to a deep enough level as is required by the depth of the changes the dialog is expecting to make. Best Regards, Andy Bower Dolphin Support http://www.object-arts.com --- Are you trying too hard? http://www.object-arts.com/Relax.htm --- |
In reply to this post by Louis Sumberg-2
Louis Sumberg <[hidden email]> wrote in message
news:9sttbn$3c4$[hidden email]... ... > The one oddity I'm left with is that a client has services and each service > knows its client, which seems somewhat circular to me. Perhaps I'm still > stuck in the relational data modeling world, I'm not sure, but it seems to > work and there aren't any zombies (particularly client and service > instances) left after I run and exit the application. That, to me, is a > good sign. This is not really an oddity, but a normal aspect of many designs. What you have is a composite object structure and the parent contains children, and the children have a reference to their parent. This can be very powerful. Often it makes sense for child objects to be able to get something from there parent. The garbage collector in Dolphin is supposed to handle these kinds of circular object references without problems. For an example of something similar you can look at PlayShape. You will see that the PlayShape contains a reference to its Playground parent which contains it. If you look at Playground<<add: you will see how the parent get set. This is also a normal part of the way views and presenters work in Dolphin. CompositPresenter(s) can hold other presenters in subPresenters and those sub-presenters have a reference to their parentPresenter that contains them. If your price formula is as simple as (rate * hours) your price method could simply be: ========== price ^self hours * clientParent rate ========== rather than calling #priceFor: , unless you really need the client to do the calculation (to allow for more complex formulas perhaps). Then in the client you could have an addSevice: method like this: ============== addService: aService aService clientParent: self. services add: aService. ============== This hides the implementation of parent setting and makes things more transparent. This is handy since you may be able to add services to clients in different ways. I would also be tempted to call the service's reference to its parent "clientParent" or "parentClient" rather than "client" as I think it imparts more information about the variables role in context rather than just type. That way every time we see the variable we are reminded of the parent/child relationship between the objects which is an important design aspect. Consider what I have said here, but also consider that I am not totally familiar with your problem domain. Good luck, and I hope my comments have been helpful. If you want to learn more about OO design concepts you might take a look at a book called _The Design Patterns Smalltalk Companion_ . Chris |
In reply to this post by Andy Bower
Chris and Andy, thanks for the explanations of Dialog vis-a-vis the subject
copy. Hit me on the head twice and I start getting it. As I indicated in my previous post, I've changed the application since my original post so I'm not getting the side effects but I did take a closer look at fiddling with #copy. It seems that what I would've wanted is for ClientDialog's subject (model) to be the client but its subject copy to be a shallower copy of client, one that has an empty listmodel for services. Thus, when rate is changed in the dialog and #setPrices: is called, there are no services in the dialog's subjectCopy to change (hence no side effects), but when the user hits ok, there *are* services in the dialog's subject which do get changed. However, I didn't like the idea of changing #copy for the model. After all, if someone wants a real copy they should get a real copy. So I looked at changing subjectCopy in ClientDialog, because that's really where the requirement is (i.e., to avoid side effects in the model). Looking at AspectBuffer, I see there's #subject, which returns the subject model, and there's #value, which returns the subject copy. I figured that when the ClientDialog is created (and #model: is called), I could modify the subjectCopy (setting services to an empty listmodel), but it didn't work and I ran into some interesting things. If I bring up the ClientDialog and then inspect it, I see the model is an AspectBuffer. When I inspect the AspectBuffer, I see that subject is a Client and subjectCopy is a Client. What's odd is when I evaluate "subject = subjectCopy" I get false. I would've expected true for "=" and false for "==". Anyway, it would appear that subject and subjectCopy are really different objects. However, if I bring up an inspector on subjectCopy (a Client) and evaluate "services list: ListModel new", the services in the shellview disappear, which seems to indicate that I've really modified the subject, not the subjectCopy. Maybe two hits on the head just ain't enough. -- Louis |
In reply to this post by Christopher J. Demers
"Christopher J. Demers" <[hidden email]> wrote:
> This is not really an oddity, but a normal aspect of many designs. What you > have is a composite object structure and the parent contains children, and > the children have a reference to their parent. This can be very powerful. > Often it makes sense for child objects to be able to get something from > there parent. The garbage collector in Dolphin is supposed to handle these > kinds of circular object references without problems. Thank you for the validation. I wasn't sure if it was kosher or not. I'll also take a look at PlayShape et al and see about picking up the book you mentioned. > If your price formula is as simple as (rate * hours) your price method could > simply be: > ========== > price > ^self hours * clientParent rate > ========== > rather than calling #priceFor: , unless you really need the client to do > the calculation (to allow for more complex formulas perhaps). I figure on leaving the call to #priceFor: in #price mainly because logically it's the client that does the pricing and doing it this way, as you pointed out, allows for easy changes if the formula is complex. (The pricing does happen to be a bit more complex than shown -- rate is a Rate object, not just a Number.) > Then in the client you could have an addSevice: method like this: > ============== > addService: aService > > aService clientParent: self. > services add: aService. > ============== > This hides the implementation of parent setting and makes things more > transparent. This is handy since you may be able to add services to clients > in different ways. > > I would also be tempted to call the service's reference to its parent > "clientParent" or "parentClient" rather than "client" as I think it imparts > more information about the variables role in context rather than just type. > That way every time we see the variable we are reminded of the parent/child > relationship between the objects which is an important design aspect. Good points -- I'm changing the method and instance variable name. Thanks again for the help. Oh, on a general note, I have a small business application I wrote in Visual Basic that's been in production for a couple of years and the client is pretty happy with it. I've been toying with porting it, at least for my own pleasure, to Dolphin, and also seeing if I could generalize it for other business sectors. It may well be that something like this is out there, at least the model object -- if so, I'd love to see something, if not, perhaps I'll try to write something up if I'm successful. -- Louis |
In reply to this post by Andy Bower
Hello all,
> Without engaging too much brain (because I'm using it for something else at > the minute) this seems likely to be the problem to me. The dialog framework > assumes that a copy of the model be made to a deep enough level as is > required by the depth of the changes the dialog is expecting to make. I haven't had time to follow this thread (appologies if this is rehashing stuff that's already been said), but, I'll add that I had some trouble with dialogs connected to a few different complex objects. AFAIK, the dialog framework copies only the "top layer" of aspects. If the model is heavily composed and subpresenters are connected to it and to some of its "sub-models", then a real mess can result. In at least one case, I was being bitten by some poor decisions that I never had time to correct; in others, users hit me with "why can't I just change that _here_?" types of requests, and the dialog's scope expanded. Either way, Dialog's buffering behavior became a problem. In the end, I created a non-buffered dialog subclass that acts like a modal shell. Of course, I have to copy the model and apply aspects myself if I want cancel behavior, but, it is a lot simpler to in some situations. There's another issue though: sometimes a Cancel button is a bad thing. As the dialogs became more complex, there was an opportunity for the user to cancel a LOT of work that they didn't want to lose. For the most complex of the affected dialogs, I ultimately removed cancel and apply changes as they are made; there are some modal/cancellable prompters along the way which helps to make it less drastic. Interestingly, it's easier to sell "geez, you mean I can't make all those changes go away by pressing cancel" than it is to have to answer yes to "you mean that cancel undid ALL OF THAT!!??". Perhaps related, at one point I had a problem with the copy semantics of some of my domain objects. Storing some aspects in value models rather than using "external" value adaptors helped, as did looking carefully at how Squeak's (2.7 I think it was, in case it matters) morphs copy themselves. IIRC, appropriate use of #postCopy fixed some serious ills. There might be something in the archives about it, or I can browse a little and report back if there's interest. Have a good one, Bill -- Wilhelm K. Schwab, Ph.D. [hidden email] |
In reply to this post by Louis Sumberg-2
Louis,
> The problem I ran into is when I wanted to display clients and services. > Clients are displayed through a list presenter. The services for the > selected client appear through another listpresenter. I want the listview > showing each service to display the service's hours along with its price. > While I can set the #getContentsBlock: for the hours column to [:service | > service price], I can't show a column for price because there's no #price > method in Service. Here's another approach: When you populate the service ListPresenter, you can set the #getContentsBlock of the price column to something like: [:it | selectedClient priceFor: it]. where I'm assuming that "selectedClient" is some variable accessible at the time the block is created, and its value is captured for use in calculating each service's price. Alternatively you could set up something like [:it | self priceForSelectedService: it] as you open the window, and then the ListPresenter would be asking your application for the price, which it would compute from its knowledge of the selected client. > -- Louis -- chris |
Chris,
Thanks for both approaches. I recall prior discussion of blocks and that some context is saved in a block. It seems that one can't specify a block in the VC where the block makes reference to self because there's no sense of self when the block is executed. Not just for self, but for instance variables and methods. But if the block is assigned from a method, then self, instance variables and methods do have meaning within the block. In my shell, when a new client is selected, I added: (servicesPresenter view columns at: 2) getContentsBlock: [:service | clientsPresenter selection priceFor: service]. The first line returns the 'Price' column in the listview, the rest sets the block for that column, where the shell's currently selected client is somehow captured in the block (as 'clientsPresenter selection'). This is virtually the same as your first example and I do see how the second would work as well. Very clever stuff indeed. It does seem like this is a good general approach to creating computed columns in listviews. For example, in PersonalMoney, a Transaction does not currently know what its balance is. Show the account's list of transactions in a listview though, and it's begging for a balance column on the end, as in a checkbook. One could add an instance variable to Transaction and have the Account update all the transactions' balances whenever a transaction is added, changed, or removed. Or, you can have a computed column whose block is like what you have below, e.g., [:trans | self balanceFor: trans]. In this Services application, having made the changes to the model, where a Service now does know its client and thus can return its price, a simple block, i.e., [:service | service price], entered in the VC works fine -- it seems to tie the model tighter and make the GUI simpler. Hopefully that's a good thing. -- Louis > Here's another approach: > > When you populate the service ListPresenter, you can set the > #getContentsBlock of the price column to something like: > > [:it | selectedClient priceFor: it]. > > where I'm assuming that "selectedClient" is some variable accessible at the > time the block is created, and its value is captured for use in calculating > each service's price. > > Alternatively you could set up something like > > [:it | self priceForSelectedService: it] > > as you open the window, and then the ListPresenter would be asking your > application for the price, which it would compute from its knowledge of the > selected client. |
Free forum by Nabble | Edit this page |