Hot Towel SPA Master-Detail Scenario

With the recent release of ASP.NET and Web Tools 2012.2 Update, more tools are available for the developer to build applications more efficiently. For Single Page Applications (SPA), it used to take quite a lot of time, to make a hardened application shell, before you can actually start the “real coding” for the application itself. Stuff like hash-based routing, transitions, navigation, swapping views were quite an overhead before.

Hot Towel SPA

No, this isn’t the water theraphy stuff in exotic Bali, Indonesia – it is a full-fledged Single Page Application Visual Studio MVC Template written by John Papa. Note that if you are not using ASP .Net MVC type of projects, you can use the Hot Towelette Nuget package.

The template also includes very handy libraries such as

Master-Detail SPA Scenario

So, after downloading the template, I wanted to create a master-detail scenario, to see how good this template is. Let’s say I want to display a list of sessions on the master page, and when the user clicks an item there, the application will transition into the details page.

Let’s build the back-end code first (not utilizing Breeze.js for simplicity sake). I have two api methods that

  •  Returns the list of sessions
  •  Given a session “id”, returns the whole object graph of that particular session
        // data param 
        public class SessionObj
        {
            public string ID { get; set; }
        }
    
        public class SessionController : ApiController
        {
            // GET ~/api/Session/SessionList
            [HttpGet]
            public object[] SessionList()
            {
                return new object[] { new {Name="Dr. Phil", DOB = DateTime.Now.AddYears(-50)},
                    new {Name="John McCain", DOB = DateTime.Now.AddYears(-60)},
                   new {Name="Nancy Pelosi", DOB = DateTime.Now.AddYears(-20)},
                   new {Name="Seal Team 6", DOB = DateTime.Now.AddYears(-33)} };
            }
    
            [HttpPost]
            public object[] Session(SessionObj data)
            {
                // simulate hard-core processing
                System.Threading.Thread.Sleep(10000);
                // query the full object graph and return
                // var fullSession = _db.Sessions.Single(z => z.ID == data.ID);
                return new object[] { new {ID = 1, Name = "Blah Blah", Cost =100},
                                      new {ID = 2, Name = "Blah Blah 2", Cost = 200}
                };
            }
        }
    
    

Our ViewModel for “Master Page”

We’ll just modify the viewmodel code that came with the template (home.js). Sessions property/variable will store our list of sessions downloaded via ajax call from the server, exposed by our back-end api GET method (SessionList).

define(['services/logger', 'services/datacontext'], function (logger, datacontext) {
    var vm = {
        activate: activate,
        title: 'Home View',
        Sessions: ko.observableArray([])
    };

    return vm;

    //#region Internal Methods
    function activate() {
        var self = this;
        return datacontext.getSessions(self.Sessions)
        .then(function () {
            logger.log('Sessions Loaded', null, 'home', true);
        });
  
    }
    //#endregion
});

Our View for “Master Page”

We’ll add a table to the HTML view that came with the template (home.html). If you look at the declarative binding closely, we bound Sessions to each row of the table.

<section>
    <h2 class="page-title" data-bind="text: title"></h2>
    <table class='table table-bordered table-condensed table-hover'>
        <thead>
            <tr>
                <th>Name</th>
                <th>DOB</th>
            </tr>
        </thead>
        <tbody data-bind="foreach: Sessions">
            <tr>
                <td>
                    <a class ='btn btn-small btn-primary' data-bind='attr: { href: "#/details/" + Name }'><i class='icon-edit'></i>&nbsp;<span data-bind='    text: Name'></span></a>
                </td>
                <td data-bind='text: moment(DOB).format("MMMM Do YYYY, h:mm:ss a")'>
                </td>
        </tbody>
    </table>
</section>

Our ViewModel for “Detail Page”

This time, lets write the module, using the revealing module pattern way of coding javascript modules (detail.js). RowData property/variable will store our object graph for the particular session chosen, and then downloaded via ajax call from the server, exposed by our back-end api POST method (Session).

define(['services/logger', 'services/datacontext'], function (logger, datacontext) {
    "use strict";
    var title = ko.observable(),
    RowData = ko.observableArray([]),
    getSessionFull = function (sessionName) {
        return datacontext.getSessionFull(sessionName, RowData);
    },
    activate = function (routeData) {
       
        var sessionName = routeData.id;
        title(sessionName);
        return getSessionFull(sessionName).
        then(function () {
            logger.log('Details View Activated', null, 'details', true);
        });

    };
    return {
        title: title,
        activate: activate,
        RowData: RowData
    }

});

Our View for “Detail Page”

We’ll add a table to the HTML view that came with the template (details.html). If you look at the declarative binding closely, we bound RowData to each row of the table, pretending that this our complex object graph for a session.

<section>
    <h2 class="page-title" data-bind="text: title"></h2>
    <table class='table table-bordered table-condensed table-hover'>
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
                <th>Cost</th>
            </tr>
        </thead>
        <tbody data-bind="foreach: RowData">
            <tr>
                <td data-bind='text: ID'>
                </td>
                <td data-bind='text: Name'>
                </td>
                <td data-bind='text: Cost'>
                </td>
        </tbody>
    </table>
</section>

Defining our routes

The original route definitions (shell.js) that came with the template has to be modified such that we can actually pass the “id” to our hash-based navigation, which in turn would be our key to fetching the correct “details” for the detail page. We also do not want to show “Details” as part of the navigation menu as opposed to how the template originally is configured.

define(['durandal/system', 'durandal/plugins/router', 'services/logger'],
    function (system, router, logger) {
        var shell = {
            activate: activate,
            router: router
        };
        
        return shell;

        //#region Internal Methods
        function activate() {
            return boot();
        }

        function boot() {
            router.mapNav('home');
            //router.mapNav('details'); // original code from template        
            // ------- new routing definition to support passing the id -------- 
            router.mapRoute('details/:id', 'viewmodels/details', 'Details', false);
            log('Hot Towel SPA Loaded!', null, true);
            return router.activate('home');
        }

        function log(msg, data, showToast) {
            logger.log(msg, data, system.getModuleId(shell), showToast);
        }
        //#endregion
    });

The key to this routing definition is

router.mapRoute('details/:id', 'viewmodels/details', 'Details', false);

The 4th parameter tells the router not the display the “details” view as part of the navigation menu. If you look back again at our details viewmodel code, you’ll notice that the activate function received a parameter

    activate = function (routeData) {
        logger.log('Details View Activated', null, 'details', true);
        var sessionName = routeData.id;
        title(sessionName);
        getSessionFull(sessionName);
        return true;
    };

routeData was passed in by the “framework” so that we can actually get the id to facilitate getting the details for the session chosen. Also, if you noticed our viewmodel for the master page, the link to the detail is constructed in such a way that we are passing the id through hash-based navigation. You can learn more about hash-based navigation, particularly the one used here at http://sammyjs.org/.

<a class ='btn btn-small btn-primary' data-bind='attr: { href: "#/details/" + Name }'><i class='icon-edit'></i>&nbsp;<span data-bind='    text: Name'></span></a>

Data Context

define(['services/dataservice'], function (dataservice) {
    "use strict";
    var getSessions = function (results) {
        return dataservice.getSessions()
        .then(function (data) {
            results(data);
        })
    },
    getSessionFull = function (id, results) {
        return dataservice.getSessionFull(id)
            .then(function (data) {
                results(data);
            });
    };
    return {
        getSessions: getSessions,
        getSessionFull: getSessionFull
    }
});

Data Services

define(['durandal/http'], function (http) {
    "use strict";
    var getSessions = function () {
        return http.get('/api/Session/SessionList');
    },
    getSessionFull = function (id) {
        return http.post('/api/Session/Session', { ID: id });
    };

    return {
        getSessions: getSessions,
        getSessionFull: getSessionFull
    }
});
Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer’s view in any way.
Tagged with: ,
Posted in SPA
3 comments on “Hot Towel SPA Master-Detail Scenario
  1. HansDev says:

    Nice article.
    I have followed all the steps given in this article, but when I run the application I am getting error as datacontext object as undefined.
    I am new to SPA and please let me know if I am doing anything wrong.

    • ericpanorel says:

      In your home.js file, did you “inject” the datacontext object? Your first line should look something like this:
      define(['services/logger', 'services/datacontext'], function (logger, datacontext) {
      ……
      });

  2. MarkG says:

    Very useful thanks

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>