jones.busy

technical musings of a caffeine converter

NAVIGATION - SEARCH

Separating Knockout Viewmodel from View

As mentioned in a previous post, I’m becoming a big fan of Knockout. I don’t favour MVVM over MVC per se, simply like the idea of being able to manipulate, and react to the model changing on the client side without the need for a return server trip every time.

Whilst it is perfectly normal for a Knockout view model to be declared inside the cshtml View to which it is bound, I have found that very quickly, the script can become quite bulky and difficult to maintain.

Take for instance the following script inside a View of mine:

   1: @section Scripts

   2: {

   3:     <script type="text/javascript">

   4:         /// <reference path="../jquery-1.9.1.js" />

   5:         /// <reference path="../knockout-2.2.1.js" />

   6:         /// <reference path="knockout.extensions.js" />

   7:         /// <reference path="../knockout.mapping-latest.js" />

   8:         /// <reference path="../jquery-ui-1.10.2.js" />

   9:         (function () {

  10:  

  11:             var viewModel = ko.mapping.fromJS(@Html.Raw(Json.Encode(Model)));

  12:             viewModel.currentView = ko.observable('company');

  13:             viewModel.editMode = ko.observable(false);

  14:             viewModel.editMode.subscribe(detailsRendering.toggleModelEdit);

  15:  

  16:             var btnActiveClass = 'btn-success';

  17:  

  18:             var manageSelection = function () {

  19:                 removeSelection();

  20:                 viewModel.currentView($(this).data("view-id"));

  21:                 addSelection($(this));

  22:             };

  23:  

  24:             var addSelection = function (element) {

  25:                 element.addClass("btn-primary");

  26:             };

  27:  

  28:             var removeSelection = function () {

  29:                 $(".detailsSelector").removeClass("btn-primary");                

  30:             };

  31:  

  32:             var toggleModelEdit = function (edit) {

  33:                 if (edit) {

  34:                     $("#lockModelBtn").removeClass(btnActiveClass);

  35:                     $("#unlockModelBtn").addClass(btnActiveClass);

  36:                 } else {

  37:                     $("#unlockModelBtn").removeClass(btnActiveClass);

  38:                     $("#lockModelBtn").addClass(btnActiveClass); 

  39:                 }

  40:             };

  41:  

  42:             $(".detailsSelector").on('click', manageSelection);

  43:             $("#lockModelBtn").on('click', function () { viewModel.editMode(false); });

  44:             $("#unlockModelBtn").on('click', function () { viewModel.editMode(true); });

  45:  

  46:             addSelection($("#companyDetailsSelector"));            

  47:  

  48:             toggleModelEdit(false);

  49:  

  50:             ko.applyBindings(viewModel);

  51:         })();

  52:     </script>   

  53: } 

It’s not very complicated but already I am finding it a little fiddly to follow. And, I’m not yet finished with the code, so it’ll only get bigger. I could separate out the areas responsible for initialising the view model from those responsible for reacting to user interaction with multiple script tags but a) that would add extra js code and b) I have a lot of overlap between the two areas of responsibility so scope is an important issue.

What I opted to go with is the Revealing Module Pattern (RMP) that provides a nice separation of concern in simplistic fashion. I created three RMP functions:

1. Initialise View Model

2. Initialise View

3. Handle User Interaction with the View

The resulting script is as follows:

   1: /// <reference path="../jquery-1.9.1.js" />

   2: /// <reference path="../knockout-2.2.1.js" />

   3: /// <reference path="knockout.extensions.js" />

   4: /// <reference path="../knockout.mapping-latest.js" />

   5: /// <reference path="../jquery-ui-1.10.2.js" />

   6: var details = function () {

   7:     

   8:     var initialiseViewModel = function (data) {

   9:         var viewModel = data;

  10:         viewModel.currentView = ko.observable('company');

  11:         viewModel.editMode = ko.observable(false);

  12:         viewModel.editMode.subscribe(detailsRendering.toggleModelEdit);

  13:  

  14:         ko.applyBindings(viewModel);

  15:  

  16:         return viewModel;

  17:     }

  18:  

  19:     return {

  20:         initialiseViewModel: initialiseViewModel

  21:     };

  22: }();

  23:  

  24: var detailsView = function () {

  25:  

  26:     wireEvents = function (vm) {

  27:         $(".detailsSelector").on('click', function () { detailsRendering.manageSelection(vm, $(this)); });

  28:         $("#lockModelBtn").on('click', function () { vm.editMode(false); });

  29:         $("#unlockModelBtn").on('click', function () { vm.editMode(true); });

  30:     },

  31:  

  32:     initialiseView = function (vm) {

  33:  

  34:         wireEvents(vm);

  35:         detailsRendering.manageSelection(vm, $("#companyDetailsSelector"));

  36:         detailsRendering.toggleModelEdit(false);

  37:     }

  38:  

  39:     return {

  40:         initialiseView: initialiseView

  41:     };

  42: }();

  43:  

  44: var detailsRendering = function () {

  45:  

  46:     var btnActiveClass = 'btn-success',

  47:     btnPrimaryClass = 'btn-primary',

  48:  

  49:     addSelection = function (element) {

  50:         element.addClass(btnPrimaryClass);

  51:     },

  52:  

  53:     removeSelection = function () {

  54:         $(".detailsSelector").removeClass(btnPrimaryClass);

  55:     }

  56:  

  57:     toggleModelEdit = function (edit) {

  58:         if (edit) {

  59:             $("#lockModelBtn").removeClass(btnActiveClass);

  60:             $("#unlockModelBtn").addClass(btnActiveClass);

  61:         } else {

  62:             $("#unlockModelBtn").removeClass(btnActiveClass);

  63:             $("#lockModelBtn").addClass(btnActiveClass);

  64:         }

  65:     },

  66:     manageSelection = function (viewModel, element) {

  67:          removeSelection();

  68:          viewModel.currentView(element.data("view-id"));

  69:          addSelection(element);

  70:      }

  71:  

  72:     return {        

  73:         toggleModelEdit: toggleModelEdit,

  74:         manageSelection: manageSelection

  75:     };

  76: }();

The first module, “details” (named after the view), providers a function for setting up the view model and returning. It does nothing else and knows of nothing else. The only dependency it has is on the initial input of the model which has come from the server.

The second module, “detailsView”, expects the view model and sets up the components in view from wiring the click events, to setting up the default view by calling into the third module, “detailsRendering”, which again expects the view model as an input and provides the functionality for manipulating the view based on either the user’s input or manually in the case of the detailsView initial setup. These three modules are in the same js file, but could quite easily be separated out, in which case, I’d be wise to use something like require to handle dependencies and also minimise the load.

To get the ball rolling, the html embedded script now looks like this:

   1: @section Scripts

   2: {

   3:     <script src="~/scripts/app/page.details.js"></script>   

   4:     

   5:     <script type="text/javascript">

   6:         (function () {

   7:             var viewModel = details.initialiseViewModel(ko.mapping.fromJS(@Html.Raw(Json.Encode(Model))));

   8:             detailsView.initialiseView(viewModel);

   9:         })();

  10:     </script>    

  11: }

Rather than returning the view model from the initial function, I could have called the initialiseView method from within but this way, I have the option of making further calls if needs be without the need to chain. Having spent most of my time in C# and Xaml, I’d love to hear from html and javascript guys on what their preferred approach to client side MVVM is.

Add comment