objective c - NSTreeController/NSOutlineView loses its selection -
i'm developing desktop cocoa application. in app have view-based nsoutlineview binded nstreecontroller:

the nstreecontroller in entity mode , driven core data. works expected until underlaying model graph changes. whenever new object inserted registered nsmanagedobjectcontext nstreecontroller refresh content , binded nsoutlineview shows result properly. content of controller sorted "title" nssortdescriptor , set sorting during application startup. drawback selectionindexpath doesn't change if preserve selection box checked in nstreecontroller's preferences. want keep selection on object selected before new node appeared in tree.
i've subclassed nstreecontroller debug what's happening selection during change of object graph. can see nstreecontroller changes it's content via kvo setcontent: method doesn't invoked. setselectionindexpaths: called via nstreecontrollertreenode kvo parameter contains previous indexpath.

so, clear:
- top level 1
- folder 1-1
- folder 1-2
- top level 2
- folder 2-1
- *folder 2-3 <== selected
- folder 2-4
in initial stage "folder 2-3" selected. "folder 2-2" inserted nsmanagedobjectcontext [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 keep selection on "folder 2-3", hence i've set "preseve selection" seems nstreecontroller ignore property or misunderstood something.
how can force nstreecontroller keep selection?
update1:
unfortunately none of mutation methods (insertobject:atarrangedobjectindexpath:, insertobjects:atarrangedobjectindexpaths: etc.) has ever called in nstreecontroller subclass. i've override of factory methods debug what's going under hood , that's can see when new managed object inserted 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 in entity mode , binded managedobjectcontext of application delegate. have root entity called "folders" , has property called "children". it's to-many relationship other entity called subfolders. subfolders entity subclass of folders, has same properties parent. can see on first attached screenshot nstreecontroller's entity has been set folders entity , it's working expected. whenever insert new subfolder managedobjectcontext appears in tree under proper folder (as subnode, sorted nssortdescriptor binded nstreecontroller), none of nstreecontroller mutation methods called , if newly inserted subfolder appears earlier in list pulls down selection remains in same position.
i can see setcontent: method called during application launch, that's all. seems nstreecontroller observe root nodes (folders) , reflect model changes somehow via kvo. (so, when create new subfolder , add parent [folder addchildrenobject:subfolder] it's appearing in tree, none of tree mutation methods invoked.)
unfortunately cannot use nstreecontroller mutation methods directly (add:, addchild:, insert:, insertchild:) because real applicataion updates models in background thread. background thread uses own managedobjectcontext , merge changes in batches mergechangesfromcontextdidsavenotification. makes me crazy, because working fine expect nsoutlineview's selection. when bunch of subfolders merged main managedobjectcontext background thread tree updates itself, lost selection object selected before merge.
update2:
i've prepared small sample demonstrate issue: http://cl.ly/3k371n0c250p
- expand "folder 1" select select "subfolder 9999"
- press "new subfolder". create 50 subfolder in background operation batches.
- as can see, selection lost "subfolder 9999" if saved before content change in mytreecontroller.m
by reading of docs , headers, nstreecontroller uses nsindexpaths store selection. means idea of selection chain of indexes tree of nested arrays. far knows, is preserving selection in situation describe. problem here you're thinking of selection in terms of "object identity" , tree controller defines selection "a bunch of indexes nested array". behavior describe (afaict) expected out-of-the-box behavior nstreecontroller.
if want selection preservation object identity, suggestion subclass nstreecontroller , override mutating methods such capture current selection using -selectednodes before mutation, re-set selection using -setselectionindexpaths: array created asking each formerly selected node new -indexpath after mutation.
in short, if want behavior other stock behavior, you're going have write yourself. curious how hard took stab @ appears work cases bothered 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]; (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: little brutal (performance-wise) able working calls -setcontent: well. hope 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]; (id object in selectedobjects) { nstreenode* node = [self nodeofobject: object]; if (node.indexpath.length) { [array addobject: node.indexpath]; } } [self setselectionindexpaths: array]; } of course, relies on objects being identical. i'm not sure guarantees respect coredata across (unknown me) background operation.
Comments
Post a Comment