• If you are citizen of an European Union member nation, you may not use this service unless you are at least 16 years old.

  • Stop wasting time looking for files and revisions. Connect your Gmail, DriveDropbox, and Slack accounts and in less than 2 minutes, Dokkio will automatically organize all your file attachments. Learn more and claim your free account.

View
 

Runtime-Key Value Coding

Page history last edited by Josh Huckabee 10 years, 8 months ago

Key Value Coding gives you the ability to access properties on objects in a generic way; without having to hard code specific method names into your code.  Used correctly, the Key Value Coding features provided by Runtime can dramatically reduce the amount of code in your application while improving performance along the way.

 

This chapter will show you how to use Key Value Coding in your own applications, including how to define your own computed property handlers and to use the built-in caching mechanism to improve performance.  Before you can use these more advanced properties of SproutCore, however, you must first learn to use the most foundational feature of KVC: Universal Accessors

 

Universal Accessors

 

Once you've learned how to create basic objects, most programming languages then teach you to start writing accessor functions for each property you want to expose on your object.  For example, here is how you might define a basic Contact object if you were following the styled used in Java:

 

MyApp.Contact = SC.Object.extend({

  

  // firstName property - getter and setter

  

  _firstName: "John",  // default value

 

  getFirstName: function() {

    return this._firstName;

  },

 

  setFirstName: function(val) {

    this._firstName = val;

    return this;

  },

 

  // lastName property - getter and setter

  

  _lastName: "Doe", // default value

 

  getLastName: function() {

    return this._lastName ;

  },

 

  setLastName: function(val) {

    this._lastName = val;

    return this;

  },

 

  // fullName property - getter only

  getFullName: function() {

    return this.getFirstName() + ' ' + this.getLastName();

  }

});  

 

In the example above, each property in your object requires at least one method to read and two methods to read/write.  What's more, the content of these methods is often nearly identical.

 

OOP developers will tell you these accessor methods are very important to your application because they provide encapsulation.  That is, although in the example above the "firstName" accessor methods simply read and write an internal property right now - they isolate the external caller from these details.  In the future if you wanted to do something completely different to store the firstName property you could without having to change any code that calls it.

 

Accessor methods are indeed a very fundamental model of any well designed object oriented system, but we're writing applications for the cloud.  Every line of code we write has to download over the net and execute on the client before the app can run.  It is important, therefore, to find some way to gain the benefits of accessors without having to write so much code.

 

SproutCore Runtime solves this problem by implementing two methods we call universal accessors.  These two functions are called simply get() and set().  When you call get() on an object, the accessor will fetch the value of the property.  When you call set() it will update the property.  

 

If the property defined on your object is a simple static value, then get() and set() will work out of the box.  If you want to do something more rich, you can define a special method called a computed property that will be called instead.

 

We will dive into the specifics of these accessors in a later section, but just to give you a taste, here is how the same Contact object should be written using SproutCore:

 

MyApp.Contact = SC.Object.extend({

 

  // firstName and lastName properties.  note we just supply a default

  firstName: 'John',

  lastName:  'Doe',

 

  // computed fullName property

  fullName: function() {

    return this.get('firstName') + ' ' + this.get('lastName');

  }.property('firstName', 'lastName').cacheable()

 

});

 

Much better!

 

Universal accessors are defined as part of the SC.Observable mixin, which is included automatically in SC.Object.  We describe how you can add SC.Observable to your own objects as well in the Key Value Observing chapter.  For now, we'll focus on just working with SC.Objects.

 

When to use Universal Accessors

 

In general, to preserve encapsulation, SproutCore expects you to always use get() and set() to access all public properties on your objects.  This is very important.  If you don't use these universal accessors, then you may prevent observers from being notified; break caching; and will probably actually make your applications slower.  Resist the temptation to skip using universal accessors on your object.

 

Of course, some properties on your object are not public; they are intended only for internal use.  By convention, SproutCore applications always begin private properties with an underscore ("_").  The general rule is that you should never use get() and set() to access private properties.

 

Always use get() and set() to access any property that does not begin with an underscore (i.e. public properties).  Never use get() and set() to access a property that begins with an underscore (i.e. private properties).

 

Getting Property Values

 

To get a property value on an object, simply use the get() method, naming the property you want to retrieve.  For example, to get the firstName on a contact object like above, you would use:

 

contact.get('firstName');

 

You can name literally any property you want to get on an object and get() will try to retrieve it for you.  Usually, if the property is not defined on the object, get() will simply return 'undefined'.

 

Since SproutCore expects you to use get() very often, it is designed to be very simple and very fast.  For this reason it can only retrieve values defined directly on the object you call it on.  If you need to get an object that is several steps away, you can use getPath() instead.  

 

For example, here is how you might get the "city" from the "address" object defined on a contact:

 

contact.getPath('address.city');

 

getPath() is slightly more expensive than plain old get() because it has to parse the property path.  However, it is also quite a bit safer since null values in the property path (for example, if 'address' above is null) do not cause an error; getPath() will simply return null.

 

Setting Property Values

 

To modify a property value on an object, use the set() method, passing the key of the property you want to modify and the new value you want to set.  For example, to change the firstName property on a contact you would use:

 

contact.set('firstName', 'John');

 

In addition to updating the property itself, set() will also handle invalidating any relevant caches and notifying any observers that the property has changed.  This is why it is so important to always use get() and set() on your objects.

 

Note that if you implement a computed property, you can make it read only.  If a property is read only set() will not fail or throw and exception, but it will not actually change the property value.  To determine that a value has actually changed, you will need to get() the value again to verify it.

 

Unlike many implementations, set() always returns the receiver object that you called set() on.  This allows you to chain calls to set() in order to modify several properties at once:

 

contact.set('firstName', 'John').set('lastName', 'Doe');

 

Just like get(), set() is also optimized for performance.  Because of this, set() only works on properties defined directly on the receiver object.  If you need to set a property a few steps away, you can use setPath() instead:

 

contact.setPath('address.city', 'Los Altos');

 

Like getPath(), setPath() will not throw an exception if an object in the path (such as 'address' above) is null; it will simply not make a change.  Also like getPath(), setPath() is slightly slower than set().  You should use it when you need to access a property that is a few levels deep, but otherwise stick with using set() whenever possible.

 

Computed Properties 

 

Reading and writing simple properties is fine, but if you want to do something more complex (such as the fullName property in the example above), you're going to want to write a function.

 

Computed properties are simply methods that have been specially annotated to tell SproutCore that you want get() and set() to call these methods when you try to access that value.

 

To mark a function as a computed property, simply define your method and then add the .property() declaration at the end:

 

fullName: function() {

  return this.get('firstName') + " " + this.get('lastName');

}.property()

 

Now when you call contact.get('fullName'), get() will actually call the fullName computed property and return its result instead.

 

Dependent Keys

 

In addition to simply marking your computed properties, the property() handler can also take any number of property keys as parameters.  These are are called  dependent keys and they tell SproutCore which other properties on the same object might cause the return value of this computed property to change.

 

For example, the fullName property above would change its return value anytime the firstName or lastName properties change.  You would note this in the computed property like so:

 

fullName: function() {

  return this.get('firstName') + ' ' + this.get('lastName');

}.property('firstName', 'lastName')

 

Like get() and set(), property() is optimized for speed and so it only accepts simple property keys as parameters.  You cannot pass a property path (such as 'address.city') and have it work.

 

It is very important that you always name any dependent keys that you can on a computed property so that SproutCore can handle caching and observer notifications for you.  

 

If you have a case where a computed property might change that is not directly related to another property key, you can also manually tell SproutCore whenever a property has changed using the notifyPropertyChanges() method.  This is covered in more detail in Key Value Observing.

 

Cacheable

 

A new feature in SproutCore 1.0, the cacheable() helper method allows get() and set() to automatically cache the return value of a computed property until one of its dependent keys has changed.  This can dramatically simplify your code while improving performance and reducing your memory footprint.

 

To enable caching on a computed property, just chain the cacheable() helper method after you call property().  Make sure you declare any dependent keys as well or manually invalidate the property if needed to keep caching properly up to date.

 

The example below shows you how cacheable() can impact a property.  fullName() is defined cacheable and depends on firstName and lastName.  The console.log() statement will show you when the property is actually recomputed vs when it is returned from cache:

 

MyApp.Contact =  SC.Object.extend({

 

  firstName: "John",

  lastName: "Doe",

 

  fullName: function() {

    console.log('COMPUTING fullName!'); 

    return this.get('firstName') + ' ' + this.get('lastName');

  }.property('firstName', 'lastName').cacheable()

});

 

var contact = MyApp.Contact.create();

contact.get('fullName');

> COMPUTING fullName!

=> 'John Doe'

 

contact.get('fullName');

=> 'John Doe' <-- used cache!

 

contact.set('firstName', 'Jane'); <-- invalidates cache

contact.get('fullName');

> COMPUTING fullName!

=> 'Jane Doe'

 

To manually invalidate a property that has been cached just call object.notifyPropertyChange('keyName').  See Key Value Observing for more info.

 

Editable Computed Properties

 

The examples we've shown so far show a computed property that is read only.  Attempting to change the 'fullName' property in the examples above would simply have no effect on the value of fullName at all.

 

Anytime you call set() on a computed property, SproutCore will invoke your computed property method, passing the name of the key as well as the new value you should set.  To make your computed property editable, simply check for the presence of this second parameter:

 

fullName: function(key, value) {

  // setter   

  if (value !== undefined) {

    var parts = value.split(' '); // parse full name

    this.set('firstName', parts[0]);

    this.set('lastName', parts[1]);

  }

 

  return this.get('firstName') + ' ' + this.get('lastName');

}.property('firstName', 'lastName').cacheable()

 

Even when called as a setter, your computed property should always return the value of the property.  (For example, the method above always returns the computed full name, even if it just set the new firstName and lastName properties itself.)  This is important because set() will use the return value of the computed property to update its cache.  Returning a different value may cause set() to save the wrong value.

 

Writing Generic Computed Properties 

 

One of the best features of JavaScript is that functions are first class objects that you can pass around from place to place.  You can use this facility to create sometimes very useful generic computed properties that depend on the key name they have been assigned to in order to function properly.  You do this by simply inspecting the first parameter, which is the key name that was used to access the property.

 

For example, the generic computed property below will simply echo back the key name that was used to access it.  Note that this is defined as a computed property even though it does not actually belong to a single class.

 

ECHO = function(key) {

  return 'you had me at ' + key;

}.property().cacheable();

 

var object = SC.Object.create({

 

  hello: ECHO,

  goodbye: ECHO

 

});

 

object.get('hello');

=> 'you had me at hello'

 

object.get('goodbye');

=> 'you had me at goodbye'

 

Unknown Property

 

Sometimes you can't know up front all the different properties that you might want to expose on an object.  Other languages support this condition through something like a methodMissing() helper method, that will be called anytime you try to access an undefined property.  

 

SproutCore's universal accessors support this through the unknownProperty() method.  Anytime you try to get() or set() a property on an object that is currently undefined, SproutCore will call the unknownProperty() method on your object instead.

 

The default implementation of unknownProperty() will simply return 'undefined' in the case of a get() or will simply set the new value in the case of a set().  You can override this method if you like to provide a more customized implementation.

 

unknownProperty() is defined just like a computed property.  It will be passed the key name that was accessed and a value if it is being called as a setter.  The example below implements our generic ECHO computed property from above, but for any property on the object:

 

object = SC.Object.create({

 

  unknownProperty: function(key, value) {

 

    // read only...

    return "you had me at " + key ;

  }

 

});

 

object.get('hello');

=> 'you had me at hello'

 

object.get('goodbye');

=> 'you had me at goodbye'

 

object.get('foo bar')

=> 'you had me at foo bar'

 

Note that because unknownProperty() may be called under many different circumstances, it does not support dependent keys or caching so you should not use this property under normal conditions.  It is generally more efficient to define explicit computed properties like we defined above.

 

Property Helpers

 

In addition to the plain get()/set() methods, SC.Observables also defines a few other useful utility methods for modifying properties on an object:

 

  • setIfChanged(keyName, value) - works just like set() except that it will first check the value and only set it it if the current value is different from the new value.  This can avoid notifications and reduce cache invalidation in some cases.
  • incrementProperty(keyName) - this will get a property value, add 1 and then set it again.  This assumes the property value is a number.  You will get an exception if the value can't handle addition.
  • decrementProperty(keyName) - this will get a property value, delete 1, and then set it again.  This assumes the property value is a number.  You will get an exception if the value can't handle addition.

 

Moving On 

 

That's pretty much all you need to know about key value coding.  But really, KVC is only half the story.  To really get the most out of KVC you must understand key value observing as well.  Read on for more!

 

Learn about Key Value Observing »

or Back to Runtime Programming Guide Home »

Comments (0)

You don't have permission to comment on this page.