TextField not updated in a sheet for document-based Cocoa app

  • Hi,

    I've written a document-based Cocoa application on OS X which displays graphs visually by opening an ASCII file.

    My program has also the possibility to append ASCII files to the current document: when a user chooses the "Append …" menu in the File
    Menu a sheet opens with a NSTextField appendingTextField which *should* display the current files (along with a "Cancel" button), and
    if the file's matches the current document by comparing some values (date / time / location) etc.

    While appending and combining these files together takes some time (can take up to more than 10s)
    the sheet is properly displayed (with its TextField and button) for the current document window, but the textField stringValue can't be set
    Maybe because it's the sheet is on a different thread ?…

    I've tried to get the sheet's textField string setting on the main queue to allow the update, but this doesn't work either….
    Does someone have any clues ?

    What I've done so far:

    init method:

    appendFilesSheet = [[AppendFilesProcessingSheetWindowController alloc]
                                initWithWindowNibName:@"AppendFilesProcessingSheetWindowController"];

    Then in:

    - (IBAction)appendfFiles:(id)sender
    {
    // Open the panel to append files
    NSOpenPanel *openPanel = [NSOpenPanel openPanel];
    [openPanel beginSheetModalForWindow:window completionHandler:nil];
          NSArray *filesToAppend;

        NSInteger result = [openPanel runModal];
        if (result == NSFileHandlingPanelOKButton)
        {
            filesToAppend = [openPanel URLs];
        }

        [openPanel orderOut:self];
        [NSApp endSheet:openPanel];

        openPanel = nil;  // preventing strong ref cycle
        …
      …..

    NSString *appendedTsoftString;

        [NSApp beginSheet:[appendFilesSheet window]
              modalForWindow:window
                modalDelegate:nil
              didEndSelector:nil
                  contextInfo:nil];


    // Combine the files….
        appendedTsoftString = [[NSString alloc] initWithString:[self combineFiles:filesToAppend withError:&appendError]];

    So inside the combineFiles method I try to update the appendTextField


    // Update count since we got new append files
        countAppendedFiles = [tsfFiles count];

        void (^appendBlock)(void);

        appendBlock = ^{

            dispatch_async(dispatch_get_main_queue(), ^{
                [[appendFilesSheet appendTextField] setStringValue:@"Appending Files..."];
                //[[appendFilesSheet appendTextField] setNeedsDisplay: YES];
                NSLog(@"[appendFilesSheet appendTextField] stringValue: %@", [[appendFilesSheet appendTextField] stringValue]);
            });
        };

        //Run the block on a different thread.
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_async(queue,appendBlock);
        …

    Any help would be greatly appreciated!

    Cheers,

    Gilles
  • On 12/06/2012, at 12:29 AM, Gilles Celli wrote:

    > the sheet is properly displayed (with its TextField and button) for the current document window, but the textField stringValue can't be set
    > Maybe because it's the sheet is on a different thread ?…
    >
    > I've tried to get the sheet's textField string setting on the main queue to allow the update, but this doesn't work either….
    > Does someone have any clues ?
    > appendBlock = ^{
    >
    > dispatch_async(dispatch_get_main_queue(), ^{
    > [[appendFilesSheet appendTextField] setStringValue:@"Appending Files..."];
    > //[[appendFilesSheet appendTextField] setNeedsDisplay: YES];
    > NSLog(@"[appendFilesSheet appendTextField] stringValue: %@", [[appendFilesSheet appendTextField] stringValue]);
    > });
    > };
    >
    > //Run the block on a different thread.
    > dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    > dispatch_async(queue,appendBlock);

    Since the text field is UI, you can't drive it from a thread other than the main thread. I think this includes calling its -setStringValue: method.

    So you need to invoke that on the main thread - use [textField performSelectorOnMainThread:withObject:waitUntilDone:];

    That will only work if you're not also blocking the main thread. I couldn't quite unravel your code, but it looks like nothing will happen because the main run loop isn't running while your loop is hogging the main thread. If you can do your files work on another thread, that could work, otherwise you'll have to break down the work your loop does and call it in pieces so that the normal work of the main thread is able to run. An easy way to do this is to use NSOperationQueue using the +mainQueue object which does the work (NSOperations) in pieces on the main thread. If your operations are thread-safe, this approach can be changed to a threaded approach by simply making an operation queue object of your own.

    Another problem is that you start your work as soon as you've called beginSheet... on your panel. That method returns very quickly, and before the window itself is loaded, animated into position and run modally. That means your work has started before the window is available, so it may be that your references to its text fields are nil. It might be better to wait until the window is actually loaded (windowDidLoad) before doing your work, and make sure the work doesn't block the main thread.

    --Graham
  • Graham,

    Thanks for your fast response.
    I'll look at this ASAP, and will let you know how it goes :-)

    Cheers,

    Gilles

    On Jun 12, 2012, at 2:27 AM, Graham Cox wrote:

    >
    > On 12/06/2012, at 12:29 AM, Gilles Celli wrote:
    >
    >> the sheet is properly displayed (with its TextField and button) for the current document window, but the textField stringValue can't be set
    >> Maybe because it's the sheet is on a different thread ?…
    >>
    >> I've tried to get the sheet's textField string setting on the main queue to allow the update, but this doesn't work either….
    >> Does someone have any clues ?
    >> appendBlock = ^{
    >>
    >> dispatch_async(dispatch_get_main_queue(), ^{
    >> [[appendFilesSheet appendTextField] setStringValue:@"Appending Files..."];
    >> //[[appendFilesSheet appendTextField] setNeedsDisplay: YES];
    >> NSLog(@"[appendFilesSheet appendTextField] stringValue: %@", [[appendFilesSheet appendTextField] stringValue]);
    >> });
    >> };
    >>
    >> //Run the block on a different thread.
    >> dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    >> dispatch_async(queue,appendBlock);
    >
    >
    > Since the text field is UI, you can't drive it from a thread other than the main thread. I think this includes calling its -setStringValue: method.
    >
    > So you need to invoke that on the main thread - use [textField performSelectorOnMainThread:withObject:waitUntilDone:];
    >
    > That will only work if you're not also blocking the main thread. I couldn't quite unravel your code, but it looks like nothing will happen because the main run loop isn't running while your loop is hogging the main thread. If you can do your files work on another thread, that could work, otherwise you'll have to break down the work your loop does and call it in pieces so that the normal work of the main thread is able to run. An easy way to do this is to use NSOperationQueue using the +mainQueue object which does the work (NSOperations) in pieces on the main thread. If your operations are thread-safe, this approach can be changed to a threaded approach by simply making an operation queue object of your own.
    >
    > Another problem is that you start your work as soon as you've called beginSheet... on your panel. That method returns very quickly, and before the window itself is loaded, animated into position and run modally. That means your work has started before the window is available, so it may be that your references to its text fields are nil. It might be better to wait until the window is actually loaded (windowDidLoad) before doing your work, and make sure the work doesn't block the main thread.
    >
    > --Graham
    >
    >
  • Graham,

    Following your suggestions I tried with [[NSOperationQueue mainQueue] addOperationWithBlock: to update the textField while processing files....but with less or more success...

    While I put [NSApp beginSheet:modalForWindow:modalDelegate:didEndSelector:contextInfo: inside [[NSOperationQueue mainQueue] addOperationWithBlock: the sheet window is not loaded directly...it displays after some file processing has been done ...

    1. First I put the [NSApp beginSheet:modalForWindow:modalDelegate:didEndSelector:contextInfo: ... before processing the files
    (this consist on reading the ASCII files with initWithContentsOfFile: then extracting every channel with NSScanner, this can take up to 2-10 seconds  depending on the file size).

    First problem:
    The sheet with the textField is not displayed directly after the openPanel (to select the appended files) is closed...
    This is as you suggested due to the fact that the sheet's window has not finished to load...so how do I invoke windowDidLoad for appendFilesSheet (NSWindowController)...

    2. While processing the files the textField is only updated after the whole process has finished...this is not what I expect...would
    be nice to update the textField by showing which file it actually processes etc.

    I put again the whole code inside NSOperationQueue mainQueue block:

    // Do some basic processing...
    // Compare the appended files location name with the original's document file location and update the sheet's textField
    // Some pseudo-code here…

    for (int i=0 ; i < numberOfFilesToAppend; i++)
    {
    NSString *nameOfLocation = [[asciiFilesToAppend objectAtIndex:i] locationName];
    ....
    ....

    if [nameOfLocation isEqualToString originalLocationName]
    {
      [[NSOperationQueue mainQueue] addOperationWithBlock:^(void) {

          NSString *stationLocation =
      [[NSString alloc] initWithFormat:@"Location of the instrument location:%@", [asciiFilesToAppend objectAtIndex:i] stationLocation];
      [[appendFilesSheet appendTextField] setStringValue:stationLocation];
      [[appendFilesSheet appendTextField] setNeedsDisplay:YES];
            [stationLocation release];

          }];
    }

    }

    But no luck...the appendTextField is only updated after the whole process has finished...showing only the last object's name

    I'm pretty sure I'm doing something wrong here...so any clarification, or suggestions is of course greatly appreciated ;-)

    Cheers,

    Gilles
  • Hi,

    I'm interested how people will comment on this.

    A Finite State Machine Implementation would look something like this:

    -(void) nextState
    {
    lastState = currentState;
    currentState = nextState;

    switch (currentState)
      {
    // Do Something to make nextState get called again, e.g. set of an
    ASync Request.
      case: kCaseA
      [self doSomethingCaseA];
      nextState = kCaseB;
      break;

      case: kCaseB
      [self doSomethingCaseB_UsingDataFromCaseA:caseAData];
      nextState = kCaseC;
      break;

      case: kCaseC
      [self doSomethingCaseC_UsingDataFromCaseB:caseCData];
      nextState = kLastState;
      break;

      case: kLastState
      return;
      break;

      default:
      //UNKNOWN STATE
      }
    }

    -(void) doStateMachine
    {

    lastState = UNKNOWN_STATE;
    currentState = kCaseA;
    [self nextState];
    }

    The above assumes the doSomethingCase Methods will set of some ASync
    Task that will call nextState sometime in the future.

    However an Infinite State Machine would look something like this:

    -(void) doStateMachine
    {
      myDataFromCaseA = [self doSomethingCaseA];
      myDataFromCaseB = [self doSomethingCaseB_UsingDataFromCaseA:
    myDataFromCaseA];
      myDataFromCaseC = [self doSomethingCaseC_UsingDataFromCaseB:
    myDataFromCaseB];
    }

    In this case, the doSomethingCase methods are causing the thread to
    wait until the ASync Response has completed and the data has been
    obtained.

    Since there is no formal definition of the number of states here,
    could be said to be an Infinite State Machine? I've been programming
    since the days of mini computers in assembler. In assembler this
    would be implemented is using an "Exchange Instruction" to alter the
    PC on the stack and cause it to return to the correct place once the
    ASync Task (usually an interrupt) had finished.

    I also think RBK Dewar refers to this technique as an "Infinite State
    Machine".

    Any comments welcomed.

    Cheers
    Dave
  • On 14/06/2012, at 8:03 AM, Dave wrote:

    > In assembler this would be implemented is using an "Exchange Instruction" to alter the PC on the stack and cause it to return to the correct place once the ASync Task (usually an interrupt) had finished.

    Ah, those were the days - push a calculated address on the stack and do a 'RET' to cause a jump to that address... thankfully such tricks are wholly unnecessary these days. In fact a simple switch...case statement does the same job in most cases.

    I'm not sure of the answer to your question though, seems to me you could simply queue each task then the next executes as soon as the one ahead of it finishes.

    --Graham
  • On 14 Jun 2012, at 05:12, Graham Cox wrote:

    >
    > On 14/06/2012, at 8:03 AM, Dave wrote:
    >
    >> In assembler this would be implemented is using an "Exchange
    >> Instruction" to alter the PC on the stack and cause it to return
    >> to the correct place once the ASync Task (usually an interrupt)
    >> had finished.
    >
    >
    > Ah, those were the days - push a calculated address on the stack
    > and do a 'RET' to cause a jump to that address... thankfully such
    > tricks are wholly unnecessary these days. In fact a simple
    > switch...case statement does the same job in most cases.
    >
    > I'm not sure of the answer to your question though, seems to me you
    > could simply queue each task then the next executes as soon as the
    > one ahead of it finishes.
    >

    The point is you can't queue B until the data from A has been
    obtained and that might take a long while. There are two ways to deal
    with it, either have a call back from the lower level that returns
    the data (in which case you have to specify an address/selector to go
    to when the data has been obtained sucessfully), OR you can invert
    the control and make the interface look as it is demand based, even
    though the data is being obtained as and when it is ready.
  • On 15/06/2012, at 3:56 AM, Dave wrote:

    >
    > On 14 Jun 2012, at 05:12, Graham Cox wrote:
    >
    >>
    >> On 14/06/2012, at 8:03 AM, Dave wrote:
    >>
    >>> In assembler this would be implemented is using an "Exchange Instruction" to alter the PC on the stack and cause it to return to the correct place once the ASync Task (usually an interrupt) had finished.
    >>
    >>
    >> Ah, those were the days - push a calculated address on the stack and do a 'RET' to cause a jump to that address... thankfully such tricks are wholly unnecessary these days. In fact a simple switch...case statement does the same job in most cases.
    >>
    >> I'm not sure of the answer to your question though, seems to me you could simply queue each task then the next executes as soon as the one ahead of it finishes.
    >>
    >
    > The point is you can't queue B until the data from A has been obtained and that might take a long while. There are two ways to deal with it, either have a call back from the lower level that returns the data (in which case you have to specify an address/selector to go to when the data has been obtained sucessfully), OR you can invert the control and make the interface look as it is demand based, even though the data is being obtained as and when it is ready.

    I can't really see the problem here. You can still queue the tasks, even if the queued task doesn't have the data available when it's queued - the data will be available by the time it comes to run, so it can just ask for it then. It's really simple to create demand-based data providers in Cocoa using informal or formal protocols, delegation, or passing a predefined selector/target as you suggest. Or using NSInvocation. There are many solutions, there's probably no One True Way™ but certainly you have the tools at your disposal - there are plenty of solid reliable solutions that require no particular tricks, and certainly nothing skanky of the sort you were reminiscing about!

    --Graham
  • On 15 Jun 2012, at 00:54, Graham Cox wrote:

    >
    > On 15/06/2012, at 3:56 AM, Dave wrote:
    >
    >>
    >> On 14 Jun 2012, at 05:12, Graham Cox wrote:
    >>
    >>>
    >>> On 14/06/2012, at 8:03 AM, Dave wrote:
    >>>
    >>>> In assembler this would be implemented is using an "Exchange
    >>>> Instruction" to alter the PC on the stack and cause it to return
    >>>> to the correct place once the ASync Task (usually an interrupt)
    >>>> had finished.
    >>>
    >>>
    >>> Ah, those were the days - push a calculated address on the stack
    >>> and do a 'RET' to cause a jump to that address... thankfully such
    >>> tricks are wholly unnecessary these days. In fact a simple
    >>> switch...case statement does the same job in most cases.
    >>>
    >>> I'm not sure of the answer to your question though, seems to me
    >>> you could simply queue each task then the next executes as soon
    >>> as the one ahead of it finishes.
    >>>
    >>
    >> The point is you can't queue B until the data from A has been
    >> obtained and that might take a long while. There are two ways to
    >> deal with it, either have a call back from the lower level that
    >> returns the data (in which case you have to specify an address/
    >> selector to go to when the data has been obtained sucessfully), OR
    >> you can invert the control and make the interface look as it is
    >> demand based, even though the data is being obtained as and when
    >> it is ready.
    >
    >
    > I can't really see the problem here. You can still queue the tasks,
    > even if the queued task doesn't have the data available when it's
    > queued - the data will be available by the time it comes to run, so
    > it can just ask for it then. It's really simple to create demand-
    > based data providers in Cocoa using informal or formal protocols,
    > delegation, or passing a predefined selector/target as you suggest.
    > Or using NSInvocation.

    I can't see how queuing the Request would help or work in this case.
    The point is that I don't know which request will be next because
    it's dependent on the data returned by the previous request.

    e.g.

    myResponse = doTaskA;
    if (myResponse.param == 1)
        myResponse = doTaskB;
    else
        myResponse = doTaskC;

    There is no point in queuing TaskB *and* TaskC since you don't know
    which one you need to run until you get the response back from TaskA.

    > There are many solutions, there's probably no One True Way™ but
    > certainly you have the tools at your disposal - there are plenty of
    > solid reliable solutions that require no particular tricks, and
    > certainly nothing skanky of the sort you were reminiscing about!

    The way I am doing it is not skanky in fact it works really well. The
    code was already written using selector/target delgates and was a 2000
    + Lines of Code Class, doing it using an Infinite State Machines type
    thing made it less than half that and the code is MUCH easier to
    follow and MUCH easier to extend in the future.

    All the Best
    Dave
previous month june 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 30  
Go to today