I read several posts on different forums claiming that the ArrayController is THE solution to manage the relationship between the View (NSTableView) and the Model (NSMutableArray). It tempts me to use such a Controller to clarify the code and facilitate maintenance, but, once again, the only complete examples come in Objective-C, not to mention the Apple’s examples with annoying pages of copyrights and warnings.
IB is a great visual overview of the bindings, but examples come with pure textual documentation (bindings are rarely illustrated, maybe I rely too much on visual comprehension).
So the precise connections between M, V and C are not clear to me. For example:
I used for some weeks the non-arrayControlled arrays, making the necessary bindings to have the tableView calling my main script, I provide the two mandatory handlers:
on numberOfRowsInTableView_(aTableView) -- returns array's count,
on tableView_objectValueForTableColumn_row_(aTableView, aColumn, aRow) returns array's contents for each line.
when needed, I call aTableView’s reloadData() and everything is running fine.
So what could be the benefits of an ArrayController, and how implement it?
The main benefit is writing less code and a faster representation of your cells. In good Objective-C there is only the code the difference but for ASOC there is a huge speed difference
Sounds good, I use many lists. But as I don’t code in Objective-C (I just begin to decipher it) what are the steps to make an NSArrayController work? Say we have the properties:
gArrayController : missing value
gArray : missing value
gTable : missing value
declared in the appDelegate and connected as IBOutlets. How do you establish connections between the MVC elements? In other words, how do you populate gTable with gArray?
You could really spare me hours of reading and coding (for me, time is not money but sleeping hours)
You have declared your table, data and controller so what you need to do in IB is to bind the data to the controller and the controller to the table in the bindings panel. The binding to your data is through your model key path which means it has to be a public property of that class. The property is it’s key, which is in your case gArray. The binding types speaks for itself. The binding between your model key and controller is a controller binding and the binding between the controller and the table is a value binding.
I don’t know what you mean by “loader” here, but those two methods are required if you use a data source to populate your table, but you don’t need them if you use bindings with an array controller. All you need to do is bind the content array of the array controller to your array (gArray) and bind each column’s value to the array controller with the Controller key of arrangedObjects and a Model Key Path of one of the keys in your array (I’m assuming here that your array is an array of dictionaries, which you would need if your table has more than one column).
You don’t need to implement the data source delegates, that’s the beauty of bindings. The NSArrayController does it all for you.
Just bind the values and controller and remove the data source delegates from your project (I would recommend to comment the code) and if you have done it right it should work. Also remove the data source connection in IB from your table. Because you won’t need that any more.
on applicationWillFinishLaunching_(aNotification)
set gArray to current application's NSMutableArray's alloc's init()
repeat with n from 1 to 10
gArray's addObject_(n as integer)
end repeat
end applicationWillFinishLaunching_
How did you make the binding in IB? it seems that you didn’t bind gArray with the model kay path to NSArrayController in IB correctly. Or the value binding between the controller and the table view isn’t correct.
Your referencing bindings should just say arrangedObjects ---- value TableColumn. When you only have one column, and your array is just a simple array (not an array of dictionaries) then you just bind the column to the array controller with Controller Key “arrangedObjects”, with nothing in the Model Key Path field.
Wow it’s working at last! And that’s dozen of code lines off, actually. No more handlers. Gee, we are far from the days of the Toolbox’s List Manager.
In fact – I should have done this:
repeat with n from 40 to 100
gArrayController's addObject_(n)
end repeat
gArrayController's addObject_(1)
gArrayController's removeObject_(50)
to avoid changing the array from code: the idea is to think to the array as if it was “encapsulated” by the controller, isn’t it?
Now I have to rewrite whole portions my app, but it’s worth it! But before, I’d like to explore for a while. For example:
Why does my ArrayController not automatically sort its array? I checked the “Auto Rearrange Content” option-- is it unsufficient? It does sort only if I click on the column’s header.
How do you put data on 2, 3 columns? Is gArrayController’s addObject_({“Number and Inverse”,n, 1/n}) ok?
Thanks to you, DJ&Ric, I could have spent nights and nights on this stuff.
I’m not sure that I think of it that way – the controller just handles the interface between your array and the table. I normally change my array directly rather than through the array controller, I really don’t know if it’s ok to do it the way you showed.
No, it’s not sufficient. You have to create Sort Descriptors under the Controller Content Parameters section of the bindings pane in the inspector.
No, that’s not right. You need to have your array be an array of dictionaries. So if you wanted your 2 columns with a number in one and its inverse in the other, you could do this:
property parent : class "NSObject"
property gArray : missing value
on applicationWillFinishLaunching_(aNotification)
set gArray to current application's NSMutableArray's alloc()'s init()
repeat with counter from 1 to 10
set theDict to {theNumber:counter, theInverse:1 / counter}
gArray's addObject_(theDict)
end repeat
set my gArray to gArray
end applicationWillFinishLaunching_
Then when you bind the value of column 1, you would bind it to the array controller with Controller Key, arrangedObjects, and with the Model Key Path of theNumber. Likewise, with column 2, the Model Key Path would be theInverse.
So far, so good. The binding system offers the same logic as the coding, it’s just the concept and the vocabulary which was new to me. Without your help I should have missed that!
Of course it’s much more convenient when you have records (or dictionaries) stored in a table, this avoids rather simple but tedious imbricated IF tests in the tableView:objectValueForTableColumn:row: handler (not to mention zero-based Obj-C to convert to one-based AS lists). You may focus on other parts of the application and let the dull work to the controller. And you may give orders directly to the controller via menu commands by binding too. That’s fine. No, that’s great!
And brings new questions:
¢ For example, it’s obvious that a controller knows how to sort a column (it does it when you click on the header). Why could it not do it by default, a sort of “keep-this-column-sorted-even-if-I-insert-a-new-object-anywhere” option in IB?
¢ If I want to know if an object is already defined in a array, I can just ask in AS “if myObj in myList” or in ASOC “if myArray’s contains_(myObj)”. Can it be done with a record/dictionary structure? Something like “if intKey:newNumber in intKeys of myArray”. see what I mean? testing only one property of the record/dictionary for occurrences?
If it were a default, it wouldn’t know which key to sort on, that’s why you have to define the sort descriptors, to tell the controller which key (or keys) to sort on and with what kind of sorter.
You can get all the keys with the allKeys() method and all the values with the allValues() method, so depending on whether you’re search the keys or the values, you can use one of these methods to return an array on which you can use the containsObject: method. If you want to find out if a particular key contains a certain value then you can use myArray’s valueForKey_(myKey) to return an array of all the values for myKey, and then test that array with containsObject:
In IB, when the view table is selected, there is (under “table content”) a field named “Sort Descriptors”, is it possible to set the descriptor by binding, for example:
Bind to:
Controller Key
compare:
Model Key Path
that is, is a correct manner, because when I try I get
2011-04-15 12:50:43.239 AutoText[6016:903] An uncaught exception was raised
2011-04-15 12:50:43.240 AutoText[6016:903] [<NSArrayController 0x20068e1a0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key compare:.
2011-04-15 12:50:43.242 AutoText[6016:903] *** Terminating app due to uncaught exception ‘NSUnknownKeyException’, reason: ‘[<NSArrayController 0x20068e1a0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key compare:.’
Compare: is not a key, it’s a method. A key is one of the keys in your dictionary – it should be one of the keys that you used as the value for the Model Key Path when you bound your columns.
if “cID” is one of your keys, then this is the correct way to specify a sort descriptor. I haven’t tried to use the bindings for the table view to do sorting, but I have done it with the controller, so try this.
create a property called sorters
add a line to your applicationWillFinishLaunching method (after the “set sortByID to …”. line): setSorters_(current application’s NSArray’s arrayWithObject_(sortByID))
In IB, in the bindings pane for the array controller, go to the Sort Descriptor section. Choose your app delegate from the pull down list and click the “bind to” check box. In the Model Key Path field, type in sorters. There should be nothing in the controller key field.
If you want auto sorting behavior, then click the Auto Rearrange Content box in the attributes pane.
Ric
Browser: Safari 533.18.5
Operating System: Mac OS X (10.6)
I have followed your step-by-step tutorial (thank you for your quasi-infinite patience, Ric), and verified, verified again. All seems to be defined exactly how you told me to do.
But I don’t understand: I have this property in my app Delegate, ok, so there is no exception when I tell IB to bind it to my controller. But how, in these two lines:
set sortByID to current application's NSSortDescriptor's sortDescriptorWithKey_ascending_("eCardID",true)
setSorters_(current application's NSArray's arrayWithObject_(sortByID))
does IB explicitly make the “bridge” between a “missing value” property and the actual sorter? Could it be something like:
set sorters to(current application's NSArray's arrayWithObject_(sortByID))
Of course I tried this version too, the table stays empty. In fact, it SEEMS to contain at last one (invisible) line, because when I click where it should be, I obtain the desired effect (my editor moves to the right text).
Strange. I’m sure it’s about to be fixed, but where is the bug hiding?
Regards,
Edit : gErrorController’s setSortDescriptors_(sorters) works divinely. If the explicit setting works, I must have goofed in IB. Sorry for that, and one zillion thanks.