Why doesn't -[NSArrayController selection] et al fire keyPathsForValuesAffectingKey?

  • I have a table bound to an array controller containing Foo objects and wanted to enable a "Perform Foo" button whenever there is a non-nil selection.  Not trusting those pesky proxy objects you get from an array controller's -selection, I wrote a -selectedFoo method which will return either the first selected Foo or nil if there is no selection, and bound my button's 'enabled' binding to it via NSIsNotNil value transformer.

    Now, NSArrayController has five methods which give the selection in some form, and they are all documented to be "observable using key-value observing".  So to make this work I implemented a +keyPathsForValuesAffectingSelectedFoo which returned the path to one of these keys, but when that didn't work I put in all five.

    But I never could get it to work.  By logging, I determined the problem:  None of these five observers are firing the -selectedFoo getter when the array and table controller's selection changes.  When I added, as a test, some other object's key path to keyPathsForValuesAffectingKey, and changed its value, the -selectedAgent method was invoked and updated the enabled state of my button as desired.  So the problem seems to be that "observeable using key-value observing" doesn't mean what I think it means.  Where am I going wrong?

    I solved the problem by eliminating all this code and binding my button's 'enabled' binding instead to the array controller's 'selection' and, to my surprise, it worked.  My surprise is because, according to superclass NSObjectController documentation, 'selection' returns NSNoSelectionMarker when there is no selection, not nil which is what my NSIsNotNil value transformer would expect.

    So, what should work does not work and what should not work does work.

    Thanks,

    Jerry Krinock

    // fooArrayController is an IBOutlet

    + (NSSet*)keyPathsForValuesAffectingSelectedFoo {
        return [NSSet setWithObjects:
            // Shotgun approach.  Try 'em all!!
            @"fooArrayController.selectedObjects",
            @"fooArrayController.selectionIndex",
            @"fooArrayController.selection",
            @"fooArrayController.selectionIndexes",
            @"fooArrayController.selectedObjects",
            nil] ;
    }

    - (Foo*)selectedFoo {
        NSArray* selectedFoos = [fooArrayController selectedObjects] ;
        if ([selectedFoos count] > 0) {
            return [selectedFoos objectAtIndex:0] ;
        }

        return nil ;
    }
  • On Apr 2, 2010, at 20:25, Jerry Krinock wrote:

    > Now, NSArrayController has five methods which give the selection in some form, and they are all documented to be "observable using key-value observing".  So to make this work I implemented a +keyPathsForValuesAffectingSelectedFoo which returned the path to one of these keys, but when that didn't work I put in all five.

    I only see 4 distinct keys in your list below, since one of them is duplicated.

    > But I never could get it to work.  By logging, I determined the problem:  None of these five observers are firing the -selectedFoo getter when the array and table controller's selection changes.  When I added, as a test, some other object's key path to keyPathsForValuesAffectingKey, and changed its value, the -selectedAgent method was invoked and updated the enabled state of my button as desired.  So the problem seems to be that "observeable using key-value observing" doesn't mean what I think it means.  Where am I going wrong?

    I'm suspicious of the "fooArrayController" part. Is it *just* an instance variable? If so, then it gets to be a property only by virtue 'accessInstanceVariablesDirectly' being YES (which is bad magic, but a different story). If so, then it's not KVO compliant, though the only scenario I can think of where this would cause keyPathsForValuesAffecting... to fail is if something was already observing "selectedFoo" before the nib file containing it was loaded.

    TBH, if I had a reason to want to observe the selection (keyPathsForValuesAffecting... being a special case of that), I'd create a real NSIndexSet* selectedItemIndexes property of my own, bind the array controller's "selectionIndexes" binding to that (so that the array controller maintains the real property for me), and then observe the real property, leaving all reference to the array controller itself out of my code.

    > I solved the problem by eliminating all this code and binding my button's 'enabled' binding instead to the array controller's 'selection' and, to my surprise, it worked.  My surprise is because, according to superclass NSObjectController documentation, 'selection' returns NSNoSelectionMarker when there is no selection, not nil which is what my NSIsNotNil value transformer would expect.

    I think the answer is that it works because it's a frameworks binding, and frameworks bindings are very clever in ways we aren't actually told about. It probably knows whether it's allowed to pass on the marker value, or if it must convert the marker to some kind of "real" object first -- hence nil. Or perhaps passing NSNoSelectionMarker to the value transformer is simply special-cased somewhere.
  • On Fri, Apr 2, 2010 at 8:25 PM, Jerry Krinock <jerry...> wrote:
    > But I never could get it to work.  By logging, I determined the problem:  None of these five observers are firing the -selectedFoo getter when the array and table controller's selection changes.  When I added, as a test, some other object's key path to keyPathsForValuesAffectingKey, and changed its value, the -selectedAgent method was invoked and updated the enabled state of my button as desired.  So the problem seems to be that "observeable using key-value observing" doesn't mean what I think it means.  Where am I going wrong?

    Because +keyPathsForValuesAffectingValueForKey relies on
    NSKeyValueObservingOptionPrior, which NSArrayController doesn't
    support.

    File a bug. It'll get dup'ed, but do it anyway.

    --Kyle Sluder
  • On Fri, Apr 2, 2010 at 8:25 PM, Jerry Krinock <jerry...> wrote:
    > + (NSSet*)keyPathsForValuesAffectingSelectedFoo {
    >    return [NSSet setWithObjects:
    >        // Shotgun approach.  Try 'em all!!
    >        @"fooArrayController.selectedObjects",
    >        @"fooArrayController.selectionIndex",
    >        @"fooArrayController.selection",
    >        @"fooArrayController.selectionIndexes",
    >        @"fooArrayController.selectedObjects",
    >        nil] ;
    > }

    Oh, and also the documentation is quite clear that you can't bind
    through a to-many property. So even if NSArrayController did support
    prior-value notification, this wouldn't work. From the Key-Value
    Technology Compliance section of the Model Object Implementation
    Guide:

    "Important: Note that you cannot set up dependencies on to-many
    relationships. For example, suppose you have an Order object with a
    to-many relationship (orderItems) to a collection of OrderItem
    objects, and OrderItem objects have a price attribute. You might want
    the Order object have a totalPrice attribute that is dependent upon
    the prices of all the OrderItem objects in the relationship. You can
    not do this by implementing keyPathsForValuesAffectingValueForKey: and
    returning orderItems.price as the keypath for totalPrice. You must
    observe the price attribute of each of the OrderItem objects in the
    orderItems collection and respond to changes in their values by
    updating totalPrice yourself."

    --Kyle Sluder
  • On Apr 2, 2010, at 21:42, Kyle Sluder wrote:

    >> + (NSSet*)keyPathsForValuesAffectingSelectedFoo {
    >> return [NSSet setWithObjects:
    >> // Shotgun approach.  Try 'em all!!
    >> @"fooArrayController.selectedObjects",
    >> @"fooArrayController.selectionIndex",
    >> @"fooArrayController.selection",
    >> @"fooArrayController.selectionIndexes",
    >> @"fooArrayController.selectedObjects",
    >> nil] ;
    >> }
    >
    > Oh, and also the documentation is quite clear that you can't bind
    > through a to-many property.

    Wait, where is the to-many property being bound through?
  • On Fri, Apr 2, 2010 at 10:05 PM, Quincey Morris
    <quinceymorris...> wrote:
    > On Apr 2, 2010, at 21:42, Kyle Sluder wrote:
    >> Oh, and also the documentation is quite clear that you can't bind
    >> through a to-many property.
    >
    > Wait, where is the to-many property being bound through?

    Ugh, that's not what I meant to say. I meant that you can't use the
    automatic notifications mechanism with to-many properties. Sorry!

    --Kyle Sluder
  • On Fri, Apr 2, 2010 at 10:05 PM, Quincey Morris
    <quinceymorris...> wrote:
    > Wait, where is the to-many property being bound through?

    Oh, I see what you're saying. It's not being observed *through*, it's
    being observed *to*. Which would be okay, I guess, if
    NSArrayController weren't a bad KVO citizen.

    Way too much coffee, way to little sleep, way too excited about tomorrow. :)

    --Kyle Sluder
  • On 2010 Apr 02, Quincey Morris wrote:

    > I only see 4 distinct keys in your list below, since one of them is duplicated.

    Yes, you're correct.  There are only 4.

    > I'm suspicious of the "fooArrayController" part. Is it *just* an instance variable?

    It's an IBOutlet, and also there is a getter which returns the IBOutlet.

    > If so, then it gets to be a property only by virtue 'accessInstanceVariablesDirectly' being YES (which is bad magic, but a different story). If so, then it's not KVO compliant, though the only scenario I can think of where this would cause keyPathsForValuesAffecting... to fail is if something was already observing "selectedFoo" before the nib file containing it was loaded.

    There is something else observing "selectedFoo", and it is in the nib, another "detail" array controller, to which is bound a table which displays the 'bars', shall we say, of the 'selectedFoo'.

    So you're telling me that all of the keys in a key path must be KVO-compliant.  I hadn't thought so, because in the key path "fooArrayController.selection", the fooArrayController is hard-wired to an IBOutlet and never changes.  But now I see a possible explanation is that the Cocoa Bindings Wizard created its observer before the fooArrayController got wired to its outlet.

    On 2010 Apr 02, Kyle Sluder wrote:

    > Because +keyPathsForValuesAffectingValueForKey relies on NSKeyValueObservingOptionPrior, which NSArrayController doesn't support.

    That would explain the problem also.

    So the answer is either Quincey's explanation, or this bug, or both.

    > File a bug. It'll get dup'ed, but do it anyway.

    If I don't hear more from you on this, Kyle, I'll test and file a bug tomorrow.

    Other Details...

    On 2010 Apr 02, Quincey Morris wrote:

    > TBH, if I had a reason to want to observe the selection (keyPathsForValuesAffecting... being a special case of that), I'd create a real NSIndexSet* selectedItemIndexes property of my own, bind the array controller's "selectionIndexes" binding to that (so that the array controller maintains the real property for me), and then observe the real property, leaving all reference to the array controller itself out of my code.

    Yes, that seems like it would work.

    >> I solved the problem by eliminating all this code and binding my button's 'enabled' binding instead to the array controller's 'selection' and, to my surprise, it worked.  My surprise is because, according to superclass NSObjectController documentation, 'selection' returns NSNoSelectionMarker when there is no selection, not nil which is what my NSIsNotNil value transformer would expect.
    >
    > I think the answer is that it works because it's a frameworks binding, and frameworks bindings are very clever in ways we aren't actually told about. It probably knows whether it's allowed to pass on the marker value, or if it must convert the marker to some kind of "real" object first -- hence nil. Or perhaps passing NSNoSelectionMarker to the value transformer is simply special-cased somewhere.

    That's what I was afraid of.
  • On Apr 2, 2010, at 22:17, Jerry Krinock wrote:

    > So you're telling me that all of the keys in a key path must be KVO-compliant.  I hadn't thought so, because in the key path "fooArrayController.selection", the fooArrayController is hard-wired to an IBOutlet and never changes.  But now I see a possible explanation is that the Cocoa Bindings Wizard created its observer before the fooArrayController got wired to its outlet.

    No, KVO compliance isn't required for a bound-to key that never changes.

    The issue I had in mind is that that the "fooArrayController" key *does* change -- when the nib is loaded to changes from nil to its correct value. Thus, anything that tries to observe it before nib loading is going to be observing a nil object and will never know any different. Things that observe it after the outlet is set during nib loading will be fine.

    However, I think this is a false trail. Kyle had the correct answer.
  • On Fri, Apr 2, 2010 at 8:25 PM, Jerry Krinock <jerry...> wrote:
    > I solved the problem by eliminating all this code and binding my button's 'enabled' binding instead to the array controller's 'selection' and, to my surprise, it worked.  My surprise is because, according to superclass NSObjectController documentation, 'selection' returns NSNoSelectionMarker when there is no selection, not nil which is what my NSIsNotNil value transformer would expect.

    While this works (I imagine that -[NSButtonCell setObjectValue:]
    treats NSNoSelectionMarker specially) I would think that the "right"
    thing to do would be to subclass NSArrayController and write a
    -canPerformFoo method. But you would need to self-observe the
    selection property, which is perilous because
    -removeObserver:forKeyPath: doesn't take a context argument (another
    bug every Cocoa developer should file a duplicate of). So you could
    use MAKVONotificationCenter (or OFBinding, our analogue) to
    self-observe. Perhaps it would be easier to instead put the
    -canPerformFoo method on your window controller.

    --Kyle Sluder
  • OK, I filed Bug ID# 7827354.  (See bottom of this message.)  But I don't see the need for MAKVONotificationCenter here...

    On 2010 Apr 02, at 22:33, Kyle Sluder wrote:

    > I would think that the "right" thing to do would be to subclass NSArrayController and write a
    > -canPerformFoo method.

    Yes, so I went back and did it the "right" way.  To be general, I called it simply -(BOOL)hasSelection

    > But you would need to self-observe the selection property,

    Indeed, because of the bug in NSArrayController, this doesn't work:

    + (NSSet*)keyPathsForValuesAffectingHasSelection {
        return [NSSet setWithObject@"selection"] ;
    }

    > which is perilous because -removeObserver:forKeyPath: doesn't take a context argument (another
    > bug every Cocoa developer should file a duplicate of).  So you could use MAKVONotificationCenter (or OFBinding, our analogue) to self-observe.

    I already use MAKVONotificationCenter in this project, but I don't see the need for it here.  In the array controller's -dealloc,

        [self removeObserver:self
                  forKeyPath:@"selectedObjects"] ;

    Why is that perilous?  (It seems to work.)

    > Perhaps it would be easier to instead put the -canPerformFoo method on your window controller.

    I already have my own NSArrayController subclass for other reasons.

    **************************************

    Apple Bug ID# 7827354
    Title: NSArrayController Observeable Methods Don't Do +keyPathsForValuesAffectingKey

    Summary:  There are four methods which give the selection in some form, and they are all documented to be "observable using key-value observing".  However, none of them trigger change notifications when their name is returned in +keyPathsForValuesAffectingKey.

    Steps to Reproduce:

    1.  Require an instance variable which is a function of the current selection in an NSArrayController.  Here is a simple example:

    // In @interface,

    /*!
    @brief    Returns whether or not any content item in the receiver
    is selected.
    @details  This property should be observeable using KVO.
    */
    - (BOOL)hasSelection ;

    // In @implementation,

    + (NSSet*)keyPathsForValuesAffectingHasSelection {
        return [NSSet setWithObjects:
            // Any one of the following should be sufficient.
            @"selection",
            @"selectedObjects",
            @"selectionIndex",
            @"selectionIndexes",
            nil] ;
    }

    - (BOOL)hasSelection {
        return ([[self selectedObjects] count] > 0) ;
    }

    2.  Bind some binding in a user-interface element, for example the 'enabled' binding of some button which requires a selection in the array controller, to the 'hasSelection' property.

    3.  Build and run the project.

    4.  Change the number of items selected in the array controller from 0 to some nonzero number, and vice versa.

    Expected Results:

    When the number of selected objects changes, the object observing 'hasSelection' should invoke -hasSelection.

    Actual Results:

    -hasSelection does not get invoked.

    Notes:

    I am advised by Kyle Sluder that the bug underlying this bug is the bug that +keyPathsForValuesAffectingValueForKey relies on NSKeyValueObservingOptionPrior, and that NSArrayController doesn't support NSKeyValueObservingOptionPrior.  See discussion here:

    http://lists.apple.com/archives/Cocoa-dev/2010/Apr/msg00088.html

    You must read the entire thread because I originally discovered this problem indirectly, and we also hypothesized other causes which turned out to be wrong.
  • On Mon, Apr 5, 2010 at 3:08 AM, Jerry Krinock <jerry...> wrote:
    > I already use MAKVONotificationCenter in this project, but I don't see the need for it here.  In the array controller's -dealloc,
    >
    >    [self removeObserver:self
    >              forKeyPath:@"selectedObjects"] ;
    >
    > Why is that perilous?  (It seems to work.)

    Because if NSArrayController also self-observes on @"selectedObjects",
    you've removed that observation from under its feet.

    MAKVONotificationCenter and OFBinding solve this problem by creating
    separate objects, so they'll only ever register for a keypath once.

    --Kyle Sluder
previous month april 2010 next month
MTWTFSS
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30    
Go to today