NSArrayController Frustration

  • I have an NSMutableArray called docArray which is bound with Interface
    Builder to an NSArrayController (Content Array (File's
    Owner.docArray)) to which is bound an NSTableView.    The File's owner
    is a subclass of NSWindowController.  I am expecting that when I do
    [docArray addObject:x] that the array controller will observe the
    change and do a reloadData on the NSTableView.  That isn't happening.

    I can't just use addObject: from the array controller, because the
    NSWindowController and its window, etc, will not always be open, and
    docArray doesn't reside within the NSWindowController.

    What do I need to do here?
  • I'll pose a possible answer to my own question.

    I probably need to alloc and init both docArray and docArrayController
    at the same time and do all addObject and removeObject operations for
    docArray through docArrayController.  Then when I open the
    corresponding NSWindowController, I would pass in the
    docArrayController and bind the NSTableView to it manually, unbinding
    it from the TableView when I close the window.  That would allow the
    docArrayController to tell the TableView to update whenever there was
    a change to docArray.

    I would still need to be sure the TableView was updated every time
    isDocumentEdited changed for any document in docArray, perhaps using a
    NSValueTransformer to change the BOOL of isDocumentEdited into an "X"
    for a column of the TableView.  This is my first venture into Cocoa
    Bindings, so I'm not sure if a change in isDocumentEdited within a
    changing docArray can be observed.

    There may also be a problem if I have one thread doing an addObject to
    docArray while another thread is binding or unbinding the TableView,
    but I expect i could solve that by putting addObject and bind (or
    attach window) under the same NSLock.

    On Nov 24, 2007, at 6:58 PM, David Carlisle wrote:

    > I have an NSMutableArray called docArray which is bound with
    > Interface Builder to an NSArrayController (Content Array (File's
    > Owner.docArray)) to which is bound an NSTableView.    The File's
    > owner is a subclass of NSWindowController.  I am expecting that when
    > I do [docArray addObject:x] that the array controller will observe
    > the change and do a reloadData on the NSTableView.  That isn't
    > happening.
    >
    > I can't just use addObject: from the array controller, because the
    > NSWindowController and its window, etc, will not always be open, and
    > docArray doesn't reside within the NSWindowController.
    >
    > What do I need to do here?
  • > I am expecting that when I do [docArray addObject:x] that the array
    > controller will observe the change and do a reloadData on the
    > NSTableView.  That isn't happening.

      Read the Key Value Coding and Key Value Observing documentation.
    Your problem is a common one. If you're modifying the bound-to array,
    you need to enclose the modification with will/
    didChangeValueForKey: ... otherwise you're "changing the array behind
    the controller's back" because you're not sending proper KVO
    notifications to let it know a change occurred.

    --
    I.S.
  • On Nov 24, 2007, at 5:58 PM, David Carlisle wrote:

    > I am expecting that when I do [docArray addObject:x] that the array
    > controller will observe the change and do a reloadData on the
    > NSTableView.  That isn't happening.

    Your expectation is incorrect.  What your NSArrayController is bound
    to is not the docArray *object* but the docArray *property* (or key)
    of your NSWindowController subclass.

    There are a few different ways you can resolve this.  You've proposed
    one yourself -- to do all manipulations via the NSArrayController --
    which I do not recommend because it violates encapsulation and leads
    people to mix model and controller level code unnecessarily.

    I. Savant suggested that you surround changes to docArray with
    {will,did}ChangeValueForKey: pairs to ensure KVO change
    notifications.  I do not recommend this because it *also* violates
    encapsulation -- you're mixing in "maintenance" code with the code
    that uses the property.  This is fragile in that it will be easy to
    forget a spot, post the wrong notification due to a typo, etc.

    So, since I've shot down the first two ideas posted, what *do* I
    recommend?  One of the following:

    (1)  Make your manipulations of the docArray property specific, by
    implementing the appropriate ordered-relationship-KVC accessor methods:

      - (NSUInteger)countOfDocArray
      - (id)objectInDocArrayAtIndex:(NSUInteger)index;
      - (void)insertObject:(id)object inDocArrayAtIndex:(NSUInteger)index;
      - (void)removeObjectFromDocArrayAtIndex:(NSUInteger)index;

    And so on.  The above four are the minimum methods to implement,
    beyond -docArray and -setDocArray: -- the full naming pattern is
    described in the Key-Value Coding Programming Guide, and it's also
    documented in <Foundation/NSKeyValueCoding.h> under -valueForKey: and -
    mutableArrayValueForKey:.

    Then, instead of manipulating docArray directly, manipulate it only
    using the above methods (which are allowed to manipulate the array
    directly).  Key-Value Observing -- used by bindings -- will
    automatically do the right thing and cause the above methods to post
    appropriate observer notifications for every manipulation of the
    docArray property that occurs via them.

    (2)  Whether or not you do #1 above, you can just use the
    NSMutableArray that you get back from [self
    mutableArrayValueForKey:@"docArray"] to manipulate the array.  It will
    not actually return your own array; instead, it will return a proxy
    object that also causes the appropriate KVO notifications to be posted
    for every manipulation of your own array.  If you *do* do #1 above, it
    will even cause the methods you have written to be invoked.

    I'm sure this seems complicated -- "I just want to observe an array!"
    you're no doubt saying -- but if you remember that observation and
    (therefore) binding happens at the property rather than the object
    level, it will make a lot more sense.  As long as you have the
    appropriate methods implemented for the type of property you want to
    manipulate, and you manipulate the property through those methods, its
    implementation will be irrelevant.

      -- Chris
  • On Nov 25, 2007, at 8:50 PM, Chris Hanson wrote:

    > On Nov 24, 2007, at 5:58 PM, David Carlisle wrote:
    >
    >> I am expecting that when I do [docArray addObject:x] that the
    >> array controller will observe the change and do a reloadData on
    >> the NSTableView.  That isn't happening.
    >
    > Your expectation is incorrect.  What your NSArrayController is
    > bound to is not the docArray *object* but the docArray *property*
    > (or key) of your NSWindowController subclass.
    >
    > There are a few different ways you can resolve this.  You've
    > proposed one yourself -- to do all manipulations via the
    > NSArrayController -- which I do not recommend because it violates
    > encapsulation and leads people to mix model and controller level
    > code unnecessarily.
    >
    > I. Savant suggested that you surround changes to docArray with
    > {will,did}ChangeValueForKey: pairs to ensure KVO change
    > notifications.  I do not recommend this because it *also* violates
    > encapsulation -- you're mixing in "maintenance" code with the code
    > that uses the property.  This is fragile in that it will be easy to
    > forget a spot, post the wrong notification due to a typo, etc.
    >
    > So, since I've shot down the first two ideas posted, what *do* I
    > recommend?  One of the following:
    >
    > (1)  Make your manipulations of the docArray property specific, by
    > implementing the appropriate ordered-relationship-KVC accessor
    > methods:
    >
    > - (NSUInteger)countOfDocArray
    > - (id)objectInDocArrayAtIndex:(NSUInteger)index;
    > - (void)insertObject:(id)object inDocArrayAtIndex:(NSUInteger)index;
    > - (void)removeObjectFromDocArrayAtIndex:(NSUInteger)index;
    >
    > And so on.  The above four are the minimum methods to implement,
    > beyond -docArray and -setDocArray: -- the full naming pattern is
    > described in the Key-Value Coding Programming Guide, and it's also
    > documented in <Foundation/NSKeyValueCoding.h> under -valueForKey:
    > and -mutableArrayValueForKey:.
    >
    > Then, instead of manipulating docArray directly, manipulate it only
    > using the above methods (which are allowed to manipulate the array
    > directly).  Key-Value Observing -- used by bindings -- will
    > automatically do the right thing and cause the above methods to
    > post appropriate observer notifications for every manipulation of
    > the docArray property that occurs via them.
    >
    > (2)  Whether or not you do #1 above, you can just use the
    > NSMutableArray that you get back from [self
    > mutableArrayValueForKey:@"docArray"] to manipulate the array.  It
    > will not actually return your own array; instead, it will return a
    > proxy object that also causes the appropriate KVO notifications to
    > be posted for every manipulation of your own array.  If you *do* do
    > #1 above, it will even cause the methods you have written to be
    > invoked.
    >
    > I'm sure this seems complicated -- "I just want to observe an
    > array!" you're no doubt saying -- but if you remember that
    > observation and (therefore) binding happens at the property rather
    > than the object level, it will make a lot more sense.  As long as
    > you have the appropriate methods implemented for the type of
    > property you want to manipulate, and you manipulate the property
    > through those methods, its implementation will be irrelevant.
    >
    > -- Chris

    I have an example of my running into a similar problem and solving it
    in a similar way. See items 29 through 32 here:

    http://www.bagelturf.com/cocoa/rwok/rwok4/index.html

    In my case I had an array of dictionaries whose elements were bound
    to table columns. By implementing the methods that Chris describes in
    (1) above, the magic (really just advanced technology) occurs and
    changes are automatically seen and reflected in the interface.

    The trick to understanding KVO is to realize that there is no such
    thing as "observing" going on. "Observing" describes the effect
    experienced, not the way the effect is implemented. As Chris points
    out, it's the properties (via keys, as in *Key* Value Observing) that
    are observed, not the objects themselves -- there is no Object
    Observing.

    KVO is an active process: changes to properties are actively
    propagated to observers via messages. In the case of an array,
    changing the members does nothing for an observer of the array
    because the members are not actively involved in the observation.
    Method (1) above provides for manipulation of the array by
    implementing observable properties and hence the messages get to the
    observers. Method (2) just hides all of that from you via a proxy
    object.
  • On Nov 25, 2007, at 10:30 PM, Steve Weller wrote:

    >>>
    >>> I am expecting that when I do [docArray addObject:x] that the
    >>> array controller will observe the change and do a reloadData on
    >>> the NSTableView.  That isn't happening.
    >>
    >> Your expectation is incorrect.  What your NSArrayController is
    >> bound to is not the docArray *object* but the docArray *property*
    >> (or key) of your NSWindowController subclass.
    >

    Thanks to everybody for the suggestions.  It helps to know what to
    look for in the documentation.

    My docArray actually resides in another object called globalObject, so
    I bound the NSArrayController to the NSWindowController at
    keyPath:@"globalObject.docArray".  The NSWindowController has an
    accessor method for globalObject.  Then using the suggested method #2,
    I replaced operations on docArray in globalObject with operations on
    [self mutableArrayValueForKey:@"docArray"].  That seems to have
    solved the problem.

    But it might not work correctly in a multithreaded environment.  The
    NSWindowController may come and go asynchronously while other threads
    may be adding or removing objects from docArray, so I am binding and
    unbinding the NSArrayController contentArray manually in globalObject
    within the same lock that restricts adding and removing objects from
    docArray.  This is intended to control the way that NSArrayController
    adds and removes observers from each document in docArray so that
    everything remains consistent.

    But when I NSLog the way NSArrayController creates and removes
    observers for each document, it is not occurring as neatly as I had
    expected.  Sometimes the log shows that NSArrayController adds
    observers to documents in docArray after the bind method has returned
    and the lock has been unlocked.  It also creates and removes observers
    much more often than I had expected.

    It will probably become apparent later if this is a cause for concern.
previous month november 2007 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