KVC/KVO and bindings: why am I only receiving one change notification?

First, you should bind to the contentArray (not content).

Up vote 1 down vote favorite 1 share g+ share fb share tw.

I'm seeing some quirky behaviour with Cocoa's KVC/KVO and bindings. I have an NSArrayController object, with its 'content' bound to an NSMutableArray, and I have a controller registered as an observer of the arrangedObjects property on the NSArrayController. With this setup, I expect to receive a KVO notification every time the array is modified.

However, it appears that the KVO notification is only sent once; the very first time the array is modified. I set up a brand new "Cocoa Application" project in Xcode to illustrate the problem. Here is my code: BindingTesterAppDelegate.

H #import @interface BindingTesterAppDelegate : NSObject { NSWindow * window; NSArrayController * arrayController; NSMutableArray * mutableArray; } @property (assign) IBOutlet NSWindow * window; @property (retain) NSArrayController * arrayController; @property (retain) NSMutableArray * mutableArray; - (void)changeArray:(id)sender; @end BindingTesterAppDelegate. M #import "BindingTesterAppDelegate. H" @implementation BindingTesterAppDelegate @synthesize window; @synthesize arrayController; @synthesize mutableArray; - (void)applicationDidFinishLaunching:(NSNotification *)notification { NSLog(@"load"); // create the array controller and the mutable array: self setArrayController:NSArrayController alloc init autorelease; self setMutableArray:NSMutableArray arrayWithCapacity:0; // bind the arrayController to the array arrayController bind:@"content" // see update toObject:self withKeyPath:@"mutableArray" options:0; // set up an observer for arrangedObjects arrayController addObserver:self forKeyPath:@"arrangedObjects" options:0 context:nil; // add a button to trigger events NSButton * button = NSButton alloc initWithFrame:NSMakeRect(10, 10, 100, 30); window contentView addSubview:button; button setTitle:@"change array"; button setTarget:self; button setAction:@selector(changeArray:); button release; NSLog(@"run"); } - (void)changeArray:(id)sender { // modify the array (being sure to post KVO notifications): self willChangeValueForKey:@"mutableArray"; mutableArray addObject:NSString stringWithString:@"something"; NSLog(@"changed the array: count = %d", mutableArray count); self didChangeValueForKey:@"mutableArray"; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSLog(@"%@ changed!

", keyPath); } - (void)applicationWillTerminate:(NSNotification *)notification { NSLog(@"stop"); self setMutableArray:nil; self setArrayController:nil; NSLog(@"done"); } @end And here is the output: load run changed the array: count = 1 arrangedObjects changed! Changed the array: count = 2 changed the array: count = 3 changed the array: count = 4 changed the array: count = 5 stop arrangedObjects changed! Done As you can see, the KVO notification is only sent the first time (and once more when the application exits).

Why would this be the case? Update: Thanks to orque for pointing out that I should be binding to the contentArray of my NSArrayController, not just its content. The above posted code works, as soon as this change is made: // bind the arrayController to the array arrayController bind:@"contentArray" // James29.8k863108 87% accept rate.

First, you should bind to the contentArray (not content): arrayController bind:@"contentArray" toObject:self withKeyPath:@"mutableArray" options:0; Then, the straightforward way is to just use the arrayController to modify the array: - (void)changeArray:(id)sender { // modify the array (being sure to post KVO notifications): arrayController addObject:@"something"; NSLog(@"changed the array: count = %d", mutableArray count); } (in a real scenario you'll likely just want the button action to call -addObject:) Using -NSMutableArray addObject will not automatically notify the controller. I see that you tried to work around this by manually using willChange/didChange on the mutableArray. This won't work because the array itself hasn't changed.

That is, if the KVO system queries mutableArray before and after the change it will still have the same address. If you want to use -NSMutableArray addObject, you could willChange/didChange on arrangedObjects: - (void)changeArray:(id)sender { // modify the array (being sure to post KVO notifications): arrayController willChangeValueForKey:@"arrangedObjects"; mutableArray addObject:@"something"; NSLog(@"changed the array: count = %d", mutableArray count); arrayController didChangeValueForKey:@"arrangedObjects"; } There may be a cheaper key that would give the same effect. If you have a choice I would recommend just working through the controller and leaving the notifications up to the underlying system.

1 and thank you for a very detailed answer. I changed my binding to "contentArray" instead of "content", and everything worked like a charm. As for changing the array through the controller: This was a simplified example.

In my real application, the array is a property on a model object, and it gets modified through another process. If I were to use the arrayController to modify my object, my Model classes would need to be coupled to my Controller classes, which goes completely against the MVC pattern. – e.

James Aug 21 '09 at 21:14 The button action would be add:, not addObject: (which would add the button! ). – Peter Hosey Aug 21 '09 at 23:11.

A much better way than explicitly posting whole-value KVO notifications is to implement array accessors and use them. Then KVO posts the notifications for free. That way, instead of this: self willChangeValueForKey:@"mutableArray"; mutableArray addObject:NSString stringWithString:@"something"; self didChangeValueForKey:@"mutableArray"; You would do this: self insertObject:NSString stringWithString:@"something" inMutableArrayAtIndex:self countOfMutableArray; Not only will KVO post the change notification for you, but it will be a more specific notification, being an array-insertion change rather than a whole-array change.

I usually add an addMutableArrayObject: method that does the above, so that I can do: self addMutableArrayObject:NSString stringWithString:@"something"; Note that addObject: is not currently a KVC-recognized selector format for array properties (only set properties), whereas insertObject:inAtIndex: is, so your implementation of the former (if you choose to do that) must use the latter.

Thank you! It seems I'll have to do some reading into array accessors. That definitely sounds like the missing piece in my understanding of bindings.

Just to clarify: I could have several NSMutableArray properties, and each one would need its own `insertObject:inAtIndex: method? – e. James Aug 21 '09 at 23:40 Right.

I have a pair of scripts which I run as services (using ThisService), which take an ivar declaration and generate most of the accessors that would be useful for it. Nowadays, only useful for array and set properties. Boredzo.org/make-objc-accessors – Peter Hosey Aug 22 '09 at 0:48 So you would copy the ivar declaration (e.g. , “NSMutableArray *mutableArray;”), paste it into the header, select what you just pasted, run the Make Obj-C Accessor Declarations service, paste the same ivar declaration into the implementation, select what you just pasted, and run the Make Obj-C Accessor Definitions service.

– Peter Hosey Aug 22 '09 at 0:51 Ah, that's the cheaper (non-controller) way I couldn't remember :). – orque Aug 22 '09 at 4:01.

I cant really gove you an answer,but what I can give you is a way to a solution, that is you have to find the anglde that you relate to or peaks your interest. A good paper is one that people get drawn into because it reaches them ln some way.As for me WW11 to me, I think of the holocaust and the effect it had on the survivors, their families and those who stood by and did nothing until it was too late.

Related Questions