mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-10-01 22:56:17 +00:00
Key Value Observing, with test cases.
This commit is contained in:
@@ -36,17 +36,17 @@ import "CPObject.j"
|
||||
|
||||
}
|
||||
|
||||
- addObserver:(id)anObserver forKeyPath:(CPString)aPath options:(unsigned)options context:(id)aContext
|
||||
- (void)addObserver:(id)anObserver forKeyPath:(CPString)aPath options:(unsigned)options context:(id)aContext
|
||||
{
|
||||
if (!anObserver || !aPath)
|
||||
if (!anObserver || !aPath)
|
||||
return;
|
||||
|
||||
[[_CPKVOProxy proxyForObject:self] _addObserver:anObserver forKeyPath:aPath options:options context:aContext];
|
||||
}
|
||||
|
||||
- removeObserver:(id)anObserver forKeyPath:(CPString)aPath
|
||||
- (void)removeObserver:(id)anObserver forKeyPath:(CPString)aPath
|
||||
{
|
||||
if (!anObserver || !aPath)
|
||||
if (!anObserver || !aPath)
|
||||
return;
|
||||
|
||||
[[KVOProxyMap objectForKey:[self hash]] _removeObserver:anObserver forKeyPath:aPath];
|
||||
@@ -65,6 +65,9 @@ CPKeyValueObservingOptionOld = 1 << 1;
|
||||
CPKeyValueObservingOptionInitial = 1 << 2;
|
||||
CPKeyValueObservingOptionPrior = 1 << 3;
|
||||
|
||||
//convenience
|
||||
var kvoNewAndOld = CPKeyValueObservingOptionNew|CPKeyValueObservingOptionOld;
|
||||
|
||||
// KVO Change Dictionary Keys
|
||||
CPKeyValueChangeKindKey = @"CPKeyValueChangeKindKey";
|
||||
CPKeyValueChangeNewKey = @"CPKeyValueChangeNewKey";
|
||||
@@ -95,9 +98,9 @@ var KVOProxyMap = [CPDictionary dictionary];
|
||||
return proxy;
|
||||
|
||||
proxy = [[self alloc] initWithTarget:anObject];
|
||||
|
||||
|
||||
[proxy _replaceSetters];
|
||||
|
||||
|
||||
anObject.isa = proxy.isa;
|
||||
|
||||
[KVOProxyMap setObject:proxy forKey:[anObject hash]];
|
||||
@@ -108,25 +111,25 @@ var KVOProxyMap = [CPDictionary dictionary];
|
||||
- (id)initWithTarget:(id)aTarget
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
|
||||
_targetObject = aTarget;
|
||||
_nativeClass = [aTarget class];
|
||||
_replacementMethods = [CPDictionary dictionary];
|
||||
_observersForKey = [CPDictionary dictionary];
|
||||
_changesForKey = [CPDictionary dictionary];
|
||||
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)_replaceSetters
|
||||
{
|
||||
var currentClass = [_targetObject class];
|
||||
|
||||
|
||||
while (currentClass && currentClass != currentClass.super_class)
|
||||
{
|
||||
var methodList = currentClass.method_list,
|
||||
count = methodList.length;
|
||||
|
||||
|
||||
for (var i=0; i<count; i++)
|
||||
{
|
||||
var newMethod = _kvoMethodForMethod(_targetObject, methodList[i]);
|
||||
@@ -134,7 +137,7 @@ var KVOProxyMap = [CPDictionary dictionary];
|
||||
if (newMethod)
|
||||
[_replacementMethods setObject:newMethod forKey:methodList[i].name];
|
||||
}
|
||||
|
||||
|
||||
currentClass = currentClass.super_class;
|
||||
}
|
||||
}
|
||||
@@ -153,7 +156,7 @@ var KVOProxyMap = [CPDictionary dictionary];
|
||||
{
|
||||
var proxy = [_CPKVOProxy proxyForObject:self],
|
||||
imp = class_getInstanceMethod(proxy._nativeClass, aSelector);
|
||||
|
||||
|
||||
return imp ? YES : NO;
|
||||
}
|
||||
|
||||
@@ -166,10 +169,10 @@ var KVOProxyMap = [CPDictionary dictionary];
|
||||
- (IMP)methodForSelector:(SEL)aSelector
|
||||
{
|
||||
var proxy = [_CPKVOProxy proxyForObject:self],
|
||||
imp = class_getInstanceMethod(proxy._nativeClass, aSelector),
|
||||
replacement = [proxy._replacementMethods objectForKey:aSelector];
|
||||
replacement = [proxy._replacementMethods objectForKey:aSelector],
|
||||
imp = replacement ? replacement : class_getInstanceMethod(proxy._nativeClass, aSelector);
|
||||
|
||||
return replacement ? replacement : imp;
|
||||
return imp;
|
||||
}
|
||||
|
||||
- (void)forwardInvocation:(CPInvocation)anInvocation
|
||||
@@ -180,7 +183,7 @@ var KVOProxyMap = [CPDictionary dictionary];
|
||||
if (method)
|
||||
method.apply(self, anInvocation._arguments); //FIXME
|
||||
else
|
||||
[super forwardInvocation:anInvocation];
|
||||
class_getInstanceMethod(proxy._nativeClass, @selector(forwardInvocation:)).apply(self, anInvocation.arguments);
|
||||
}
|
||||
|
||||
- (void)_addObserver:(id)anObserver forKeyPath:(CPString)aPath options:(unsigned)options context:(id)aContext
|
||||
@@ -188,54 +191,58 @@ var KVOProxyMap = [CPDictionary dictionary];
|
||||
if (!anObserver)
|
||||
return;
|
||||
|
||||
var forwarder = nil;
|
||||
|
||||
if (aPath.indexOf('.') != CPNotFound)
|
||||
return CPLog.error("WHOA, don't go crazy...");
|
||||
else
|
||||
forwarder = [[_CPKVOForwardingObserver alloc] initWithKeyPath:aPath object:_targetObject observer:anObserver options:options context:aContext];
|
||||
|
||||
var observers = [_observersForKey objectForKey:aPath];
|
||||
|
||||
if (!observers)
|
||||
{
|
||||
var observers = [_observersForKey objectForKey:aPath];
|
||||
|
||||
if (!observers)
|
||||
{
|
||||
observers = [CPDictionary dictionary];
|
||||
[_observersForKey setObject:observers forKey:aPath];
|
||||
}
|
||||
|
||||
[observers setObject:_CPKVOInfoMake(anObserver, options, aContext) forKey:[anObserver hash]];
|
||||
observers = [CPDictionary dictionary];
|
||||
[_observersForKey setObject:observers forKey:aPath];
|
||||
}
|
||||
|
||||
var changes = [CPDictionary dictionary];
|
||||
[changes setObject:[_targetObject valueForKey:aPath] forKey:CPKeyValueChangeNewKey];
|
||||
[observers setObject:_CPKVOInfoMake(anObserver, options, aContext, forwarder) forKey:[anObserver hash]];
|
||||
|
||||
if (options & CPKeyValueObservingOptionInitial)
|
||||
{
|
||||
var newValue = [_targetObject valueForKeyPath:aPath];
|
||||
|
||||
[_changesForKey setObject:changes forKey:aPath];
|
||||
if (!newValue && newValue !== "")
|
||||
newValue = [CPNull null];
|
||||
|
||||
if (options & CPKeyValueObservingOptionInitial)
|
||||
[anObserver observeValueForKeyPath:aPath ofObject:self change:changes context:aContext];
|
||||
var changes = [CPDictionary dictionaryWithObject:newValue forKey:CPKeyValueChangeNewKey];
|
||||
[anObserver observeValueForKeyPath:aPath ofObject:self change:changes context:aContext];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_removeObserver:(id)anObserver forKeyPath:(CPString)aPath
|
||||
{
|
||||
if (aPath.indexOf('.') != CPNotFound)
|
||||
return CPLog.error("WHOA, don't go crazy...");
|
||||
else
|
||||
{
|
||||
var observers = [_observersForKey objectForKey:aPath];
|
||||
{
|
||||
var observers = [_observersForKey objectForKey:aPath];
|
||||
|
||||
[observers removeObjectForKey:[anObserver hash]];
|
||||
|
||||
if (![observers count])
|
||||
[_observersForKey removeObjectForKey:aPath];
|
||||
|
||||
if (![_observersForKey count])
|
||||
{
|
||||
_targetObject.isa = _nativeClass; //restore the original class
|
||||
[KVOProxyMap removeObjectForKey:[_targetObject hash]];
|
||||
}
|
||||
if (aPath.indexOf('.') != CPNotFound)
|
||||
{
|
||||
var forwarder = [observers objectForKey:[anObserver hash]].forwarder;
|
||||
[forwarder finalize];
|
||||
}
|
||||
|
||||
[observers removeObjectForKey:[anObserver hash]];
|
||||
|
||||
if (![observers count])
|
||||
[_observersForKey removeObjectForKey:aPath];
|
||||
|
||||
if (![_observersForKey count])
|
||||
{
|
||||
_targetObject.isa = _nativeClass; //restore the original class
|
||||
[KVOProxyMap removeObjectForKey:[_targetObject hash]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)willChangeValueForKey:(CPString)aKey
|
||||
{
|
||||
if (!aKey)
|
||||
if (!aKey)
|
||||
return;
|
||||
|
||||
[[_CPKVOProxy proxyForObject:self] _sendNotificationsForKey:aKey isBefore:YES];
|
||||
@@ -244,7 +251,7 @@ var KVOProxyMap = [CPDictionary dictionary];
|
||||
|
||||
- (void)didChangeValueForKey:(CPString)aKey
|
||||
{
|
||||
if (!aKey)
|
||||
if (!aKey)
|
||||
return;
|
||||
|
||||
[[_CPKVOProxy proxyForObject:self] _sendNotificationsForKey:aKey isBefore:NO];
|
||||
@@ -252,31 +259,28 @@ var KVOProxyMap = [CPDictionary dictionary];
|
||||
|
||||
- (void)_sendNotificationsForKey:(CPString)aKey isBefore:(BOOL)isBefore
|
||||
{
|
||||
var changes = [_changesForKey objectForKey:aKey],
|
||||
oldValue = [changes objectForKey:CPKeyValueChangeOldKey],
|
||||
newValue = [changes objectForKey:CPKeyValueChangeNewKey];
|
||||
|
||||
if (!oldValue && oldValue !== "")
|
||||
oldValue = [CPNull null];
|
||||
var changes = [_changesForKey objectForKey:aKey];
|
||||
|
||||
if (!newValue && newValue !== "")
|
||||
newValue = [CPNull null];
|
||||
|
||||
if (isBefore)
|
||||
{
|
||||
changes = [CPDictionary dictionary];
|
||||
|
||||
|
||||
var oldValue = [_targetObject valueForKey:aKey];
|
||||
|
||||
if (!oldValue && oldValue !== "")
|
||||
oldValue = [CPNull null];
|
||||
|
||||
[changes setObject:1 forKey:CPKeyValueChangeNotificationIsPriorKey];
|
||||
[changes setObject:newValue forKey:CPKeyValueChangeOldKey];
|
||||
|
||||
[changes setObject:oldValue forKey:CPKeyValueChangeOldKey];
|
||||
|
||||
[_changesForKey setObject:changes forKey:aKey];
|
||||
}
|
||||
else
|
||||
{
|
||||
[changes removeObjectForKey:CPKeyValueChangeNotificationIsPriorKey];
|
||||
|
||||
|
||||
var newValue = [_targetObject valueForKey:aKey];
|
||||
|
||||
|
||||
if (!newValue && newValue !== "")
|
||||
newValue = [CPNull null];
|
||||
|
||||
@@ -285,11 +289,11 @@ var KVOProxyMap = [CPDictionary dictionary];
|
||||
|
||||
var observers = [[_observersForKey objectForKey:aKey] allValues],
|
||||
count = [observers count];
|
||||
|
||||
|
||||
while (count--)
|
||||
{
|
||||
var observerInfo = observers[count];
|
||||
|
||||
|
||||
if (isBefore && (observerInfo.options & CPKeyValueObservingOptionPrior))
|
||||
[observerInfo.observer observeValueForKeyPath:aKey ofObject:_targetObject change:changes context:observerInfo.context];
|
||||
else if (!isBefore)
|
||||
@@ -297,14 +301,109 @@ var KVOProxyMap = [CPDictionary dictionary];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isKindOfClass:(Class)aClass
|
||||
{
|
||||
return [[self class] isSubclassOfClass:aClass];
|
||||
}
|
||||
|
||||
- (BOOL)isMemberOfClass:(Class)aClass
|
||||
{
|
||||
return [self class] == aClass;
|
||||
}
|
||||
|
||||
- (CPString)className
|
||||
{
|
||||
return [self class].name;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
var _CPKVOInfoMake = function _CPKVOInfoMake(anObserver, theOptions, aContext)
|
||||
@implementation _CPKVOForwardingObserver : CPObject
|
||||
{
|
||||
id _object;
|
||||
id _observer;
|
||||
id _context;
|
||||
//a.b
|
||||
CPString _firstPart; //a
|
||||
CPString _secondPart; //b
|
||||
|
||||
id _value;
|
||||
}
|
||||
|
||||
- (id)initWithKeyPath:(CPString)aKeyPath object:(id)anObject observer:(id)anObserver options:(unsigned)options context:(id)aContext
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
_context = aContext;
|
||||
_observer = anObserver;
|
||||
_object = anObject;
|
||||
|
||||
//current ignoring options (FIXME?)
|
||||
|
||||
var dotIndex = aKeyPath.indexOf('.');
|
||||
|
||||
if (dotIndex == CPNotFound)
|
||||
[CPException raise:CPInvalidArgumentException reason:"Created _CPKVOForwardingObserver without compound key path: "+aKeyPath];
|
||||
|
||||
_firstPart = aKeyPath.substring(0, dotIndex);
|
||||
_secondPart = aKeyPath.substring(dotIndex+1);
|
||||
|
||||
//become an observer of the first part of our key (a)
|
||||
[_object addObserver:self forKeyPath:_firstPart options:kvoNewAndOld context:nil];
|
||||
|
||||
//the current value of a (not the value of a.b)
|
||||
_value = [_object valueForKey:_firstPart];
|
||||
|
||||
if (_value)
|
||||
[_value addObserver:self forKeyPath:_secondPart options:kvoNewAndOld context:nil]; //we're observing b on current a
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)changes context:(id)aContext
|
||||
{
|
||||
if (anObject == _object)
|
||||
{
|
||||
[_observer observeValueForKeyPath:_firstPart ofObject:_object change:changes context:_context];
|
||||
|
||||
//since a has changed, we should remove ourselves as an observer of the old a, and observe the new one
|
||||
if (_value)
|
||||
[_value removeObserver:self forKeyPath:_secondPart];
|
||||
|
||||
_value = [_object valueForKey:_firstPart];
|
||||
|
||||
if (_value)
|
||||
[_value addObserver:self forKeyPath:_secondPart options:kvoNewAndOld context:nil];
|
||||
}
|
||||
else /* if (anObject == _value || !_value) */
|
||||
{
|
||||
//a is the same, but a.b has changed -- nothing to do but forward this message along
|
||||
[_observer observeValueForKeyPath:_firstPart+"."+aKeyPath ofObject:_object change:changes context:_context];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)finalize
|
||||
{
|
||||
if (_value)
|
||||
[_value removeObserver:self forKeyPath:_secondPart];
|
||||
|
||||
[_object removeObserver:self forKeyPath:_firstPart];
|
||||
|
||||
_object = nil;
|
||||
_observer = nil;
|
||||
_context = nil;
|
||||
_value = nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
var _CPKVOInfoMake = function _CPKVOInfoMake(anObserver, theOptions, aContext, aForwarder)
|
||||
{
|
||||
return {
|
||||
observer: anObserver,
|
||||
options: theOptions,
|
||||
context: aContext
|
||||
context: aContext,
|
||||
forwarder: aForwarder
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+302
-13
@@ -5,11 +5,17 @@ import <Foundation/CPKeyValueObserving.j>
|
||||
{
|
||||
BOOL _sawInitialObservation;
|
||||
BOOL _sawPriorObservation;
|
||||
BOOL _sawObservation;
|
||||
|
||||
id bob;
|
||||
id obj;
|
||||
id cs101;
|
||||
id focus;
|
||||
}
|
||||
|
||||
- (void)testAddObserver
|
||||
{
|
||||
var bob = [[PersonTester alloc] init];
|
||||
bob = [[PersonTester alloc] init];
|
||||
|
||||
[bob addObserver:self forKeyPath:@"name" options:nil context:"testAddObserver"];
|
||||
|
||||
@@ -17,11 +23,24 @@ import <Foundation/CPKeyValueObserving.j>
|
||||
|
||||
[self assertTrue: [bob valueForKey:@"name"] == @"set_bob" message: "valueForKey:'name' should be 'set_bob', was: "+[bob valueForKey:@"name"]];
|
||||
[self assertTrue: bob.name == @"set_bob" message: "bob.name should be 'set_bob', was: "+bob.name];
|
||||
[self assertTrue: _sawObservation message:"Never recieved an observation"];
|
||||
}
|
||||
|
||||
- (void)testUnobservedKey
|
||||
{
|
||||
bob = [[PersonTester alloc] init];
|
||||
|
||||
[bob addObserver:self forKeyPath:@"name" options:nil context:"testUnobservedKey"];
|
||||
|
||||
[bob setValue:@"555" forKey:@"phoneNumber"];
|
||||
|
||||
[self assertTrue: [bob valueForKey:@"phoneNumber"] == @"555" message: "'phoneNumber' should be '555', was: "+[bob valueForKey:@"phoneNumber"]];
|
||||
[self assertFalse: _sawObservation message:"Should not have recieved an observation"];
|
||||
}
|
||||
|
||||
- (void)testAddTwoObservers
|
||||
{
|
||||
var bob = [[PersonTester alloc] init];
|
||||
bob = [[PersonTester alloc] init];
|
||||
|
||||
[bob addObserver:self forKeyPath:@"name" options:nil context:"testAddTwoObservers"];
|
||||
[bob addObserver:[CPObject new] forKeyPath:@"name" options:nil context:"testAddTwoObservers"];
|
||||
@@ -30,6 +49,7 @@ import <Foundation/CPKeyValueObserving.j>
|
||||
|
||||
[self assertTrue: [bob valueForKey:@"name"] == @"set_bob" message: "valueForKey:'name' should be bob, was: "+[bob valueForKey:@"name"]];
|
||||
[self assertTrue: bob.name == @"set_bob" message: "bob.name should be 'bob', was: "+bob.name];
|
||||
[self assertTrue: _sawObservation message:"Never recieved an observation"];
|
||||
}
|
||||
|
||||
- (void)testDirectIVarObservation
|
||||
@@ -42,11 +62,12 @@ import <Foundation/CPKeyValueObserving.j>
|
||||
|
||||
[self assertTrue: [bob valueForKey:@"phoneNumber"] == @"555" message: "valueForKey:'phoneNumber' should be '555', was: "+[bob valueForKey:@"phoneNumber"]];
|
||||
[self assertTrue: bob.phoneNumber == @"555" message: "bob.phoneNumber should be '555', was: "+bob.phoneNumber];
|
||||
[self assertTrue: _sawObservation message:"Never recieved an observation"];
|
||||
}
|
||||
|
||||
- (void)testRemoveObserver
|
||||
{
|
||||
var bob = [[PersonTester alloc] init];
|
||||
bob = [[PersonTester alloc] init];
|
||||
|
||||
[bob addObserver:self forKeyPath:@"name" options:nil context:"testRemoveObserver"];
|
||||
[bob addObserver:[CPString new] forKeyPath:@"name" options:nil context:"testRemoveObserver"];
|
||||
@@ -60,8 +81,8 @@ import <Foundation/CPKeyValueObserving.j>
|
||||
|
||||
- (void)testRemoveOtherObserver
|
||||
{
|
||||
var bob = [[PersonTester alloc] init],
|
||||
obj = [CPString new];
|
||||
bob = [[PersonTester alloc] init];
|
||||
obj = [CPString new];
|
||||
|
||||
[bob addObserver:self forKeyPath:@"name" options:nil context:"testRemoveOtherObserver"];
|
||||
[bob addObserver:obj forKeyPath:@"name" options:nil context:"testRemoveOtherObserver"];
|
||||
@@ -71,18 +92,19 @@ import <Foundation/CPKeyValueObserving.j>
|
||||
[bob setValue:@"bob" forKey:@"name"];
|
||||
|
||||
[self assertTrue: [bob valueForKey:@"name"] == @"set_bob" message: "valueForKey:'name' should be bob, was: "+[bob valueForKey:@"name"]];
|
||||
[self assertTrue: _sawObservation message:"Never recieved an observation"];
|
||||
}
|
||||
|
||||
- (void)testRemoveAllObservers
|
||||
{
|
||||
var bob = [[PersonTester alloc] init],
|
||||
obj1 = [CPArray new],
|
||||
obj2 = [CPString new];
|
||||
bob = [[PersonTester alloc] init];
|
||||
obj = [CPArray new];
|
||||
obj2 = [CPString new];
|
||||
|
||||
[bob addObserver:obj1 forKeyPath:@"name" options:nil context:"testRemoveAllObservers"];
|
||||
[bob addObserver:obj forKeyPath:@"name" options:nil context:"testRemoveAllObservers"];
|
||||
[bob addObserver:obj2 forKeyPath:@"name" options:nil context:"testRemoveAllObservers"];
|
||||
|
||||
[bob removeObserver:obj1 forKeyPath:@"name"];
|
||||
[bob removeObserver:obj forKeyPath:@"name"];
|
||||
[bob removeObserver:obj2 forKeyPath:@"name"];
|
||||
[bob removeObserver:nil forKeyPath:nil];
|
||||
|
||||
@@ -96,7 +118,7 @@ import <Foundation/CPKeyValueObserving.j>
|
||||
{
|
||||
_sawPriorObservation = NO;
|
||||
|
||||
var bob = [[PersonTester alloc] init];
|
||||
bob = [[PersonTester alloc] init];
|
||||
|
||||
[bob addObserver:self forKeyPath:@"name" options:CPKeyValueObservingOptionPrior context:"testPriorObservationOption"];
|
||||
|
||||
@@ -109,7 +131,7 @@ import <Foundation/CPKeyValueObserving.j>
|
||||
{
|
||||
_sawInitialObservation = NO;
|
||||
|
||||
var bob = [[PersonTester alloc] init];
|
||||
bob = [[PersonTester alloc] init];
|
||||
|
||||
bob.name = "paul";
|
||||
|
||||
@@ -121,6 +143,162 @@ import <Foundation/CPKeyValueObserving.j>
|
||||
[self assertTrue: _sawInitialObservation message: "asked for CPKeyValueObservingOptionInitial but did not recieve corresponding notification"];
|
||||
}
|
||||
|
||||
- (void)testMultipartKey
|
||||
{
|
||||
cs101 = [ClassTester new];
|
||||
bob = [PersonTester new];
|
||||
|
||||
[cs101 setTeacher:bob];
|
||||
|
||||
[cs101 addObserver:self forKeyPath:@"teacher.name" options:nil context:"testMultipartKey"];
|
||||
|
||||
[bob setName:@"bob"];
|
||||
|
||||
[self assertTrue:[cs101 valueForKeyPath:@"teacher.name"] == "set_bob" message:"teacher.name should be: set_bob, was: "+[cs101 valueForKeyPath:@"teacher.name"]];
|
||||
[self assertTrue: _sawObservation message:"Never recieved an observation"];
|
||||
}
|
||||
|
||||
- (void)testThreePartKey
|
||||
{
|
||||
focus = [CarTester new];
|
||||
cs101 = [ClassTester new];
|
||||
bob = [PersonTester new];
|
||||
|
||||
[cs101 setTeacher:bob];
|
||||
[bob setValue:focus forKey:"car"];
|
||||
|
||||
[cs101 addObserver:self forKeyPath:@"teacher.car.model" options:nil context:"testThreePartKey"];
|
||||
|
||||
[focus setValue:"ford focus" forKey:"model"];
|
||||
[self assertTrue: _sawObservation message:"Never recieved an observation"];
|
||||
}
|
||||
|
||||
- (void)testThreePartKeyPart2
|
||||
{
|
||||
focus = [CarTester new];
|
||||
cs101 = [ClassTester new];
|
||||
bob = [PersonTester new];
|
||||
|
||||
[cs101 setTeacher:bob];
|
||||
[focus setValue:"2000" forKey:"year"];
|
||||
|
||||
[cs101 addObserver:self forKeyPath:@"teacher.car.year" options:nil context:"testThreePartKeyPart2"];
|
||||
|
||||
[bob setValue:focus forKey:"car"];
|
||||
|
||||
[self assertTrue: _sawObservation message:"Never recieved an observation"];
|
||||
}
|
||||
|
||||
- (void)testRemoveMultipartKey
|
||||
{
|
||||
cs101 = [ClassTester new];
|
||||
bob = [PersonTester new];
|
||||
|
||||
[cs101 setTeacher:bob];
|
||||
|
||||
[cs101 addObserver:self forKeyPath:@"teacher.name" options:nil context:"testRemoveMultipartKey"];
|
||||
|
||||
[cs101 removeObserver:self forKeyPath:@"teacher.name"];
|
||||
|
||||
[bob setName:@"bob"];
|
||||
|
||||
[self assertFalse: _sawObservation message:"Should not have recieved an observation"];
|
||||
}
|
||||
|
||||
- (void)testRemoveThreePartKey
|
||||
{
|
||||
focus = [CarTester new];
|
||||
cs101 = [ClassTester new];
|
||||
bob = [PersonTester new];
|
||||
|
||||
[cs101 setTeacher:bob];
|
||||
[bob setValue:focus forKey:"car"];
|
||||
|
||||
[cs101 addObserver:self forKeyPath:@"teacher.car.model" options:nil context:"testRemoveThreePartKey"];
|
||||
[cs101 removeObserver:self forKeyPath:@"teacher.car.model"];
|
||||
|
||||
[focus setValue:"ford focus" forKey:"model"];
|
||||
|
||||
[self assertFalse: _sawObservation message:"Should not have recieved an observation"];
|
||||
}
|
||||
|
||||
|
||||
- (void)testCrazyKeyPathChanges
|
||||
{
|
||||
var a = [A new];
|
||||
|
||||
[a setValue:[B new] forKeyPath:"b"];
|
||||
[a setValue:[C new] forKeyPath:"b.c"];
|
||||
[a setValue:[D new] forKeyPath:"b.c.d"];
|
||||
[a setValue:[E new] forKeyPath:"b.c.d.e"];
|
||||
[a setValue:[F new] forKeyPath:"b.c.d.e.f"];
|
||||
|
||||
[a addObserver:self forKeyPath:"b.c.d.e.f" options:nil context:"testCrazyKeyPathChanges"];
|
||||
|
||||
[a setValue:[D new] forKeyPath:"b.c.d"];
|
||||
|
||||
[self assertTrue: _sawObservation message:"Never recieved an observation"];
|
||||
}
|
||||
|
||||
- (void)testCrazyKeyPathChanges2
|
||||
{
|
||||
var a = [A new];
|
||||
|
||||
[a setValue:[B new] forKeyPath:"b"];
|
||||
[a setValue:[C new] forKeyPath:"b.c"];
|
||||
[a setValue:[D new] forKeyPath:"b.c.d"];
|
||||
[a setValue:[E new] forKeyPath:"b.c.d.e"];
|
||||
[a setValue:[F new] forKeyPath:"b.c.d.e.f"];
|
||||
|
||||
[a addObserver:self forKeyPath:"b.c.d.e.f" options:nil context:"testCrazyKeyPathChanges2"];
|
||||
|
||||
[a setValue:nil forKeyPath:"b.c"];
|
||||
|
||||
[self assertTrue: _sawObservation message:"Never recieved an observation"];
|
||||
}
|
||||
|
||||
- (void)testCrazyKeyPathChanges3
|
||||
{
|
||||
var a = [A new];
|
||||
|
||||
[a setValue:[B new] forKeyPath:"b"];
|
||||
[a setValue:[C new] forKeyPath:"b.c"];
|
||||
[a setValue:[D new] forKeyPath:"b.c.d"];
|
||||
[a setValue:[E new] forKeyPath:"b.c.d.e"];
|
||||
[a setValue:[F new] forKeyPath:"b.c.d.e.f"];
|
||||
|
||||
[a addObserver:self forKeyPath:"b.c.d.e.f" options:nil context:"testCrazyKeyPathChanges3"];
|
||||
|
||||
[a setValue:7 forKeyPath:"b.c.d.e.f"];
|
||||
|
||||
[self assertTrue: _sawObservation message:"Never recieved an observation"];
|
||||
}
|
||||
|
||||
- (void)testPerformance
|
||||
{
|
||||
bob = [PersonTester new];
|
||||
|
||||
[bob setValue:"initial bob" forKey:"name"];
|
||||
|
||||
var startTime = new Date();
|
||||
|
||||
for(var i=0; i<1000; i++)
|
||||
[bob setValue:i+"bob" forKey:"name"];
|
||||
|
||||
var total = new Date() - startTime;
|
||||
|
||||
[bob addObserver:[CPObject new] forKeyPath:"name" options:nil context:nil];
|
||||
|
||||
startTime = new Date();
|
||||
|
||||
for(var i=0; i<1000; i++)
|
||||
[bob setValue: i+"bob" forKey:"name"];
|
||||
|
||||
var secondTotal = new Date() - startTime;
|
||||
|
||||
[self assertTrue: (secondTotal < total*2) message: "Overheard of one observer exceeded 100%. first: "+total+" second: "+secondTotal+" %"+FLOOR(secondTotal/total*100)];
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)changes context:(id)aContext
|
||||
{
|
||||
var oldValue = [changes objectForKey:CPKeyValueChangeOldKey],
|
||||
@@ -131,8 +309,11 @@ import <Foundation/CPKeyValueObserving.j>
|
||||
case "testAddObserver":
|
||||
[self assertTrue: newValue == "set_bob" message: "newValue should be: set_bob was: "+newValue];
|
||||
[self assertTrue: oldValue == [CPNull null] message: "oldValue should be CPNull was: "+oldValue];
|
||||
[self assertTrue: anObject == bob message: "anObject should be: "+[bob description]+", was: "+[anObject description]];
|
||||
break;
|
||||
case "testUnobservedKey":
|
||||
[self assertFalse: YES message: "not observing this key, should never get here"];
|
||||
break;
|
||||
|
||||
case "testAddTwoObservers":
|
||||
[self assertTrue: newValue == "set_bob" message: "newValue should be: set_bob was: "+newValue];
|
||||
[self assertTrue: oldValue == [CPNull null] message: "oldValue should be CPNull was: "+oldValue];
|
||||
@@ -184,7 +365,57 @@ import <Foundation/CPKeyValueObserving.j>
|
||||
[self assertFalse:YES message:"Should never have received this notification"];
|
||||
|
||||
break;
|
||||
|
||||
case "testMultipartKey":
|
||||
[self assertTrue: aKeyPath == "teacher.name" message:"Keypath should be: teacher.name, was: "+aKeyPath];
|
||||
[self assertTrue: newValue == "set_bob" message:"New value should be: set_bob, was: "+newValue];
|
||||
[self assertTrue: anObject == cs101 message: "anObject should be: "+[cs101 description]+", was: "+[anObject description]];
|
||||
break;
|
||||
|
||||
case "testRemoveMultipartKey":
|
||||
[self assertFalse:YES message:"Should never have received this notification"];
|
||||
break;
|
||||
|
||||
case "testRemoveThreePartKey":
|
||||
[self assertFalse:YES message:"Should never have received this notification"];
|
||||
break;
|
||||
|
||||
case "testThreePartKey":
|
||||
[self assertTrue: aKeyPath == "teacher.car.model" message:"Keypath should be: teacher.car.model, was: "+aKeyPath];
|
||||
[self assertTrue: newValue == "ford focus" message:"New value should be: ford focus, was: "+newValue];
|
||||
[self assertTrue: anObject == cs101 message: "anObject should be: "+[cs101 description]+", was: "+[anObject description]];
|
||||
break;
|
||||
|
||||
case "testThreePartKeyPart2":
|
||||
[self assertTrue: aKeyPath == "teacher.car" message:"Keypath should be: teacher.car, was: "+aKeyPath];
|
||||
[self assertTrue: newValue.year == "2000" message:"New value should be a car with year: 2000, was: "+[newValue description]];
|
||||
[self assertTrue: anObject == cs101 message: "anObject should be: "+[cs101 description]+", was: "+[anObject description]];
|
||||
break;
|
||||
|
||||
case "testCrazyKeyPathChanges":
|
||||
[self assertTrue: [anObject class] == A message:"Should be observing an A class, was: "+[anObject class]];
|
||||
[self assertTrue: [newValue class] == D message:"Changed class was a D class, got: "+[newValue class]];
|
||||
[self assertTrue: aKeyPath == "b.c.d" message:"Expected keyPath b.c.d, got: "+aKeyPath];
|
||||
break;
|
||||
|
||||
case "testCrazyKeyPathChanges2":
|
||||
[self assertTrue: [anObject class] == A message:"Should be observing an A class, was: "+[anObject class]];
|
||||
[self assertTrue: newValue == [CPNull null] message:"Expected null, got: "+newValue];
|
||||
[self assertTrue: aKeyPath == "b.c" message:"Expected keyPath b.c, got: "+aKeyPath];
|
||||
break;
|
||||
|
||||
case "testCrazyKeyPathChanges3":
|
||||
[self assertTrue: [anObject class] == A message:"Should be observing an A class, was: "+[anObject class]];
|
||||
[self assertTrue: newValue == 7 message:"Expected 7, got: "+newValue];
|
||||
[self assertTrue: aKeyPath == "b.c.d.e.f" message:"Expected keyPath b.c.d.e.f, got: "+aKeyPath];
|
||||
break;
|
||||
|
||||
default:
|
||||
[self assertFalse:YES message:"unhandled observation, must be an error"];
|
||||
return;
|
||||
}
|
||||
|
||||
_sawObservation = YES;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -202,6 +433,7 @@ import <Foundation/CPKeyValueObserving.j>
|
||||
{
|
||||
CPString name;
|
||||
CPString phoneNumber;
|
||||
CarTester car;
|
||||
}
|
||||
|
||||
- (void)setName:(CPString)aName
|
||||
@@ -209,4 +441,61 @@ import <Foundation/CPKeyValueObserving.j>
|
||||
name = "set_"+aName;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation ClassTester : CPObject
|
||||
{
|
||||
PersonTester teacher;
|
||||
CPArray students;
|
||||
}
|
||||
|
||||
- (void)setTeacher:(PersonTester)aPerson
|
||||
{
|
||||
teacher = aPerson;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation CarTester : CPObject
|
||||
{
|
||||
CPString model;
|
||||
CPString year;
|
||||
}
|
||||
|
||||
- (void)setModel:(CPString)aModel
|
||||
{
|
||||
model = aModel;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation A : CPObject
|
||||
{
|
||||
id b;
|
||||
}
|
||||
@end
|
||||
@implementation B : CPObject
|
||||
{
|
||||
id c;
|
||||
}
|
||||
@end
|
||||
@implementation C : CPObject
|
||||
{
|
||||
id d;
|
||||
}
|
||||
@end
|
||||
@implementation D : CPObject
|
||||
{
|
||||
id e;
|
||||
}
|
||||
@end
|
||||
@implementation E : CPObject
|
||||
{
|
||||
id f;
|
||||
}
|
||||
@end
|
||||
@implementation F : CPObject
|
||||
{
|
||||
id g;
|
||||
}
|
||||
@end
|
||||
Reference in New Issue
Block a user