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.

Format dates using Knockout JS Custom Bindings with Moment.js

I have spent the past 5 years predominantly within the Xaml arena and whilst I have appreciated the new skills I have learnt in terms of the specific technology, one of the greatest benefits to me has been the coupled approach towards design patterns synonymous with xaml. I’m talking about MVVM, composite applications, and how these, in turn, introduced me to the concepts of IOC, Repository and Unit Of work patterns. Whilst I realise these have been around for a lot longer than Xaml, the prevalence of them in the asp.net web forms arena was non-existent, at least for me at the time.

Fast forward 5 years and with MVC, I am fast rekindling my love of asp.net. Not only is the base framework a significant improvement on web forms, but the explosion of complementary JS frameworks from JQuery to Knockout have really enriched the choice for web developers to pick the right tools for the job at hand.

Recently, I have been looking at Knockout and Web Api with view to minimising the number of repeated page or partial page refreshes when updating data. Using JQuery Ajax to grab data is a great way to remove the overhead of returning page details on top of the actual data itself. Coupled with knockoutjs to provide a intuitive workflow is a natural choice but my first pass demonstrated immediately the need for some formatting:

image

These came about from the following Knockout syntax:

   1: <tbody data-bind="foreach: companies">

   2:     <tr>

   3:         <td data-bind="text: name"></td>                    

   4:         <td data-bind="text: dateCreated"></td> 

   5:     </tr>

   6: </tbody>

 

In xaml, where databinding is (relatively) automatic, a simple StringFormat would provide the option here to mould the date required but using knockoutjs, one way I found to manipulate data bindings is using the custom bindings feature coupled with a suitable 3rd party framework if required. Moment.js is an excellent library for manipulating dates. Using the custom binding with knockout and providing the option to pass in a custom format string, I came up with the following:

   1: ko.bindingHandlers.date = {

   2:     update: function (element, valueAccessor, allBindingsAccessor, viewModel) {

   3:         var value = valueAccessor();

   4:         var formatString = allBindingsAccessor().formatString;

   5:         var date = moment(value());

   6:         if (formatString == null) {

   7:             $(element).text(date.format('DD/MM/YY'));

   8:         }

   9:         else {

  10:             $(element).text(date.format(formatString));

  11:         }

  12:     }

  13: };

This will look for the formatString parameter and will default to the english format of dd/mm/yy if it is not present. In my markup, I now have the following:

   1: <td data-bind="date: dateCreated, formatString: 'MM-DD-YY'"></td>                    

   2: <td data-bind="date: dateModified"></td>    

Which gives me the following output:

imageMuch cleaner, although just a start as the next step would be to think about auto-detecting current UI culture for the browser and being able to apply that as the default.