Handling ‘this’ in ko.command

Update: this feature is now available as part of the ko.plus library available on GitHub and NuGet!


The problem of context – the this value – in JavaScript is one that seems to keep causing problems.  Languages with similar syntax (C#, Java) do not allow the developer to alter the value of this and so people don’t always expect that it can change.

JavaScript likes to be different though.

I don’t want to get into too much detail on how this behaves – there are already more than enough articles out there that cover the topic to a greater depth than I could (e.g. Scope in Javascript); instead, this is a post about how I have worked around the issue in my ko.command library.

Problematic Command Context

Take the following simple example usage of the command library.

function ViewModel() {
    this.value = ko.observable(123);
    this.increment = ko.command(function() {
        this.value(this.value()+1);
    });
}

var viewModel = new ViewModel();
viewModel.increment();
//viewModel.value() => 124

The increment command adds 1 to the value of an observable property on the same view model; everything is working so far, but what if we move the implementation of the command action onto the prototype?

function ViewModel() {
    this.value = ko.observable(123);
    this.increment = ko.command(this._increment);
}

ViewModel.prototype._increment = function() {
    this.value(this.value()+1);
}

We might expect this to behave in the same way as before, but now when we call increment we get an error that, after a little investigation, we can see is because the value of this within the _increment function is not set to the view model.

Now It (mostly) Just Works

This behaviour was actually down to a design decision in the earlier version of the library to set the context of the various callbacks to the command itself; only recently has this started to cause problems that have prompted me to fix it.

The updated behaviour (available for download from Github) now endeavours to “just work” wherever possible.  This means that in the scenario above there are no code changes required to use the prototype implementation.

To be specific, the command action will always be invoked in the context from which the command was called which should (in most cases) behave in a way that seems to make sense.

There are some specific scenarios where a little more work is needed though.

CanExecute Function

The canExecute property on any ko.command instance is currently implemented as a computed observable, and as explained in the Knockout documentation, computed observables can be a little tricky when dealing with the context.

The behaviour does make sense when you consider the cause: computed observables will be re-evaluated whenever a dependent observable changes, so there is no way for them to (automatically) guarantee the context in which it will be invoked.

It is, however, possible to explicitly specify a context for a computed observable, so ko.command has been extended to mimic this implementation:

function ViewModel() {
    this.value = ko.observable(123);
    this.increment = ko.command({
        context: this,
        action: this._increment,
        canExecute: this.somethingThatReliesOnScope
    });
}

In this example the value of this when running the canExecute function will always be set to the current instance of ViewModel.

Note: explicitly setting the context in this way will also override the default behaviour when invoking commands, so use it carefully!

Asynchronously-Invoked Callbacks

The callbacks to an asynchronous command can be attached using the done/fail/always functions and are invoked once the promise returned by the action has completed.

function ViewModel() {
    this.value = ko.observable(123);
    this.incrementAsync = ko.command(function() {
        var promise = $.Deferred();
        promise.resolve();
        return promise.promise();
    }).done(this._increment);
}

But in what context will they be executed?

This scenario is a little more complicated because the promise implementation itself is able to specify the context in which callbacks should be invoked (in jQuery this is achieved using resolveWith).  In this case we have 3 choices:

  1. Do nothing.  Leave the context for the callback as whatever is set by the promise
  2. Replace the promise context with the context from the command
  3. Replace the promise context, but only if it has been explicitly specified.

For the time being I decided to leave the behaviour unchanged as it feels like changing it – forcing the context back to the command context – would be breaking the expected behaviour of whoever has explicitly (or implicitly) set the context of the callback.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s