Implementing a KVO/Bindings-Compliant Bridge-Pattern in Cocoa
cocoa, cocoa-bindings, key-value-coding, key-value-observing
Solution
Here is the problem:
[self willChangeValueForKey:keyPath]; <--- At this point in time the actual observer needs to unsubscribe to street [self didChangeValueForKey:keyPath]; <--- and add itself to the new value.
By not providing the new value you are denying the observer the opportunity to unsubscribe.
Here is a hacked version that works and demonstrates the problem.
/* --- Bridge ----------------------------------------- */
....
.....
@interface Bridge : NSObject {
NSMutableDictionary* observedKeys;
NSObject* obj;
//**** Dictionary for old values just before we send the didChangeValue notification.
NSMutableDictionary * oldValues;
}
...
.....
- (id)init {
if( !( self = [super init] ) ) { return nil; }
observedKeys = [NSMutableDictionary new];
//************* Initialize the new dictionary
oldValues = [NSMutableDictionary new];
return self;
}
....
.....
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog( @">>>> Detected Change in keyPath: %@", keyPath );
// **** Cache the old value before telling everyone its going to change.
[oldValues setValue:[change valueForKey:NSKeyValueChangeOldKey] forKey:keyPath];
[self willChangeValueForKey:keyPath];
// **** Simulate the change by removing the old value.
[oldValues removeObjectForKey:keyPath];
// **** Now when we say we did change the value, we are not lying.
[self didChangeValueForKey:keyPath];
}
-(id)valueForUndefinedKey:(NSString*)key {
// **** Important part, return oldvalue if it exists
id oldValue;
if(oldValue = [oldValues valueForKey:key]){
return oldValue;
}
/* Register an observer for the key, if not already done */
if( ![observedKeys objectForKey:key] ) {
[observedKeys setObject:[NSNumber numberWithBool:YES] forKey:key];
NSLog(@"adding observer for:%@", key);
[obj addObserver:self forKeyPath:key options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}
return [obj valueForKey:key];
}
....
......
Problem
I'm trying to implement a simple object bridge in cocoa where the bridge object acts as a kvo/bindings-compliant drop in for some arbitrary other NSObject instance. Here is my problem (more details in the code below): A bridge object acts as a drop in for a Person-Object, with an NSString* property called name and an Address* property address. Binding to the keyPath "name" or "address" of the Bridge works nicely. Trouble starts when binding some object to the keyPath "address.street" of the bridge and a new Address-Object is set for Person's address property. That results in KVO-related exceptions that look like this: `Cannot remove an observer <NSKeyValueObservance 0x126b00> for the key path "street" from <Address 0x12f1d0> because it is not registered as an observer` This happens even though the bridge notices the change in the "address"-Property and emits a willChangeValueForKeyPath/didChangeValueForKeyPath tuple. The code below produces the the problem. It's self-contained objective-c code that can be saved in a file "BridgeDemo.m" and compiled run with ``` gcc -o test BridgeDemo.m -framework AppKit -framework Foundation; ./test ``` If you know a solution to this problem or can offer me a better approach solving the same problem you make me a very happy programmer! BridgeDemo.m: ``` #import <Foundation/Foundation.h> #import <AppKit/AppKit.h> /* --- Address ----------------------------------------- */ @interface Address : NSObject { NSString* street; NSNumber* zipCode; NSString* city; } @property(retain) NSString* street; @property(retain) NSNumber* zipCode; @property(retain) NSString* city; @end @implementation Address @synthesize street, zipCode, city; -(id)init { if( !( self = [super init] ) ) { return nil; } self.street = @"Elm Street"; self.zipCode = @"12345"; self.city = @"Crashington"; return self; } -(void) modifyStreet { self.street = @"Main Street"; } -(void)dealloc { [street release]; [zipCode release]; [city release]; [super dealloc]; } @end /* --- Person ----------------------------------------- */ @interface Person : NSObject { NSString* name; Address* address; } @property(retain) NSString* name; @property(retain) Address* address; @end @implementation Person @synthesize address, name; -(id)init { if( !( self = [super init] ) ) { return nil; } self.name = @"Tom"; self.address = [[Address new] autorelease]; return self; } - (void)modifyAddress { Address* a = [[Address new] autorelease]; a.street = @"Jump Street"; a.zipCode = @"54321"; a.city = @"Memleakville"; self.address = a; } - (void)dealloc { [address release]; [name release]; [super dealloc]; } @end /* --- Bridge ----------------------------------------- */ @interface Bridge : NSObject { NSMutableDictionary* observedKeys; NSObject* obj; } @property(retain) NSObject* obj; @end @implementation Bridge @synthesize obj; - (id)init { if( !( self = [super init] ) ) { return nil; } observedKeys = [NSMutableDictionary new]; return self; } - (void)forwardInvocation:(NSInvocation*)inv { [inv invokeWithTarget:obj]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { return [obj methodSignatureForSelector:aSelector]; } - (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSLog( @">>>> Detected Change in keyPath: %@", keyPath ); [self willChangeValueForKey:keyPath]; [self didChangeValueForKey:keyPath]; } -(id)valueForUndefinedKey:(NSString*)key { /* Register an observer for the key, if not already done */ if( ![observedKeys objectForKey:key] ) { [observedKeys setObject:[NSNumber numberWithBool:YES] forKey:key]; [obj addObserver:self forKeyPath:key options:NSKeyValueObservingOptionNew context:nil]; } return [obj valueForKey:key]; } - (void)dealloc { for( NSString* key in [observedKeys allKeys] ) { [obj removeObserver:self forKeyPath:key]; } [obj release]; [observedKeys release]; [super dealloc]; } @end /* --- MyObserver ------------------------------------ */ @interface MyObserver : NSObject { Address* address; NSString* street; } @property(retain) Address* address; @property(retain) NSString* street; @end @implementation MyObserver @synthesize street, address; -(void)dealloc { [street release]; [super dealloc]; } @end /* This works fine */ void testBindingToAddress() { NSLog( @"Testing Binding to 'address' --------------" ); NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; Bridge* b = [[Bridge new] autorelease]; b.obj = [Person new]; MyObserver* o = [[MyObserver new] autorelease]; [o bind:@"address" toObject:b withKeyPath:@"address" options:nil]; NSLog( @"Before modifyStreet: %@", o.address.street ); [[b valueForKey:@"address"] performSelector:@selector(modifyStreet)]; NSLog( @"After modifyStreet: %@", o.address.street ); [b performSelector:@selector(modifyAddress)]; NSLog( @"After modifyAdress: %@", o.address.street ); [pool drain]; } /* This produces an exception */ void testBindingToStreet() { NSLog( @"Testing Binding to 'address.street' --------------" ); NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; Bridge* b = [[Bridge new] autorelease]; b.obj = [Person new]; MyObserver* o = [[MyObserver new] autorelease]; [o bind:@"street" toObject:b withKeyPath:@"address.street" options:nil]; NSLog( @"Before modifyStreet: %@", o.street ); [[b valueForKey:@"address"] performSelector:@selector(modifyStreet)]; NSLog( @"After modifyStreet: %@", o.street ); [b performSelector:@selector(modifyAddress)]; NSLog( @"After modifyAdress: %@", o.street ); [pool drain]; } /* --- main() ------------------------------------ */ int main (int argc, const char * argv[]) { testBindingToAddress(); testBindingToStreet(); return 0; } ```