NSTreeController/NSOutlineView 失去选择
Posted
技术标签:
【中文标题】NSTreeController/NSOutlineView 失去选择【英文标题】:NSTreeController/NSOutlineView loses its selection 【发布时间】:2013-05-03 10:09:34 【问题描述】:我正在开发一个桌面 Cocoa 应用程序。在应用程序中,我有一个基于视图的 NSOutlineView 绑定到 NSTreeController:
NSTreeController 处于实体模式,由 Core Data 驱动。一切都按预期工作,直到底层模型图发生变化。每当一个新对象插入到已注册的 NSManagedObjectContext 中时,NSTreeController 都会刷新其内容,并且绑定的 NSOutlineView 会正确显示结果。控制器的内容使用 NSSortDescriptor 按“标题”排序,我在应用程序启动期间设置了此排序。唯一的缺点是即使在 NSTreeController 的首选项中选中了保留选择框, selectionIndexPath 也不会改变。我想保留在新节点出现在树中之前选择的对象上的选择。
我对 NSTreeController 进行了子类化,以调试在对象图更改期间选择发生的情况。我可以看到 NSTreeController 通过 KVO 更改了它的内容,但没有调用 setContent:
方法。比通过 NSTreeControllerTreeNode KVO 调用的 setSelectionIndexPaths:
但参数包含先前的 indexPath。
所以,要清楚:
*** 1 文件夹 1-1 文件夹 1-2 *** 2 文件夹 2-1 *文件夹 2-3 文件夹 2-4在初始阶段选择“文件夹 2-3”。然后用[NSEntityDescription insertNewObjectForEntityForName:@"Folder" inManagedObjectContext:managedObjectContext];
将“文件夹2-2”插入到NSManagedObjectContext中:
我想保留“文件夹 2-3”上的选择,因此我设置了“Preseve 选择”,但似乎 NSTreeController 完全忽略了这个属性,或者我误解了一些东西。
如何强制 NSTreeController 保持其选择?
更新1:
不幸的是,我的 NSTreeController 子类中没有调用过任何变异方法(insertObject:atArrangedObjectIndexPath:
、insertObjects:atArrangedObjectIndexPaths:
等)。我已经重写了大多数工厂方法来调试引擎盖下发生的事情,这就是当新的托管对象插入上下文时我可以看到的:
-[FoldersTreeController observeValueForKeyPath:ofObject:change:context:] // Content observer, registered with: [self addObserver:self forKeyPath:@"content" options:NSKeyValueObservingOptionNew context:nil]
-[FoldersTreeController setSelectionIndexPaths:]
-[FoldersTreeController selectedNodes]
-[FoldersTreeController selectedNodes]
FoldersTreeController 处于实体模式并绑定到 Application 委托的 managedObjectContext。我有一个名为“Folders”的根实体,它有一个名为“children”的属性。它与称为子文件夹的其他实体是一对多的关系。 Subfolders 实体是 Folders 的子类,因此它具有与其父级相同的属性。正如您在第一个附加屏幕截图中看到的那样,NSTreeController 的实体已设置为文件夹实体,并且按预期工作。每当我将新的子文件夹插入 managedObjectContext 时,它就会出现在正确文件夹下的树中(作为子节点,按绑定到 NSTreeController 的 NSSortDescriptor 排序),但是没有调用任何 NSTreeController 突变方法,并且如果新插入的子文件夹出现在前面列表它会拉下所有内容,但选择保持在同一位置。
我可以看到在应用程序启动期间调用了setContent:
方法,但仅此而已。似乎 NSTreeController 观察根节点(文件夹)并通过 KVO 以某种方式反映模型更改。 (因此,当我创建一个新的子文件夹并将其添加到其父级时,[folder addChildrenObject:subfolder]
它出现在树中,但没有调用任何树突变方法。)
不幸的是,我不能直接使用 NSTreeController 变异方法(add:
、addChild:
、insert:
、insertChild:
),因为真正的应用程序会在后台线程中更新模型。后台线程使用自己的managedObjectContext,并与mergeChangesFromContextDidSaveNotification
批量合并更改。这让我抓狂,因为一切正常,期待 NSOutlineView 的选择。当我将一堆子文件夹从后台线程合并到主 managedObjectContext 时,树会自行更新,但是我丢失了在合并之前选择的对象的选择。
更新2:
我准备了一个小样本来演示这个问题:http://cl.ly/3k371n0c250P
-
展开“文件夹 1”,然后选择选择“子文件夹 9999”
按“新建子文件夹”。它会在后台批量创建50个子文件夹。
如您所见,“子文件夹 9999”中的选择将丢失,即使它在 MyTreeController.m 中的内容更改之前保存
【问题讨论】:
【参考方案1】:通过阅读文档和标题,NSTreeController 使用 NSIndexPaths 来存储选择。这意味着它的选择思想是嵌套数组树的索引链。据它所知,它 在您描述的情况下保留了选择。这里的问题是您正在考虑根据“对象身份”进行选择,而树控制器将选择定义为“嵌套数组中的一堆索引”。您描述的行为是 (AFAICT) NSTreeController 的预期开箱即用行为。
如果您希望通过对象身份保存选择,我的建议是继承 NSTreeController 并覆盖所有变异方法,以便在变异之前使用-selectedNodes
捕获当前选择,然后使用-setSelectionIndexPaths:
重新设置选择通过在突变后询问每个以前选择的节点的新-indexPath
创建的数组。
简而言之,如果您想要股票行为以外的其他行为,您将不得不自己编写。我很好奇这会有多难,所以我尝试了一些似乎对我费心测试的案例有用的东西。这里是:
@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
编辑:这有点残酷(性能方面),但我也能够得到一些有用的东西来调用-setContent:
。希望这会有所帮助:
- (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];
当然,这取决于对象实际上是相同的。我不确定在您(我不知道)后台操作中对 CoreData 的保证是什么。
【讨论】:
感谢您的详细解答。这正是我想要实现的,不幸的是,在我的 NSTreeController 子类中没有调用过任何突变方法(insertObject:atArrangedObjectIndexPath:
、insertObjects:atArrangedObjectIndexPaths:
等)。我已经重写了大多数工厂方法来调试引擎盖下发生的事情。我已将问题更新为更具体。
还是没有解决问题。我更新了示例以演示确切的问题:cl.ly/3k371n0c250P 调用 setContent:
方法时选择已经更改(使用惰性 fetchig。如果我禁用惰性获取,则根本不会调用 setContent:
方法。 )
我没主意了;在我看来,处理这些更新的 NSTreeController 逻辑对我们来说是不透明的。在我的脑海中,我推测您可能能够将 NSTreeController 从它自己的 NSManagedObjectContext 中挂起,然后在通知期间手动管理选择,其中更改被编组到单独的上下文中。我想另一种选择是放弃绑定并自己实现 NSOutlineViewDataSource 。不管怎样,听起来工作量很大。可能是时候问:“这真的值得付出努力吗?”
最后,我放弃了绑定,并按照您的建议自己实现了 NSOutlineViewDataSource。它运行良好。以上是关于NSTreeController/NSOutlineView 失去选择的主要内容,如果未能解决你的问题,请参考以下文章