开发者

Moving array controller initialization from nib to code breaks table view bindings

开发者 https://www.devze.com 2023-04-13 01:45 出处:网络
My window controller subclass is the nib\'s owner. I instantiate my array controller, in code, in my document subclass. Both the doc开发者_StackOverflowument and window controller use it in code.
  1. My window controller subclass is the nib's owner.
  2. I instantiate my array controller, in code, in my document subclass. Both the doc开发者_StackOverflowument 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:


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.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号