在 Cocoa 中实现符合 KVO/绑定的桥接模式

发布于 2024-07-07 11:16:17 字数 5773 浏览 10 评论 0原文

我正在尝试在 cocoa 中实现一个简单的对象桥,其中桥对象充当任意其他 NSObject 实例的 kvo/绑定兼容的下降。

这是我的问题(更多详细信息请参见下面的代码):

桥对象充当 Person-Object 的替代者,具有名为 name 的 NSString* 属性和一个名为 address 的 Address* 属性。 绑定到桥的 keyPath“名称”或“地址”效果很好。 当将某个对​​象绑定到桥的 keyPath“address.street”并且为 Person 的 address 属性设置新的地址对象时,就会出现问题。 这会导致与 KVO 相关的异常,如下所示:

无法删除观察者对于来自

的关键路径“street” 因为它没有注册为观察者

即使桥注意到“地址”属性的变化并发出 willChangeValueForKeyPath/didChangeValueForKeyPath 元组,也会发生这种情况。

下面的代码产生了问题。 它是独立的 Objective-C 代码,可以保存在文件“BridgeDemo.m”中并编译运行。

gcc -o test BridgeDemo.m -framework AppKit -framework Foundation; ./test

如果您知道此问题的解决方案,或者可以为我提供更好的方法来解决相同的问题,您可以让我成为非常快乐的程序员!

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;
}

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;
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

烟─花易冷 2024-07-14 11:16:17

问题是这样的:

[self willChangeValueForKey:keyPath]; <--- 此时实际观察者需要取消订阅 street
[self didChangeValueForKey:keyPath]; <--- 并将其自身添加到新值中。

如果不提供新值,您就剥夺了观察者取消订阅的机会。

这是一个可以运行并演示问题的破解版本。

/* --- 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];
}
....
......

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];
}
....
......
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文