Skip to content

Ember's BufferedProxy and analog to Ruby's method_missing

by cory on June 27th, 2013
Last week I wrote about state machines in ember-data, and I wanted to follow that up this week with a pattern that can be used to separate the editing and saving of ember-data models.

If you recall, the issue we ran into was that ember-data would complain when you tried to change a model while its data was still being synchronized to the data store (it generated a `willSetProperty` error). In this post we'll see how to intercept changes to the model to prevent updating its properties while its data is still in flight. The ember mixin that we'll use to do this was created by Kris Selden and is called `Ember.BufferedProxy`.

Summary

If you want to skip ahead, you can check out the interactive visual demo here. Playing with it should give you a good idea of how the BufferedProxy works in practice. For more context, check out the previous post and previous demo.

Ember's version of method_missing

Ember object have a little-known feature that allows us to override the setting and getting of properties on an object. Truthfully, it's a white lie to say that this is analogous to method_missing, as you'll see. It's similar, but only applies to properties that are unknown to the object. So it's sort of a method_missing but only for getters and setters of unknown properties. For overriding `get`-ing and `set`-ing of a property on an object we can define methods named `unknownProperty` and `setUnknownProperty` to handle these cases, respectively.

The ember documentation for get explains `unknownProperty`:
Gets the value of a property on an object. If the property is computed, the function will be invoked. If the property is not defined but the object implements the unknownProperty method then that will be invoked.

Say we have the following code:
var Dog = Ember.Object.extend({
  name: "Fido",

  unknownProperty: function(key) {
    console.log("Tried to get the unknown property: " +
                key); 
  }
});
var myDog = Dog.create();
alert("my dog's name is: " + myDog.get('name'));
// alerts: "my dog's name is: Fido"
alert("my dog has fleas? " + myDog.get('hasFleas'));
// alerts: "my dog has fleas? undefined"
// console logs: "Tried to get unknown property: hasFleas"

Any property that is not part of the blueprint for the object will result in calling the `unknownProperty` method, as shown. Any property that was part of the blueprint of the object (in this case, the Dog object knows that it has the property `name`) will not result in calling `unknownProperty`. The relevant code in ember is in the `ember-metal` package. You can imagine how intercepting `get`-ing of unknown properties could be useful in order to look them from some other source. This is what we'll do when we use our BufferedProxy.

`setUnknownProperty` works similarly. See the code below:
var Dog = Ember.Object.extend({
  name: "Fido",

  setUnknownProperty: function(key, value) {
    console.log("Tried to set the unknown property: " + 
                key + " to: " + value); 
  }
});
var myDog = Dog.create();
myDog.set('hasFleas', true);
// console logs: 
//   "Tried to set the unknown property: hasFleas to: true"


If we put these two together with a separate proxy object to hold these changes, we end up with something like this:
var Dog = Ember.Object.extend({
  name: "Fido",
  proxyObject: {},

  unknownProperty: function(key) {
    console.log("Got the unknown property: "
                + key);
    var proxyObject = this.get('proxyObject');
    return proxyObject[key];
  },
  setUnknownProperty: function(key, value) {
    console.log("Set the unknown property: " + key + " to: " + value); 
    var proxyObject = this.get('proxyObject');
    proxyObject[key] = value;
  }
});
var myDog = Dog.create();

myDog.get('hasFleas'); // returns undefined
// console logs: "Got the unknown property: hasFleas"

myDog.set('hasFleas', true);
// console logs: "Set the unknown property: hasFleas to: true"

myDog.get('hasFleas'); // returns true
// console logs: "Got the unknown property: hasFleas"


That should make sense. We're storing unknown properties in the `proxyObject` and looking them up there as well.

There's an important caveat here, though: In order for ember's data bindings to properly reflect data changes into the UI, ember needs to know when those properties change. It uses the property change notifications `propertyWillChange` and `propertyDidChange` internally to do this, and when you don't have a `setUnknownProperty` method defined, then ember transparently handles these notifications for you and everything will work as expected. But if you've overridden `setUnknownProperty`, then ember doesn't know when the property values have actually changed, so we need to manage that ourselves. Here's an example of how this could go wrong if we don't set up property notifiers:
var Dog = Ember.Object.extend({
  name: "Fido",
  puppy: null,
  proxyObject: {},

  puppyHasFleasObserver: function() {
    if (this.get('puppy.hasFleas')) {
      console.log("The puppy just got fleas!");
    }
  }.observes('puppy.hasFleas'),

  unknownProperty: function(key) {
    console.log("Got the unknown property: " + key);
    var proxyObject = this.get('proxyObject');
    return proxyObject[key];
  },
  setUnknownProperty: function(key, value) {
    console.log("Set the unknown property: " + key + " to: " + value); 
    var proxyObject = this.get('proxyObject');
    proxyObject[key] = value;
  }
});
var myPuppy = Dog.create({name: "Puppy"});
var myDog = Dog.create({name: "Fido", puppy: myPuppy});
myPuppy.set('hasFleas', true);
// console DOES NOT log:
//   "The puppy just got fleas!"
// Why not?


We expected that the `puppyHasFleasObserver` would be triggered and the console would log that the puppy just got fleas, but it doesn't. Why not? Because ember's property system never triggered the change notifications that it uses for property observation. This is a rather contrived example, but you can see how this would cause some trouble in an actual app, with a GUI that doesn't update properly when values change.

We can fix this by adding the appropriate calls to `propertyWillChange` and `propertyDidChange` like this:
var Dog = Ember.Object.extend({
  name: "Fido",
  puppy: null,
  proxyObject: {},

  puppyHasFleasObserver: function() {
    if (this.get('puppy.hasFleas')) {
      console.log("The puppy just got fleas!");
    }
  }.observes('puppy.hasFleas'),

  unknownProperty: function(key) {
    console.log("Got the unknown property: " + key);
    var proxyObject = this.get('proxyObject');
    return proxyObject[key];
  },
  setUnknownProperty: function(key, value) {
    console.log("Set the unknown property: " + key + " to: " + value); 
    var proxyObject = this.get('proxyObject');
    var originalValue = proxyObject[key];
    
    if (originalValue === value) return;
    
    this.propertyWillChange(key);
    proxyObject[key] = value;
    this.propertyDidChange(key);
  }
});


Now that we've added the property change notifiers, our contrived example should work as expected:
var myPuppy = Dog.create({name: "Puppy"});
var myDog = Dog.create({name: "Fido", puppy: myPuppy});
myPuppy.set('hasFleas', true);
// console logs "The puppy just got fleas!" -- hooray!


(If you'd like to try this yourself, the easiest way to do it is probably to find a web page with ember already running on it and just copy and paste these examples into the console. You can use the previous demo, for example. Just open the console and copy/paste.)

Applying setUnknownProperty and unknownProperty with BufferedProxy

With this newfound knowledge, we can apply a pattern that intercepts all changes to a model, and buffers them until we are ready to synchronize. At that point we will flush the changes to the model and save the model, allowing the user to make further changes while the model is in flight. We'll use Kris Selden's Buffered Proxy. It's worth taking a moment to look through and understand that code.

To use the BufferedProxy we simply `extend` the controller that has the model as its `content` property (in this model, `content` is the blog post whose title is being edited). Our value bindings in the UI are bound to the controller, which under normal circumstances would proxy them directly to the content, but now that we have the BufferedProxy mixed in it will intercept and hold on to requests to change any property that isn't part of its set of properties.

Here is a interactive demo on jsbin showing the BufferedProxy in action. First, try changing the blog post title. Notice that the model's state doesn't change, and its `isDirty` property doesn't change to true. That's because our changes to the title are stored by the BufferedProxy. You can see that the `hasBufferedChanges` property is true. As soon as you hit enter the code will apply any buffered changes and save the model, so the changes get sent to the server as expected. If instead you click the "Apply buffered changes" button, the change you've made to the post's title (that is stored in the Buffer) will be applied, and you'll see the state for the post change and its `isDirty` property change to true, as expected.

Now, if you switch the latency to "high", you can make a change to the title, hit enter to save it, and immediately start making other changes before the synchronization to the backend completes. The changes will be stored in the buffered proxy, which will successfully prevent the `willSetProperty` error we encountered previously when we attempted to apply the changes directly to the model while it was `inFlight`.

If you think it through, there's still a way to cause that `willSetProperty` error, but I'll leave that as an exercise for the reader.

From → general

3 Comments
  1. otte permalink

    Great post and clear explanations. I was looking forward to this post and I happy you made it.

  2. cory permalink

    Thank you! I'm glad you enjoyed it.

  3. otte permalink

    Just a quick follow on question. In the code in jsbin, your save function was like this:

    save: function() {
    var record = this.get('content.content');
    this.applyBufferedChanges();
    record.save();
    },

    If I using ember-data callbacks in my save() method as shown below, will I add the line ' this.applyBufferedChanges()' before the callback as I done here or should it be after the callback just above this line: ' this.transaction.commit()'

    save: function(){
    var comment = this.get('content');

    this.applyBufferedChanges();

    comment.one('didCreate', this, function() {
    this.set('isAddingNew', false);
    });
    this.transaction.commit();
    }

Leave a Reply

Note: XHTML is allowed. Your email address will never be published.

Subscribe to this comment feed via RSS