Update: this feature is now available as part of the ko.plus library available on GitHub and NuGet!
The command pattern is a design pattern that encapsulates all the information required to perform an operation in a new object, allowing that operation to be performed later. Working in WPF using the MVVM pattern it is almost impossible to get away from commands and the ICommand
interface, so when I started writing view models in knockout that had to perform actions I started to miss the commands quite quickly.
Whenever I wanted to do something simple, like make an AJAX call…
var ViewModel = function() {
this.doSomethingOnTheServer = function() {
$.ajax(/*...*/);
};
};
…I would decide to notify the user that the operation was processing…
var ViewModel = function() {
var _self = this;
this.isRunning = ko.observable(false);
this.doSomethingOnTheServer = function() {
_self.isRunning(true);
$.ajax(/*...*/).always(function() {
_self.isRunning(false);
});
};
};
…and then to notify them if there was an error…
var ViewModel = function() {
var _self = this;
this.isRunning = ko.observable(false);
this.errorMessage = ko.observable();
this.doSomethingOnTheServer = function() {
_self.isRunning(true);
_self.errorMessage('');
$.ajax(/*...*/)
.always(function() {
_self.isRunning(false);
})
.fail(function(_, message) {
_self.errorMessage(message);
});
};
};
…and before long my view model was becoming unmanageably large.
Enter the Command
Instead of writing a view model a thousand lines long I decided to encapsulate all of that boilerplate code in a nice new object: Command
var ViewModel = function() {
this.doSomethingOnTheServer = new Command({
action: function() {
return $.ajax(/*...*/);
},
done: function(data) {
//...
}
});
};
var vm = new ViewModel();
vm.doSomethingOnTheServer.execute();
Note: because my commands in knockout are invariably AJAX I have made it a requirement that the ‘action’ of the command always return a jQuery.Deferred object.
So what are we doing here?
Notification Properties
Our view model needs to have 2 properties to store the status of the operation: isRunning
and errorMessage
. I could add a hasError
flag for completeness, but the absence of an error message can be used to infer the absence of an error.
We can create these using normal knockout observable properties:
var Command = function() {
var _self = this,
//flag to indicate that the operation is running
_isRunning = ko.observable(false),
//property to save the error message
_errorMessage = ko.observable();
//public properties
this.isRunning = _isRunning;
this.errorMessage = _errorMessage;
};
The Action
When we create a command we will need to specify the action that will be performed. Let’s pass this in as a constructor parameter, and throw an error nice and early if no action has been set:
var Command = function(options) {
//check an action was specified
if (!options.action) throw 'No action was specified in the options';
//... rest unchanged ...
};
Now that we have an action we can start to implement the execute
method that will do the work. This method needs to:
-
Set
isRunning
to true and clear any old error message -
Invoke the
action
from the constructor options -
Check that the action function has returned a Deferred object, and attach appropriate event handlers:
- Always set
isRunning
back to false - If the operation failed, set the
errorMessage
property
- Always set
var Command = function(options) {
//...
var _execute = function() {
//notify that we are running and clear any existing error message
_isRunning(true);
_errorMessage('');
//invoke the action and get a reference to the deferred object
var promise = options.action.apply(this, arguments);
//check that the returned object *is* a deferred object
if (!promise || !promise.done || !promise.always || !promise.fail)
throw 'Specified action did not return a promise';
//set up our callbacks:
promise
//always notify that the operation is complete
.always(function() {
_isRunning(false);
})
//save the error message if there is one
.fail(function(_, message) {
_errorMessage(message);
});
};
//...
this.execute = _execute;
};
Note: I am using apply
to call the action
method (instead of calling it directly) as it allows us to pass parameters if needed.
Completed Handlers
So far so good, but it’s rare that you don’t want to do something more than just notify the user when an operation completes. Let’s add the ability to pass in success and failure event handlers on the constructor options:
var Command = function(options) {
var _execute = function() {
//...as before...
//attach any success or failure handlers
if (options.done) promise.done(options.done);
if (options.fail) promise.fail(options.fail);
};
};
Note: as we are using the jQuery Deferred object to attach the event handlers they will automatically be passed any relevant arguments (e.g. AJAX data, error messages etc) so we don’t have to do any extra work here.
Fin
And that’s it. The full source for the Command
is:
var Command = function(options) {
//check an action was specified
if (!options) throw 'No options were specified';
if (!options.action) throw 'No action was specified in the options';
var _self = this,
//flag to indicate that the operation is running
_isRunning = ko.observable(false),
//property to save the error message
_errorMessage = ko.observable(),
//execute function
_execute = function() {
//notify that we are running and clear any existing error message
_isRunning(true);
_errorMessage('');
//invoke the action and get a reference to the deferred object
var promise = options.action.apply(this, arguments);
//check that the returned object *is* a deferred object
if (!promise || !promise.done || !promise.always || !promise.fail)
throw 'Specified action did not return a promise';
//set up our callbacks
promise
//always notify that the operation is complete
.always(function() {
_isRunning(false);
})
//save the error message if there is one
.fail(function(_, message) {
_errorMessage(message);
});
//attach any success or failure handlers
if (options.done) promise.done(options.done);
if (options.fail) promise.fail(options.fail);
};
//public properties
this.isRunning = _isRunning;
this.errorMessage = _errorMessage;
this.execute = _execute;
};
The source is also available on github along with unit tests