5
votes

I'm changing a cocoa binding programatically. I'm binding a NSTextField's value to the selection of an ArrayController. After I manually change the binding, I'm getting the "not key-value coding compliant for the key.." error, with the key being the old key, not the new one.

Check out the code:

NSTextField *textField = [self listTextField];   

NSDictionary *currentBindInfo = [textFieldTableViewCell infoForBinding:NSValueBinding];
NSLog(@"pre-change bindings for textField: %@", currentBindInfo);

/* Change the binding.  [Tried unbind: first, no difference] */
[textField bind:NSValueBinding
                      toObject:[currentBindInfo valueForKey:NSObservedObjectKey] 
                   withKeyPath:@"objectValue.iLifeProductName"
                       options:[currentBindInfo valueForKey:NSOptionsKey]];

/* Log the info so we can confirm it changed. debugging. */
NSLog(@"post-change bindings for textField: %@", [textFieldTableViewCell infoForBinding:NSValueBinding]);

To troubleshoot, I call 'infoForBinding' before and after the change and it looks to be changed correctly. I can see the old value, then I call bind:toObject... and dump the infoForBinding a second time, and the value has changed for the binding:

2011-07-06 22:36:23.137 My App 2011[14640:407] pre-change bindings for listTextFieldTableViewCell: {
NSObservedKeyPath = "selection.osxProductName";
NSObservedObject = "...sameTextField... 0x4009cc380>";
NSOptions =     {...same... };
}

2011-07-06 22:36:23.138 My App 2011[14640:407] post-change bindings for listTextFieldTableViewCell: {
NSObservedKeyPath = "selection.iLifeProductName";
NSObservedObject = "...sameTextField... 0x4009cc380>";
NSOptions =     {...same... };
}

But the code is still calling the original key:

2011-07-06 22:36:23.231 My App 2011[14640:407] [ valueForUndefinedKey:]: the entity ILifeVersion is not key value coding-compliant for the key "osxProductName".

--

The NSArrayController is bound to a ManagedObjectContext, the entity name is being changed earlier with this:

  [[self listAC] setEntityName:entityName];

Is the original keyValuePath being cached somewhere that I need to clear out? Is there a message like willChange/didChangeValueForKeyValuePath that I need to send to the binding or arrayController when I change the observed keypath?

Ideas?

Thanks!

1
Why do you need to change the binding programmatically? I've done a lot of work with Cocoa bindings and this seems a little odd. What are you actually trying to do?Rob Keniger
I have a NSPopupButton and a TableView. The Popup has a set of items corresponding to MOC entities, and the TableView displays instances of those entities. Changing the popup selection means the tableview should show instances of a different entity. To do that, I change the ArrayController's entity in code, then change the binding of field since different entities have different keypaths. (If I set them all to keypath of "description", it works fine, but I want to bind to the proper keypath so I can make the field editable, too.) Why do it in code? To avoid a bunch of arrayControllers.Woodster
Should you not unbind the fields before changing the ArrayController's entity? Otherwise the bound key paths will all be incorrect.Rob Keniger
RK: That's a good idea, thank you. I tried re-ordering them and adding the unbind back in, but same result: the binding isn't letting go of the original key path, I'm afraid.Woodster
I find this code weird: [textFieldTableViewCell infoForBinding:NSValueBinding] Generally when working with tables, it is the column you bind, not table cells. Or are you using Lion's new view-based tables?Mike Abdullah

1 Answers

0
votes

As @noa pointed out, you’re looking at the binding on the cell, but changing the binding on its control. That’s bound (ahem) to cause problems.

Replace this:

[textField bind:NSValueBinding
                  toObject:[currentBindInfo valueForKey:NSObservedObjectKey] 
               withKeyPath:@"objectValue.iLifeProductName"
                   options:[currentBindInfo valueForKey:NSOptionsKey]];

with this:

[textFieldTableViewCell bind:NSValueBinding
                  toObject:[currentBindInfo valueForKey:NSObservedObjectKey] 
               withKeyPath:@"objectValue.iLifeProductName"
                   options:[currentBindInfo valueForKey:NSOptionsKey]];

And see if it works better.


The explanation for this is a bit arcane, and I’m doing it from memory, so please excuse me if I get some of the details wrong.

Because NSControls and their NSCell works so closely together, you can actually bind to either the control or the cell in most instances, and you’ll get very similar results. That is, there’s code in the control to call the proper methods on its NSCell if the control’s been bound to, and vice-versa.

This means that if, in XIB, you bind to one or the other things will work, which is good. It also means you can bind to a cell in cases where you have multiple cells per view, so that’s good. HOWEVER, it can lead to confusion, because in fact you can actually bind to both your view and its cell, and in fact bind them in different ways, and then they’ll crosstalk.

In your example, I believe you’re adding a second binding to the NSControl in addition to the one on its NSCell. You’re doubly-bound. That’s no good.

In terms of best practice, I try to bind only to NSControls unless I have a good reason to drop down to NSCells. Partly because it matches what I do in XIB, partly because any standard helps reduce exactly this problem, and partly because NSCells are being gently deprecated.