KVO and the observeValueForKeyPath bottleneck
-
I'm just curious about how people are handling the fact that when you do
KVO, all your notifications are bottlenecked through a single method,
observeValueForKeyPath:... This is a very unpleasant and crude architecture
(in contrast to NSNotification where, when a notification comes in, it is
automatically routed to the selector of your choice). I really don't want a
series of "ifs" here. I can imagine a simple dispatcher architecture based
on NSSelectorFromString; is this the sort of thing people are using? Thx -
m.
--
matt neuburg, phd = <matt...>, <http://www.tidbits.com/matt/>
A fool + a tool + an autorelease pool = cool!
AppleScript: the Definitive Guide - Second Edition!
<http://www.amazon.com/gp/product/0596102119> -
Hi Matt,
Yeah I'm actually doing something similar though I wasn't aware of
NSSelectorFromString.
For a custom view that responds to a few dozen attributes of the
controller's current selection....
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
NSString *keyPathName;
if ([keyPath isEqualToString:@"selection.bgImage.color"] == YES){
// handle special case of this attribute of a relationship
keyPathName = @"bgImageColor";
} else {
// convert "selection.attribute" keypath to appropriate accessor
name -- by abusing pathExtension method :)
keyPathName = [keyPath pathExtension];
}
[self setValue:[object valueForKeyPath:keyPath] forKey:keyPathName];
}
So observing "selection.attribute" will call my setAttribute accessor
with the new value via KVC.
Possibly not the most proper setup, but it seems to work well enough
for this view's situation.
George -
On Jul 14, 2006, at 1:44 PM, Matt Neuburg wrote:
> I'm just curious about how people are handling the fact that when
> you do
> KVO, all your notifications are bottlenecked through a single method,
> observeValueForKeyPath:... This is a very unpleasant and crude
> architecture
> (in contrast to NSNotification where, when a notification comes in,
> it is
> automatically routed to the selector of your choice). I really
> don't want a
> series of "ifs" here. I can imagine a simple dispatcher
> architecture based
> on NSSelectorFromString; is this the sort of thing people are
> using? Thx -
> m.
> --
Good question; I hope this sparks more discussion.
I can think of two approaches using the context parameter, one of
which I implemented recently. In my main controller I am observing
several array controllers, where the content object types vary from
controller to controller. When the selection on any controller
changes, I get the path to a file and display it in a viewer. When
registering as an observer, I set the context to a string constant
for the key path to the file name. Here is a simplified example:
-(void)awakeFromNib
{
[inputFilesArrayController addObserver:self forKeyPath:@"selection"
options:nil context:@"selection.sourceFilePath"];
[processedFilesArrayController addObserver:self
forKeyPath:@"selection" options:nil context:@"selection.savedToPath"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)
object change:(NSDictionary *)change context:(void *)context
{
// In awakeFromNib:, KVO registration done with context set to the
key value to retrieve.
// If it is a string (thus a path), then display it.
if (context) {
id displayPath = [object valueForKeyPath:context];
// We may get an placeholder marker or nil
if (displayPath && [displayPath isKindOfClass:[NSString class]]) {
[fileViewerController open:displayPath];
}
}
}
This worked OK for the given situation. If I had a more complex
scenario, I would consider enumerating constants to use as the
context and then use a switch() in observeValueForKeyPath:.
Something like this:
-(void)awakeFromNib
{
[firstArrayController addObserver:self
forKeyPath:@"selection.attribute" options:nil context:EnumConstant1];
[secondArrayController addObserver:self
forKeyPath:@"selection.attribute" options:nil context:EnumConstant2];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)
object change:(NSDictionary *)change context:(void *)context
{
switch (context) {
case EnumConstant1:
// ...do something...
break;
case EnumConstant2:
// ...do something else...
break;
default:
break;
}
}
I haven't tried the second example, so it may not be as suitable as I
think. Comments?
Cheers,
Aaron -
On 14/07/2006, at 19.44, Matt Neuburg wrote:
> I'm just curious about how people are handling the fact that when
> you do
> KVO, all your notifications are bottlenecked through a single method,
> observeValueForKeyPath:... This is a very unpleasant and crude
> architecture
> (in contrast to NSNotification where, when a notification comes in,
> it is
> automatically routed to the selector of your choice). I really
> don't want a
> series of "ifs" here. I can imagine a simple dispatcher
> architecture based
> on NSSelectorFromString; is this the sort of thing people are
> using? Thx -
> m.
Here is what I am currently using:
- (void)awakeFromNib
{
[myController addObserver:self forKeyPath:@"selectedObjects"
options:0 context:@selector(selectionChanged:)];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)
object change:(NSDictionary *)change context:(void *)context
{
if ([self respondsToSelector:(SEL)context])
[self performSelector:(SEL)context withObject:change];
}
Just be really careful to always use a context that is a selector,
otherwise you will probably crash. [NSObject respondsToSelector:]
will probably work for a pointer that is not a selector, but I
wouldn't count on it.
You can of course pass the object or keyPath as well if you need it.
Unfortunately there is no [NSObject
performSelector:withObject:withObject:withObject:] if you need all
three arguments. -
On 17/07/2006, at 12.38, Jakob Olesen wrote:
> - (void)awakeFromNib
> {
> [myController addObserver:self forKeyPath:@"selectedObjects"
> options:0 context:@selector(selectionChanged:)];
> }
>
> - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)
> object change:(NSDictionary *)change context:(void *)context
> {
> if ([self respondsToSelector:(SEL)context])
> [self performSelector:(SEL)context withObject:change];
> }
Sorry to follow up on myself, but it just occurred to me that the
information fits perfectly in an NSNotification:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)
object change:(NSDictionary *)change context:(void *)context
{
[self performSelector:(SEL)context withObject:[NSNotification
notificationWithName:keyPath object:object userInfo:change]];
}
The method you dispatch to is then identical to a notification
handler. (You could even use it for both in simple cases) -
On or about 7/17/06 3:38 AM, thus spake "Jakob Olesen" <stoklund...>:
> - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)
> object change:(NSDictionary *)change context:(void *)context
> {
> if ([self respondsToSelector:(SEL)context])
> [self performSelector:(SEL)context withObject:change];
> }
By an incredible coincidence, yesterday (before this reponse arrived at my
desk), after considering all the responses I'd gotten, I decided upon an
approach and implemented it in my app, and it was word for word, letter for
letter identical with this. m.
--
matt neuburg, phd = <matt...>, http://www.tidbits.com/matt/
pantes anthropoi tou eidenai oregontai phusei
AppleScript: the Definitive Guide - Second Edition!
http://www.amazon.com/gp/product/0596102119
Take Control of Word 2004, Tiger, and more -
http://www.takecontrolbooks.com/tiger-customizing.html
Subscribe to TidBITS! It's free and smart. http://www.tidbits.com/ -
On 17/07/2006, at 16.38, Matt Neuburg wrote:
> On or about 7/17/06 3:38 AM, thus spake "Jakob Olesen"
> <stoklund...>:
>
>> - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)
>> object change:(NSDictionary *)change context:(void *)context
>> {
>> if ([self respondsToSelector:(SEL)context])
>> [self performSelector:(SEL)context withObject:change];
>> }
>
> By an incredible coincidence, yesterday (before this reponse
> arrived at my
> desk), after considering all the responses I'd gotten, I decided
> upon an
> approach and implemented it in my app, and it was word for word,
> letter for
> letter identical with this. m.
Cool, then I can get the SCO lawyers to help me sue you :-)
Seriously, is there any reason for checking respondsToSelector?
Wouldn't it be better to get an exception (or crash) if you mistype
the selector or somebody else registers you as an observer? -
On or about 7/17/06 7:57 AM, thus spake "Jakob Olesen" <stoklund...>:
>
> On 17/07/2006, at 16.38, Matt Neuburg wrote:
>
>> On or about 7/17/06 3:38 AM, thus spake "Jakob Olesen"
>> <stoklund...>:
>>
>>> - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)
>>> object change:(NSDictionary *)change context:(void *)context
>>> {
>>> if ([self respondsToSelector:(SEL)context])
>>> [self performSelector:(SEL)context withObject:change];
>>> }
>>
>> By an incredible coincidence, yesterday (before this reponse
>> arrived at my
>> desk), after considering all the responses I'd gotten, I decided
>> upon an
>> approach and implemented it in my app, and it was word for word,
>> letter for
>> letter identical with this. m.
>
> Cool, then I can get the SCO lawyers to help me sue you :-)
>
> Seriously, is there any reason for checking respondsToSelector?
> Wouldn't it be better to get an exception (or crash) if you mistype
> the selector or somebody else registers you as an observer?
No. That's not how notification (or, for that matter, delegation) behaves.
Both those behaviors are considered perfectly acceptable. m.
--
matt neuburg, phd = <matt...>, http://www.tidbits.com/matt/
pantes anthropoi tou eidenai oregontai phusei
AppleScript: the Definitive Guide - Second Edition!
http://www.amazon.com/gp/product/0596102119
Take Control of Word 2004, Tiger, and more -
http://www.takecontrolbooks.com/tiger-customizing.html
Subscribe to TidBITS! It's free and smart. http://www.tidbits.com/ -
On Jul 17, 2006, at 7:38 AM, Matt Neuburg wrote:
> On or about 7/17/06 3:38 AM, thus spake "Jakob Olesen"
> <stoklund...>:
>
>> - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)
>> object change:(NSDictionary *)change context:(void *)context
>> {
>> if ([self respondsToSelector:(SEL)context])
>> [self performSelector:(SEL)context withObject:change];
>> }
>
> By an incredible coincidence, yesterday (before this reponse
> arrived at my
> desk), after considering all the responses I'd gotten, I decided
> upon an
> approach and implemented it in my app, and it was word for word,
> letter for
> letter identical with this. m.
I'm just curious ... with the trivial approach above, and the other
ones involving the context being a keypath and so on ... has anyone
considered what will happen if a superclass or subclass (of whatever
class that is) is also doing KVO (on either same or different keys)
of the same objects?
Chris Kane
Cocoa Frameworks, Apple -
On Mon, 17 Jul 2006 09:09:04 -0700, Chris Kane <ckane...> said:
> On Jul 17, 2006, at 7:38 AM, Matt Neuburg wrote:
>
>> On or about 7/17/06 3:38 AM, thus spake "Jakob Olesen"
>> <stoklund...>:
>>
>>> - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)
>>> object change:(NSDictionary *)change context:(void *)context
>>> {
>>> if ([self respondsToSelector:(SEL)context])
>>> [self performSelector:(SEL)context withObject:change];
>>> }
>>
>> By an incredible coincidence, yesterday (before this reponse
>> arrived at my
>> desk), after considering all the responses I'd gotten, I decided
>> upon an
>> approach and implemented it in my app, and it was word for word,
>> letter for
>> letter identical with this. m.
>
>
> I'm just curious ... with the trivial approach above, and the other
> ones involving the context being a keypath and so on ... has anyone
> considered what will happen if a superclass or subclass (of whatever
> class that is) is also doing KVO (on either same or different keys)
> of the same objects?
I hadn't, but I presume you wouldn't be asking the question unless the
answer were, "Something bad...?" :) I guess you're hinting that one needs to
call super.
Still, isn't it the strange implementation of KVO itself that gets us into
this bind? I mean, we can call super as part of the above implementation,
but aren't you really saying that we *always* need to call super, or KVO
will break down? The docs do not mention this (though, to be fair, the
example in KVOBasics.html does in fact call super). So this is a trap that
one was always prone to fall into, regardless of how observeValue... was
implemented.
The thing I'm really wondering is: Why was KVO implemented in this oddly
bottleneck-based way? There is terrific prior art, in the form of ordinary
notifications and the notification center (broadcaster-type architecture);
is there some technical reason why KVO doesn't use that?
m.
--
matt neuburg, phd = <matt...>, <http://www.tidbits.com/matt/>
A fool + a tool + an autorelease pool = cool!
AppleScript: the Definitive Guide - Second Edition!
<http://www.amazon.com/gp/product/0596102119> -
On 7/17/06, Matt Neuburg <matt...> wrote:
> On or about 7/17/06 7:57 AM, thus spake "Jakob Olesen" <stoklund...>:
>
>> Seriously, is there any reason for checking respondsToSelector?
>> Wouldn't it be better to get an exception (or crash) if you mistype
>> the selector or somebody else registers you as an observer?
>
> No. That's not how notification (or, for that matter, delegation) behaves.
> Both those behaviors are considered perfectly acceptable. m.
Delegation and notifications actually behave completely oppositely in
this respect. If you fail to implement a delegate method then nothing
happens except the object uses the default behavior. If you fail to
implement a notification method (one you explicitly signed up for, not
one that you get "for free" as part of being a delegate) then you get
a nice runtime exception when the notification is sent.
Since this case is much more analogous to notifications than
delegation, getting the exception is probably the right approach.
Mike -
On Jul 17, 2006, at 10:49 AM, Matt Neuburg wrote:
> On Mon, 17 Jul 2006 09:09:04 -0700, Chris Kane <ckane...> said:
>> On Jul 17, 2006, at 7:38 AM, Matt Neuburg wrote:
>>
>>> On or about 7/17/06 3:38 AM, thus spake "Jakob Olesen"
>>> <stoklund...>:
>>>
>>>> - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)
>>>> object change:(NSDictionary *)change context:(void *)context
>>>> {
>>>> if ([self respondsToSelector:(SEL)context])
>>>> [self performSelector:(SEL)context withObject:change];
>>>> }
>>>
>>> By an incredible coincidence, yesterday (before this reponse
>>> arrived at my
>>> desk), after considering all the responses I'd gotten, I decided
>>> upon an
>>> approach and implemented it in my app, and it was word for word,
>>> letter for
>>> letter identical with this. m.
>>
>>
>> I'm just curious ... with the trivial approach above, and the other
>> ones involving the context being a keypath and so on ... has anyone
>> considered what will happen if a superclass or subclass (of whatever
>> class that is) is also doing KVO (on either same or different keys)
>> of the same objects?
>
> I hadn't, but I presume you wouldn't be asking the question unless the
> answer were, "Something bad...?" :) I guess you're hinting that one
> needs to
> call super.
No, I wasn't thinking that. Though having to call super could still
be a valid conclusion.
Simply calling super would be problematic, because NSObject doesn't
implement it (well, it throws).
No, I was pondering something more subtle to do with the context. The
basic "shunting" pattern here that is somewhat popular is
fundamentally broken, I think, because people are not considering
subclassing, and I wonder if the circumlocutions to avoid the problem
are worth the trouble.
> The thing I'm really wondering is: Why was KVO implemented in this
> oddly
> bottleneck-based way? There is terrific prior art, in the form of
> ordinary
> notifications and the notification center (broadcaster-type
> architecture);
> is there some technical reason why KVO doesn't use that?
I'm going to have to leave you hanging on these...
Chris Kane
Cocoa Framework, Apple -
On or about 7/17/06 11:24 AM, thus spake "Chris Kane" <ckane...>:
> Simply calling super would be problematic, because NSObject doesn't
> implement it (well, it throws).
True. But that is what the example does here:
<http://developer.apple.com/documentation/Cocoa/Conceptual/KeyValueObserving
/Concepts/KVOBasics.html#//apple_ref/doc/uid/20002252-179866>
(Interestingly, the comment in that example is completely different from the
comment in the copy on my machine.) The code shown in the example will break
if super is NSObject. So perhaps I should file a bug on the docs.
>> The thing I'm really wondering is: Why was KVO implemented in this oddly
>> bottleneck-based way? There is terrific prior art, in the form of ordinary
>> notifications and the notification center (broadcaster-type architecture); is
>> there some technical reason why KVO doesn't use that?
>
> I'm going to have to leave you hanging on these...
Ooooh, I think I like the sound of that; maybe I won't need to file a bug
after all! :) m.
--
matt neuburg, phd = <matt...>, http://www.tidbits.com/matt/
pantes anthropoi tou eidenai oregontai phusei
AppleScript: the Definitive Guide - Second Edition!
http://www.amazon.com/gp/product/0596102119
Take Control of Word 2004, Tiger, and more -
http://www.takecontrolbooks.com/tiger-customizing.html
Subscribe to TidBITS! It's free and smart. http://www.tidbits.com/ -
On Jul 17, 2006, at 2:31 PM, Matt Neuburg wrote:
> KeyValueObserving
> /Concepts/KVOBasics.html#//apple_ref/doc/uid/20002252-179866>
>
> (Interestingly, the comment in that example is completely different
> from the
> comment in the copy on my machine.) The code shown in the example
> will break
> if super is NSObject. So perhaps I should file a bug on the docs.
Actually, that was why it says "if it implements it". Now I suppose
one could argue that NSObject does implement it, but as Chris said,
it just throws. -
On Jul 17, 2006, at 1:00 PM, Scott Anguish wrote:
> On Jul 17, 2006, at 2:31 PM, Matt Neuburg wrote:>> /Concepts/KVOBasics.html#//apple_ref/doc/uid/20002252-179866>
>> <http://developer.apple.com/documentation/Cocoa/Conceptual/KeyValueObserving
>>
>> (Interestingly, the comment in that example is completely different
>> from the
>> comment in the copy on my machine.) The code shown in the example
>> will break
>> if super is NSObject. So perhaps I should file a bug on the docs.
>
> Actually, that was why it says "if it implements it". Now I
> suppose one could argue that NSObject does implement it, but as
> Chris said, it just throws.
The problem with that would be that "if it implements it" can change
over time. Next month you change a superclass of the class to start
listening for KVO notifications (perhaps you install a software update
of the OS!), and ... your hard-coded knowledge of what the super class
did and didn't do is no longer true.
You can't just blindly call super, and face the exception (hmm, is the
exception there for a reason? could be a clue), but if you don't you
cut off your superclasses from receiving necessary KVO notifications,
given the design.
Nobody's figured out the tricky problem yet ....
[I didn't design KVO, I'm just a bystander.]
Chris Kane
Cocoa Frameworks, Apple -
On 17/07/2006, at 18.09, Chris Kane wrote:
> I'm just curious ... with the trivial approach above, and the other
> ones involving the context being a keypath and so on ... has
> anyone considered what will happen if a superclass or subclass (of
> whatever class that is) is also doing KVO (on either same or
> different keys) of the same objects?
Well, not until you brought it up.
If you are subclassing a foreign class without documented behavior,
the context pointer is useless. Basically you don't know if the
superclass is already observing an object, so the context pointer
could be mine or the superclass', who knows?
Further, if you are expecting to be subclassed, you must document
exactly which objects and key paths you are observing, otherwise the
subclass can accidentally overwrite your context pointer. (What
happens if you call addObserver twice on the same object/key path?)
Quick test: you get two callbacks, calling removeObserver removes
them one at a time, last in first out. (linked list?)
Calling addObserver twice with identical arguments (incl. context)
also gives you two callbacks.
So, if you don't trust your subclasses and superclasses, you are
given a void pointer that you may or may not have created yourself.
That is very close to useless.
This is what I would do, if I was writing class intended for
subclassing: Use
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)
object change:(NSDictionary *)change context:(void *)context
{
[self performSelector:(SEL)context withObject:[NSNotification
notificationWithName:keyPath object:object userInfo:change]];
}
and tell subclasses that they can depend on it being so. Tell
subclasses to not call super with a context pointer that is not a
valid selector. Know that one change may cause multiple callbacks.
When writing a subclass: Ignore context completely, dispatch on
keyPath and/or object
Alternative solution: Create your own, private helper object and
register that instead.
@interface KVOHelper {
id owner;
}
@end
@implementation KVOHelper
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)
object change:(NSDictionary *)change context:(void *)context
{
[owner performSelector:(SEL)context withObject:[NSNotification
notificationWithName:keyPath object:object userInfo:change]];
}
@end
Don't tell anyone about that object, and you'll be fine.
The KVO API expects observers to have a single personality, so that
is probably the way to go if you are in the middle of a class hierarchy. -
On or about 7/17/06 1:42 PM, thus spake "Chris Kane" <ckane...>:
> On Jul 17, 2006, at 1:00 PM, Scott Anguish wrote:>>> /Concepts/KVOBasics.html#//apple_ref/doc/uid/20002252-179866>
>> On Jul 17, 2006, at 2:31 PM, Matt Neuburg wrote:
>>> <http://developer.apple.com/documentation/Cocoa/Conceptual/KeyValueObserving
>>>
>>> (Interestingly, the comment in that example is completely different
>>> from the
>>> comment in the copy on my machine.) The code shown in the example
>>> will break
>>> if super is NSObject. So perhaps I should file a bug on the docs.
>>
>> Actually, that was why it says "if it implements it". Now I
>> suppose one could argue that NSObject does implement it, but as
>> Chris said, it just throws.
>
> The problem with that would be that "if it implements it" can change
> over time. Next month you change a superclass of the class to start
> listening for KVO notifications (perhaps you install a software update
> of the OS!), and ... your hard-coded knowledge of what the super class
> did and didn't do is no longer true.
>
> You can't just blindly call super, and face the exception (hmm, is the
> exception there for a reason? could be a clue), but if you don't you
> cut off your superclasses from receiving necessary KVO notifications,
> given the design.
And there's a memory management problem. The "context" is not automatically
retained. Therefore if ClassA provided a context, only ClassA knows whether
that context needs to be released (or what it means). Gosh, this looks
troublesome on *any* implementation.
So ClassB needs a way to distinguish the notifications that belong to it (so
that it can pass along those that don't). The only way I can think of to do
this is to include an unmistakeable token in the context. But what would
this be - a hard-coded string? Yuck. m.
--
matt neuburg, phd = <matt...>, http://www.tidbits.com/matt/
pantes anthropoi tou eidenai oregontai phusei
AppleScript: the Definitive Guide - Second Edition!
http://www.amazon.com/gp/product/0596102119
Take Control of Word 2004, Tiger, and more -
http://www.takecontrolbooks.com/tiger-customizing.html
Subscribe to TidBITS! It's free and smart. http://www.tidbits.com/ -
On Jul 17, 2006, at 2:16 PM, Jakob Olesen wrote:
> On 17/07/2006, at 18.09, Chris Kane wrote:
>
>> I'm just curious ... with the trivial approach above, and the other
>> ones involving the context being a keypath and so on ... has
>> anyone considered what will happen if a superclass or subclass (of
>> whatever class that is) is also doing KVO (on either same or
>> different keys) of the same objects?
>
> Well, not until you brought it up.
>
> If you are subclassing a foreign class without documented behavior,
> the context pointer is useless. Basically you don't know if the
> superclass is already observing an object, so the context pointer
> could be mine or the superclass', who knows?
>
> Further, if you are expecting to be subclassed, you must document
> exactly which objects and key paths you are observing, otherwise the
> subclass can accidentally overwrite your context pointer. (What
> happens if you call addObserver twice on the same object/key path?)
>
> Quick test: you get two callbacks, calling removeObserver removes
> them one at a time, last in first out. (linked list?)
> Calling addObserver twice with identical arguments (incl. context)
> also gives you two callbacks.
>
> So, if you don't trust your subclasses and superclasses, you are
> given a void pointer that you may or may not have created yourself.
> That is very close to useless.
>
> This is what I would do, if I was writing class intended for
> subclassing: Use
>
> - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)
> object change:(NSDictionary *)change context:(void *)context
> {
> [self performSelector:(SEL)context withObject:[NSNotification
> notificationWithName:keyPath object:object userInfo:change]];
> }
>
> and tell subclasses that they can depend on it being so. Tell
> subclasses to not call super with a context pointer that is not a
> valid selector. Know that one change may cause multiple callbacks.
>
> When writing a subclass: Ignore context completely, dispatch on
> keyPath and/or object
You and Matt Neuburg and Jim Correia (privately) have all now
identified all the parts of the problem and the solution. But I don't
like the "sub/superclasses can't use whatever context they like" that
results from your "document the behavior and requirements" approach.
So I'll present a slightly different solution.
When you and a superclass both register for KVO notifications, the
registrations are necessarily distinct, and you will potentially
receive multiple calls to observeValueForKeyPath:... for the same
changes, some due to your superclass, some due to your registrations.
The ones for you you need to process and not pass up to super. The
ones not for you you need to pass up to super and not process (that
is, not use or expect anything about the context). You cannot look at
the keypath, recognize it, and say "this is for me" and consume it,
because the keypath may be interesting to the superclass too, but you
can't pass all of those along either, because the superclass may not
be interested (and is certainly not interested in your context if this
invocation happens to contain your context). Similarly keypath+object
is not reliable to distinguish "mine" from "somebody else's".
The solution is that the context pointer must be used to provide a
globally unique value that you can recognize in your class. If you
recognize the value, this notification is for you, otherwise you pass
the method call up to super and return. You have to use a value that
the other classes in the hierarchy won't (or can't) use.
You cannot use NULL as the context pointer, because the superclass (or
a subclass) might also use NULL. NULL is a shared value. You can't
use selectors, either, because their values are global to the process,
and a sub or superclass could potentially use the same one. Plus,
recognizing many possible selector values would be a pain and time
consuming. It's better to pick one context value.
Matt Neuburg said:
> So ClassB needs a way to distinguish the notifications that belong
> to it (so
> that it can pass along those that don't). The only way I can think
> of to do
> this is to include an unmistakeable token in the context. But what
> would
> this be - a hard-coded string? Yuck. m.
You can't include something *inside* the context, because you can't
safely look through the pointer at anything in particular, as the
pointer might not be pointing to the data structure you know about.
You have to generate a unique pointer value and act based on that. An
== comparison is dirt cheap if you can do it.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)obj
change:(NSDictionary *)change context:(void *)ctx {
if (ThisClassUniqueObservationContext != ctx) {
[super observeValueForKeyPath:keyPath ofObject:obj
change:change context:ctx];
return;
}
if ([keyPath isEqual:@"isBrillig"]) {
... and so on ...
}
... and so on ...
}
If you assume that @"" string constants are NOT globally uniqued in a
binary, but only to a given compilation unit, then
ThisClassUniqueObservationContext could be a constant string:
static NSString * ThisClassUniqueObservationContext = @"MyContext";
That's not necessarily a safe assumption, however. The following is
certainly bad, since CStrings are uniqued and shared within a binary:
static const char * ThisClassUniqueObservationContext = "MyContext";
What *I* have done when I needed to deal with this is create some
useless but unique data and use its address:
static char __MyContext = 0;
static char *ThisClassUniqueObservationContext = &__MyContext;
Note that if this were "const", that could thwart things, since
integer constants might also be uniqued and shared by the linker.
What could still happen is that the compiler could notice that this
data is static (local to this compilation unit) and never stored to
anywhere in the compilation unit and nothing ever writes through the
address to it (or ThisClassUniqueObservationContext) or passes the
address to a function or whatever, and decide in an optimizing kind of
way to make the data const for me, and possibly subject it to linker
uniquing. But that possibility doesn't keep me up at night.
But whatever way you dice it, "yuck". Usually I've not wanted the
context pointer for anything and just wanted to use NULL ... but for
this potential problem.
> Alternative solution: Create your own, private helper object and
> register that instead.
> [...]
> The KVO API expects observers to have a single personality, so that
> is probably the way to go if you are in the middle of a class
> hierarchy.
I've done that solution too. There you've made the observer different
rather than the context different. I've also used that to work around
"can't register self for notifications about self" issue, which should
someday be fixed. Once an object can register itself to receive
notifications about itself, this class/superclass/subclass collision
issue will probably become more common in practice than it is now.
Chris Kane
Cocoa Frameworks, Apple -
On Jul 17, 2006, at 9:00 PM, Chris Kane wrote:
> The solution is that the context pointer must be used to provide a
> globally unique value that you can recognize in your class. If you
> recognize the value, this notification is for you, otherwise you
> pass the method call up to super and return. You have to use a
> value that the other classes in the hierarchy won't (or can't) use.
>
> You cannot use NULL as the context pointer, because the superclass
> (or a subclass) might also use NULL. NULL is a shared value. You
> can't use selectors, either, because their values are global to the
> process, and a sub or superclass could potentially use the same
> one. Plus, recognizing many possible selector values would be a
> pain and time consuming. It's better to pick one context value.
So one potential solution here is to allocate a dictionary, use that
as the context for your class, and also use it as a dispatch table.
Since the dictionary is dynamically allocated using it as the void *
context will guarantee uniqueness in the same way as Chris' "ugly"
solution in so far as all addresses within the address space are
unique. It won't guarantee uniqueness if anyone else uses arbitrary
context pointers like this:
static void *ObservationContext = (void *)2091;
The observer would look something like this:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)
object change:(NSDictionary *)change context:(void *)context
{
if (context == dispatchDictionary) {
SEL selector = NSSelectorFromString([dispatchDictionary
objectForKey: keyPath]);
// dispatch to self based on the selector looked up in the table
} else {
[super observeValueForKeyPath: keyPath ofObject: object change:
change context: context];
}
}
While this solves the problem Matt wanted to solve, there is still a
fair bit of ugliness and hand waving going on.
You have to make sure you have one dispatch table per class. You have
to keep the dispatch table up to date (as observers are registered).
Your class has to know about the dispatch table and correctly
implement -observeValueForKeyPath:ofObject:change:context:. It is
left as an exercise for the reader to accomplish this correctly,
without cut and paste programming.
If I've glossed over something (besides the intentional hand waving),
hopefully someone will point it out.
Jim -
On 18/07/2006, at 3.00, Chris Kane wrote:
> You and Matt Neuburg and Jim Correia (privately) have all now
> identified all the parts of the problem and the solution.
Well, not really. Note that removeObserver:forKeyPath: does not take
a context argument. It just removes the last context added. This
means it is not safe to remove yourself as an observer until dealloc
(or didTurnIntoFault). You also need to trust that sub- and
superclasses will do the same.
Otherwise you have to use a helper object.
> The solution is that the context pointer must be used to provide a
> globally unique value that you can recognize in your class. If you
> recognize the value, this notification is for you, otherwise you
> pass the method call up to super and return. You have to use a
> value that the other classes in the hierarchy won't (or can't) use.
This is of course the way to go, but in the process we have
unanswered Matt's original question, "How are you guys doing the
dispatch thing?"
...and it is really painful to waste a perfectly good void pointer on
multiple personality resolution. :-)
How about this:
static SEL dispatch[10]; // make it big enough or die violently
static void* sel2ctx(SEL s)
{
int i;
for (i=0; i<sizeof(dispatch)/sizeof(dispatch[0]); i++) {
if (!dispatch[i]) dispatch[i]=s;
if (s==dispatch[i]) return dispatch+i;
}
abort(); // I told you...
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)
object change:(NSDictionary *)change context:(void *)context
{
SEL *entry = (SEL*)context;
if (entry>=dispatch && entry<dispatch+sizeof(dispatch)/sizeof
(dispatch[0])) {
[self performSelector:*entry withObject:...];
}
else {
[super observeValueForKeyPath:keyPath ofObject:object
change:change context:context];
}
}
fgrep -c @selector MyClass.m would be an upper bound on the necessary
size of the dispatch table
Oh, and you would have to pre-fill the table if you are
multithreaded. Call sel2ctx in +initialize or use an initializer list:
sed -n 's/^.*\(@selector([^)]*)\).*$/ \1,/p' MyClass.m | sort -u
This is not just a matter of performance, also convenience. You
already have to keep your addObserver and removeObserver calls in
sync. Adding custom dispatch code to observeValueForKeyPath makes it
even harder to maintain.
Also, keyPath+object is not always so great for deciding what to do.
Imagine observing the same keyPath on objects in two collections. You
have to check the collections. Now imagine the same object present in
both collections. You get two identical callbacks.
So what is the right thing to do? It depends.
For a heavy-weight class (few instances, lots of state) use a helper
object.
If you need removeObserver outside dealloc/didTurnIntoFault, use a
helper object.
If you need to observe self, use a helper object.
For a light-weight class (lots of instances, little state) use a
unique context and custom dispatch if it can be kept simple,
otherwise a dispatch table.
If you are working alone and writing a non-reusable NSObject
subclass, just use a selector directly. (Don't complicate things
until you have to).
> You can't include something *inside* the context, because you can't
> safely look through the pointer at anything in particular, as the
> pointer might not be pointing to the data structure you know
> about. You have to generate a unique pointer value and act based
> on that. An == comparison is dirt cheap if you can do it.
I have seen Windows code ask the virtual memory manager if an address
is readable, then check for a magic value. Messy and not 100% safe,
but possible.
> If you assume that @"" string constants are NOT globally uniqued in
> a binary, but only to a given compilation unit, then
> ThisClassUniqueObservationContext could be a constant string:
>
> static NSString * ThisClassUniqueObservationContext = @"MyContext";
CFSTR() does global uniqueing, at least in the open-sourced version,
but there is also a mystery __builtin___CFStringMakeConstantString
implementation.
> Note that if this were "const", that could thwart things, since
> integer constants might also be uniqued and shared by the linker.
Really? I thought only C++ allowed that. C99? After all you are
passing a pointer to your constant outside the compilation unit.
> But whatever way you dice it, "yuck". Usually I've not wanted the
> context pointer for anything and just wanted to use NULL ... but
> for this potential problem.
I agree.
NSNotificationCenter causes less trouble by taking a selector instead
of a void pointer. It still has the problem with removing observers,
though. removeObserver:name:object: does not take a selector
argument, so it is the same mess if you try to unregister before
dealloc. -
On Jul 17, 2006, at 9:00 PM, Chris Kane wrote:
> If you assume that @"" string constants are NOT globally uniqued in
> a binary, but only to a given compilation unit, then
> ThisClassUniqueObservationContext could be a constant string:
>
> static NSString * ThisClassUniqueObservationContext = @"MyContext";
Even if the strings are globally uniqued across the entire binary,
assuming a FBFroobazle class, something like this is probably good
enough:
static NSString * FBFroobazzleValueObservationContext =
@"FBFroobazzleValueObservationContext";
Certainly better than
static void * FBFroobazzleValueObservationContext = (void *)2192;
There is the chance that someone else will use the string
@"FBFroobazzleValueObservationContext" as an observation context
elsewhere in the inheritance tree, but I think that a) it would be a
programmer error b) is probably not worth losing any sleep over.
And there is certainly a smaller chance of a collision than with an
arbitrarily picked number.
Jim -
> FROM : George Orthwein
> DATE : Sat Jul 15 07:36:10 2006
>
> For a custom view that responds to a few dozen attributes of the
> controller's current selection....
>
> - (void)observeValueForKeyPath:(NSString *)keyPath
> ofObject:(id)object
> change:(NSDictionary *)change
> context:(void *)context
> {
> NSString *keyPathName;
> if ([keyPath isEqualToString:@"selection.bgImage.color"] == YES){
> // handle special case of this attribute of a relationship
> keyPathName = @"bgImageColor";
> } else {
> // convert "selection.attribute" keypath to appropriate accessor
> name -- by abusing pathExtension method :)
> keyPathName = [keyPath pathExtension];
> }
> [self setValue:[object valueForKeyPath:keyPath] forKey:keyPathName];
> }
>
> So observing "selection.attribute" will call my setAttribute accessor
> with the new value via KVC.
Interestingly, though (or perhaps not, for those who already realized this),
if what you are planning to do when you get notified thru observeValue... is
set a corresponding ivar, it might be much simpler to avoid the entire
problem and use bind:... instead of addObserver:... at the outset.
This goes back to my earlier thread, "two-way bindings and one-way
bindings":
<http://www.cocoabuilder.com/archive/message/cocoa/2006/7/6/166949>
The point there was that if I say bind:..., I get a "one-way" binding, which
in effect means that I start observing the thing I'm bound to. In that
thread I was concentrating on the downside, which is that the thing I'm
bound to does not automatically also start observing me; but now let's talk
about the upside. The upside is that my ivar gets set automatically when the
other object's observed attribute changes - without ever passing through
observeValue...!
So, if what you were going to do was call setValue:forKey: on your own ivar,
you could skip the whole addObserver: thing and just use bind:, because
that's exactly what then *will* happen; setValue:forKey: will be called on
your ivar.
Moreover, if you wanted to do anything else when this happens, you can
implement the accessors to your ivar and do it there.
Moreover, in the case of something like an NSController's "content", this
approach works, when ordinary observing with addObserver: does not. As has
been pointed out several times recently, the change dictionary passed to
observeValue... is empty. But if you start with bind:, it works great; the
new value just gets handed to you directly.
Thus, taking advantage of the aspect of bind:... that you get "for free"
could constitute an economical workaround to the observeValueForKeyPath
bottleneck. Of course this is officially a misuse of bindings, but on the
other hand we've already agreed that the observeValueForKeyPath bottleneck
architecture is wretched, so one may be forgiven for thinking, "any port in
a storm..." m.
--
matt neuburg, phd = <matt...>, <http://www.tidbits.com/matt/>
A fool + a tool + an autorelease pool = cool!
AppleScript: the Definitive Guide - Second Edition!
<http://www.amazon.com/gp/product/0596102119> -
On Jul 18, 2006, at 8:44 PM, Matt Neuburg wrote:
> Interestingly, though (or perhaps not, for those who already
> realized this),
> if what you are planning to do when you get notified thru
> observeValue... is
> set a corresponding ivar, it might be much simpler to avoid the entire
> problem and use bind:... instead of addObserver:... at the outset.
Thanks for this idea, though I actually arrived at my current
observeValueForKeyPath: implementation because I had initially using
bindings! :)
http://www.cocoabuilder.com/archive/message/cocoa/2006/4/17/161225
I already had all the setters there that were called with
bind:toObject:withKeyPath:options: so when I switched to KVO, it
naturally led into trying to call them from my
observeValueForKeyPath: method.
One nice thing is that I can switch back and forth easily just by
swapping out:
[sender bind:@"attributeName" toObject:docPrefsController
withKeyPath:@"selection.attributeName" options:nil];
for
[docPrefsController addObserver:sender
forKeyPath:@"selection.attributeName" options:nil context:NULL];
As you point out, one passes through observeValueForKeyPath: and one
calls the accessors directly.
The main reason I switched away from using bind: was that I found my
setters getting called something like a dozen times when the binding
was first initiated with bind:toObject:withKeyPath:options:. Perhaps
there's a way to coalesce those initial updates. I found it was
bogging down the initial display of the view since it triggered a
dozen or so redraws.
(Ok, I just double checked this and I'm only seeing a single initial
call now when I use bind:! Not sure what I was seeing before because
in my notes I even mention some accessors getting called 24 times! I
probably setup something wrong at some point, though I thought I had
also seen this mentioned in the archives somewhere. More
investigation is in order....)
One thing that tripped me up when I switched to KVO is that
addObserver:forKeyPath:options:context: does NOT send the current
value on initial registration. This means that to sync the view
initially I have to enumerate through the relevant attributes and
call setValue:forKey: manually.
http://www.cocoabuilder.com/archive/message/cocoa/2006/5/20/163920
> The point there was that if I say bind:..., I get a "one-way"
> binding, which
> in effect means that I start observing the thing I'm bound to. In that
> thread I was concentrating on the downside, which is that the thing
> I'm
> bound to does not automatically also start observing me; but now
> let's talk
> about the upside. The upside is that my ivar gets set automatically
> when the
> other object's observed attribute changes - without ever passing
> through
> observeValue...!
>
> So, if what you were going to do was call setValue:forKey: on your
> own ivar,
> you could skip the whole addObserver: thing and just use bind:,
> because
> that's exactly what then *will* happen; setValue:forKey: will be
> called on
> your ivar.
True. I guess if you want to avoid the KVO "bottleneck" you can just
use "one way" bindings instead!
> Moreover, in the case of something like an NSController's
> "content", this
> approach works, when ordinary observing with addObserver: does not.
> As has
> been pointed out several times recently, the change dictionary
> passed to
> observeValue... is empty. But if you start with bind:, it works
> great; the
> new value just gets handed to you directly.
Is this right?. If you bind to "arrangedObjects" the entire array is
sent each time it is modified. I tried binding to "content" and
wasn't getting any updates on modifications so maybe my test was
wrong anyhow. But it doesn't look like you can get just the change/
removed values using bind.
> Of course this is officially a misuse of bindings,
I don't think implementing "one way" or "read only" bindings is a
misuse, though it almost seems that way since there aren't any
examples or mentions of it in the docs that I remember seeing. I
think I'll file a doc request.
It does seem to be a sly way of avoiding the observeValueForKeyPath
bottleneck though.
I think I better add a disclaimer since many details of this whole
thing are still fuzzy in my head: Anything stated above may be
completely wrong. There, I feel better. ;)
George -
On Jul 20, 2006, at 3:25 PM, George Orthwein wrote:
> I don't think implementing "one way" or "read only" bindings is a
> misuse, though it almost seems that way since there aren't any
> examples or mentions of it in the docs that I remember seeing. I
> think I'll file a doc request.
binding between model objects isn't recommended at all. the bindings
doc is clear about how we recommend doing it (controller to model,
view to controller.. it would be useful to extend that to the
occasional view to model as well though, I'll add it to the list).
We don't say how we don't recommend doing it in general though..
> It does seem to be a sly way of avoiding the observeValueForKeyPath
> bottleneck though.
-
On Thu, 20 Jul 2006 16:14:51 -0400, Scott Anguish <scott...> said:
>
> On Jul 20, 2006, at 3:25 PM, George Orthwein wrote:
>
>> I don't think implementing "one way" or "read only" bindings is a
>> misuse, though it almost seems that way since there aren't any
>> examples or mentions of it in the docs that I remember seeing. I
>> think I'll file a doc request.
>
> binding between model objects isn't recommended at all. the bindings
> doc is clear about how we recommend doing it (controller to model,
> view to controller.. it would be useful to extend that to the
> occasional view to model as well though, I'll add it to the list).
However, those terms don't carry all that much meaning in real life. They
are useful as conceptual notions of the role a class is to play in the app,
but what exactly each of those things *is* becomes very muddy in actual
practice.
For example, I'm using bind: to cause my main app controller, MyObject, to
have its "paths" ivar synced with a "paths" value supplied (thru manual KVO)
by a matrix subclass, MyMatrix. Would you try to claim that this is wrong?
Why? On the grounds that MyMatrix isn't a view? (It's a NSMatrix! It's
sitting right in a window!!) Would you try to claim that MyObject isn't a
controller? (Its job is exactly to coordinate the mediation between data and
interface; if that isn't a controller, what is?) And yet formally there
isn't the slightest difference between this and using bind: to cause an
arbitrary ClassA to observe an ivar in an arbitrary ClassB.
Or, let's look it another way: as I pointed out in an earlier thread, when
you say bind: to bind MyObject to the NSUserDefaultsController, nothing
special happens; NSUserDefaultsController isn't any more bind-savvy than
MyObject, and the binding is still one-way only (it's just the observing
part, in one direction). Yet NSUserDefaultController is an NSController
subclass and has "controller" in its name. In fact, I've even done a
double-bind, binding from MyObject to NSUserDefaultController, and from
NSUserDefaultController's "values" back to MyObject, using the same key, to
create a true two-way binding so that the keys are in constant sync in both,
and nothing bad has happened. In fact, what happened is good! It's a
binding!
So it seems to me wrong on the facts and wrong on the law to say you can't
use bind: like this. It's wrong on the facts because it works great, whereas
the addObserver: / observeKeyValue... thing (as posters on this thread and
others have already pointed out) doesn't work particularly well. It's wrong
on the law because it is plainly *meant* to work, creating a nice
automatically maintained pairing between a key in one object and a key in
another object, whereas the addObserver: / observeKeyValue... thing is an
architectural nightmare for your code.
(Anyway, you don't say not to use the addObserver: / observeKeyValue...
thing between two model objects. Yet I fail to see the difference between
this and bind: - the effect is just the same, except that bind: works
better.)
In short, I have come to see the warning against bind: as a kind of canard;
it is a cute but purely theoretical grumble. In actual practice, I am now
using bind: like crazy and my code has been wonderfully streamlined. Once
we're past awakeFromNib, where all the bind: commands are given (wherever a
binding could not be set up in the nib, of course), MyObject becomes
self-contained; it finds within its own ivars all the values of all the bits
of the interface, and it can change any of those bits by setting one of
those ivars. No outlets to the view, no consultation of the view as if it
contained data (i.e. as if it were a model). Isn't that exactly what
bindings are for??? m.
--
matt neuburg, phd = <matt...>, <http://www.tidbits.com/matt/>
A fool + a tool + an autorelease pool = cool!
AppleScript: the Definitive Guide - Second Edition!
<http://www.amazon.com/gp/product/0596102119> -
in my original reply, when I said controller I meant specifically
NS*Controller
On Jul 20, 2006, at 9:57 PM, Matt Neuburg wrote:
> For example, I'm using bind: to cause my main app controller,
> MyObject, to
> have its "paths" ivar synced with a "paths" value supplied (thru
> manual KVO)
> by a matrix subclass, MyMatrix. Would you try to claim that this is
> wrong?
as much as I can tell here, yes.
The recommendation is that bindings should not be used between two
model objects. Binding views directly to model objects should only be
done (and even then it isn't something that is currently suggested)
when there is no updating of the values due to changes in the U
(essentially read-only).
> In short, I have come to see the warning against bind: as a kind of
> canard;
> it is a cute but purely theoretical grumble.
We've talked at length with engineering about it, and it isn't
something that we suggest doing.
> Isn't that exactly whatobjects
> bindings are for??? m.
>
bindings are intended to eliminate glue code between views and model
KVO is the better solution. if the observeValeForKeyPath: method is
a bottleneck or results in ugly code it is quite possible to
encapsulate that observing code into an object... -
On Jul 21, 2006, at 4:11 AM, Scott Anguish wrote:
> in my original reply, when I said controller I meant specifically
> NS*Controller
So the only recommended application of "one way" bindings would be
between an NS*Controller and an NSView subclass, or also possibly
between an NS*Controller and a model class?
That's good to know and I don't think it's come up in past
discussions, or at least stated as plainly. The lack of any mention
of "one way" bindings is what I was referring to in the docs...
though it now seems that there is only a specific application for
them anyway. (Well, NS*Controller and a custom view should be pretty
common....)
It's a good point for this discussion though, since we seemed to be
settling in on:
> True. I guess if you want to avoid the KVO "bottleneck" you can
> just use "one way" bindings instead!
A general statement like doesn't work, since KVO can be used in
situations where "one way" bindings aren't recommended (Matt's
position notwithstanding :) ).
Thanks,
George -
On Jul 21, 2006, at 11:46 AM, George Orthwein wrote:
> On Jul 21, 2006, at 4:11 AM, Scott Anguish wrote:
>> in my original reply, when I said controller I meant specifically
>> NS*Controller
>
> So the only recommended application of "one way" bindings would be
> between an NS*Controller and an NSView subclass, or also possibly
> between an NS*Controller and a model class?
I'm not sure what you mean by "one-way". We have what are called
"read-only" bindings (where the attribute that you've bound to an
object will not cause any changes in the UI to be propagated back to
the object).
>
> That's good to know and I don't think it's come up in past
> discussions, or at least stated as plainly. The lack of any mention
> of "one way" bindings is what I was referring to in the docs...
> though it now seems that there is only a specific application for
> them anyway. (Well, NS*Controller and a custom view should be
> pretty common....)
>
> It's a good point for this discussion though, since we seemed to be
> settling in on:
>
>> True. I guess if you want to avoid the KVO "bottleneck" you can
>> just use "one way" bindings instead!
just to ensure clarity. this is not my position and I didn't write
that.
>
> A general statement like doesn't work, since KVO can be used in
> situations where "one way" bindings aren't recommended (Matt's
> position notwithstanding :) ).
Regardless of matt's position, it isn't recommended.


