Bug in Panther NSTableView + Workaround

  • I found a bug in Panther's NSTableView (haven't checked to see if it's
    present in Jaguar), and wanted to share the workaround. This probably
    won't come up if you're using the Controller layer, but that's
    obviously not an option if you're targetting Jaguar too.

    The scenario is this:

    Two NSTableViews, one master and one slave that acts as the detail
    view. Basically, a standard to-many relationship. Select something in
    the master table, and the slave refreshes itself with contextual data.
    Not exactly rocket science.

    The problem arises when the slave table does not contain the same
    number of records for each selection in the master. Here's an example
    record set:

    Master 1 Master 2
    -------- --------
    slave a  slave a
        slave b

    Everything is fine as long as the master table view has the focus ring.
    But if the following conditions are met:

    1. Slave table has focus ring
    2. While slave still has focus, a new row selection is clicked in the
    master
    3. The number of slave rows for the new master selection is less than
    the current number of slave rows

    ... you'll get something like this:

    *** -[NSCFArray objectAtIndex:]: index (1) beyond bounds (1)

    After quite a bit of debugging, it looks like a table with a focus ring
    that is losing focus to another view forgets to call
    numberOfRowsInTableView prior to reloading its contents. So it still
    assumes it has a certain number of rows prior to calling
    tableView:objectValueForTableColumn:row: That row count may or may not
    still be valid, so you may get an out of bounds message.

    The solution is to implement this delegate method to force a reload
    (with row count check) before the slave table before it has an
    opportunity to do it itself:

    - (void)tableViewSelectionIsChanging:(NSNotification *)aNotification
    {
        if ([aNotification object] == masterTable)
            [slaveTable reloadData];
    }

        - Scott

    --
    Tree House Ideas
    http://treehouseideas.com/
  • On Nov 7, 2003, at 2:04 PM, Scott Stevenson wrote:

    > I found a bug in Panther's NSTableView (haven't checked to see if it's
    > present in Jaguar), and wanted to share the workaround. This probably
    > won't come up if you're using the Controller layer, but that's
    > obviously not an option if you're targetting Jaguar too.
    >
    >
    > The scenario is this:
    >
    > Two NSTableViews, one master and one slave that acts as the detail
    > view. Basically, a standard to-many relationship. Select something in
    > the master table, and the slave refreshes itself with contextual data.
    > Not exactly rocket science.
    >
    > The problem arises when the slave table does not contain the same
    > number of records for each selection in the master. Here's an example
    > record set:
    >
    > Master 1    Master 2
    > --------    --------
    > slave a        slave a
    > slave b
    >
    > Everything is fine as long as the master table view has the focus
    > ring. But if the following conditions are met:
    >
    > 1. Slave table has focus ring
    > 2. While slave still has focus, a new row selection is clicked in the
    > master
    > 3. The number of slave rows for the new master selection is less than
    > the current number of slave rows
    >
    > ... you'll get something like this:
    >
    > *** -[NSCFArray objectAtIndex:]: index (1) beyond bounds (1)
    >
    > After quite a bit of debugging, it looks like a table with a focus
    > ring that is losing focus to another view forgets to call
    > numberOfRowsInTableView prior to reloading its contents. So it still
    > assumes it has a certain number of rows prior to calling
    > tableView:objectValueForTableColumn:row: That row count may or may not
    > still be valid, so you may get an out of bounds message.
    >
    >
    > The solution is to implement this delegate method to force a reload
    > (with row count check) before the slave table before it has an
    > opportunity to do it itself:
    >
    > - (void)tableViewSelectionIsChanging:(NSNotification *)aNotification
    > {
    > if ([aNotification object] == masterTable)
    > [slaveTable reloadData];
    > }

    If I understand things your data source has it contents (the data it
    fronts for) changing based on users selection in a separate table.

    When the contents change in the data source you should be letting your
    table view know that either via reloadData or by using
    noteNumberOfRowsChanged (if appropriate). If you are not doing this
    then NSTableView doesn't know that the data it is displaying has
    changed.

    IMO changing focus should not require that NSTableView consider that
    row count has changed. Really only reloadData or
    noteNumberOfRowsChanged should.

    Not sure I agree that this is a bug in NSTableView.

    -Shawn
  • This happens in Jaguar too, I had a problem like this. Its best to
    reload the data in the table when it changes. My data table object
    generates a notification whenever it changes.
  • On Nov 7, 2003, at 3:08 PM, Shawn Erickson wrote:

    > If I understand things your data source has it contents (the data it
    > fronts for) changing based on users selection in a separate table.
    >
    > When the contents change in the data source you should be letting your
    > table view know that either via reloadData or by using
    > noteNumberOfRowsChanged (if appropriate)

    I don't believe this is a problem with the datasource methods. I've
    written table/outline apps before (under Jaguar). In this app, the
    synchronization works in every scenario except when the slave has
    focus. There's no problem if the master has focus. That feels like a
    bug, since the focus ring shouldn't affect behavior of the datsource
    methods.

    > IMO changing focus should not require that NSTableView consider that
    > row count has changed.

    I see the angle you're coming at, but this is a case where it's
    required. The slave is losing focus because a new selection is being
    made in the master. The slave row count is variable.

    When the slave loses focus, it appears to automatically reload. If it's
    going to reload, I think it should count the rows as part of the
    process. That seems straightforward to me. But since it doesn't, I have
    to force a reload during tableViewSelectionIsChanging.

    The sequence of events appears to be:

    - Slave has focus
    - New master selection made
    - tableViewSelectionIsChanging is called for master table
    - Slave loses focus and reloads (without calling
    numberOfRowsInTableView)
    - tableViewSelectionDidChange is called for master table

    The code is going to be open source.

          - Scott

    --
    Tree House Ideas
    http://treehouseideas.com/
  • On Nov 8, 2003, at 12:00 AM, Scott Stevenson wrote:

    >
    > On Nov 7, 2003, at 3:08 PM, Shawn Erickson wrote:
    >
    >> If I understand things your data source has it contents (the data it
    >> fronts for) changing based on users selection in a separate table.
    >>
    >> When the contents change in the data source you should be letting
    >> your table view know that either via reloadData or by using
    >> noteNumberOfRowsChanged (if appropriate)
    >
    > I don't believe this is a problem with the datasource methods. I've
    > written table/outline apps before (under Jaguar). In this app, the
    > synchronization works in every scenario except when the slave has
    > focus. There's no problem if the master has focus. That feels like a
    > bug, since the focus ring shouldn't affect behavior of the datsource
    > methods.
    >
    >
    >> IMO changing focus should not require that NSTableView consider that
    >> row count has changed.
    >
    > I see the angle you're coming at, but this is a case where it's
    > required. The slave is losing focus because a new selection is being
    > made in the master. The slave row count is variable.
    >
    > When the slave loses focus, it appears to automatically reload. If
    > it's going to reload, I think it should count the rows as part of the
    > process. That seems straightforward to me. But since it doesn't, I
    > have to force a reload during tableViewSelectionIsChanging.
    >
    > The sequence of events appears to be:
    >
    > - Slave has focus
    > - New master selection made
    > - tableViewSelectionIsChanging is called for master table
    > - Slave loses focus and reloads (without calling
    > numberOfRowsInTableView)
    > - tableViewSelectionDidChange is called for master table

    The two tables are only related to each other by your code... right? So
    the act of a user changing the selection in the "master" table view
    results in your data source changing for the "slave" table view. You
    need to call reloadData because you change your data in response to the
    user.

    The focus change is unrelated to this... (expect it loads table data
    using cached row count, because you haven't told it things have
    changed).

    I guess I don't follow what you perceive as the problem.

    -Shawn
  • I think that both Scott and I are fully aware that we need to call
    reloadData - and we both do (at least I do)! I'll try to create a
    reproducible case in a small sample project early next week. This is
    not an as easy problem as you appear to think it is. Something has
    changed between Jaguar and Panther with regards to how table views
    behave. It could well be that we used an undocumented feature of
    pre-Panther AppKit that has since been changed.

    j o a r

    On 2003-11-08, at 09.10, Shawn Erickson wrote:

    > The two tables are only related to each other by your code... right?
    > So the act of a user changing the selection in the "master" table view
    > results in your data source changing for the "slave" table view. You
    > need to call reloadData because you change your data in response to
    > the user.
    >
    > The focus change is unrelated to this... (expect it loads table data
    > using cached row count, because you haven't told it things have
    > changed).
    >
    > I guess I don't follow what you perceive as the problem.
  • On Nov 8, 2003, at 1:28 AM, j o a r wrote:

    > I think that both Scott and I are fully aware that we need to call
    > reloadData

    Yes.  :)

    The problem has nothing to do with forgetting to call reloadData.

    The slave table is losing focus and automatically reloads. But it does
    so without checking the number of rows first. It just starts calling
    objectValueForTableColumn:row:, and therefore overruns the array if the
    new slave row count is unequal. There would be no problem if it called
    -numberOfRowsInTableView on the slave table first.

    In the interest of making this issue completely clear and preventing
    someone else from running into the same problem, here's a hypothetical
    example:

    There's a Trainer table and a Dog table.

    Trainer 1 has two dogs, Spot and Clifford. Trainer 2 has one dog named
    Rex.

    Trainer 1 is selected but the Dog table has focus. While the Dog table
    still has focus, Trainer 2 is selected.

    At this point, the Dog view reloads its contents automatically (due to
    focus change). It asks the datasource for the first Dog, which is Rex.
    It then asks the datasource for the second Dog -- but there is no
    second Dog! The table view reloaded but forgot to ask for the row count
    first, so it overruns the array. And -tableViewSelectionDidChange
    doesn't get called until the array has already been overrun.

    The solution is to use -tableViewSelectionIsChanging as an opportunity
    to call -reloadData, and force a new row count on the Dog table before
    it does a partial reload itself.

    Note that this is a non-issue if the Trainer (master) table has focus
    the whole time, because its row count isn't contingent on the contents
    of the Dog table. I think this is why the bug is elusive. The issue
    relates *exclusively* to the table losing focus and reloading itself
    without checking row count. There are many situations where this would
    be fine because the row count is constant.

        - Scott