Why is an NSArrayController "losing" its sort after an object is inserted? - objective-c

I have an NSArrayController bound to a property of an NSManagedObject subclass. The subclass is automatically generated by mogenerator (which creates a set property for the relationship).
The NSArrayController is bound to network.posts where 'posts' is a 1-M relationship. Note I am binding to 'posts' rather than the 'postsSet' mutableSet accessor generated by mogenerator.
The set represents a relationship with another entity. The array controller has a sort descriptor set and, when the window it is associated with opens, data is displayed in the correct sort order.
Then I add a new object by instantiating an entity and then adding it to the relationship.
The NSArrayController is correctly observing this change and the new object appears in arrangedObjects however, after the insert the sort order of arrangedObjects is lost and the records appear in a different order.
I have verified that the sort descriptor is still set correctly. The NSArrayController has autoRearrangeContent=YES. I've even tried manually calling -rearrangeObjects after the insert but the sort order remains wrong.
If I close the window and re-open the newly instantiated NSArrayController has the data with the correct sort order again. Until I do another insert.
My experience has been that NSArrayController has automatically kept the correct sort order when objects are added/removed but maybe that was a lucky coincidence?
I can't find any description of the correct behaviour in the Apple document so I've no idea what to expect, what might be going wrong (if anything), or - in either case - what I should do about it.
1) Given a sortDescriptor should NSArrayController be keeping arrangedObjects sorted after objects are inserted/removed from the underlying collection?
2) If so, what might prevent this from happening?
3) And, if not, what is the correct way to keep arrangedObjects sorted?
I'd be grateful for any help. It's not easy to provide useful source code in this situation since, in principle, there isn't any. But I'm happy to clarify and answer any follow-up questions.

Turn off lazy fetching. Not sure the reason but when the controller is being lazy it doesn't resort things when they are edited or added.

By what are you sorting? Core data does not support ordered collections. To preserve any kind of order, you have to add some sort of attribute (pun intended) and sort by it.
Behind the scenes, CD is using NSSet/NSMutableSet - not NSArray/NSMutableArray - for collections. That your objects come up in the same order more than once is entirely due to caching, not by CD maintaining your collection's order.
Update for Lion (10.7)
With regard to my "does not support ordered collections" statement: If you're targeting 10.7 and above in your application, [NSManagedObject now gives you ordered relationships.][1] Use -mutableOrderedSetValueForKey: and -mutableOrderedSetValueForKey: to set and retrieve NSOrderedSets. Yay!

Perhaps instead of binding the NSArrayController to your attributesSet (generated by mogenerator), you should instead bind to the underlying dynamic attributes property itself, which is created by Core Data framework? attributes stands for your relationship name.

Related

CoreData and NSTreeController

At the moment I'm sticking around with the nstreecontroller that is backend with core data. My problem is that no rearrange get triggered if I edit a field or add a new row.
Structure:
NSTreeController that is configured with prepare content ON and uses lazy fetching ON. The controller is bound to appDelegate.managedObjectContext and to appDelegate.mySortDescriptor (which is a function that returns an array with a nssortdescriptor inside).
Outlineview that has his columns bound to treecontroller.arrangedObjects.name. The Outlineview is also in SourceList mode.
An entity called "Item" that has a one-to-many relationship (children) to Item and a one-to-one relationship (parent) to Item. And of course there is a field called "name" that holds the displayvalue.
two storages. One is only hold in memory and the default one. The temporary storage is used to set up the root entities (I don't want them to get stored because they're just "visual")
Until here everything is fine except for:
On launch the sortdescriptor get read in but not executed (I've to call myself the rearrange function)
no modification on the core data triggers the rearrange method
I already tried to solve the problem at me own with following results:
Removing "Uses lazy fetch" will solve the problem o_O but who wants that?!
Calling rearrange in outlineView:setObjectValue:forTableColumn:byItem: (outlineview datasource) will solve the problem. But I do not really understand why this gets called (I'm using coredata, so it shouldn't get called?). On the other hand, calling rearrange to often messes up the sorting again (Ex. if you save a cell which hasn't changed his value)
That's the reason why I told myself that you've to face the fact that you're doing something wrong :)
What I actually want to have is the following:
CoreData backend
Outlineview is always sorted (the user cannot change it)
Skipping the save of the root objects
Less work is always welcome
thanks a lot and best regards, matthieu riolo
PS: Hints are appreciated too

Objective-C undo manager questions

I'm reading a book on Objective-c and learning about the undo manager. The concept seems very simple but the provided example seems overly complex. Basically, I have a table view connected to an NSArrayController and I add or remove people to an array and I can edit their names and stuff. Because the example uses NSArrayController and bindings, add and remove are automatic and all of the editing is automatic.
To use the undo manager, from what I understand, I need to implement my own methods to add/remove/edit.
These methods I've implemented to do the adding and removing and get called automatically due to key value coding:
- (void)removeObjectFromEmployeesAtIndex:(int)index;
- (void)insertObject:(Person *)p inEmployeesAtIndex:(int)index;
Then for editing, I had to register the class as an observer and observe changes to edit:
- (void)changeKeyPath:(NSString *)keyPath
ofObject:(id)obj
toValue:(id)newValue
Here are my questions:
Why do I have to do so much? My understanding was that using the NSArrayController and bindings was supposed to make things like adding/removing/editing items easier and more automatic. But if I have to implement all of these methods manually anyway just to add undo support, why use NSArrayController or bindings at all?
What's going on behind the scenes? In Interface Builder, the add button is connected to the add method on the NSArrayController. How then does my insertObject method get called? I know it's through key value coding but what makes the NSArrayController's add method get overridden just b/c my document implements this method?
The solution is asymmetric. I use one concept to handle undoing add/remove and another concept to handle undoing edits. Couldn't I also just observe changes to the array? I suppose it would complicate the observeValueForKeyPath method, but would that make more sense?
1) Nearly, but not quite. If you think of your application code being divided into three overall areas: Model, View and Controller (as documented here) then the Cocoa/XCode environment provides you with a 'code-free' way of handling the basics of each: IB for the view, Core Data for the model, and Bindings / Object Controllers for the controller.
Undo management is primarily a concern of the model, not the view or controller. So it's not really Bindings or the Object controller's job to manage this stuff. It looks like your problem is that you're using arrays as your data objects, which are too lightweight to handle this stuff. If you want undo support, you'll want to use core data to handle the model and give you this stuff for free, or hand-roll your own model objects, (which will probably contain arrays) which handle this logic.
FWIW, once you've done this, bindings will indirectly make your life much easier, as when an undo command reverts your data to its previous state, the view will automatically reflect the changes.
Also, NSArrayController's name is slightly misleading -- it isn't there to 'control arrays'. It's really for controlling data objects which have to-many relationships to other data objects. Which brings me on to...
2) KVC allows you to treat a to-many relationship between an object and other objects as an array or set, regardless of how the relationship is actually implemented. It does so by requiring you to implement methods fitting a naming convention, which very closely match the primitive methods of arrays and sets. KVC-compliant objects will return a proxy array or set when you call mutableArrayValueForKey: or mutableSetValueForKey:, which exposes those methods as an array. Roughly, that's how NSArrayController knows what to call --- KVC maps between the primitive objects of an array and some methods whose manes it generates from the key. Since you don't want to use arrays as your data objects, it's generally very useful to be able to treat any to-many relationship as if it were just an ordinary collection.
3) I think this is related to you handling undo in the wrong place. Implement KVC-compliant methods to get/set properties in your data objects, have them update the undoManger at the same time as setting the data. You'll need a special method for the undomanager to revert changes, as you don't want undos to be recorded as undoable. Or you could just use Core Data and get all this stuff for free...

Elegantly add object to arraycontroller, which itself is linked to another arraycontroller?

I'm new to OS X programming but generally liking it. I have the following problem:
I have two core data entities linked through a one-to-many with their respective arraycontrollers (Stock Controller and Price History Controller, where the latter controller is bound to the Stock Controller, with Controller Key = selection and Model Key Path = priceHistory, which is the relationship that links the stock entity to the PriceDataPoint entity, controlled by the Price History Controller.
This all works like magic in my UI, where I can select stocks and add/remove price points to each one when it is selected. However, I need to be able to do this programmatically as well.
If I simply call [stockController add:self] the UI updates with new objects with the correct default values, linked to the selected stock -- even though 'self' is not the correct class/entity. This is one point of confusion for me, which I don't understand (I understand that the Stock Controller knows about the selected stock through the KVO binding and would likely set the relationship as required, but I don't get how it 'casts' 'self', which is a fairly random class into the object type required (a plain NSManagedObject)? Secondly, if I do this, how do I get a reference back to that object so I can edit its values?
More importantly, however, if I then follow the Apple examples, create a new NSManagedObject through NSEntityDescription:insertNewObjectForEntity: and use [stockController addObject:Newly created Object] I can write the values I want before adding it but the relationship to the 'parent' stock is not set by the addObjects: method. I am sure I can figure out how to write this, too but with everything else in Cocoa being so elegant this just feels odd so I am hoping that someone here can clear this up very quickly and point me to an elegant way of doing it.
First, let's dispel the magic by stating Core Data is built to work with the Cocoa Bindings mechanism (which gives you array controllers, among other things) and is built atop Key Value Coding / Key Value Observing mechanisms. The Core Data documentation clearly states you should have a good background in these technologies to understand how / why things are working.
For your first issue, you said you're confused by the -add: method of NSArrayController. Have you looked at the signature/prototype? The argument (where you pass self) is (id)sender, which is the hallmark of a basic action (see "target/action" in the docs). You can pass self, some other object, or even nil if you want. The argument is NOT the object you wish to add to the collection managed by the array controller.
For your second issue, you're on the right track if you want to customize things or keep a reference to the newly-inserted object. You can use the -addObject: method (which does take the object to add as its argument) but you'll have to tell the array controller to -fetch: (another action w/sender as argument) in order to refresh the controller AND anything bound to its contents (such as a table/outline).

Proposed solution to NSTreeController displaying duplicate entities

As many of you may know, an NSTreeController bound to an outline view can display duplicates while presenting core data entities.
A temporary solution is to add 'parent == nil' to the predicates, but this only returns parent entities. If, for instance, a user is searching for a sub-entity, the requested sub-entity won't be displayed.
A (proposed) solution is to subclass NSTreeController and add a NSMutableSet variable, which keeps track of entities that are currently being displayed. This variable should be alloced on init, and released on dealloc.
When "fetchWithRequest:merge:error:" is called, the set should be emptied (I'm not sure whether this would be more efficient than releasing it and allocating it again). Everytime an entity is going to be added to display, check if the set contains it. If it doesn't, add it. Otherwise, find which is closer to the root (which is the subentity) and either skip it if its the subentity, or swap it with the previously included one.
I think there should be relatively little impact on performance (considering NSSet uses hashing). The problem I'm having is finding the correct method to override to add this behavior. Specifically, where NSTreeController processes fetched entities after "fetchWithRequest:merge:error:" is called.
Is it fair to say you're really looking for a way to filter the tree with a search term without losing the tree structure? The inherent problem (beyond forcing the tree controller to include the parent nodes of a search match) is that the parents may or may not actually match the search result, so it's confusing to display them.
I think yours is more a problem of UI, isn't it? In that case, the best approach (and one I've seen many well-known companies and independent developers take) is to display search results in a plain table. This way the results can be sorted by various attributes and you don't have to disable drag and drop in the outline view in search mode (to avoid the user trying to change the tree structure when only part of the tree is displayed out of context).
Expanding on Joshua's answer, I was able to implement Search Functionality into my own NSOutlineView, however it was limited to the root/parent objects in the view.
I think (like Joshua said) if you wanted to filter all objects you would have to display the results in a NSTableView.

Placing items from a Core Data entity into an NSOutlineView programmatically?

Sorry if this seems like a silly question - I am an amateur when it comes to Objective-C and Cocoa and even less knowledgable when it comes to Core Data usage.
So here's the situation: I have an NSOutlineView that I've already populated with a few items manually with an NSTreeController. What I need to do now is take the items in one of my Core Data entities and append them to the NSOutlineView's current contents.
Obviously this is beyond the abilities of bindings, so it will need to be done programmatically. What should I do? I assume that I need to do a fetch and then iterate through the returned items, adding each to the outline view. Is this correct? If so, would anybody be able to show an example of how this is done?
Thanks!
Create an NSFetchRequest with an NSPredicate that gets only those whose "parent" is nil (the root/top-level objects). Sort them by some attribute that makes sense (as the fetch results will be an unordered collection - an NSSet). Then implement the NSOutlineViewDataSource to mix/mingle the information as you see fit as it's provided to the outline.
Caution: It's best to cache your results, observing the context for changes and refreshing the cache on each change.