views:

1165

answers:

4

I've had a crack at implementing bindings for my own NSView subclass. It works, but there are problems with retain cycles when binding to File's Owner from a nib file. After reading into it a little, I discovered that Apple had the same problem a few years back but have fixed it with some magic undocumented class (NSAutounbinder).

There is a lengthy discussion of the retain cycle problem here http://www.cocoabuilder.com/archive/message/cocoa/2004/6/12/109600 . The workaround is to to unbind all bindings before the window controller is released, not before it is deallocated, in a place like windowWillClose:. This seems like an unnecessary hack to me.

My question is this: Is there any way to make custom bindings that work as well as the ones made by Apple, without using undocumented features? Am I going about this the wrong way?


UPDATE 2: I have found a solution that allows manually implemented bindings to work exactly like Apple's bindings. It takes advantage of the undocumented NSAutounbinder class, without actually using undocumented features. I will post the solution later today.


UPDATE: I've tried using exposeBinding:, and it doesn't seem to make any difference. However, the NSObject implementation of bind:toObject:withKeyPath:options: half works. It propogates changes from bindee to binder (i.e. from model/controller to view), but doesn't work the opposite way. Also, although the bindee is obviously being observed, observeValueForKeyPath:ofObject:change:context: is never triggered.

Example project here: http://www.tomdalling.com/wp-content/BindingsTest.zip

Apple's documentation indicates that you do, in fact, have to override bind:toObject:withKeyPath:options: to implement manual bindings. See here: http://developer.apple.com/documentation/Cocoa/Conceptual/CocoaBindings/Concepts/HowDoBindingsWork.html


SIDE NOTE: I've investigated how the undocumented NSAutounbinder works, and here's what I know.

When a binding is created to an NSWindowController, the bound object is actually an NSAutounbinder that is acquired from the NSWindowController with -[NSWindowController _autounbinder]. NSAutounbinder is a non-retaining proxy for the NSWindowController object. It is non-retaining to avoid the retain cycle problem.

When -[NSWindowController release] is called and retainCount == 1, The NSAutounbinder unbinds all bindings to itself. This ensures that there are no dangling pointers to the object before it is deallocated.

+2  A: 

You may want to check out the NSKeyValueBindingCreation Protocol. It lets you create bindings programmatically through code. (Remember to do the work in an awakeFromNib method if you need to reference IBOutlet variables or they could be nil.)

Ryan Ballantyne
Thanks for your answer, but I'm trying to implement my own bindings for a custom class, not use existing bindings.
Tom Dalling
I think that's what the +exposeBinding: method of that protocol is for. I could be wrong of course, but this seems to be the documented and sanctioned way to create your own bindings.
Ryan Ballantyne
exposedBindings is only used for making interface builder plugins from what I've read
Tom Dalling
I'm not talking about -exposedBindings. That just tells you what bindings an object exposes. I'm talking about +exposeBinding, which (at least, if I'm reading the docs right) lets an object expose any key path as a binding.
Ryan Ballantyne
-exposedBindings (usually) just outputs what whatever you specify with +exposeBinding
Tom Dalling
I just tried using exposeBinding: in a demo project, and it doesn't seem to work. Thanks for the tip off though, as I hadn't thought to try it.
Tom Dalling
Huh. I just tried it too, and I couldn't get it to work either. However, googling brought up two reports of it working on the first page of results, which makes me wonder what I did wrong...Anyway, sorry I wasn't more help. Best of luck.
Ryan Ballantyne
+3  A: 

The short answer is, no you can't get it to work with no workaround in the calling code and nibs. Even NSAutounbinder misses some cases for the NSDocument and NSWindowController, if Apple can't get it working correctly for 2 classes they specially rig up those of us without access to innards of AppKit have basically no chance.

Having said that, there are two workaround that are maybe a bit nicer than unbinding in windowWillClose:.

  1. Do not bind to File's Owner, but instead to drag an NSObjectController as root level object into the nib and bind to that, then setContents: on the object controller during awakeFromNib.
  2. Turn on Garbage Collection. If that is an option it solves all the object cycle issues ;-) Obviously GC introduces its own issues, and if you need 10.4 compatibility it is a non-starter.
Louis Gerbarg
+2  A: 

See mmalc's GraphicsBindings example for a good example of how to implement your own bindings. You need to implement the NSKeyValueBindingCreation informal protocol to get it working. To let your controllers know there are things that can be bound, call exposeBinding in the + (id)initialize method of your view:

+ (void)initialize { [self exposeBinding:@"ILIKEBINDAGE"]; }

You'll then need to implement each of the bindings managing methods in the NSKeyValueBindingCreation protocol. You basically need to setup KVO for the view so that it knows when to update based on the application's behaviors and handle the cleanup (unbind:).

It's a lot of extra, fairly ugly code so it may be that using traditional glue code works better and is easier to read.

Justin Williams
mmalc's GraphicsBindings example has the retain cycle problem I mentioned. Also, exposeBinding: has been discussed in Ryan Ballantyne's answer.
Tom Dalling
+4  A: 

Here is the best solution I can find. I've got a more detailed discussion and demo code here: http://www.tomdalling.com/cocoa/implementing-your-own-cocoa-bindings

Basically, you DO NOT override bind:toObject:withKeyPath:options: or unbind:. The default implementation on NSObject will use NSAutounbinder to avoid retain cycles. As Louis Gerbarg pointed out, there are still situations where NSAutounbinder doesn't kick in. However, you can get your bindings working at least as well as Apple's bindings.

Because the default implementation of bind:toObject:withKeyPath:options: doesn't update the model when the view changes, view-driven changes must be propagated manually. You can use -[NSObject infoForBinding:] to get all the information necessary to update the bound object. I've added my own method on NSObject with a category:

-(void)propagateValue:(id)value forBinding:(NSString*)binding;

It handles getting the bound object, the bound key path, and applying the value transformer. The implementation is available from the link at the top.

Tom Dalling