Command Pattern with jQuery.Deferred & Knockout

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:

  1. Set isRunning to true and clear any old error message

  2. Invoke the action from the constructor options

  3. 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
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