And an answer: What to do if your table view shows the whole array in every row

2007-12-09 00:30:33 UTC

Before writing my previous post, I had asked some people on IRC for help, including David Smith, who is a fellow Adium developer. He remembered a solution, but uncertainly. One thing he said, though, proved relevant: “You have two NSTableColumns; which one is it [NSTableView] creating the content binding from?”

David was referring to the content binding of NSTableView, which I had dismissed as an artifact of an earlier design of NSTableView, or perhaps a simplified path for those people who had only one column in their table view. (I saw the mention in the docs that the table view binds it automatically to the value of one of its columns’ value bindings, but didn’t make any connections then.)

This latter explanation made the content binding incompatible with my app, since I had two parallel arrays of NSStrings (one array contained keys, and the other contained values). So I continued looking for a solution, and eventually gave up and wrote the post.

After I published it, Scott Anguish posted a comment on it:*

you can’t have simple arrays of strings as the content of an array controller. it doesn’t work. You need to have an array of objects of some sort that have an attribute, even if they’re just NSDictionaries with a single attribute called “string”.

I don’t believe that NSArrayController has a special case for NSString, but Scott’s comment got me thinking. I added a category to NSString that added a -selfString method, then added that name as the model key path in the table columns’ value bindings. I was wondering, you see, whether this would be enough to satisfy NSTableColumn and NSArrayController.

It still didn’t work. At this point, I had the idea to fire up F-Script Anywhere and have a look around. I did that, and was digging down to the table columns when I noticed this:

That’s FSA’s object browser, showing the table view. So the table view’s content binding was bound to the keys array controller, with a model key path of lastMessageKeys.

That’s why the keys were showing up fine—they were in the array that was in the table view’s content binding. The values array was a different array with a different array controller, so NSTableView threw up its hands and simply showed the entire array’s description.

This is the point that Scott came close to: NSTableView expects you to have one array of model objects—no more. My parallel arrays of NSStrings do not qualify, because only one of the array controllers can be the value of the table view’s content binding.

So what role do the columns play? Well, each row in the table view (and by that, I mean *the whole row*) corresponds to one of those model objects. The value in each column is then determined by the model key path in the value binding of the corresponding NSTableColumn.

Thus, all the NSTableColumns’ value bindings must be bound to the same array controller and the same controller key, and each column should be bound to a different model key path. Your columns’ value bindings should look like this:

Column “Foo” Column “Bar”
Bind to: Your one array controller
Controller key: arrangedObjects (or whatever)
Model key path: foo Model key path: bar

In my case, of course, this required creating a new model class for a key-value pair, to replace the parallel arrays of NSStrings. This is a good thing, since it brings conceptual purity to my design. Remember, Cocoa is designed around MVC; I didn’t have enough M in my app, which was the problem.

So, in summary, here’s the fix for an NSTableColumn that shows the entire array in every row:

  1. Replace your evil parallel arrays with a model class, and a single array of instances of that class.
  2. Delete all but one of your array controllers, since you now have only one array. Update the remaining controller’s model key path to point to your new array (I assume you renamed it).
  3. Bind all of the table columns to your new One True Array Controller, distinguishing the columns only by the model key path, which identifies which property you want the column to show for each model object.

Many thanks to both David and Scott for their help in discovering the root of the problem.

* Scott posted his comment on the previous post with no last name; he identified himself in a comment on this post.

5 Responses to “And an answer: What to do if your table view shows the whole array in every row”

  1. Scott Anguish Says:

    OK, first I clarified my last name… :-)

    No, there is nothing special about an array of strings failing. an array of any simple object type will fail. You need to have an array of model objects that support attributes. From there you can delete all the extra stuff array/arraycontrollers. I figured once you got over the hump of not being able to use simple arrays, you’d get there. :-)

    And if you feel there is a need for a simple tableview and bindings example in the Cocoa Bindings documentation file a bug _now_ if you can.

  2. Peter Hosey Says:

    No, there is nothing special about an array of strings failing. an array of any simple object type will fail. You need to have an array of model objects that support attributes.

    Right. That’s what I figured out: The columns need to all specify various attributes of a single array of objects.

    They could be NSStrings, if you wanted to have two columns bound to length and description. They could be NSCalendarDates, which gives you rather more options, but those are “discouraged” since Leopard. They could be NSURLs. (This should all be fine as long as the objects are immutable; mutable objects will fail because they are not guaranteed to mutate in the KVO-compliant ways, which may cause the data in your views to become stale.) For pretty much anything else, you need to roll your own model class.

    The point that I learned is that you must have exactly one array of model objects (whatever their class)—no more.

    And if you feel there is a need for a simple tableview and bindings example in the Cocoa Bindings documentation file a bug now if you can.

    mmalc has multiple examples. The problem that I had was that no documentation that I’ve seen has emphasized the importance of exactly one array of model objects.

    I probably should file a bug about that, once I pin down a good place for Apple to put such a statement.

  3. Jordy/Jediknil Says:

    But, sometimes it is useful to be able to use NSStrings as values. I added a similar function but called it -string, to match NSMutableString‘s -setString: method. This was for an app that really did only need a one-column table view. (Of course, yes, -description does work as well, but I wanted editable strings. For some reason. Hmm, maybe I was breaking MVC there.)

  4. Eric Anderson Says:

    You could always write your own controller to correlate multiple arrays into keys.
    OR
    Why not just use an object if you have two arrays that perfectly match up and just have an array of that object?

  5. Peter Hosey Says:

    Eric Anderson:

    You could always write your own controller to correlate multiple arrays into keys.

    Sure. I’d like to see such a solution, but it doesn’t scale unless you can make the binding variadic in IB (like enabled1, enabled2, etc.). That would require an IB plug-in; I don’t know how much easier it is on Leopard, having not yet tried it, but doing it in Tiger is a PITA.

    OR
    Why not just use an object if you have two arrays that perfectly match up and just have an array of that object?

    That’s what I suggested: “In my case, of course, this required creating a new model class for a key-value pair, to replace the parallel arrays of NSStrings.”

Leave a Reply

Do not delete the second sentence.