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中:

*** 1 文件夹 1-1 文件夹 1-2 *** 2 文件夹 2-1 *文件夹 2-2 文件夹 2-3 文件夹 2-4

我想保留“文件夹 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 失去选择的主要内容,如果未能解决你的问题,请参考以下文章