Re: Bindings - registering change notification for multiple keys
Re: Bindings - registering change notification for multiple keys
- Subject: Re: Bindings - registering change notification for multiple keys
- From: Ken Thomases <email@hidden>
- Date: Wed, 2 Jul 2008 03:16:12 -0500
On Jul 1, 2008, at 1:38 PM, dreamcat7 wrote:
I would like to to save the changed items permanently in my data
store after the user interacts with the ui. Control elements are
bound to a settings NSDictionary in my data model.
For any bindings notifications, the following method
- (void)registerAsObserver
{
[model addObserver:self forKeyPath:@"prefs.debug"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:NULL];
}
will only register for observing one value at a time. However I may
have a wide variety of controls and settings that need to trigger
the same code to write out the dictionary to the plist file (or to
any arbitrary data storage mechanism i might use). And cant write
anything like @"prefs.*" because this syntax does not constitute a
valid key-path.
And if there is any way to listen for all values within a path or set.
This is one of the pitfalls of using an NS(Mutable)Dictionary as a
model object, rather than as merely an implementation detail of one of
your model objects.
Given the code snippet you provided, your model contains at least two
objects: the one you refer to as "model" and a dictionary. "model"
has a property named "prefs" which is a to-one relationship to the
dictionary. So, your "model" does not have a property regarding the
"debug" value, it only has a relationship to an object with such a
property.
As a model object, NSMutableDictionary is somewhat problematic. It is
only KVO-compliant if it is modified with setValue:forKey:. Any
change made via setObject:forKey:, the NSMutableDictionary primitive
method, is not broadcast to observers.
More significantly, one of the central tenets of OOP is
encapsulation. You, the model class programmer, need to always be in
charge of access to your objects' data. If somebody wishes to modify
that data, they need to invoke your custom code as the gatekeeper.
With a custom model class, this is straightforward. However, if you
expose an NSMutableDictionary as part of your model, rather than
keeping it a private encapsulated part of the model's implementation,
then client code will only have to ask your model for that mutable
dictionary and after that can bypass your custom code. This is true
even if they use setValue:forKeyPath: to perform the modification. In
that case, your custom code will have its -prefs method invoked, but
none of your methods will be invoked when the dictionary is actually
modified.
Put another way, your custom code is able to act as a gatekeeper for
gaining access to the mutable dictionary, but once it hands out that
access there is no longer any opportunity for your custom code to
exercise control over, or respond to, what a client does with that
mutable dictionary.
Here's what I recommend: either 1) demote the NSMutableDictionary
which is currently the "prefs" property of "model" to an
implementation detail, and make its keys into properties of "model"
itself, or 2) promote the model state which is currently maintained in
the NSMutableDictionary to a proper model object by wrapping it in a
custom class (say, "Preferences") -- again, making the dictionary an
implementation detail -- and making the keys into properties of the
Preferences class. In either case, there should be not key path which
directly accesses the NSMutableDictionary without going through a
custom setter of your own making.
Now, how to deal with all those properties? Well, do you really need
for the set of properties to be dynamic, or is it in fact static and
you're just annoyed by the size (and the attendant repetitive work)?
If you really need for there to be a dynamic set of properties, you
can accomplish that using valueForUndefinedKey: and
setValue:forUndefinedKey:. Basically, you have "virtual" properties
-- they won't have proper individual accessors, but the above-named
methods will be invoked whenever anything tries to access them via
KVC, and in your implementations you can simulate their existence (by
accessing the NSMutableDictionary which is part of your
implementation, for example).
Whether the set of properties is static or dynamic, it's troublesome
to observe them all. One possible solution for that is to use a
synthetic property which represents the state of the whole set of
properties. This property exists solely so that clients of the class
may watch for changes in this multi-property state. It has no
meaningful value. That is, clients may observe it, but they'll never
really care what its value is, only when it "changes".
Let's suppose you go the route of creating a Preferences class. In
that case, let's call the synthetic property "anyPreferences". It's
read-only, so we just need a simple getter:
-(id)anyPreferences
{
return self;
// The choice to return self is essentially arbitrary
// We could instead return a different NSObject each time: return
[[[NSObject alloc] init] autorelease];
// That would guarantee that any attempt to compare the "old" and
"new" values of this property would indicate a difference,
// but it could cause a spike in memory usage.
}
Now, classes can observe an instance of Preferences for changes in its
"anyPreferences" property. But under what circumstance would they be
notified of a change in that property? Well, the normal ones.
Something should arrange for will/didChangeValueForKey: to be called
on the instance of Preferences with @"anyPreferences" as the parameter.
In the case of a static set of properties, the best thing to do is
implement +keyPathsForValuesAffectingAnyPreferences in the Preferences
class and return a set of the names of all of those properties:
+keyPathsForValuesAffectingAnyPreferences
{
return [NSSet setWithObjects:@"debug", /* list the other preference
names here... */, nil];
}
(If you're targeting Tiger, you would instead use
+setKeys:triggerChangeNotificationsForDependentKey: to accomplish this
same thing:
+(void) initialize
{
if (self == [Preferences class])
{
[self setKeys:[NSArray arrayWithObjects:@"debug", /* list the other
preference names here... */, nil]
triggerChangeNotificationsForDependentKey:@"anyPreferences"];
}
}
)
By doing this, you arrange for the KVO machinery itself to generate
the necessary will/didChangeValueForKey:@anyPreferences" calls
whenever you modify one of the other properties in a KVO-compliant
manner.
The above approach won't work for a dynamic set of properties. Notice
that +keyPathsForValuesAffectingAnyPreferences is a class method (as
is +initialize, where
+setKeys:triggerChangeNotificationsForDependentKey: is called). It
must do its work without reference to any specific instance of the
Preferences class, so it can't learn what the dynamic set of
properties supported by such an instance is. In that case, make your
implementation of setValue:forUndefinedKey: manually trigger
notifications of change in the "anyPreferences" property, by
explicitly invoking will/didChangeValueForKey: before and after
performing the actual modification.
So i am looking at other ways. Can you listen for all objects held
by an NSArray ?
Sort of. You can observe a to-many property (which is often
implemented as an NS(Mutable)Array) of an object. Keep in mind,
though, to only modify the to-many relationship in KVO-compliant
fashion.
Of course there is the method setKeys:
triggerChangeNotificationsForDependentKey: however i did find all
the explanations about this really difficult to follow.
Basically, it informs the KVO machinery of a relationship among the
properties of the class upon which it's called. It says that a
certain property, named by the dependentKey parameter, should be
considered to have changed whenever any of the other named properties
changes. Where I used it above, I was telling KVO that the
"anyPreferences" property should be considered to have changed
whenever the "debug" property changes. This causes KVO to send out
its change notifications for "anyPreferences" even though nothing
specifically modified that property (i.e. nobody ever is likely to
invoke [somePreferencesInstance setValue:@"blah"
forKey:@"anyPreferences"]).
Awhile back i tried doing with the apple example but unfortunately
no luck.
Dunno what problems you had, so can't clear them up. :)
And don't the array of triggering keys need to be first registered
individually anyway ? (perhaps thats why it failed to work for me).
No. Properties aren't "registered", really. KVC/KVO have various
documented ways of trying to access a property when required. They do
this on demand at run-time. There isn't a list anywhere of the
properties for a class. If an attempt to access the property doesn't
find accessor methods or instance variables, they fall back onto the
"undefined key" methods I mentioned earlier. The default
implementations of those methods raise an exception, which is the only
indication that a property doesn't exist. If you override those
methods to do something else, then you can extend the set of
properties in any fashion you care to imagine -- again, dynamically at
run-time.
I know this is a lot to take in. I'm verbose like that. Sorry. ;)
I hope it's helpful, though.
Cheers,
Ken
_______________________________________________
Cocoa-dev mailing list (email@hidden)
Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com
Help/Unsubscribe/Update your Subscription:
This email sent to email@hidden