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



