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 (Cocoa-dev@lists.apple.com)

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:
http://lists.apple.com/mailman/options/cocoa-dev/archive%40mail-archive.com

This email sent to [EMAIL PROTECTED]

Reply via email to