How to cancel a loading document in NSDocument's readFromURL:ofType:error method ?

  • Hi,

    I searched the mailing-list but didn't find an answer….so sorry if this was posted before:

    I've setup a document based application which can read large ASCII data files (>150MB).

    When opening the document the method readFromURL:ofType:error is used which then opens a small feedback window
    showing the file name and an animated  progress bar with a "Cancel" NSButton.

    Simple question: How do I cancel the document when it is loading the data (sometimes it can take 10 seconds or more to load the data) if a user clicks the Cancel button  ?
    Should I create an IBAction which generates an NSError ?
    But how does readFromURL:ofType:error get this error message (should I use a delegate ?)

    Any clue is greatly appreciated.

    Cheers,

    Gilles
  • Set an ivar in your document which the read method checks periodically. If the user cancelled, then return NO with an NSUserCancelledError.

    On 9 Feb 2012, at 16:01, Gilles Celli wrote:

    > Hi,
    >
    > I searched the mailing-list but didn't find an answer….so sorry if this was posted before:
    >
    > I've setup a document based application which can read large ASCII data files (>150MB).
    >
    > When opening the document the method readFromURL:ofType:error is used which then opens a small feedback window
    > showing the file name and an animated  progress bar with a "Cancel" NSButton.
    >
    > Simple question: How do I cancel the document when it is loading the data (sometimes it can take 10 seconds or more to load the data) if a user clicks the Cancel button  ?
    > Should I create an IBAction which generates an NSError ?
    > But how does readFromURL:ofType:error get this error message (should I use a delegate ?)
    >
    > Any clue is greatly appreciated.
    >
    > Cheers,
    >
    > Gilles
  • On Thu, Feb 9, 2012 at 8:01 AM, Gilles Celli <gilles.celli...> wrote:
    > Hi,
    >
    > I searched the mailing-list but didn't find an answer….so sorry if this was posted before:
    >
    > I've setup a document based application which can read large ASCII data files (>150MB).
    >
    > When opening the document the method readFromURL:ofType:error is used which then opens a small feedback window
    > showing the file name and an animated  progress bar with a "Cancel" NSButton.

    If you're targeting Snow Leopard or later, you should override
    +canConcurrentlyReadDocumentsOfType: to return YES. That will cause
    -initWithContentsOfURL:ofType:error: (and therefore
    -readFromURL:ofType:error:) to execute on a background thread.

    Then you get to the canceling part. The traditional approach would be
    to set a flag when the user clicks Cancel, and periodically check this
    flag from within your -readFromURL:… implementation, returning an
    NSUserCancelledError if you detect it has been set.

    A more modern approach might be to use NSOperationQueue. Instead of a
    loop, -readFromURL:… would divide its work into operations and enqueue
    those on a queue. Clicking the Cancel button would enqueue an
    operation that would shut down the -readFromURL:'s operation queue and
    cause it to return an NSUserCancelledError.

    --Kyle Sluder
  • Mike, Kyle,

    Thanks for the quick answers!

    Yes I'm targeting Mac OS X 10.6 and later so canConcurrentlyReadDocumentsOfType: is a welcome addition, I completely forgot that.

    Strangely if I put the method canConcurrentlyReadDocumentsOfType: inside my NSDocument I get a warning when trying to open a document (It seems to be a QuickLook error ?) on Mac OS X 10.7.3:

    [QL] QLError(): +[QLSeamlessDocumentOpener seamlessDocumentOpenerForURL:] should only be called in the main thread

    Now for the cancel question: If I take the more traditional approach to cancel the operation inside readFromURL, should I fire up a new thread to check the flag's status ?

    Gilles

    On Feb 9, 2012, at 7:32 PM, Kyle Sluder wrote:

    > On Thu, Feb 9, 2012 at 8:01 AM, Gilles Celli <gilles.celli...> wrote:
    >> Hi,
    >>
    >> I searched the mailing-list but didn't find an answer….so sorry if this was posted before:
    >>
    >> I've setup a document based application which can read large ASCII data files (>150MB).
    >>
    >> When opening the document the method readFromURL:ofType:error is used which then opens a small feedback window
    >> showing the file name and an animated  progress bar with a "Cancel" NSButton.
    >
    > If you're targeting Snow Leopard or later, you should override
    > +canConcurrentlyReadDocumentsOfType: to return YES. That will cause
    > -initWithContentsOfURL:ofType:error: (and therefore
    > -readFromURL:ofType:error:) to execute on a background thread.
    >
    > Then you get to the canceling part. The traditional approach would be
    > to set a flag when the user clicks Cancel, and periodically check this
    > flag from within your -readFromURL:… implementation, returning an
    > NSUserCancelledError if you detect it has been set.
    >
    > A more modern approach might be to use NSOperationQueue. Instead of a
    > loop, -readFromURL:… would divide its work into operations and enqueue
    > those on a queue. Clicking the Cancel button would enqueue an
    > operation that would shut down the -readFromURL:'s operation queue and
    > cause it to return an NSUserCancelledError.
    >
    > --Kyle Sluder
  • On 9 Feb 2012, at 20:23, Gilles Celli wrote:

    > Mike, Kyle,
    >
    > Thanks for the quick answers!
    >
    > Yes I'm targeting Mac OS X 10.6 and later so canConcurrentlyReadDocumentsOfType: is a welcome addition, I completely forgot that.
    >
    > Strangely if I put the method canConcurrentlyReadDocumentsOfType: inside my NSDocument I get a warning when trying to open a document (It seems to be a QuickLook error ?) on Mac OS X 10.7.3:
    >
    > [QL] QLError(): +[QLSeamlessDocumentOpener seamlessDocumentOpenerForURL:] should only be called in the main thread

    Maybe try setting a breakpoint on QLError to see when it gets called?
    >
    > Now for the cancel question: If I take the more traditional approach to cancel the operation inside readFromURL, should I fire up a new thread to check the flag's status ?

    What would this gain you? You set the cancel flag on the main thread. And you have a worker thread for reading the document supplied by the system, so why add another thread to the mix?
  • > From: Gilles Celli <gilles.celli...>
    > Subject: Re: How to cancel a loading document in NSDocument's readFromURL:ofType:error method ?
    > To: "Kyle Sluder" <kyle.sluder...>, "Mike Abdullah" <cocoadev...>
    > Cc: <cocoa-dev...>
    > Date: Thursday, 2012 February 9, 14:23
    >
    > Thanks for the quick answers!
    >
    > Yes I'm targeting Mac OS X 10.6 and later so
    > canConcurrentlyReadDocumentsOfType: is a welcome addition, I
    > completely forgot that.
    >
    > Strangely if I put the method
    > canConcurrentlyReadDocumentsOfType: inside my NSDocument I
    > get a warning when trying to open a document (It seems to be
    > a QuickLook error ?) on Mac OS X 10.7.3:
    >
    > [QL] QLError(): +[QLSeamlessDocumentOpener
    > seamlessDocumentOpenerForURL:] should only be called in the
    > main thread
    >
    > Now for the cancel question: If I take the more traditional
    > approach to cancel the operation inside readFromURL, should
    > I fire up a new thread to check the flag's status ?
    >
    > Gilles
    >
    >
    >> On 2012 Feb 9, at 19:32, Kyle Sluder wrote:
    >>> On Thu, 2012 Feb 9 at 08:01, Gilles Celli <gilles.celli...> wrote:
    >>> I searched the mailing-list but didn't find an
    > answer….so sorry if this was posted before:
    >>>
    >>> I've setup a document based application which can
    > read large ASCII data files (>150MB).
    >>>
    >>> When opening the document the method
    > readFromURL:ofType:error is used which then opens a small
    > feedback window
    >>> showing the file name and an animated 
    > progress bar with a "Cancel" NSButton.
    >>
    >> If you're targeting Snow Leopard or later, you should
    > override
    >> +canConcurrentlyReadDocumentsOfType: to return YES.
    > That will cause
    >> -initWithContentsOfURL:ofType:error: (and therefore
    >> -readFromURL:ofType:error:) to execute on a background
    > thread.
    >>
    >> Then you get to the canceling part. The traditional
    > approach would be
    >> to set a flag when the user clicks Cancel, and
    > periodically check this
    >> flag from within your -readFromURL:… implementation,
    > returning an
    >> NSUserCancelledError if you detect it has been set.
    >>
    >> A more modern approach might be to use
    > NSOperationQueue. Instead of a
    >> loop, -readFromURL:… would divide its work into
    > operations and enqueue
    >> those on a queue. Clicking the Cancel button would
    > enqueue an
    >> operation that would shut down the -readFromURL:'s
    > operation queue and
    >> cause it to return an NSUserCancelledError.

    I don't get it.  I though with OS X one of the great
    benefits was finally having pre-emptive multi-processing
    instead of co-operative multi-processing.

    Sure, when the user clicks "Cancel" it's an "event",
    it gets stuffed on an event queue, the wheels grind
    round and round and eventually that event gets popped
    off of the queue and paid attention to.  But then...
    then the event processor, the action, should see that
    it needs to interrupt the file transfer cold,
    as close to immediately as possible, the buffers
    that were being used for the transfer freed, and
    control returned to the regularly scheduled programming.
    Neat. Sweet. And at least somewhat close to immediate
    in user terms.  Not, Eyeore style... "ohhh, maybe...
    some... day..., maybe after it gets done filling
    the next buffer or 10... we may think about getting
    around to doing something about it."

    The master, the user, wants it done now!

    (Stuff like this gets sooo frustrating when Apple
    has the best there is... We love their systems
    so much...and yet...)
  • On Feb 9, 2012, at 7:08 PM, Jeffrey Oleander <jgo456...> wrote:

    >
    > I don't get it.  I though with OS X one of the great
    > benefits was finally having pre-emptive multi-processing
    > instead of co-operative multi-processing.
    >
    > Sure, when the user clicks "Cancel" it's an "event",
    > it gets stuffed on an event queue, the wheels grind
    > round and round and eventually that event gets popped
    > off of the queue and paid attention to.  But then...
    > then the event processor, the action, should see that
    > it needs to interrupt the file transfer cold,
    > as close to immediately as possible, the buffers
    > that were being used for the transfer freed, and
    > control returned to the regularly scheduled programming.
    > Neat. Sweet. And at least somewhat close to immediate
    > in user terms.  Not, Eyeore style... "ohhh, maybe...
    > some... day..., maybe after it gets done filling
    > the next buffer or 10... we may think about getting
    > around to doing something about it."

    I think you misunderstand "preemptive multitasking" to imply more power than it actually provides.

    In a vast majority of modern application programming situations, it isn't safe to just kill a background thread when the user hits Cancel. It might be in the middle of I/O (reading a document from disk); it might be holding critical resources (such as, say, the malloc lock); other threads may be waiting for it to provide important data (the NSError object explaining why loading failed).

    If you wish for an environment where such things are possible, you might consider taking a contracting position working on formally-proven systems.

    --Kyle Sluder
  • Mike,

    I'm not sure what you mean to set the cancel flag on the main thread.
    I have done this so far, but I'm still stuck:

    In my NSDocument readFromURL:ofType:error: method it init's a progressLoading WindowController (which shows up the window with progress bar and Cancel button).
    In my progressLoadingWC I have a BOOL cancelLoadingFlag which is checked in readFromURL method…however nothing seems to happen if I call NSError…the document is still opened...

    Also opening the file and trying to click the button is nearly impossible since it slows down heavily. Here's an excerpt of my code

    -(BOOL)readFromURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError
    {
        asciiFileContents = [[NSString alloc] initWithContentsOfURL:absoluteURL
                                                        encoding:NSISOLatin1StringEncoding error:outError];
        if (!progressLoadingWindowController)
        {
            progressLoadingWindowController = [[ProgressLoadingWindowController alloc] init];
            [[progressLoadingWindowController fileNameOutlet] setStringValue:[absoluteURL lastPathComponent]];
            [[progressLoadingWindowController loadProgressBar] setUsesThreadedAnimation:YES];
            [[progressLoadingWindowController loadProgressBar] startAnimation:self];

        }

        // Display the progressLoading window
        [progressLoadingWindowController showWindow:self];

        if ( [progressLoadingWindowController cancelLoadingFlag] )
        {
            NSLog(@"User cancelled opening...");

            *outError = [NSError errorWithDomain:NSCocoaErrorDomain
                                            code:NSUserCancelledError userInfo:nil];
        }

        myDocWindowController = [[TsoftViewerWindowController alloc] initWithTsfFileString:asciiFileContents];

    [self addWindowController:myDocWindowController];

    [asciiFileContents release];


        return YES;

    }

    I'm sure I'm doing something wrong here :-(

    Any clues ?

    Gilles

    On Feb 9, 2012, at 11:53 PM, Mike Abdullah wrote:

    >
    > On 9 Feb 2012, at 20:23, Gilles Celli wrote:
    >
    >> Mike, Kyle,
    >>
    >> Thanks for the quick answers!
    >>
    >> Yes I'm targeting Mac OS X 10.6 and later so canConcurrentlyReadDocumentsOfType: is a welcome addition, I completely forgot that.
    >>
    >> Strangely if I put the method canConcurrentlyReadDocumentsOfType: inside my NSDocument I get a warning when trying to open a document (It seems to be a QuickLook error ?) on Mac OS X 10.7.3:
    >>
    >> [QL] QLError(): +[QLSeamlessDocumentOpener seamlessDocumentOpenerForURL:] should only be called in the main thread
    >
    > Maybe try setting a breakpoint on QLError to see when it gets called?
    >>
    >> Now for the cancel question: If I take the more traditional approach to cancel the operation inside readFromURL, should I fire up a new thread to check the flag's status ?
    >
    > What would this gain you? You set the cancel flag on the main thread. And you have a worker thread for reading the document supplied by the system, so why add another thread to the mix?
    >
  • > I'm sure I'm doing something wrong here :-(
    >
    > Any clues ?

    Yep, here we go:

    On 10 Feb 2012, at 15:28, Gilles Celli wrote:

    > In my NSDocument readFromURL:ofType:error: method it init's a progressLoading WindowController (which shows up the window with progress bar and Cancel button).
    > In my progressLoadingWC I have a BOOL cancelLoadingFlag which is checked in readFromURL method…however nothing seems to happen if I call NSError…the document is still opened...
    >
    > Also opening the file and trying to click the button is nearly impossible since it slows down heavily. Here's an excerpt of my code
    >
    > -(BOOL)readFromURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError
    > {
    > asciiFileContents = [[NSString alloc] initWithContentsOfURL:absoluteURL
    > encoding:NSISOLatin1StringEncoding error:outError];

    OK, good, you're grabbing file contents on the worker thread. How long does this typically take? Why not show your progress display before this?

    > if (!progressLoadingWindowController)
    > {
    > progressLoadingWindowController = [[ProgressLoadingWindowController alloc] init];
    > [[progressLoadingWindowController fileNameOutlet] setStringValue:[absoluteURL lastPathComponent]];
    > [[progressLoadingWindowController loadProgressBar] setUsesThreadedAnimation:YES];
    > [[progressLoadingWindowController loadProgressBar] startAnimation:self];
    >
    > }

    Don't do this. AppKit is not thread safe. Creating a window controller on a secondary thread is not supported.
    >
    > // Display the progressLoading window
    > [progressLoadingWindowController showWindow:self];
    >
    > if ( [progressLoadingWindowController cancelLoadingFlag] )
    > {
    > NSLog(@"User cancelled opening...");
    >
    > *outError = [NSError errorWithDomain:NSCocoaErrorDomain
    > code:NSUserCancelledError userInfo:nil];

    Never blindly assign to error pointers. Check the caller is actually interested in receiving an error first.
    > }
    >
    > myDocWindowController = [[TsoftViewerWindowController alloc] initWithTsfFileString:asciiFileContents];

    Or is it this line of code that takes a long time to run? If so, what's it doing?
    >
    > [self addWindowController:myDocWindowController];

    Furthermore, it is not this method's job to create the UI. That should be done in -makeWindowControllers instead.
    >
    > [asciiFileContents release];
    >
    >
    > return YES;
    >
    > }
  • Opening the ASCII file can take up to 15-20 sec ( > 150MB), so I made some changes as you suggested by moving the progressWindowController  alloc/init before the allocation of asciiFileContents...
    And you're right I should have put addWindowController: in makeWindowController....

    This makes the opening of the file a little more responsive (clicking the button) but not really what I expect...

    > Never blindly assign to error pointers. Check the caller is actually interested in receiving an error first.
    Which caller do you mean ?

    Maybe the way to go is to use a new detached thread only for the progress window, or to use Kyle's approach by using NSOperationQueue..

    Of course if you guys have any other suggestions they will be greatly appreciated :-)

    Gilles

    On 10 févr. 2012, at 17:08, Mike Abdullah wrote:

    >> I'm sure I'm doing something wrong here :-(
    >>
    >> Any clues ?
    >
    > Yep, here we go:
    >
    > On 10 Feb 2012, at 15:28, Gilles Celli wrote:
    >
    >> In my NSDocument readFromURL:ofType:error: method it init's a progressLoading WindowController (which shows up the window with progress bar and Cancel button).
    >> In my progressLoadingWC I have a BOOL cancelLoadingFlag which is checked in readFromURL method…however nothing seems to happen if I call NSError…the document is still opened...
    >>
    >> Also opening the file and trying to click the button is nearly impossible since it slows down heavily. Here's an excerpt of my code
    >>
    >> -(BOOL)readFromURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError
    >> {
    >> asciiFileContents = [[NSString alloc] initWithContentsOfURL:absoluteURL
    >> encoding:NSISOLatin1StringEncoding error:outError];
    >
    > OK, good, you're grabbing file contents on the worker thread. How long does this typically take? Why not show your progress display before this?
    >
    >> if (!progressLoadingWindowController)
    >> {
    >> progressLoadingWindowController = [[ProgressLoadingWindowController alloc] init];
    >> [[progressLoadingWindowController fileNameOutlet] setStringValue:[absoluteURL lastPathComponent]];
    >> [[progressLoadingWindowController loadProgressBar] setUsesThreadedAnimation:YES];
    >> [[progressLoadingWindowController loadProgressBar] startAnimation:self];
    >>
    >> }
    >
    > Don't do this. AppKit is not thread safe. Creating a window controller on a secondary thread is not supported.
    >>
    >> // Display the progressLoading window
    >> [progressLoadingWindowController showWindow:self];
    >>
    >> if ( [progressLoadingWindowController cancelLoadingFlag] )
    >> {
    >> NSLog(@"User cancelled opening...");
    >>
    >> *outError = [NSError errorWithDomain:NSCocoaErrorDomain
    >> code:NSUserCancelledError userInfo:nil];
    >
    > Never blindly assign to error pointers. Check the caller is actually interested in receiving an error first.
    >> }
    >>
    >> myDocWindowController = [[TsoftViewerWindowController alloc] initWithTsfFileString:asciiFileContents];
    >
    > Or is it this line of code that takes a long time to run? If so, what's it doing?
    >>
    >> [self addWindowController:myDocWindowController];
    >
    > Furthermore, it is not this method's job to create the UI. That should be done in -makeWindowControllers instead.
    >>
    >> [asciiFileContents release];
    >>
    >>
    >> return YES;
    >>
    >> }
  • On 10 Feb 2012, at 21:25, Gilles Celli wrote:

    > Opening the ASCII file can take up to 15-20 sec ( > 150MB), so I made some changes as you suggested by moving the progressWindowController  alloc/init before the allocation of asciiFileContents...
    > And you're right I should have put addWindowController: in makeWindowController....
    >
    > This makes the opening of the file a little more responsive (clicking the button) but not really what I expect...
    >
    >> Never blindly assign to error pointers. Check the caller is actually interested in receiving an error first.
    > Which caller do you mean ?

    Cocoa is calling your method. You should do:

    if (outError) *outError = …

    Otherwise it'll crash should outError be nil. The static analyser can warn you of this.
    >
    > Maybe the way to go is to use a new detached thread only for the progress window, or to use Kyle's approach by using NSOperationQueue..

    Again I repeat: AppKit is NOT thread safe. You must perform all UI on the main thread. You already have a detached thread. There is likely no point trying to start up another one.
previous month february 2012 next month
MTWTFSS
    1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29        
Go to today