views:

163

answers:

2

Hello,

I have a multithreaded application that has many concurrent operations going on at once. When each thread is finished it calls one of two methods on the main thread

performSelectorOnMainThread:@selector(operationDidFinish:)
// and
performSelectorOnMainThread:@selector(operationDidFail:withMessage:)

When an operation fails, I launch a sheet that displays the error message and present the user with 2 buttons, "cancel" and "try again". Here is the code I use to launch the sheet:

// failureSheet is a NSWindowController subclass

[NSApp beginSheet:[failureSheet window]
   modalForWindow:window
    modalDelegate:self
   didEndSelector:@selector(failureSheetDidEnd:returnCode:contextInfo:)
      contextInfo:nil];

The problem is that if 2 concurrent operations fail at the same time, then the current sheet that is displayed gets overwritten with the last failure message, and then the user's "try again" action will only retry the last failed operation. Ideally, I would like to "queue" these failure sheets. If 2 operations fail at the same time then you should see 2 sheets one right after the other, allowing the user to cancel or retry them individually.

I've tried using:

[NSApp runModalSessionForWindow:[failureSheet window]] 

which seems to do what I want, but doesn't work in my situation. Maybe it isn't thread safe?

For example the following code works...

- (void)displaySheet
{
    [NSApp beginSheet:[failureSheet window]
       modalForWindow:window
        modalDelegate:self
       didEndSelector:@selector(failureSheetDidEnd:returnCode:contextInfo:)
          contextInfo:nil];

    [NSApp runModalForWindow:[failureSheet window]];

    [NSApp endSheet:[failureSheet window]];

    [[failureSheet window] orderOut:nil];
}

// Calling this method from a button press works...
- (IBAction)testDisplayTwoSheets
{
    [self displaySheet];
    [self displaySheet];
}

However if I have 2 different threaded operations invoke displaySheet (on the main thread) when they are done, I only see one sheet and when I close it the modal session is still running and my app is essentially stuck.

Any suggestions as to what I'm doing wrong?

+3  A: 

If you want to queue them, then just queue them. Create an NSMutableArray of result objects (you could use the operation, or the failure sheet itself, or a data object that gives you the information for the sheet; whatever is convenient). In operationDidFinish: (which always runs on the main thread, so no locking issues here), you'd do something like this:

[self.failures addObject:failure];
if ([[self window] attachedSheet] == nil)
{
    // Only start showing sheets if one isn't currently being shown.
    [self displayNextFailure];
}

Then you'd have:

- (void)displayNextFailure
{
    if ([self.failures count] > 0)
    {
        MYFailure runFailure = [self.failures objectAtIndex:0];
        [self.failures removeObjectAtIndex:0];
        [displaySheetForFailure:failure];
    }
}

And at the end of failureSheetDidEnd:returnCode:contextInfo:, just make sure to call [self displayNextFailure].

That said, this is probably a horrible UI if it can happen often (few things are worse than displaying sheet after sheet). I'd probably look for ways to modify the existing sheet to display multiple errors.

Rob Napier
It definitely doesn't happen often, it is just a corner case that I am trying to handle. I thought about implementing something like this myself, wasn't sure if there was an easier way. This seems simple enough though, thanks for the answer.
nick
If it can happen *at all*, you should change your UI to handle that sanely. You might even want to mix the errors in with the queue of jobs, for something more like Xcode's Build Results window.
Peter Hosey
A: 

I don't think you're using the "runModalForWindow:" command properly. You wouldn't use both [NSApp beginSheet...] and "runModalForWindow:" at the same time. One is for document modal (where the application keeps running but the window with the sheet is locked) and one is for application modal (which stops everything in the entire app until the sheet is dismissed). You only want the application modal dialog so do this...

For application modal start the window with the following only:

[[failureSheet window] center];
[NSApp runModalForWindow:[failureSheet window]];

Hook the "OK" button and others to regular IBAction methods. They'll get called when a button is pressed. In the IBAction methods you need to do something like this to dismiss the window and process the action:

-(IBAction)okBtnPressed:(id)sender {
    [NSApp stopModal];
    NSLog(@"ok button");
    [[sender window] close];
}
regulus6633