Dialogs and side effects

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
12 messages Options
Reply | Threaded
Open this post in threaded view
|

Dialogs and side effects

Louis Sumberg-2
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


Reply | Threaded
Open this post in threaded view
|

Re: Dialogs and side effects

Jan Theodore Galkowski-2
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/
*********************************************************************


Reply | Threaded
Open this post in threaded view
|

Re: Dialogs and side effects

James Foster-3
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
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
>
>


Reply | Threaded
Open this post in threaded view
|

Re: Dialogs and side effects

Christopher J. Demers
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


Reply | Threaded
Open this post in threaded view
|

Re: Dialogs and side effects

Louis Sumberg-2
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


Reply | Threaded
Open this post in threaded view
|

Re: Dialogs and side effects

Andy Bower
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
---


Reply | Threaded
Open this post in threaded view
|

Re: Dialogs and side effects

Christopher J. Demers
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


Reply | Threaded
Open this post in threaded view
|

Re: Dialogs and side effects

Louis Sumberg-2
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


Reply | Threaded
Open this post in threaded view
|

Re: Dialogs and side effects

Louis Sumberg-2
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


Reply | Threaded
Open this post in threaded view
|

Re: Dialogs and side effects

Bill Schwab-2
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]


Reply | Threaded
Open this post in threaded view
|

Re: Dialogs and side effects

Chris Uppal-3
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


Reply | Threaded
Open this post in threaded view
|

Re: Dialogs and side effects

Louis Sumberg-2
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.