Question

I'm developing a desktop Cocoa application. In the app I have a view-based NSOutlineView binded to an NSTreeController:

enter image description here enter image description here

The NSTreeController is in entity mode and driven by Core Data. Everything works as expected until the underlaying model graph changes. Whenever a new object inserted into the registered NSManagedObjectContext the NSTreeController refresh its content and the binded NSOutlineView shows the result properly. The content of the controller sorted by "title" with an NSSortDescriptor and I set this sorting during the application startup. The only drawback is that the selectionIndexPath doesn't change even if the preserve selection box is checked in the NSTreeController's preferences. I want to keep the selection on the object that was selected before the new node appeared in the tree.

I've subclassed NSTreeController to debug what's happening with the selection during the change of object graph. I can see that the NSTreeController changes it's content via KVO but the setContent: method doesn't invoked. Than the setSelectionIndexPaths: called via the NSTreeControllerTreeNode KVO but the parameter contains the previous indexPath.

enter image description here

So, to be clear:

  • Top Level 1
    • Folder 1-1
    • Folder 1-2
  • Top Level 2
    • Folder 2-1
    • *Folder 2-3 <== Selected
    • Folder 2-4

In the initial stage the "Folder 2-3" selected. Then "Folder 2-2" inserted into the NSManagedObjectContext with [NSEntityDescription insertNewObjectForEntityForName:@"Folder" inManagedObjectContext:managedObjectContext];:

  • Top Level 1
    • Folder 1-1
    • Folder 1-2
  • Top Level 2
    • Folder 2-1
    • *Folder 2-2 <== Selected
    • Folder 2-3
    • Folder 2-4

I want to keep the selection on "Folder 2-3", hence I've set the "Preseve selection" but it seems that NSTreeController completely ignore this property or I misunderstood something.

How I can force NSTreeController to keep its selection?

UPDATE1:

Unfortunately none of the mutation methods (insertObject:atArrangedObjectIndexPath:, insertObjects:atArrangedObjectIndexPaths: etc.) has ever called in my NSTreeController subclass. I've override most of the factory methods to debug what's going under the hood and that's what I can see when a new managed object inserted into the context:

-[FoldersTreeController observeValueForKeyPath:ofObject:change:context:] // Content observer, registered with: [self addObserver:self forKeyPath:@"content" options:NSKeyValueObservingOptionNew context:nil]
-[FoldersTreeController setSelectionIndexPaths:]
-[FoldersTreeController selectedNodes]
-[FoldersTreeController selectedNodes]

The FoldersTreeController is in entity mode and binded to the managedObjectContext of Application delegate. I have a root entity called "Folders" and it has a property called "children". It's a to-many relationship to an other entity called Subfolders. The Subfolders entity is a subclass of Folders, so it has the same properties as its parent. As you can see on the first attached screenshot the NSTreeController's entity has been set to the Folders entity and it's working as expected. Whenever I insert a new Subfolder into the managedObjectContext it appears in the tree under the proper Folder (as a subnode, sorted by NSSortDescriptor binded to the NSTreeController), but none of the NSTreeController mutation methods are called and if the newly inserted subfolder appears earlier in the list it pulls down everything but the selection remains in the same position.

I can see that the setContent: method is called during the application launch, but that's all. It seems that NSTreeController observe the root nodes (Folders) and reflect model changes somehow via KVO. (So, when I create a new Subfolder and add it to its parent with [folder addChildrenObject:subfolder] it's appearing in the tree, but none of the tree mutation methods are invoked.)

Unfortunately I cannot use the NSTreeController mutation methods directly (add:, addChild:, insert:, insertChild:) because the real applicataion updates the models in a background thread. The background thread uses its own managedObjectContext and merge the changes in batches with mergeChangesFromContextDidSaveNotification. It makes me crazy, because everything is working fine expect the NSOutlineView's selection. When I bunch of Subfolders merged into the main managedObjectContext from the background thread the tree updates itself, but I lost the selection from the object that was selected before the merge.

Update2:

I've prepared a small sample to demonstrate the issue: http://cl.ly/3k371n0c250P

  1. Expand "Folder 1" then select Select "Subfolder 9999"
  2. Press "New subfolder". It will create 50 subfolder in the background operation with batches.
  3. As you can see, the selection will be lost from "Subfolder 9999" even if its saved before the content change in MyTreeController.m
Was it helpful?

Solution

By my reading of the docs and headers, NSTreeController uses NSIndexPaths to store selection. This means that its idea of selection is a chain of indexes into a tree of nested arrays. So as far as it knows, it is preserving the selection in the situation you describe. The problem here is the you're thinking of selection in terms of "object identity" and the tree controller defines selection as "a bunch of indexes into nested array". The behavior you describe is (AFAICT) the expected out-of-the-box behavior for NSTreeController.

If you want selection preservation by object identity, my suggestion would be to subclass NSTreeController and override all mutating methods such that you capture the current selection using -selectedNodes before the mutation, then re-set the selection using -setSelectionIndexPaths: with an array created by asking each formerly selected node for its new -indexPath after the mutation.

In short, if you want behavior other than the stock behavior, you're going to have to write it yourself. I was curious how hard this would be so I took a stab at something that appears to work for the cases I bothered to test. Here 'tis:

@interface SOObjectIdentitySelectionTreeController : NSTreeController
@end

@implementation SOObjectIdentitySelectionTreeController
{
    NSArray* mTempSelection;
}

- (void)dealloc
{
    [mTempSelection release];
    [super dealloc];
}

- (void)p_saveSelection
{
    [mTempSelection release];
    mTempSelection = [self.selectedNodes copy];
}

- (void)p_restoreSelection
{
    NSMutableArray* array = [NSMutableArray array];
    for (NSTreeNode* node in mTempSelection)
    {
        if (node.indexPath.length)
        {
            [array addObject: node.indexPath];
        }
    }

    [self setSelectionIndexPaths: array];
}

- (void)insertObject:(id)object atArrangedObjectIndexPath:(NSIndexPath *)indexPath
{
    [self p_saveSelection];
    [super insertObject: object atArrangedObjectIndexPath: indexPath];
    [self p_restoreSelection];
}

- (void)insertObjects:(NSArray *)objects atArrangedObjectIndexPaths:(NSArray *)indexPaths
{
    [self p_saveSelection];
    [super insertObjects:objects atArrangedObjectIndexPaths:indexPaths];
    [self p_restoreSelection];
}

- (void)removeObjectAtArrangedObjectIndexPath:(NSIndexPath *)indexPath
{
    [self p_saveSelection];
    [super removeObjectAtArrangedObjectIndexPath:indexPath];
    [self p_restoreSelection];
}

- (void)removeObjectsAtArrangedObjectIndexPaths:(NSArray *)indexPaths
{
    [self p_saveSelection];
    [super removeObjectsAtArrangedObjectIndexPaths:indexPaths];
    [self p_restoreSelection];
}

@end

EDIT: It a little brutal (performance-wise) but I was able to get something working for calls to -setContent: as well. Hope this helps:

- (NSTreeNode*)nodeOfObject: (id)object
{
    NSMutableArray* stack = [NSMutableArray arrayWithObject: _rootNode];
    while (stack.count)
    {
        NSTreeNode* node = stack.lastObject;
        [stack removeLastObject];
        if (node.representedObject == object)
            return node;

        [stack addObjectsFromArray: node.childNodes];
    }

    return nil;
}

- (void)setContent:(id)content
{
    NSArray* selectedObjects = [[self.selectedObjects copy] autorelease];

    [super setContent: content];

    NSMutableArray* array = [NSMutableArray array];
    for (id object in selectedObjects)
    {
        NSTreeNode* node = [self nodeOfObject: object];
        if (node.indexPath.length)
        {
            [array addObject: node.indexPath];
        }
    }

    [self setSelectionIndexPaths: array];
}

Of course, this relies on the objects actually being identical. I'm not sure what the guarantees are with respect to CoreData across your (unknown to me) background operation.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top