Lazy Fetching & Maintaining a sorted NSArrayController - cocoa

I've got a sample project at
https://github.com/ericgorr/LazyFetching
The setup is pretty straightforward. In my CoreData model, I have two entities:
Shelf which has a name property and a to-many relationship (items) to the Item entity
Item which has a name property has a relationship to the Shelf entity (it's "parent")
I then have two NSArrayControllers:
Shelf Array Controller with the Mode 'Entity Name', the Entity Name of 'Shelf'. It Prepares Content and using Lazy Fetching.
Item Array Controller with the Mode 'Entity Name', the Entity Name of 'Item'. It does not prepare content and does not use lazy fetching. It's Content Set is bound to the Shelf Array Controller's selection and 'items' property.
On the Window, I've got two NSTableViews and two + buttons, each ties to one of the array controllers.
What I did was to:
Press the top + button and named the new Shelf entry 'a'
Press the top + button again and named the Shelf entry 'b'
Pressed the top column header to sort the Shelf entries in ascending order.
Make sure the 'b' entry is selected
Pressed the bottom + button and renamed the Item entry to anything
What I see happen then is the rows in the upper (Shelf) table change order so that the 'b' entry is listed first.
If I press the Info button, what it will do is:
NSLog the sort descriptors for the Shelf Array Controller
Print the names of the entries in the arrangedObjects array of the Shelf Array Controller
Call rearrangeObjects on Shelf Array Controller
Print out the names of the entries in the arrangedObjects array of the Shelf Array Controller
After all of this, I see, as expected, a single sort descriptor on the Shelf Array Controller: name, ascending, compare:
And the following output for arrangedObjects:
2013-05-19 16:08:56.023 LazyFetching[11791:303] b
2013-05-19 16:08:56.024 LazyFetching[11791:303] a
and
2013-05-19 16:08:56.024 LazyFetching[11791:303] b
2013-05-19 16:08:56.024 LazyFetching[11791:303] a
For some reason, it is no longer sorting the items in the Shelf Array Controller.
If I turn off Lazy Fetching for the Shelf Array Controller, everything seems to work correctly.
So, what I don't understand is why I am seeing this behavior with Lazy Fetching enabled. It seems possible that it is a bug. If so, I can certainly file what is likely to be a duplicate report. But, I am guessing, there is some good explanation and a standard way of dealing with it so items remain sorted by the sort descriptors in the arrangedObjects array of the array controller.

I found the only way I could force sorting after turning on lazy fetching for an NSArrayController (in IB) was to send a performSelector:withObject:afterDelay: in awakeFromNib with the delay set to 0:
(void)awakeFromNib {
[self performSelector:#selector(sort) withObject:self afterDelay:0.0];
}
(void)sort {
NSSortDescriptor* sorting = [NSSortDescriptor sortDescriptorWithKey:#""
ascending:YES];
[self.AC_Controller setSortDescriptors:[NSArray arrayWithObject:sorting]];
}
These methods were included in my main window controller where the NSArrayController resides in the associated nib file.
it seems that upon loading the nib containing the NSArrayController, the nib loading machinery invokes lazy fetching as per the IB setting but this overrides any sorting you may be trying to set programmatically or via IB in the NSTableView - by waiting for the next run loop the sorting will then "take" OK.
This code assumes you have an IBOutlet connected to the NSArrayController of course.

Related

How is NSTableView Content Sorted After Array Controller Add?

After adding a managed object in array controller class, the bound table view 'places' it into the correct date sorted order.
However, as the table view is building it's rows, the new object has been placed at the bottom of the array controller's content array using:
Edit: Solution: Don't use the array controller's content. The issue was in:
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
....
// originally
NSManagedObject *ci = [[self.arrayController content] objectAtIndex:row];
// should be: (note arrangedObjects replacing content)
NSManagedObject *ci = [self.arrayController.arrangedObjects objectAtIndex:row];
....
}
The image below shows an example of the placement. Console output from within -tableView: viewForTableColumn: row: at left (from original code above) and ordering in the table view on right. LHR is the new object (notice date order ascending).
Tried:
Saving the moc right after adding newObject
Calling Array Controller's arrangeObjects method
Attempting to manually setSortDescriptors
Binding the table view's selection index to the array controller (for grins)
This is also messing up the selectedRow even though the row with LHR is selected in the table view right after adding. The only way to correct the array controller's order is to manually sort the column.
How can the array controller's content be in sync with the table view? Even more, why might they not be in the same order?
The array controller "arranges" its content. It does this by calling its -arrangeObjects: method. The default implementation filters the contents using the array controller's filterPredicate and then sorts it using its sortDescriptors. A subclass could override that method to arrange the content differently.
The arranged contents can be accessed using the arrangedObjects property. This is what corresponds to rows in the table view. So, you should always use this if you're indexing by a table row. For example:
NSManagedObject *ci = [self.arrayController.arrangedObjects objectAtIndex:row];
The array controller's sortDescriptors may be set via bindings (e.g. if the table view's sortDescriptors binding is bound to the array controller). Depending on how you set up the table view bindings, this may be automatic. For NSCell-based table views, you typically bind the table columns value binding and don't bind the table views bindings. In that case, the table view automatically binds its content, selectionIndexes, and sortDescriptors bindings to the same controller as its columns. For view-based table views, you typically don't bind table columns and have to bind those table view bindings explicitly if you want them bound.
You can also set the array controller's sortDescriptors programmatically.
Try invoking rearrangeObjects on the NSArrayController and then doing the selection you need subsequent to that in a dispatch_async block. It's maddening, but some of the bindings are deferred to the next runloop turn, so you can't simply add an object, call rearrangeObjects, and select the new object in synchronous code. You have to use GCD to schedule things
See this question/answer for more discussion. It's is the exact same issue. Adding an object and trying to select it.
Enabling the Auto Rearrange Content option of the Array Controller made it for me.

how to rebind nstableview IN COCOA

I have a NSTableView and a NSArrayController for the NSTableView.
there is only one column in the table,"name"
at first, user open one file, in the arrayController will be #[#{#"name":#"1"},#{#"name":#"2"}]
after sometime, the user opened another file, now the content of array controller should be #[#{#"name":#"x"},#{#"name":#"y"},#{#"name":#"z"}].
how can I bind the new data to the NSTableView,(not add)?
You don't need to do anything if you have the bindings set up right. The content array of the array controller should be bound to an array (the one you show in your post), so when you change, or add to that array, the array controller's arranged objects will automatically change -- this doesn't require any "rebinding".

What's the Difference between "Content Values" and "Content Objects"

I'm exploring bindings right now, and have an NSPopUpButton -
It presents me a number of options for bindings under Value Selection - Content, Content Objects, Content Values, and then Selected Object, Selected Value, and Selected Tag. Could someone please explain the difference between these?
Those are explained in the Cocoa Bindings Reference for NSPopUpButton, although that reference is not quite clear.
Content is an array controller that provides elements to the popup button. The array controller should be bound to an array. In order to determine how each element in the array is shown in the popup button, -description is sent to each object in the array.
You may customise this in two ways:
If you want the Selected Object binding to provide an object distinct from the array elements managed by the array controller to which Content was bound, you can bind Content Objects to another array controller. It could also be the same array controller but with a different key path;
If you want the popup button options to be something different than the description of each element in the array managed by the array controller to which Content was bound, you can bind Content Values to another array controller that manages an array whose elements contain the popup options. It could also be the same array controller but with a different key path.
A simple example: suppose you have the following class:
#interface Customer : NSObject
#property (copy) NSString *name;
#property (copy) NSString *phoneNumber;
#end
and you haven’t overridden the -description method. In this case, -descriptionis useless and the name property would be a good choice for the popup options. You’d bind:
Content to an array controller that manages an array of Customer instances, controller key arrangedObjects;
Content Values to the same array controller, controller key arrangedObjects, model keypath name.
You can then bind Selected Object to something else, for example a property in your application delegate or window controller. Cocoa bindings would then assign the selected Customer instance to that property.
Now suppose you are not interested in the whole Customer object that’s been selected, but only its phone number. In this case, you can bind Content Objects to the same array controller, controller key arrangedObjects, model keypath phoneNumber. When a popup option is selected, Cocoa bindings will set phoneNumber instead of an entire Customer instance. In summary: if you don’t bind Content Objects, Selected Object represents the original object in the array. If you bind Content Objects, then Selected Object can be something different.
You’d bind Selected Value if you were not interested in the original objects (or the content objects), but the actual strings shown in the popup options according to the Content Values bindings.
Quick recipe for providing data to the popup button:
Bind Content if you have objects (not only strings) that represent the popup options;
Bind Content Values if the options that are shown to the user cannot be obtained via Content by sending -description to the array elements;
Bind Content Objects if you want Selected Object to return something different from the array elements from Content.
Quick recipe for obtaining the current selection in a popup button:
Bind Selected Object if you want to know the full object (either from Content or Content Objects) representing the current popup selection;
Bind Selected Value if you only want the string that’s currently selected in the popup.
And lastly, you’d use Selected Tag if the popup options are actually taken from a menu whose items have a tag set.
#Object refers to any KVC-compliant object. #ObjectValue refers to the key path used to get the value from that object.
So, for your pop-up binding, ContentObjects would be bound to, say, an NSArrayController's arrangedObjects. Say this refers to an array of dictionaries or managed objects. You can't meaningfully present a dictionary in a pop-up (you get the start of the description output, e.g <NSCFDictionary... or similar), so this is where the contentValues binding comes in. This would be something like your NSArrayController's arrangedObjects.name, where name is a key from your dictionary or managed object.
I hope this helps, I struggled with the same concept myself when I started with bindings.

NSTableView bindings how to add a row

I'm working on a application with the this interface (sorry the language is Dutch):
http://www.flickr.com/photos/pluueer/5756159100/
The Add function (incl. the four NSTextFields) under the NSTableView are to moved to a sheet someday, but for now this is fine. I've set up bindings according to a tutorial (http://cocoadevcentral.com/articles/000080.php), but the tutorial doesn't supply how to add rows in the way I want to (just adds an empty row which you need to edit in the NSTableView).
I've got a connection between the 'Voeg toe' (Dutch for 'Add') button and the Array Controller. But after clicking I get the message:
2011-05-28 23:37:56.149 Hop Calc[4345:a0f] -[__NSPlaceholderDictionary initWithObjects:forKeys:]: number of objects (0) not equal to number of keys (4)
It makes sense, because I've not implemented anything for adding rows, but I just don't know how.
"Add a row to the table" is the wrong way to think of it. Your table represents a collection and a controller provides the information to the table, mediating between the table (view) and the collection (model). Since you mentioned bindings, the collection is likely managed by an NSArrayController. So you want to add a new object (of the kind your array controller manages) to the array controller's content array.
Simplest way: Connect the Add button to the -add: action of the NSArrayController. It'll add an empty row.
If you want more control, connect the Add button to your own custom action in some controller. That action will create an instance of whatever's represented by your array controller, prepopulate it (or whatever you want to do), then, using an outlet it holds to your NSArrayController, will call the array controller's -addObject: method to add the object (the possibly a -rearrangeObjects call to get the array controller to re-sort its contents).

Table view not updating according to bindings - Part Two

Title borrowed from this question, of which this one is not a duplicate. See my answer there for what was wrong for that questioner; I'm the author of that answer, and my problem is not that one.
I have a table view with three columns, whose Value bindings are bound to three properties of the arrangedObjects of an array controller. The array controller's contentArray is bound to the visitationResults of my document object; the items in that array are instances of a model class (VisitationResult). I have also bound the array controller's selectionIndexes and sortDescriptors to properties of my document.
I am mutating my property through a couple of accessors:
- (void) addVisitationResult:(VisitationResult *)newVisitationResult {
[self insertObject:newVisitationResult inVisitationResultsAtIndex:[self countOfVisitationResults]];
NSLog(#"arrayController arrangedObjects: %#", [arrayController arrangedObjects]);
}
That NSLog statement runs, and confirms that the array controller is gathering and arranging my model objects. This means that I am going through my property and getting KVO notifications for my document (which proves that the earlier questioner's problem, that of bypassing the property, is not the problem I'm having).
I added NSLog statements in my model object class's accessor methods. One of them is being called—by the array controller, in order to sort the objects (that property is the sort key). The other two, which the array controller doesn't know about, never get called.
Thus, my table view remains blank.
I found the problem: It's because I had explicitly bound the selectionIndexes and sortDescriptors bindings of the table view.
This wasn't necessary, anyway: I just checked, and the documentation says:
selectionIndexes
Typically, selectionIndexes is bound automatically to the NSArrayController that the first NSTableColumn is bound to.
sortDescriptors
Typically this binding is created automatically, binding to the sort descriptors of the NSArrayController of the initially bound NSTableColumn.
It appears that not only is it not necessary, but binding either or both of these two will break the table view.

Resources