How to save a document without blocking the main thread

  • Hi,

    I'm hoping third time is lucky, because I have a hard time imagining
    absolutely no-one on this list would have any light to shed on this
    matter. (Not that anybody _has_ to of course, even if they do :-) But
    maybe I have not been very successful at presenting the situation in a
    clear manner.

    So, the issue is: in the project I'm working on saving a file to disk
    can be a particularly lengthy operation. If I save a document's state
    to disk using the usual methods this can, even on the latest hardware,
    result in an unresponsive app with a spinning beach-ball for an
    extended period of time (many seconds, minutes even if the files are
    very large).

    I need to therefore write out the document in a separate thread. To
    bloc changes to the document at hand, and provide feedback, a modal
    sheet is presented that reflects the saving progress, but leaves the
    runloop free to respond to user actions thus maintaining a healthy
    feel to the app and allowing the user to work on other documents.

    I have no problems saving the file. That all happens correctly. The
    problem is that after the file is saved and worked on some more, then,
    when trying to write the modifications out to disk NSDocument (I
    suppose) complains that the file has been modified by another
    application.

    I think the reason for this is: The initial part of the saving process
    is done through the usual channels provided by NSDocument: If the
    document is untitled a Save Panel appears, if not the routine to write
    out the data is called straight away. This is where I fork a thread
    that does the actual saving. However: after forking the thread the
    method returns immediately and, *I suppose*, the framework's saving
    mechanism at that point believes a successful save operation has
    completed and records some state info about the doc's on disk
    representation (like time of saving). Then when a new save command is
    issued by the user the document checks its on disk representation
    state and compares it to its internal data and discovers that the last
    modification has a later date the one stored in memory. So the doc
    must have been changed by another app and the user is alerted. [This
    is conjecture on my part, but seems to me the most likely scenario.]

    What I need then is a way to synchronise the internally recorded state
    info with the on disk representation after I am done writing out the
    data. Is there a way to do this? Or do I need to take the whole
    process into my own hands right from the start, bypassing the
    NSDocument's saving services altogether? I'd rather not, if I don't
    really have to.

    I hope this makes cear what is going on, and some help, any pointers,
    on this matter would be really appreciated.

    The method where I hook into the saving mechanism and fork the thread
    is the following:

    - (void)saveToURL:(NSURL *)absoluteURL ofType:(NSString *)typeName
    forSaveOperation:(NSSaveOperationType)saveOperation delegate:
    (id)delegate didSaveSelector:(SEL)didSaveSelector contextInfo:(void
    *)contextInfo
    {
    [NSApplication detachDrawingThread:@selector(saveDocumentToDisk:)
    toTarget:self withObject:[NSDictionary
    dictionaryWithObjectsAndKeys:absoluteURL, @"absoluteURL", typeName,
    @"typeName", [NSNumber numberWithInteger:saveOperation],
    @"saveOperation", nil]];
    }

    The relevant parts of the save method, as a method in the NSDocument
    subclass:

    - (void)saveDocumentToDisk:(NSDictionary *)info
    {
    NSURL  *absoluteURL = [info objectForKey:@"absoluteURL"];
    NSString *path = [absoluteURL path];

    if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
      success = [[NSFileManager defaultManager] createDirectoryAtPath:path
    withIntermediateDirectories:YES attributes:nil error:NULL];
    }

    if (success) {

    [...save state...everything is written into the package...]

    }

    // Freshly saved state: on disk rep is synched with interal rep
    if (success) {
      [self updateChangeCount:NSChangeCleared];
    }
    }

    This is what I have now, it almost works, but not quite well enough.
    [All on Leopard in a garbage collected app by the way.]

    Cheers,
    António

    -----------------------------------------------
    Touch is a language without words
    -----------------------------------------------
  • Le 20 déc. 07 à 15:51, Antonio Nunes a écrit :

    > Hi,
    >
    > I'm hoping third time is lucky, because I have a hard time imagining
    > absolutely no-one on this list would have any light to shed on this
    > matter. (Not that anybody _has_ to of course, even if they do :-)
    > But maybe I have not been very successful at presenting the
    > situation in a clear manner.
    >
    > So, the issue is: in the project I'm working on saving a file to
    > disk can be a particularly lengthy operation. If I save a document's
    > state to disk using the usual methods this can, even on the latest
    > hardware, result in an unresponsive app with a spinning beach-ball
    > for an extended period of time (many seconds, minutes even if the
    > files are very large).
    >
    > I need to therefore write out the document in a separate thread. To
    > bloc changes to the document at hand, and provide feedback, a modal
    > sheet is presented that reflects the saving progress, but leaves the
    > runloop free to respond to user actions thus maintaining a healthy
    > feel to the app and allowing the user to work on other documents.
    >
    > I have no problems saving the file. That all happens correctly. The
    > problem is that after the file is saved and worked on some more,
    > then, when trying to write the modifications out to disk NSDocument
    > (I suppose) complains that the file has been modified by another
    > application.
    >
    > I think the reason for this is: The initial part of the saving
    > process is done through the usual channels provided by NSDocument:
    > If the document is untitled a Save Panel appears, if not the routine
    > to write out the data is called straight away. This is where I fork
    > a thread that does the actual saving. However: after forking the
    > thread the method returns immediately and, *I suppose*, the
    > framework's saving mechanism at that point believes a successful
    > save operation has completed and records some state info about the
    > doc's on disk representation (like time of saving). Then when a new
    > save command is issued by the user the document checks its on disk
    > representation state and compares it to its internal data and
    > discovers that the last modification has a later date the one stored
    > in memory. So the doc must have been changed by another app and the
    > user is alerted. [This is conjecture on my part, but seems to me the
    > most likely scenario.]
    >
    > What I need then is a way to synchronise the internally recorded
    > state info with the on disk representation after I am done writing
    > out the data. Is there a way to do this? Or do I need to take the
    > whole process into my own hands right from the start, bypassing the
    > NSDocument's saving services altogether? I'd rather not, if I don't
    > really have to.
    >
    > I hope this makes cear what is going on, and some help, any
    > pointers, on this matter would be really appreciated.

    I don't know if it may solve your problem, but have a look at "Message
    Flow in the Document Architecture"

    http://developer.apple.com/documentation/Cocoa/Conceptual/Documents/Article
    s/ObjectInteractions.html


    It show you in details what NSDocument do when it save a document.
  • On Dec 20, 2007, at 3:05 PM, Jean-Daniel Dupas wrote:

    > I don't know if it may solve your problem, but have a look at
    > "Message Flow in the Document Architecture"
    >
    > http://developer.apple.com/documentation/Cocoa/Conceptual/Documents/Article
    s/ObjectInteractions.html

    >
    > It show you in details what NSDocument do when it save a document.

    Thanks Jean-Daniel, third time is lucky indeed, that was just the
    pointer I needed. I had missed this in the docs, and it provides
    enough details to sort things out. It's a bit late (or very early in
    the morning if you like), so my head is not at its best and I'll have
    to reverify after my sanity-nap, but it looks like I got it to work.

    This is the new strategy:

    I let the flow run normally up until
    saveToURL:ofType:forSaveOperation:delegate:didSaveSelector:contextInfo
    : which I override so that I can fork a thread at that point. At this
    moment the URL still points to the real target directory (which it
    won't after the writeSafelyToURL:ofType:forSaveOperation: method). I
    pack  whatever data I need into a dictionary and pass it as the object
    to the thread.

    The thread entry method unpacks the information in the dictionary and
    uses that to resume the chain of commands by calling
    saveToURL:ofType:forSaveOperation:error: the data is now saved in
    writeToURL:ofType:error: which eventually gets called

    When the flow of commands returns to the thread entry method there is
    some minor clean up to do: if the saving was unsuccessful we need to
    alert the user. If successful and this was an untitled document we
    want to add it to the recent documents menu. This I still need to
    figure out since the simple call [[NSDocumentController
    sharedDocumentController] noteNewRecentDocument:self] doesn't seem to
    work here.

    In code:

    // Hook into the flow to fork a thread
    - (BOOL)saveToURL:(NSURL *)absoluteURL ofType:(NSString *)typeName
    forSaveOperation:(NSSaveOperationType)saveOperation delegate:
    (id)delegate didSaveSelector:(SEL)didSaveSelector contextInfo:(void
    *)contextInfo
    {
    [NSApplication detachDrawingThread:@selector(saveDocumentToDisk:)
    toTarget:self withObject:[NSDictionary
    dictionaryWithObjectsAndKeys:absoluteURL, @"absoluteURL", typeName,
    @"typeName", [NSNumber numberWithInteger:saveOperation],
    @"saveOperation", nil]];

    // Pretend everything is hunky-dory. If errors occur they will be
    handled.
    return YES;
    }

    // Thread entry:
    - (void)saveDocumentToDisk:(NSDictionary *)info
    {
    NSURL  *absoluteURL = [info objectForKey:@"absoluteURL"];
    NSString *typeName = [info objectForKey:@"typeName"];
    NSSaveOperationType saveOperation = [[info
    objectForKey:@"saveOperation"] integerValue];
    NSError  *error;

    BOOL success = [self saveToURL:absoluteURL ofType:typeName
    forSaveOperation:saveOperation error:&error];

    if ( ! success) {
      // TODO: Alert user of failure to save.
    } else {
      // TODO: Add the document to the recent documents menu if it's not
    already there
    }
    }

    // Write out the data:
    - (BOOL)writeToURL:(NSURL *)absoluteURL ofType:(NSString *)typeName
    error:(NSError **)outError
    {
    // Assume success
    BOOL success = YES;

    [...lots of code to save document data...]

    return success;
    }

    Cheers,
    António

    -----------------------------------------------------------
    And could you keep your heart in wonder
    at the daily miracles of your life,
    your pain would not seem less wondrous
    than your joy.

    --Kahlil Gibran
    -----------------------------------------------------------
previous month december 2007 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 30
31            
Go to today