Feb 13, 2011 14:24
I set out this weekend to do some experimentation with Cocoa Bindings in an effort to understand how they worked and in what cases they might be useful. After some initial minor successes, the revised goal became to create an NSView subclass that could act as an encapsulated editor of a complex data type. In the ideal, this view could, itself, be bound to properties of its relevant type on other objects. Futhermore, in the ideal, the sub-design of that control itself should be bindings-based such that the sub-controls within that view should be able to bind to keyPaths on their parent control to transmit and receive their values.
I started with the Apple-provided BindingsJoystick bindable control sample project. This project doesn't have IB integration -- let's call that a second- (or perhaps even third-) order goal. In this sample project you have a control that presents a visual X/Y area where you can click arbitrarily within the square, and an offset (r) and an angle (theta) relative to the center of the area are set on whatever the control is bound to. Likewise, it binds to the model and displays a crosshairs at the point represented by the r/theta pair pushed to it by its bindings.
Now let's suppose that instead of two bindings, one for offset and one for angle, you wanted to bind the control to a property of a complex type -- call it MyOffsetAndAngle. In this example, as it ships, you start out with model comprised of an NSMutableArray of NSMutableDictionaries with key-value pairs of { @"offset", NSNumber(float) } and { @"angle", NSNumber(float) }. Let's say you want to endeavor to replace this model with an NSMutableArray of MyOffsetAndAngle objects.
The first thing to realize is that MyOffsetAndAngle must be mutable. Why? Because if other things are bound to an instance of it, and you replace that instance in the owning array, then these other things will be left bound to an, at best stale, and at worst dealloc'ed, object.
Ok, so we'll make MyOffsetAndAngle mutable. And now we're back to square one. What do I mean? The whole idea here was to encapsulate the editing of offset and angle into a single control; Barring a bunch of glue logic of the form "mutable object replaces its state with state from the object you pass into it," you're back to pushing values for discrete properties on the object. Remember, too, that since bindings work through KVC, that logic would need to work through setValue:forKey:. How are you supposed to do that? setValue: foo forKey: @"self"? I've seen no evidence that this pattern is in regular use, which makes me very nervous about it indeed.
I thrashed for some time, and had a few triumphs and many defeats. This process led me to a few conclusions:
It seems that there's two basic paradigms. (Maybe three if you want to treat NSTreeController as different from NSArrayController, but they're not all that different conceptually -- it's collection management either way.)
The first paradigm is the single object case. In this case, you have a single object that persists across all bindings-related operations. It seems that most folks expose this single object through an NSObjectController. Since we don't have a concept of scope (i.e. what's going on "above" this object in the object graph) it's not possible to wholesale replace this object in the graph. The net result appears to be that our ability to use bindings is limited to editing the mutable properties (or the mutable properties of its children) of this object.
The second paradigm is the collection case. In these cases, the bindings and controllers have additional mechanics to support the add/remove operations, but we still have the same requirement that there be an object that persists throughout. The role of the "single object that persists across all bindings-related operations" in these cases is filled by the array (for NSArrayController) or the root node of the tree (for NSTreeController.) Furthermore, while there is support for adding and removing elements from arrays and trees, it appears there's not an inherent concept of mutating the state of an object in the collection by replacing it; It seems that you're limited to editing it by mutating its mutable properties (or the mutable properties of its children.)
From that, I observe that bindings really expect you to be operating on a fully mutable model graph, and moreover, a fully mutable model that is mutated in a specific way. If you want to provide an alternate mutation mechanism, it seems you're mostly out of luck (more on that later.) Anything you want to put into an array that's managed by an array controller (where you want bi-directional bindings to work) had better be mutable. In exploring this, I found that it's straightforward enough to create a handle/carrier/passthrough object but that's one more step to go through, and it feels kludgey. I went pretty far down the rabbit hole trying to proxy things to intercept mutations, but in the end, it seems that you have to give the bound controls something mutable to write to, or it's a non-starter. In my experimentations, while I was able to get specific cases working, I kept hitting the wall trying to come up with a system that would work on arbitrarily deep object graphs. That's not to say it can't be done, but I found the exercise to be non-trivial, to say the least.
This led me to the observation that, at least on their face, Cocoa Bindings seem to be best oriented toward dealing with "leaf types." That is to say, NSNumber, NSString, NSData, etc. I say "leaf types" and not "value types" here because value types isn't quite the right term. You can have a complex data-structure that is still effectively a value type by virtue of the semantics you give it. However, Cocoa bindings seem geared toward dealing with leaf types -- non-complex value type data at the edge of the object graph.
Going through this exercise, I have no choice but to say that my hat's off to the CoreData people for making this all work. It seems like a logical assumption that CoreData and the binding folks would have had the opportunity to work together, or that whichever one came second had ample time to wrap themselves around to meet the restrictions of whomever got there first. Then again, the conceptual requirements aren't all that different between the two. In every object persistence system I've ever seen you have a root object (or some abstract root "database" object) that persists across all sub-operations, and your object graph is inherently mutable from there all the way down to whatever atom you pick, and immutability is less a data structure issue than it is a permissions issue. My guess, after this exploration, is that if you're working with CoreData your atom size, for the purposes of this discussion, is the aforementioned "leaf types." Lo and behold, that seems to align pretty well with Cocoa Bindings!
In terms of having non-leaf-type granules for binding and editability, I'm not quite sure where I've ended up. Conceptually, my aspirations were to find a way to use bindings in a situation where the model is not mutable with leaf-type granularity. More specifically, think of a distributed inventory system where the model lives on a server and is only mutable in large-granule batches -- say, "update the entire product record" instead of "change the product name string." More importantly, the goal would be to intercept the mutuation messages from the bindings in a controller layer, and package them up at an arbitrary, non-leaf-type level, into a request which would then induce the server to mutate the model in large chunks, triggering an invalidation to the view, and prompting a refetch and redraw of the record.
The conclusion I reach is that Cocoa Bindings, at least on their face, are not well suited to this type of architecture.