简体   繁体   中英

Why are my Cocoa bindings broken?

I have a window with an NSTextField (in Snow Leopard), which I have binded to an NSString function in my WindowController class. This string will combine information about my table view's selection and count, provided by my array controller. It gets an initial value, "0 0" , but doesn't ever update, when the selection or count changes. The binding looks like this (File's Owner is MyWindowController):

替代文字

I implemented + (NSSet *)keyPathsForValuesAffecting<key> (below), but the binding never updates, even when the array controller's total count and selection change.

( Additional troubleshooting performed ) I had originally been using the Display Pattern Value binding of the NSTextField, but I needed more complicated logic than that binding afforded. I then started listening to the selection changed/changing events of the TableView that displays the array controller's contents and changing the Display Pattern Value bindings dynamically, but that felt like a hack, and overly complicated.

I'm sure there's something I'm missing, but I can't tell what. Does anyone have any ideas? I've read through Apple's key-value-observing documentation, and this seems to be all that's necessary. I've checked, and my keyPathsForValuesAffectingMyString is getting called, but myString only gets called once. I've distilled my code below ( updated x3 ).

Update 1/21

I'm still plugging away trying to figure this out. When I addObserver to self for the arrayController key paths, the notifications do fire as expected, so my key paths and the key value observing mechanism is fine. When I call [self didChangeValueForKey:@"myString"]; within my observeValueForKeyPath method for the same keys, the binding still doesn't update, leading me to believe it's a bindings problem rather than a KVO problem. I'm going to be reading up on the bindings mechanism more...

@interface MyWindowController : NSWindowController {
    IBOutlet NSArrayController *arrayController;
}

- (NSArrayController *)arrayController;
- (NSString *)myString;

@end

@implementation MyWindowController

+ (NSSet *)keyPathsForValuesAffectingMyString {
    return [NSSet setWithObjects:
            @"arrayController.arrangedObjects",
            @"arrayController.selection",
            nil];
}

- (NSArrayController *)arrayController {
    return arrayController;
}

- (NSString *)myString {
    // Just as an example; I have more complicated logic going on in my real code
    return [NSString stringWithFormat:@"%@, %@",
            [arrayController valueForKeyPath:@"arrangedObjects.@count"], 
            [arrayController valueForKeyPath:@"selection.@count"]];
}

@end

I've verified this exact same bug. Someone on Cocoabuilder had a guess as to why the bug happens:

http://www.cocoabuilder.com/archive/cocoa/284396-why-doesn-nsarraycontroller-selection-et-al-fire-keypathsforvaluesaffectingkey.html#284400

I can't speak as to whether this explanation is true, but I certainly can't get +keyPathsForValues… to work with NSArrayControllers.

I've got a workaround, but I'm not happy about it, since it shouldn't be necessary, and I would still prefer to get the bindings working properly. I won't accept this answer, and will delete it if someone posts an actual fix. </disclaimer>

@interface MyWindowController : NSWindowController {
    IBOutlet NSArrayController *arrayController;
    IBOutlet NSTextField *fieldThatShouldBeBinded;
}

- (NSString *)myString;

@end

@implementation MyWindowController

- (void)awakeFromNib {
    [arrayController addObserver:self
                      forKeyPath:@"selection"
                         options:0
                         context:NULL];
    [arrayController addObserver:self
                      forKeyPath:@"arrangedObjects"
                         options:0
                         context:NULL];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    if( object == arrayController )
        [fieldThatShouldBeBinded setStringValue:[self myString]];
}

- (NSString *)myString {
    return [NSString stringWithFormat:@"%@, %@",
            [arrayController valueForKeyPath:@"arrangedObjects.@count"], 
            [arrayController valueForKeyPath:@"selection.@count"]];
}

@end

I met the same problem and found another way (but it is still workaround). You have to declare dynamic workaround property. In implementation section, just return new empty object for it. Now, you can KVO this workaround property.

@property(nonatomic,retain) NSArray *workaround;
@dynamic workaround;
- (NSArray *)workaround { return [NSArray array]; } // new *every* time
- (void)setWorkaround:(NSArray *)unused { }

+ (NSSet *)keyPathsForValuesAffectingMyString { return [NSSet setWithObject:@"workaround"]; }

To get this work, you still need to manually bind self.workaround to arrayController.selectedObjects (or whatever):

- (void)awakeFromNib // or similar place
{
    [super awakeFromNib];
    [self bind:@"workaround" toObject:arrayController withKeyPath:@"selectedObjects" options:nil];
}

Manual binding works as expected, workaround is updated with what you have bound it to. But KVO tests whether property value is really changed (and stops propagating if it is the same). If you return new self.workaround value every time, it works.

Warning: never call -[setWorkaround:] by yourself — this will effectively flush the other side of binding ( arrayController.selectedObjects in this case).

This method has some benefits: you avoid centralized observeValueForKeyPath:... and your logic is in the right place. And it scales well, just add workaround2, 3, and so on for similar cases.

Make sure that the arrayController outlet is connected in Interface Builder. I'm guessing that it's nil.

Don't use the @count keyword. Bindings and KVO on array controllers will get updated when the content changes. If that doesn't work, then there is a problem somewhere else.

Another option is to use the display pattern bindings instead of a composite property. Bind Display Pattern Value1 to arrayController.arrangedObjects.@count and Display Pattern Value2 to arrayController.selection.@count, and set the pattern to "%{value1}@, %{value2}@"

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM