将数组控制器初始化从 nib 移动到代码会破坏表视图绑定

发布于 2024-12-09 21:06:50 字数 1769 浏览 0 评论 0原文

  1. 我的窗口控制器子类是笔尖的所有者。
  2. 我在文档子类中用代码实例化我的数组控制器。文档和窗口控制器都在代码中使用它。
  3. 我像这样绑定表列:文件所有者>> document._arrayController.arrangedObjects.attributeName
  4. 表视图不显示任何行。
  5. 窗口控制器和文档类都不会接收与表视图相关的 -addObserver 消息。

显然我没有正确绑定到这个数组控制器。我认为我缺少一些有关表视图列如何绑定到数组的基本知识。

这个问题是在一些重构过程中出现的。我曾经在笔尖中实例化数组控制器。该文档是文件所有者,并且有一个阵列控制器的出口。绑定看起来像我的阵列控制器>排列对象>属性名称。一切都很好。

由于文档通过数组控制器处理插入对象,因此我认为窗口控制器不应该负责创建它。窗口控制器是新的笔尖所有者,因此我将其从笔尖中删除。我现在在 -makeWindowControllers 中的代码中创建它。 (我问了这个有关初始化的相关问题。)

在调试这个的过程中我发现了一些其他的东西。如果窗口控制器是表视图的数据源并且我实现 -numberOfRowsInDataSource

return [[self.document._arrayController arrangedObjects] count];

表视图调用它,为所有列发送 -addObserver 消息,并实际加载每个单元格都使用绑定。但是,当加载给定单元格的值时,它不会加载 arrangedObjects 中第 n 个对象的属性值,而是加载整个对象列的属性值。它将这些数组传递给值转换器(无法正确处理它们),并在文本单元格中显示数组的描述(它们不适合在其中)。

当窗口控制器是表视图的数据源但列使用绑定时,表视图应该忽略 -numberOfRowsInTableView 的结果,或者根本不应该调用它。 (使用 return 0 响应选择器只是避免了运行时错误。数据源仅首先设置以实现单元格的工具提示。)同样,所有当我在笔尖中创建数组控制器时,这曾经工作得很好。

一些想法:

  1. 是否可以使用 IB 将表列绑定到另一个对象拥有的数组控制器?
  2. 我是否需要将数组控制器放回笔尖,并让窗口控制器与文档实例共享它? (这听起来像是可怕的设计。)
  3. 我应该有两个数组控制器,一个用于窗口控制器,另一个用于文档?

添加:

我使用表视图数据源和绑定的原因是使用以下方法实现拖放重新排序:

  • tableView:writeRowsWithIndexes:toPasteboard:
  • tableView:validateDrop:proposeRow:proposeDropOperation:
  • tableView:acceptDrop:row:dropOperation:
  1. My window controller subclass is the nib's owner.
  2. I instantiate my array controller, in code, in my document subclass. Both the document and window controller use it in code.
  3. I bind table columns like this: File Owner >> document._arrayController.arrangedObjects.attributeName.
  4. The table view does not display any rows.
  5. Neither the window controller nor document class receive -addObserver messages related to the table view.

Obviously I'm not binding to this array controller correctly. I think I'm missing something fundamental about how table view columns bind to arrays.

This problem came about during some refactoring. I used to instantiate the array controller in the nib. The document was the file owner, and had an outlet for the array controller. The bindings looked like My Array Controller > arrangedObjects > attributeName. Everything worked fine.

Since the document handles inserting objects through the array controller, I didn't think the window controller should be responsible for creating it. The window controller is the new nib owner, so I removed it from the nib. I now create it in code in -makeWindowControllers. (I asked this related question about initialization.)

While debugging this I've discovered something else. If the window controller is the table view's data source and I implement -numberOfRowsInDataSource:

return [[self.document._arrayController arrangedObjects] count];

the table view calls it, sends -addObserver messages for all the columns, and actually loads values for each cell using the bindings. But when loading a value for a given cell, instead of loading the attribute value for the nth object in arrangedObjects, it loads attribute values for the entire column of objects. It passes these arrays to value transformers (which can't handle them correctly) and displays the array's description in the text cells (in which they do not fit).

When the window controller is the table view's data source but columns use bindings, the table view should ignore the result of the -numberOfRowsInTableView, or perhaps shouldn't call it at all. (Responding to the selector with return 0 merely avoids a runtime error. The data source is only set in the first place to implement tool tips for cells.) And again, all this used to work fine when I created the array controller in the nib.

Some thoughts:

  1. Is it even possible to use IB to bind table columns to an array controller that's owned by another object?
  2. Do I need to put the array controller back in the nib, and have the window controller share it with the document instance? (That sounds like horrible design.)
  3. Should I have two array controllers, one for the window controller and another for the document?

Added:

The reason I used a table view data source along with bindings is to implement drag-and-drop reordering using these methods:

  • tableView:writeRowsWithIndexes:toPasteboard:
  • tableView:validateDrop:proposedRow:proposedDropOperation:
  • tableView:acceptDrop:row:dropOperation:

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

影子是时光的心 2024-12-16 21:06:50

一些想法:

首先,您不应该同时实现 NSTableViewDataSource 协议和使用绑定 - 通常是其中之一。如果您有特定的原因这样做,我会首先使用绑定让您的应用程序启动并运行,然后一次一步地从 NSTableViewDataSource 中分层添加您想要的任何功能,以确保一切正常。有一些绑定可用于支持工具提示。

接下来,我并不是说这是唯一的方法,但我建议将 ArrayController 放回到 xib 中。 NSController 子类和绑定到它们的控件之间似乎存在一种特殊的关系——绑定检查器中的 Controller Key: 条目强烈暗示了这一点,因为当您不使用它时它会被禁用。我不确定,但我猜测当您通过那个大 keyPath 绑定以返回文档以获取 arrayController 时,这种魔法并没有发生。

我还想知道为什么您希望 NSArrayController 位于控件绑定到它的窗口之外的其他地方?相关地,为什么你会让 WindowController 与文档共享 NSArrayController ?

NSArrayController 保存选择状态,因此每个窗口都有一个,或者更抽象地说,它们会靠近 UI,因此应该存在于每个需要它的笔尖中,这确实是有意义的。也就是说,除非您尝试做一些非常规的事情,例如在多个窗口之间共享单个选择状态(即更改窗口 A 中的选择,并且窗口 B 中的相应控件也会更改选择以匹配窗口 A)。我将在下面探讨这个问题,但简而言之,我想不出您想要共享 arrayController 而不是将多个 arrayController 绑定到相同的基础数据的任何其他原因。

如果选择共享是您的目标,我认为您最好做一些事情,例如让文档在每个窗口中 nib 创建的 ArrayController 的选择索引上设置键值观察,并让它们将选择传播到其他窗口' 数组控制器。

我把这个编码了;它似乎有效。我从 Xcode 中基于标准 NSDocument 的 Cocoa 应用程序模板开始,并向文档添加了一个 dataModel 属性,并用一些数据伪造了它。然后我在 makeWindowControllers 期间创建了两个窗口,然后添加了遵守规则等,以使它们的选择相互遵循。一切似乎都很好地结合在一起。

压缩成一个代码块:

#import <Cocoa/Cocoa.h>

@interface SODocument : NSDocument
@property (retain) id dataModel;
@end

@interface SOWindowController : NSWindowController
@property (retain) IBOutlet NSArrayController* arrayController;
@end

@implementation SODocument
@synthesize dataModel = _dataModel;

- (id)init
{
    self = [super init];
    if (self) 
    {
        // Make some fake data to bind to
        NSMutableDictionary* item1 = [NSMutableDictionary dictionaryWithObjectsAndKeys: @"Item 1", @"attributeName", nil];
        NSMutableDictionary* item2 = [NSMutableDictionary dictionaryWithObjectsAndKeys: @"Item 2", @"attributeName", nil];
        NSMutableDictionary* item3 = [NSMutableDictionary dictionaryWithObjectsAndKeys: @"Item 3", @"attributeName", nil];        
        _dataModel = [[NSMutableArray arrayWithObjects: item1, item2, item3, nil] retain];
    }
    return self;
}

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

- (NSString *)windowNibName
{
    return @"SODocument";
}

- (void)makeWindowControllers
{
    SOWindowController* wc1 = [[[SOWindowController alloc] initWithWindowNibName: [self windowNibName]] autorelease];
    [self addWindowController: wc1];

    SOWindowController* wc2 = [[[SOWindowController alloc] initWithWindowNibName: [self windowNibName]] autorelease];
    [self addWindowController: wc2];
}

- (void)addWindowController:(NSWindowController *)windowController
{
    [super addWindowController: windowController];
    [windowController addObserver:self forKeyPath: @"arrayController.selectionIndexes" options: 0 context: [SODocument class]];
}

- (void)removeWindowController:(NSWindowController *)windowController
{
    [windowController removeObserver:self forKeyPath: @"arrayController.selectionIndexes" context: [SODocument class]];
    [super removeWindowController:windowController];
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 
{
    if ([SODocument class] == context && [@"arrayController.selectionIndexes" isEqualToString: keyPath])
    {
        NSIndexSet* selectionIndexes = ((SOWindowController*)object).arrayController.selectionIndexes;
        for (SOWindowController* wc in self.windowControllers)
        {
            if (![selectionIndexes isEqualToIndexSet: wc.arrayController.selectionIndexes])
            {
                wc.arrayController.selectionIndexes = selectionIndexes;
            }
        }
    }
}
@end

@implementation SOWindowController
@synthesize arrayController = _arrayController;

-(void)dealloc
{
    [_arrayController release];
    [super dealloc];
}
@end

文档笔尖有一个 SOWindowController 的文件所有者。它有一个绑定到File's Owner.document.dataModel的NSArrayController,以及一个绑定到ArrayController.arrangedObjects.attributeName的带有一列的NSTableView。

当我创建新文档时会出现两个窗口,每个窗口显示相同的内容。当我更改其中一个的 tableView 选择时,另一个也会更改。

无论如何,希望这会有所帮助。

A few thoughts:

First, you shouldn't implement both the NSTableViewDataSource protocol and use bindings -- it's usually one or the other. If you have a specific reason to do this, I would get your app up-and-running using bindings only first, then layer in whatever functionality you want from NSTableViewDataSource one step at a time to make sure everything's working. There are bindings available for supporting ToolTips.

Next, I'm not saying this is the only approach, but I would suggest putting the ArrayController back into the xib. There seems to be a special relationship between NSController subclasses and the controls that bind to them -- the Controller Key: entry in the bindings inspector hints strongly at this, since it's disabled when you're not using one. I don't know for sure, but I'm guessing that when you bind through that big keyPath to reach back to the document to get the arrayController, that magic isn't happening.

I'm also wondering why you would want the NSArrayController to live someplace other than on the window whose controls bind to it? And relatedly why you would have the WindowController share the NSArrayController with the Document?

NSArrayControllers hold selection state, so it really does make sense that there would be one per window, or more abstractly, that they would live close to the UI, and therefore should exist in each nib that needs one. That is, unless you're trying to do something unconventional like share a single selection state between multiple windows (i.e. change the selection in window A and the corresponding controls in window B also change selections to match window A). I'll explore that one below, but in short, I can't think of any other reason you'd want to share an arrayController over having multiple arrayControllers bound to the same underlying data.

If selection sharing were your goal, I think you'd be better off doing something like having the document set up Key-Value Observances on the selectionIndexes of the nib-created ArrayControllers in each window and have them propagate the selection around to the other windows' arrayControllers.

I coded this up; It appears to work. I started out with the standard NSDocument-based Cocoa app template in Xcode, and added a dataModel property to the document, and faked it up with some data. Then I made two windows during makeWindowControllers, then I added observances, etc. to make their selections follow each other. Everything seemed to come together nicely.

Squished into one code block:

#import <Cocoa/Cocoa.h>

@interface SODocument : NSDocument
@property (retain) id dataModel;
@end

@interface SOWindowController : NSWindowController
@property (retain) IBOutlet NSArrayController* arrayController;
@end

@implementation SODocument
@synthesize dataModel = _dataModel;

- (id)init
{
    self = [super init];
    if (self) 
    {
        // Make some fake data to bind to
        NSMutableDictionary* item1 = [NSMutableDictionary dictionaryWithObjectsAndKeys: @"Item 1", @"attributeName", nil];
        NSMutableDictionary* item2 = [NSMutableDictionary dictionaryWithObjectsAndKeys: @"Item 2", @"attributeName", nil];
        NSMutableDictionary* item3 = [NSMutableDictionary dictionaryWithObjectsAndKeys: @"Item 3", @"attributeName", nil];        
        _dataModel = [[NSMutableArray arrayWithObjects: item1, item2, item3, nil] retain];
    }
    return self;
}

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

- (NSString *)windowNibName
{
    return @"SODocument";
}

- (void)makeWindowControllers
{
    SOWindowController* wc1 = [[[SOWindowController alloc] initWithWindowNibName: [self windowNibName]] autorelease];
    [self addWindowController: wc1];

    SOWindowController* wc2 = [[[SOWindowController alloc] initWithWindowNibName: [self windowNibName]] autorelease];
    [self addWindowController: wc2];
}

- (void)addWindowController:(NSWindowController *)windowController
{
    [super addWindowController: windowController];
    [windowController addObserver:self forKeyPath: @"arrayController.selectionIndexes" options: 0 context: [SODocument class]];
}

- (void)removeWindowController:(NSWindowController *)windowController
{
    [windowController removeObserver:self forKeyPath: @"arrayController.selectionIndexes" context: [SODocument class]];
    [super removeWindowController:windowController];
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 
{
    if ([SODocument class] == context && [@"arrayController.selectionIndexes" isEqualToString: keyPath])
    {
        NSIndexSet* selectionIndexes = ((SOWindowController*)object).arrayController.selectionIndexes;
        for (SOWindowController* wc in self.windowControllers)
        {
            if (![selectionIndexes isEqualToIndexSet: wc.arrayController.selectionIndexes])
            {
                wc.arrayController.selectionIndexes = selectionIndexes;
            }
        }
    }
}
@end

@implementation SOWindowController
@synthesize arrayController = _arrayController;

-(void)dealloc
{
    [_arrayController release];
    [super dealloc];
}
@end

The document nib has a File's Owner of SOWindowController. It has an NSArrayController bound to File's Owner.document.dataModel, and an NSTableView with one column bound to ArrayController.arrangedObjects.attributeName.

Two windows come up when I create a new doc, each showing the same thing. When I change the tableView selection in one, the other one changes as well.

Anyway, hope this helps.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文