Re: Prevent Asynchronous operation of beginSheetModalForWindow
Re: Prevent Asynchronous operation of beginSheetModalForWindow
- Subject: Re: Prevent Asynchronous operation of beginSheetModalForWindow
- From: Graham Cox <email@hidden>
- Date: Thu, 12 Jun 2008 23:43:46 +1000
Hi John,
I hope you don't mind me cc'ing this to the list - I think it might be
helpful to others (if not, my apologies for the noise).
On 12 Jun 2008, at 9:43 pm, John Love wrote:
Hi, Graham ...
Graham, this is a condensed summary of my initial communication:
Call it my fetish or whatever, but I strongly believe in
compartmentalization, i.e., having a separate SheetController to do
sheet stuff.
I'd say that was entirely reasonable.
For this purpose I have made SheetController*theSheet an outlet of a
FileController*theFile and theFile is an outlet of my main nib's
File's Owner. Another outlet of File's Owner is my mainWindow. In
MyDocument.m -awakeFromNib, I pass mainWindow to one of
FileController's methods to initialize some stuff pertaining to
mainWindow, including the most important setting of FileController's
NSWindow *itsWindow, an instance variable.
Well, without being aware of the details, I can't be sure this is
wrong or right - but I have to say, it smells. One strong principle of
OOP design is that objects are responsible as far as possible for
themselves and themselves alone. If you need to pass one object to
another to put the first object into a useful state, that seems to be
violating this principle. However, there are always exceptions and
this may be one of them, hence my reluctance to say that it's
definitely wrong.
Normally to control a window you use NSWindowController (or subclass
of it) which already has a 'window' outlet, so there's no reason to
have an 'itsWindow' outlet of your own. If another object needs the
window (why?) then it should ask the window controller for it rather
than keeping its own reference: [controller window];
Sometimes, I kinda have the feeling that the MVC implies "one
controller controls everything" is the ideal.
Hmmm, not really. As many controllers as needed to make the design
elegant and straightforward, but no more, would be my view. "As simple
as possible but no simpler".
Even though I am far from the end of my program's development, right
now I have a total of 7 controllers, including the two already
mentioned. I also have a StatusController*theStatus which writes to
a NSTextField of my main window. The remaining 4 don't do very much
right now. Down the road I could conceivably shrink the 7 to 5 or
whatever, but not much beyond that. Before I continue, I would
appreciate some feedback on this "philosophy". Considering my
previous AppleScript Studio experience, I never had this
philosophical problem; but now I do.
My approach has fallen into a common pattern, which I've grown
comfortable with, and which I believe to be correct. Basically, one
interface = one controller. This is strongly suggested by
NSWindowController, which has an outlet for exactly one window. By
interface I mean a single complete window or dialog box, where that is
self-contained. If the dialog is complex and has multiple switchable
views I might consider a separate controller for each one, though in
practice I don't think I've ever done that - I've just put all the
code for the entire dialog in one controller.
Let's suppose I have a document that owns a single main window
controller - this is very typical in the document-based interface -
but also has a number of additional dialog boxes that are needed to
get certain operations done. The document is the nib's File's Owner,
so it has outlets to each controller for each dialog box. Each
controller in turn has outlets (and actions) for each individual
widget/control in the dialog interface. What is a dialog for? It's to
get information from the user in order to complete some task. The
document is not interested in how that information is obtained
specifically, it only knows it needs it. So the document asks the
relevant controller "go get me this information I need from the user".
The dialog's controller in turn puts up the dialog, handles all the
interaction with the user, removes the window when the user is done,
packages up the information in a form useful to its client, and
returns it. Everyone's happy - the document got its info, the dialog
controller didn't have to care about who wanted the info or what was
done with it, and the connections between the objects were kept to a
minimum, with few dependencies.
Now whether the dialog is modal, modeless, a sheet, or whatever, is
irrelevant. The controller for that dialog can probably make that
decision (it may need to be given supplementary information, such as
the parent window, but that can be designed into its interface with
its client code - and it is also free to ignore it, so typically my
dialog controllers take a parent window argument regardless, even if I
end up ignoring it and displaying the dialog modally - the client
(document) doesn't care either way). However, because the dialog may
be modeless (or asynchronous, if you prefer) the client/controller
interfacing needs to anticipate that, by doing the work that requires
the dialog info in a callback method that is called by the dialog
controller. Theoretically the dialog controller could keep control and
not return to the caller until the dialog goes away - what I call
"inline modal" operation, but this isn't a very general approach -
(even if it worked, which it doesn't). Instead the callback model
works in every situation - modal, modeless, sheet, asking another
computer on a network, sending a message in a bottle.
So, yes to compartmentalisation. But where are the various
compartments' "walls"? It's easy to get them in the wrong place.
Looking at your sheet handling code, I think this is your problem.
Your FileController wants information from the user using a sheet, but
instead of asking the sheet controller "go get me the information I
want" it's asking "give me a sheet so *I* can get the information
myself". The FileController has no business whatsoever dealing with an
object of type NSAlert* or any other view object - instead, it should
be wanting more abstract information, i.e. the user's intentions:
should I continue with this operation or not? It is the sheet
controller's job in its entirety to get that information by hook or by
crook. Your file controller's interface to the sheet controller should
be something like this (in principle):
[sheetController showSheetOnParent:[self windowForSheet]
completionMethod:@selector(doThisWhenYoureDone) target:self];
So the file controller is really doing the bare minimum - it's saying
that OK, you might need a parent window - use this one; and it's
saying I don't know how you get the info, or how long it takes, but
when you're finished, call me back on this number and I'll take it
from there. At no point does the File Controller care about return
codes, buttons, windows or any other UI implementation detail in the
sheet controller's world. The sheet controller is only obliged to
honour its contract with its client - it could implement it by sending
an email to the user and waiting for a reply for all the client cares,
the client sees the same result (eventually!). Also, by simply never
calling the callback you've implemented Cancel...
*******************
I'll give you a concrete example from my DrawKit Demo app. One feature
is Polar Duplication, which takes the selected graphics and makes
rotated copies of them around a given point. The document needs to
know how many copies to make, what angle to rotate them by, where the
centre point is, etc. It asks the user for this information using a
dialog box. So I have a controller for that particular dialog,
GCPolarDuplicateController, which is a subclass of NSWindowController.
My document has an outlet (see mea culpa about nibs at bottom) to this
controller:
IBOutlet id mPolarDuplicateController;
My document also has an action method hooked up to the "Polar
Duplicate..." menu item:
- (IBAction) polarDuplicate:(id) sender;
When that menu item is invoked by the user, the document needs more
information to complete the operation, so it asks the polar duplicate
controller to get it. Bear in mind that the user might decide just to
cancel, or they might fill in the various fields and hit OK. They
might not do this until next Tuesday. In order to respond to the
completion of the dialog, I define an informal protocol for doing
polar duplication, thus:
@interface NSObject (PolarDuplicationDelegate)
- (void) doPolarDuplicateCopies:(int) copies centre:(NSPoint) cp
incAngle:(float) angle rotateCopies:(BOOL) rotCopies;
@end
This is actually defined in the header for GCPolarDuplicateController,
not the document. Why? Because this is the "contract" between the
controller and its client - it says, when I'm done, if you implement
this method, I'll call you with the various values you need. Don't
worry about how I get them, that's my job - you just sit tight and
wait and do the actual operation when I tell you to. A category of
NSObject is used because the controller doesn't care about what type
of object its client is, it just knows to call this method on *some*
delegated object when it's done. And of course in this case that
object happens to be the document. (n.b. an informal protocol for the
callback is but one solution - you could also pass a selector and
target, or just have both parties agree how they interface).
At the other end of things, the dialog controller defines this method:
- (void) beginPolarDuplicationDialog:(NSWindow*) parentWindow
polarDelegate:(id) delegate;
Thus we have all the pieces we need for the document to implement
polar duplication by getting the additional info from the user. The
document's entire menu handling code is:
- (IBAction) polarDuplicate:(id) sender
{
#pragma unused (sender)
[mPolarDuplicateController beginPolarDuplicationDialog:[self
windowForSheet] polarDelegate:self];
}
Note that this returns immediately, and goes back to trotting around
the event loop. Asynchronously, the dialog controller takes over. It
puts up the sheet on the parent window it is given, handles all of the
user interaction, and waits for OK or Cancel to be hit. If Cancel is
hit, it just closes the sheet and does no more. If OK is hit, it
honours its contract with its client - which is the object passed as
"delegate". It gets all the various values from the UI, and calls the
doPolarDuplicateCopies:.... method on that object. The document now
has the info needed to perform the operation, so it simply goes ahead
and does so. To the user, it looked as if the dialog intervened, and
that the dialog completed the menu choice made originally, and when OK
was clicked, the operation was done. It's this apparent "inline"
operation that can be misleading, because internally that's not what
happened at all - everything happened asynchronously and the operation
was performed by the callback.
So now, what's the code in the dialog controller look like? Well
naturally it consists of a fair bit of doing stuff with UI widgets, so
I won't list it all here. If you're interested you can download it
from http://apptree.net/drawkitmain.htm and have a look at it. But
here's the two main client interfacing methods:
- (void) beginPolarDuplicationDialog:(NSWindow*) parentWindow
polarDelegate:(id) delegate
{
mDelegateRef = delegate; // remember who our delegate is
// open the sheet
[NSApp beginSheet:[self window]
modalForWindow:parentWindow
modalDelegate:self
didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:)
contextInfo:@"polar_duplication"];
// set up aspects of the UI to a useful initial state
[self conditionallyEnableOKButton];
}
- (void) sheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode
contextInfo:(void *)contextInfo
{
#pragma unused (sheet, contextInfo)
if ( returnCode == NSOKButton )
{
// extract parameters and do something with them
int copies = [mCopiesTextField intValue];
NSPoint centre;
centre.x = [mCentreXTextField floatValue];
centre.y = [mCentreYTextField floatValue];
float incAngle = [mAngleIncrementTextField floatValue];
BOOL rotCopies = [mRotateCopiesCheckbox intValue];
// call back the delegate to get the actual work done with the info
we obtained:
[mDelegateRef doPolarDuplicateCopies:copies centre:centre
incAngle:incAngle rotateCopies:rotCopies];
}
}
I hope this is clear. To reiterate: at NO POINT does the client of the
dialog controller know or care how the info is obtained. The contract
between them is simple - two methods, one to request the info, the
other to do the work when the user has supplied the info.
Mea culpa: For expediency, the above example so happens to put all of
the UI for all of the different dialogs into the document nib along
with the various controllers. It would be better practice to put each
dialog into a separate nib, making the controller File's Owner. This
doesn't make any difference to the design of the code overall, but
adds a minor complication in that when the Polar Duplicate menu is
invoked, the document would need to instantiate the
GCPolarDuplicateController in code which in turn would load the UI
from its own nib. This doesn't change anything about the interfacing
between the controller and the document in any way - it merely means
that the controller got deliberately created by the document instead
of simply coming from the same nib file. That was just me being lazy
coding the demo app ;-)
I hope this helps,
Graham
_______________________________________________
Cocoa-dev mailing list (email@hidden)
Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com
Help/Unsubscribe/Update your Subscription:
This email sent to email@hidden