Cocoa Bindings in Actual Use, Episode Umpteen

So here’s the deal.

Monocle uses a special NSCell to paint the engine’s icon and color in the engine preferences list. That cell is used in a table, and this table’s data (along with the rest of the stuff in the engine preferences) is pulled from an array controller which pulls data directly out of user defaults (I suspect that this is widely considered to be foolish and if any more hinders do show up I might just replan my entire data handling). I think it’s the “user defaults” part that is the root of my worries, or even just the prerequisite binding to contentArray and “Handles Content as Compound Value” option.

The table column is bound to the engine object itself (a dictionary), and this is from where my issues stem. (No, I can’t just simply bind to a property of the model object since it’s an ordinary dictionary, and no, I can’t just bind to one of the two properties; color depends on a third property, which decides if it should make up a color by eyedropping the image or go with the chosen color.)

The problem I’m having is that when I do change the color or icon inside that wonderful direct edit pane that I’ve now inserted into the same panel instead of requiring a trip to the Edit sheet, the cell simply wouldn’t update until I had forced another sort of update or nudge or redraw on the list, presumably since it didn’t pick up on the whole engine object (which is what the column was bound to) updating. Like by changing the selection. Had I had a model object instead of NSDictionary, I would have synthesized an accessor giving me the data I needed from the model, and signalled that it was dependent on the other keys for color and icon info and needed to update whenever they did.

After some deep mulling, I did manage to come up with a solution. I subclassed my array controller - rather, I had already done that for other reasons - and overrode the two init methods: initWithContent: and initWithCoder:. From within them, I called a shared init method which observes its own selection key path with the NSKeyValueObservingOptionNew and NSKeyValueObservingOptionOld options, and then I implemented observeValueForKeyPath:ofObject:change:context:.

Inside observe..., when the selection observation hits, I add observers to the newly selected engine’s properties, as many as I’d like (and remove the corresponding observers for the old selection). When these properties change and I get their observations, in turn, I simply do:

[self willChange:NSKeyValueChangeSetting
    valuesAtIndexes:[self selectionIndexes] forKey:@"arrangedObjects"];
[self didChange:NSKeyValueChangeSetting
    valuesAtIndexes:[self selectionIndexes] forKey:@"arrangedObjects"];

…to force a full nudge of that engine.

It seems to work this far.

Update: I would like to point out that even if the spirit of the way I’m using what I’ve described in this post is clearly a “hack” in my situation - ie. it lets me continue to work with bindings without a special model class and under other questionable circumstances - I dare say that the actual way it’s implemented is clean. This is not something that abuses the way NSArrayController works today in private and will be broken in 10.6 (”Liger”). I’m using KVO itself to go all meta sending observers inside the array controller to itself, and I imagine that this could be useful information for someone running up against the very limits (more ‘legitimately’) of bindings. The only ‘parameters’ are the model property names, which you control, and ’selection’ which is very clearly established API for an object controller or array controller.

Comments [+]

  1. Just wondering: Have you tried playing around with -keyPathsForValuesAffectingValueForKey: ?

    This sounds quite similar to a problem I once had but back then this didn’t help me much as ’self’ wasn’t a possible key. The Foundation Release notes for X.5 suggest that things have considerably improved in this area, so there might be hope.

    By http://supersnowman.livejournal.com/ · 2007.12.20 09:09

  2. Right, and I have tried setKeys:triggerChangeNotificationsForDependentKey:, but even if keyPathsForValuesAffectingValueForKey: worked I can’t drop 10.4 support. Well, I can drop 10.4 support, but the crowds would be uneasy. (The crowd of, uh, seven or so Monocle users.)

    After some consideration and a lot of stick-poking from Scott Stevenson, it occurs even more so than previously to me that binding to an NSDictionary directly inside shared defaults is… uh, ill-advised. I think I’ll actually move to a traditional model object instead, since Cocoa makes me jump through all these hoops if I don’t - or rather, if I use an NSDictionary as a model object, which I can’t override that well. I have avoided doing so for so long only because it’s a lot of code and a reasonable amount of nib stuff to change and while I can cope with the former, the latter can be a bitch if you miss a spot.

    It is not lost on me that if I had moved to 10.5, along with the new keyPathsForValuesAffectingValueForKey:, I could also have made a category on NSDictionary and implemented +keyPathsForValuesAffecting**Key**: and probably worked around this.

    By Jesper · 2007.12.20 11:23

Leave a comment

Your e-mail address is never shown. If you type a line break in the comment, it will show up as a line break (naturally). The following HTML is allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

(required)

(required)


Please note: Your comment will not show up at once. Unless you're spamming or being abusive, you have nothing to worry about. (Read the full policy.)