1

我正在开发一个桌面 Cocoa 应用程序。在应用程序中,我有一个基于视图的 NSOutlineView 绑定到 NSTreeController:

在此处输入图像描述 在此处输入图像描述

NSTreeController 处于实体模式,由 Core Data 驱动。一切都按预期工作,直到底层模型图发生变化。每当一个新对象插入到已注册的 NSManagedObjectContext 中时,NSTreeController 都会刷新其内容,并且绑定的 NSOutlineView 会正确显示结果。控制器的内容使用 NSSortDescriptor 按“标题”排序,我在应用程序启动期间设置了此排序。唯一的缺点是即使在 NSTreeController 的首选项中选中了保留选择框, selectionIndexPath 也不会改变。我想保留在新节点出现在树中之前选择的对象上的选择。

我将 NSTreeController 子类化以调试在对象图更改期间选择发生的情况。我可以看到 NSTreeController 通过 KVO 更改了它的内容,但setContent:没有调用该方法。比setSelectionIndexPaths:通过 NSTreeControllerTreeNode KVO 调用但参数包含先前的 indexPath。

在此处输入图像描述

所以,要清楚:

  • 顶级 1
    • 文件夹 1-1
    • 文件夹 1-2
  • 顶级 2
    • 文件夹 2-1
    • *文件夹 2-3 <== 已选择
    • 文件夹 2-4

在初始阶段选择“文件夹 2-3”。然后将“文件夹 2-2”插入到 NSManagedObjectContext 中[NSEntityDescription insertNewObjectForEntityForName:@"Folder" inManagedObjectContext:managedObjectContext];

  • 顶级 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 排序),

我可以看到该setContent:方法在应用程序启动期间被调用,但仅此而已。似乎 NSTreeController 观察根节点(文件夹)并通过 KVO 以某种方式反映模型更改。(因此,当我创建一个新的子文件夹并将其添加到其父文件夹时,[folder addChildrenObject:subfolder]它出现在树中,但没有调用任何树突变方法。)

不幸的是,我不能直接使用 NSTreeController 变异方法(add:, addChild:, insert:, insertChild:),因为真正的应用程序会在后台线程中更新模型。后台线程使用自己的 managedObjectContext 并将更改与mergeChangesFromContextDidSaveNotification. 这让我抓狂,因为一切正常,期待 NSOutlineView 的选择。当我将一堆子文件夹从后台线程合并到主 managedObjectContext 时,树会自行更新,但是我丢失了在合并之前选择的对象的选择。

更新2:

我准备了一个小样本来演示这个问题:http ://cl.ly/3k371n0c250P

  1. 展开“文件夹 1”,然后选择“选择子文件夹 9999”
  2. 按“新建子文件夹”。它将在后台批量创建50个子文件夹。
  3. 如您所见,即使在 MyTreeController.m 中的内容更改之前保存了选择,“子文件夹 9999”中的选择也会丢失
4

1 回答 1

0

通过阅读文档和标题,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 的保证是什么。

于 2013-05-03T11:05:56.017 回答