Click here to Skip to main content
16,021,209 members
Articles / Web Development / HTML5

Anatomy of HTML5/JavaScript Single Page Application in Samples (basics, navigation, composition, communications with the server)

Rate me:
Please Sign up or sign in to vote.
4.99/5 (44 votes)
20 Dec 2012CPOL37 min read 199.7K   5.1K   143   36
Describes Single Page Applications and a new BPF Framework that helps to develop SPAs.

Introduction

What is Single Page Application (SPA)?

Ordinary HTML applications/web-sites fetch each page from the server every time the user wants to go to a new page. SPA applications, however, bring all the pages of the application on the client's browser at the beginning and use JavaScript in order to switch between the pages making parts of HTML visible and invisible depending on which page is chosen. This makes the HTML application as responsive as a native or Silverlight application, since the navigation between the pages does not require a round trip to the server. This advantage comes at the expense of a higher application loading time. There are some ways to speed up the loading of an SPA application, making the SPA loading experience for the user the same as when loading a usual HTML application, but this topic is beyond this article.

If you think of it, an SPA application is very similar to a Silverlight application, in that most Silverlight applications load all the pages at the beginning and later allow the users to navigate between the pages without calling the server. So understanding of the SPA concepts should come very naturally to Silverlight developers.

Navigation State

In its generic form, the browser navigation maps a unique URL to the specific navigation state of the browser application. The "navigation state of the application" determines which pages or regions within the application are shown or hidden or how they are shown.

The browser interprets the URL and sets the application to the corresponding state. This is required for e.g. enabling the user to navigate the browser application by pressing the browser "back" and "forward" navigation buttons (the browser stores the URLs and when the user presses one of the browser navigation buttons the URL changes and the application state changes correspondingly). Also, this is useful when some external page refers to some specific page (or state) within the application which is not the application's default page.

The reverse is also true: once the state of the application changes, e.g. because the user presses a tab or a hyperlink to move to a different page, the browser URL should also change to the one corresponding to the new state.

For the ordinary (non-SPA) HTML applications the navigation comes naturally (the page loads from the server based on the corresponding URL). The SPA applications, however, need to use some JavaScript magic in order to achieve the one-to-one mapping between the URLs and the navigation states of the application. Here we present a new functionality that makes the SPA navigation easier to achieve than by using other already existing frameworks.

The same navigation concepts can apply to any native single window/multipage applications especially to those built for smart phones and tablets and time permitting, I am going to write more articles about it.

Composition of an SPA Application

In my experience, building SPA application results in large HTML files since the application consists of multiple pages and all of them are loaded together. Large HTML files can be difficult to understand and modify. I created functionality allowing to split SPAs into multiple files and load and assemble them all on the client. This functionality is also discussed here.

BPF Framework

Being a great fan of WPF and Silverlight, I ambitiously called the new framework BPF standing for Browser Presentation Foundation. Its ultimate purpose is to provide the capabilities one can find in WPF and Silverlight and more. Currently it provides some generic purpose utility functionality, navigation and composition capabilities which will be described below. Composition part of the framework depends on JQuery, the rest of the framework is self-sufficient, and can be used without JQuery installed.

There are two SPAs that have been built with an earlier version of BPF library: my own business website - awebpros.com and paperplusne.com. Note that paperplusne.com website does not have correct pricing data yet for all its items (for items without pricing data I put $1000 as their price). You can still order some plastic-ware through it and someone will contact you back about the prices.

SPA Server Communications

Just like a Silverlight or a desktop application, the SPA application can minimize the amount of information it loads from the server by getting only the required data from the server (usually in JSON form) and not the generated HTML code as ordinary HTML applications do. I provide some examples of an SPA communicating with the server within this article.

Organization of this Article

We present the simplest possible samples of the ordinary and SPA HTML/JavaScript sites. We talk about the differences between them.

We discuss the navigation and present the navigation capabilities of the BPF framework.

We discuss the composition using BPF framework.

We talk about SPA communications with the server using ASP.NET MVC and provide corresponding examples.

Important Note: This article concentrates on the business logic and not on the UI design. We present the SPAs built in the simplest possible way without any concern about how ugly they look.

Prerequisites

You need to have some knowledge of HTML and JavaScript in order to read and understand this article. I refer to HTML5, JavaScript, Knockout, JQuery, Guide for Recovering Silverlight/WPF/C# Addicts. Part 1 - JavaScript and DOM as a good HTML/JavaScript primer.

Throughout this article, I use some very basic JQuery functionality to detect when the HTML document is loaded and also to provide a multiplatform way of binding to the DOM events, so some understanding of JQuery functionality is needed. A couple of paragraphs from JQuery and DOM Event should cover this information.

When discussing composition we also use JQuery selectors. You can follow the JQuery Selectors link to learn about them.

I am using JQuery-UI tabs control extensively for navigation but anything related to that is explained in the text.

Part that covers communications with the server might require some basic understanding of ASP MVC, even though we try to explain everything within the article itself.

Part of a Larger Series

This article can be considered part 3 of "HTML5, JavaScript, Knockout, JQuery, Guide for Recovering Silverlight/WPF/C# Addicts" series which also contain HTML5, JavaScript, Knockout, JQuery, Guide for Recovering Silverlight/WPF/C# Addicts. Part 1 - JavaScript and DOM and HTML5, JavaScript, Knockout, JQuery, Guide for Recovering Silverlight/WPF/C# Addicts. Part 2 - Solar System Animation built with SVG, Knockout and MVVM Pattern.

SPA Basics

Presenting Two Page SPA and Two Page Ordinary HTML Application Samples

Here we present two very simple two-page HTML applications: one ordinary and the other SPA. The code for the ordinary HTML application is located under TwoPageOrdinaryHTMLApp solution, while the code for the SPA is located under TwoPageSPA solution.

Both applications provide exactly the same user experience. There are two hyperlinks at the top of the HTML window corresponding to two pages. Clicking on top hyperlink shows first page's text, while clicking on the other one shows the other page's text:

Image 1

Image 2

The ordinary HTML application has two HTML pages Page1.htm and Page2.htm. Both have hyper links at the top of the page (one to itself and one to the other page) and the page text. For page1.htm page text is "This is page 1" and it is colored red. For page2.htm the text is "This is page 2" and it is colored blue. Here is the page1.htm code:

<body>
    <!-- hyper links for choosing the page -->
    <ul>
        <li><a href="Page1.htm">choose page1</a></li>
        <li><a href="Page2.htm">choose page2</a></li>
    </ul>

    <!-- page 1 message colored in red -->
    <p style="font-size:40px;color:red">This is page 1</p>
</body>

To start the TwoPageOrdinaryHTMLApp, right click on Page1.htm file within the Visual Studio's solution explorer and choose "View in Browser" option if you want to view it in the default browser or "Browse with" option if you want to choose the browser. You can switch between the pages by clicking the hyperlinks at the top.

The SPA solution uses JQuery (if you open Scripts folder you will see a bunch of JQuery files). You can install JQuery as a NuGet package as described in JQuery.

There is only one HTML file within TwoPageSPA project: Index.html. You can start the application by right clicking on this file within solution explorer and choosing "Run in Browser". Index.html has HTML part containing the hyperlinks and text for both page. It also has a JavaScript code for switching between the pages.

Here is the HTML code within Index.html file:

<body>
    <ul>
        <!-- href='#' is needed only to help the links look like links -->
        <li><a id="page1Link" href="#">choose page1</a></li>
        <li><a id="page2Link" href="#">choose page2</a></li>
    </ul>

    <!-- page 2 message colored in blue -->
    <!-- in the beginning page2 is not visible (display set to 'none') -->
    <p id="page2" style="font-size:40px;color:blue;display:none">This is page 2</p>

    <!-- page 1 message colored in red -->
    <p id="page1" style="font-size:40px;color:red" >This is page 1</p>

</body>

As you can see, the page links do not contain valid references - their href property is set to '#' just so that they would look and behave like links (have bluish color and change the mouse cursor to "hand").

Both pages' text is placed below the links. Page 2 text is not visible in the beginning (its display property is set to none and because of that, it is placed first so that whatever HTML code follows it won't be shifted down because of it.

Page switching is done by JavaScript code placed below the HTML code:

// use $(document).ready function to
// make sure that the code executes only after the document's
// DOM has been loaded into the browser.
$(document).ready(function () {
    // when page1Link link is clicked, page1 shows,
    // page2 hides
    $(page1Link).bind("click", function () {
        $(page1).show();
        $(page2).hide();
    });

    // when page2Link link is clicked, page2 shows,
    // page1 hides
    $(page2Link).bind("click", function () {
        $(page1).hide();
        $(page2).show();
    });
});

We use JQuery's bind function to attach event handlers to click events on the hyperlinks. When the event fires we show the contents of one of the pages and hide the contents of the other page.

One can see, however, there is a difference in behaviors of the two applications. When you change pages in the ordinary HTML application, the browser URL also changes (e.g. from http://localhost:23033/Page1.htm to http://localhost:23033/Page2.htm). After switching the pages, you can use "back" button on your browser to go to the previous page. Also if you want the users to access Page2.htm without accessing Page1 (which is a default page for the application) you can simply give a hyperlink http://localhost:23033/Page2.htm pointing straight to Page2.

If you try to switch pages in the SPA, however, you will notice that the URL does not change, back button is not working and there is no way to give the users a straight link to page2. This problem will be addressed shortly in a section of the article dedicated to SPA navigation.

Using JQuery UI Tabs for SPA Pages

It looks nicer to have tabs instead of hyperlinks when you want to switch between the pages. JQuery UI tabs can be used for this purpose.

JQuery UI is a GUI package built on top of JQuery. You can install it via NuGet in the same fashion as JQuery.

The code sample using JQuery UI tabs is located under TwoPagesSPAWithTabs solution. It consists of Index.htm file as well as the files from JQuery and JQuery UI packages. Index.htm file contains reference to a JQuery UI style sheet: <link href="Content/themes/base/jquery.ui.all.css" rel="stylesheet" type="text/css" /> at the top of the page (the style sheet is part of the JQuery UI package installation).

Here is the HTML code within Index.htm file:

<body>
    <ul id="pageTabs">
        <!-- hrefs of the links point to the ids of the pages' contents -->
        <li><a href="#page1">choose page1</a></li>
        <li><a href="#page2">choose page2</a></li>
    </ul>

    
    <!-- page 1 message colored in red -->
    <p id="page1" style="font-size:40px;color:red" >This is page 1</p>

    <!-- page 2 message colored in blue -->
    <p id="page2" style="font-size:40px;color:blue">This is page 2</p>
</body>

Note that the href attributes of the hyperlinks at the top of the code are pointing to ids of the HTML tags containing page content (e.g. hyperlink with href="#page1" points to the tag <p id="page1" ...). This is to let JQuery UI functionality figure out which content belongs to which tab.

The JavaScript code is also very simple:

$(document).ready(function () {
    $("body").tabs(); // turns the hyperlinks into tabs
});

Here is how the application looks:

Image 3

Just like in previous SPA sample, the URL is not connected to the tabs - changing tabs won't change the URL and changing URL won't change the tabs.

BPF Framework and Navigation

Here we present navigation functionality of the BPF framework.

Many SPAs use Sammy.js framework for the navigation. Sammy.js allows to map different URL hashes into various JavaScript functions. When SPA has a lot of different navigation states with some possible hierarchy, such mapping might be different to implement. BPF library, however, implements the mapping between the navigation states and the URL hashes naturally and with very little extra code.

There is a limitation to the current version of BPF framework, in that it relies on hashchange document event to detect the changes of the hash and change the navigation states of the SPA. IE7 does not support hashchange event so the navigation functionality will on work on it. The share of IE7 browser users is currently smaller than 1% and rapidly declining, so I believe this issue does not have to be addressed.

Code for BPF framework is located under BPF folder. It is also available from github.com/npolyak/bpf. The only file you need in order to access all of its functionality is bpf.min.js; so far (in the beginning of December 2012, it is pretty small - about 12Kb). This file was obtained by bundling and minification of other JavaScript files which can also be found in BPF folder and to which I will be referring from time to time.

Two Page SPA with Navigation

We'll start by using BPF functionality to modify the simple two page SPA with tabs presented above in such a way that it can be navigated. The sample is located under TwoPageSPAWithNavigation solution. The only difference between this project and TwoPageSPAWithTabs lies in JavaScript code at the bottom of Index.html file. In TwoPageSPAWithTabs project JavaScript code consisted of one line $("body").tabs(); within $(document).ready() method. The purpose of this line was to turn the hyperlinks into the JQuery tabs with the corresponding content. The JavaScript code within TwoPageSPAWithNavigation is more complex:

// get the tabs DOM elements
var tabs = $("body").tabs(); // turns the hyperlinks into tabs

// JQTabsNavAdaptor is a special constructro that adapts
// the JQuery tabs to become usable by the navigation framework
var tabsAdaptor = new bpf.nav.JQTabsNavAdaptor(tabs);

// create the navigation framework node
var tabNode = new bpf.nav.Node(tabsAdaptor);

// connect the navigation framework node to the browser's url hash
bpf.nav.connectToUrlHash(tabNode);

// set the url to correspond to the 
return bpf.nav.setKeySegmentToHash(tabNode);

Do not try to understand it all, since detailed navigation functionality design will be discussed shortly. In short, we use bpf.nav.Node functionality to connect the navigation states to the parts of the browser url. bpf.nav.JQTabsNavAdaptor adapts JQuery tabs so that the resulting adapted object has methods expected by bpf.nav.Node. The line bpf.nav.connectToUrlHash(tabNode) makes a two way connection between the nodes and the URL (nodes react to URL and vice versa). Last line bpf.nav.setKeySegmentToHash(tabNode) sets the initial URL to match the one obtained from bpf.nav.Nodes structure.

Running the sample shows SPA very similar to TwoPageSPAWithTabs, but the tabs have unique URL mapped to them and when you change the tabs, the URL also changes. Correspondingly "Back" and "Forward" browser buttons now work and switch between the tabs. Also typing "http://localhost:48476/Index.html#page2." for a URL in a browser, will get you straight to the second tab.

A More Complex Example With JQuery Tabs

HierarchicalSPAWithNavigation sample presents a more interesting of tab hierarchy. The top level tabs contain sub-tabs. Each tab/sub-tab combination maps to its own URL.

Try starting the application and playing with the tabs. Here is how the application looks:

Image 4

Pay attention that we show SubPage2 within Page2. This combination corresponds to the URL hash: "#page2.page2SubTab2.". If we change the tabs, the hash will change accordingly.

You can see that the hash always start with '#' and ends with '.' character. The links corresponding to different levels of the navigation state hierarchy are separated by the period ('.') character. Such links in this sample, match the href values of the links of the tabs stripped of '#' prefix character.

Here is the code for creating connecting the navigation (again do not try very hard to understand it as we'll discuss it in the next subsection):

// get the tabs DOM elements
var tabs = $("body").tabs(); // turns top level hyperlinks into tabs

var page1SubTabs = $("#page1SubTabs").tabs(); // turns page 1 hyperlinks into tabs

var page2SubTabs = $("#page2SubTabs").tabs(); // turns page 2 hyperlink into tabs

// creates the top level node (JQTabsNavAdaptor is used).
var topLevelNode = bpf.nav.getJQTabsNode(tabs);

// creates a child node and adds it to top level node
// as child of "page1".
bpf.nav.addJQTabsChild(topLevelNode, "page1", page1SubTabs);

// creates a child node and adds it to top level node
// as child of "page2".
bpf.nav.addJQTabsChild(topLevelNode, "page2", page2SubTabs);

// connect the navigation framework to the browser's url hash
bpf.nav.connectToUrlHash(topLevelNode);

// set the url to correspond to the current selection pattern
return bpf.nav.setKeySegmentToHash(topLevelNode);

Detailed Design of the Navigation Functionality

Navigation functionality is located under "namespace" bpf.nav within BPF library. It relies on the bpf.utils functionality and also parts of it depend on JQuery and JQuery UI (though it can work without them).

The purpose of Navigation functionality is to allow the developers to define a number of navigation states within an SPA and map each of the states into a unique browser URL so that when the state changes the URL changes also and vice versa - the URL change triggers the navigation state change.

The navigation state is assumed to be hierarchical - there can be a number of states at the top level (e.g. page 1 and page 2 from the previous sample). Each one can have sub-states (sub-pages). Each of the sub-states can have its own sub-states etc. The total navigation state of the application is determined by the unique selection of states at each level (or level states). Each level state maps to a link bound by period ('.') characters within the URL hash. It is attached to a link corresponding to the parent level state. The image below depicts a sample of the state hierarchy with the tree links corresponding to the level states and the hash strings shown next to the corresponding end nodes: e.g. Page1/SubPage2 selection will result in hash="#Page1.SubPage2.":

Image 5

Each part of the URL's hash bound by periods we call hash segment. For example hash "#Page1.SubPage2" has two segments: "Page1" and "SubPage2". Each segment maps into the level state selected at that level. At each level no more than one level state can be selected (sometimes, it is convenient to assume that no level state is selected at some level).

Navigation states consist of level states. Each navigation node can have several level child states that belong to it. One or none of these level child states may be selected as the current level state. Each of the level states has a string associated with it. This string uniquely identifies that state among the children states of the same node. This string becomes a segment bound by two period characters within the URL's hash when the level state is selected. The total hash is uniquely determined by the sequence of such segments corresponding to the level states starting from the higher level states and going down to the more refined levels.

The main functionality for capturing information about the tree of level states is bpf.nav.Node located within NavigationNode.js file. It kind of extends or subclasses (in C# sense) the functionality provided by bpf.nav.NodeBase located in NavigationNodeBase.js file. This subclassing is achieved by bpf.nav.Node constructor calling bpf.nav.NodeBase constructor within its code. We need subclassing because another class bpf.nav.ProductNode is also derived from the same bpf.nav.NodeBase class. bpf.nav.ProductNode functionality as will be discussed below. This inheritance relationship is shown at the diagram below:

Image 6

bpf.nav.Node references its parent and children (if any). bpf.nav.Node's parent and children are also derived from bpf.nav.NodeBase class. The children of bpf.nav.Node are accessible by string keys via function bpf.nav.NodeBase.getChild(key). The key maps into the corresponding segment of the URL's hash.

Each bpf.nav.Node object wraps some other object in charge of switching states at the node's level. As the above samples show, such object can be JQuery UI tabs object. bpf.nav.Node assumes that the wrapped object satisfies certain requirements - it should have some methods and fields - in Java or C# that requirement would be that the wrapped object should implement some interface. Here is the interface that it should implement written in C# notations:

public interface ISelectable
{
    // event that fires when a selection changes at the selectable
    // object's level. 
    // This event is implemented as bpf.nav.SimpleEvent in JavaScript
    event Action onSelectionChagned;

    // returns the currently selected key (hashSegment)
    // if no key is selected, it returns null
    string getSelectedKey();

    // does the selection specified by the key (or hashSegment)
    void select(string key);

    // unselects the currently selected key (if any and if the object allows it).
    void unselect();
}  

Most entities within JavaScript do not support the above interface so we have to use an adaptor to adapt the objects we are dealing with to ISelectable interface. The adaptor for JQuery UI tabs object is called bpf.nav.JQTabsNavAdaptor and it is part of the BPF library.

Image 7

Adaptor contains the object that controls the selection (in this case it is JQuery UI tabs object) and implements ISelectable interface by providing the methods that bpf.nav.Node expects.

Another adaptor built into BPF - a check box adaptor comes with bpf.nav.CheckboxNavAdaptor class. It provides a way to associate the level states with checkbox's checked and unchecked states.

bpf.nav.Node (via bpf.nav.NodeBase) has the following important methods:

  • setSelectedKeySegments(urlHash) - sets the selections for the node and its descendant to match the segments within URL's hash passed as a string to the function
  • getTotalHash() - returns a URL's hash that corresponds to the selections within the node itself and its descendants. The hash has a leading pound ('#') and trailing period ('.') characters.
These two methods provide a way to update the selection within some selectable objects (e.g. JQuery UI tabs) once the URL's hash changes and vice versa - to change the URL once a selection changes within the bpf.nav.Node hierarchy.

View Model Selection Example

This section requires some knowledge of Knockoutjs library. You can learn about Knockoutjs by using this link or many other resources including Solar System Animation built with SVG, Knockout and MVVM Pattern.

This section's sample is located under ParamNavigationTest solution. We have a View Model created by the functionality within the file StudentViewModel.js file under Scripts/ViewModels directory. It defines a collection of studentVM.students of objects of type Student. Each Student has a name field and a collection of course objects. Each course has two fields courseName and grade. The View Model studentVM has selectedStudent Knockoutjs observable field. Also each Student object has an observable selectedCourse field.

The View is located within HTML/Index.html file. It provides hyperlinks to the Student objects at the top level. The links display student's name. Once a link is clicked the corresponding student is selected and the browser window shows the name of the courses taken by the selected student under the student's hyperlink. When the course link is clicked the browser shows the student name, course name and the student's grade for that course:

Image 8

Our task is to provide a unique URL hash for each student - course selection.

We can, of course, use the bpf.nav.Node functionality described above for every student and course object. This, however, can be problematic if the number of such objects is very high or if the student or course collections change throughout the application.

BPF library has a class bpf.nav.FactoryNode located within NavigationFactoryNode.js file, that produces bpf.nav.Node objects as they are needed by the application and adds them to their parent node's childNodes collection.

We use bpf.nav.KoObservableNavAdaptor as an adaptor object to adapt the observable objects. bpf.nav.KoObservableNavAdaptor constructor takes 3 parameters:

  • the observable
  • the function to get the object by the hash segment (or key) from the View Model collection
  • the function to get the key from each object within View Model collection

Here is how we connect the navigation states to the View Model within Index.html file:

// we create the adaptor for the selected student observable
var studentLevelObservableAdaptor =
    new bpf.nav.KoObservableNavAdaptor
    (
        studentVM.selectedStudent,

        // key to student function
        function (key) {
            var foundStudent = _.chain(studentVM.students).filter(function (student) {
                return student.name === key;
            }).first().value();

            return foundStudent;
        },

        // student to key function
        function (student) {
            return student.name;
        }
    );

var topLevelNode = new bpf.nav.FactoryNode(
    studentLevelObservableAdaptor,
    null,

    // child node producing function
    function (key, data) {

        var childObj = data.getChildObjectByKey(key);

        if (!childObj)
            return;

        // create adapted child object
        var adaptedChildObj =
            new bpf.nav.KoObservableNavAdaptor
            (
                childObj.selectedCourse,
                function (courseKey) {
                    return _.chain(childObj.courses).
                        filter(function (universityCourse) {
                            return universityCourse.courseName === courseKey;
                        }).first().value();
                },
                function (course) {
                    return course.courseName;
                }
            );

        // create the node
        return new bpf.nav.Node(
            adaptedChildObj
        );
    }
);

bpf.nav.connectToUrlHash(topLevelNode);

bpf.nav.setKeySegmentToHash(topLevelNode);  

Cartesian Product Navigation

Suppose that your browser is divided into two halves. Each of the halves has tabs. You can select one tab in each of the halves. You also want to assign a URL for every possible tab combination. The resulting space of navigation states will be a Cartesian products of the states within each of the halves of the browser.

We can further complicate the task by dividing the browser into more than two parts. Also we can assume that such Cartesian product can occur not only at the top state, but at any state level.

SimpleProductNavigationTest solution contains precisely the scenario we described in the beginning of this section. The browser page is divided into two parts. Each part has tabs. We record every combination of tab selections in both parts of the page:

Image 9

Note that total URL hash for on the figure above is "#(topics/TopicA)(othertopics/SomeOtherTopicA).". The parts of the hash corresponding to the components of the Cartesian product are placed within a parenthesis. The part of the hash within a parenthesis starts with a key that uniquely identifies that part within the Cartesian product. The key is separated from the rest of the string within the parenthesis by a forward slash.

We use bpf.nav.ProductNode to map the URL hash into the Cartesian product of the states. Its usage example is located at the bottom of Index.html file within SimpleProductNavigationTest project:

// create topics JQuery UI tabs
var topics = $("#MyTopics").tabs();

// create otherTopics tabs
var otherTopics = $("#SomeOtherTopics").tabs();
        
// top level node is a product node for
// the Cartesian product of the states
var topLevelNode = new bpf.nav.ProductNode();

// add "topics" node to be a child of topLevelNode under key "topics"
bpf.nav.addJQTabsChild(topLevelNode, "topics", topics);

// add "otherTopics" node to be a child of topLevelNode under key "othertopics"
bpf.nav.addJQTabsChild(topLevelNode, "othertopics", otherTopics);

// create the one to one relationship between the states and the URL
bpf.nav.connectToUrlHash(topLevelNode);

// change the original URL to be based on the navigation states
bpf.nav.setKeySegmentToHash(topLevelNode);  

bpf.nav.ProductNode is located within ProductNavigationNode.js file. Just like bpf.nav.Node, it is kind of a sub-class of bpf.nav.NodeBase. It overrides the implementation of the functions setSelectedKeySegmentsRecursive, getUrlRecursive and chainUnselect.

A considerably more complex example can be found in ComplexProductNavigationTest solution. It involves a long sequence of tabs within tabs with Cartesian products occurring at two levels - the top one and under TopicA tab.

Here is how the SPA looks:

Image 10

The total hash part of the URL for the figure above is "#(topics/TopicA.(a1subs1/A1Subtopic1.A1Sub1Sub2)(a1subs2/A2Subtopic2))(othertopics/SomeOtherTopicA).". You can play with the application to see that the browser navigation buttons are working indeed.

Here is the HTML code of the application:

<table>
    <tr>
        <td style="vertical-align: top">
            <div id="MyTopics">
                <ul>
                    <li><a href="#TopicA">TopicA</a></li>
                    <li><a href="#TopicB">TopicB</a></li>
                </ul>

                <div id="TopicA">
                    My topic A
                    <table>
                        <tr>
                            <td style="vertical-align: top;">
                                <div id="A1Subtopics">
                                    <ul>
                                        <li><a href="#A1Subtopic1">A1Sub1</a></li>
                                        <li><a href="#A1Subtopic2">A1Sub2</a></li>
                                    </ul>

                                    <div id="A1Subtopic1">
                                        A1 Sub Topic1
                                        <div id="A1Sub1Sub">
                                            <ul>
                                                <li><a href="#A1Sub1Sub1">A1S1S1</a></li>
                                                <li><a href="#A1Sub1Sub2">A1S1S2</a></li>
                                            </ul>
                                            <div id="A1Sub1Sub1">A1 Sub1 Sub1</div>
                                            <div id="A1Sub1Sub2">A1 Sub1 Sub2</div>
                                        </div>
                                    </div>
                                    <div id="A1Subtopic2">A1 Sub Topic2</div>
                                </div>
                            </td>
                            <td style="vertical-align: top;">
                                <div id="A2Subtopics">
                                    <ul>
                                        <li><a href="#A2Subtopic1">A2Sub1</a></li>
                                        <li><a href="#A2Subtopic2">A2Sub2</a></li>
                                    </ul>

                                    <div id="A2Subtopic1">A2 Sub Topic1</div>
                                    <div id="A2Subtopic2">A2 Sub Topic2</div>
                                </div>

                            </td>
                        </tr>
                    </table>
                </div>
                <div id="TopicB">
                    The Topic B
                </div>
            </div>
        </td>
        <td style="vertical-align: top">
            <div id="SomeOtherTopics">
                <ul>
                    <li><a href="#SomeOtherTopicA">AnotherA</a></li>
                    <li><a href="#SomeOtherTopicB">AnotherB</a></li>
                </ul>
                <div id="SomeOtherTopicA">Some Other A </div>
                <div id="SomeOtherTopicB" style="background-color: pink">Some Other B</div>
            </div>
        </td>
    </tr>
</table>  

And here is the JavaScript code within $(document).ready() function:

  // create topics JQuery UI tabs
var topics = $("#MyTopics").tabs();

// create otherTopics tabs
var otherTopics = $("#SomeOtherTopics").tabs();

var A1Subtopics = $("#A1Subtopics").tabs();

var A2Subtopics = $("#A2Subtopics").tabs();

var A1Sub1Sub = $("#A1Sub1Sub").tabs();

// top level node is a product node for
// the Cartesian product of the states
var topLevelNode = new bpf.nav.ProductNode();

// add "topics" node to be a child of topLevelNode under key "topics"
var topicsNode = bpf.nav.addJQTabsChild(topLevelNode, "topics", topics);

// add "otherTopics" node to be a child of topLevelNode under key "othertopics"
bpf.nav.addJQTabsChild(topLevelNode, "othertopics", otherTopics);

var aSubtopicsProductNode = new bpf.nav.ProductNode();

topicsNode.addChildNode("TopicA", aSubtopicsProductNode);

var A1SubNode = bpf.nav.addJQTabsChild(aSubtopicsProductNode, "a1subs1", A1Subtopics);
bpf.nav.addJQTabsChild(aSubtopicsProductNode, "a1subs2", A2Subtopics);

bpf.nav.addJQTabsChild(A1SubNode, "A1Subtopic1", A1Sub1Sub);

// create the one to one relationship between the states and the URL
bpf.nav.connectToUrlHash(topLevelNode);

// change the original URL to be based on the navigation states
bpf.nav.setKeySegmentToHash(topLevelNode);

Using BPF Framework for SPA Composition

As I mentioned in Introduction, SPA might result in large HTML files due to the fact that all its pages are loaded together and switching the pages simply means that parts of HTML change their visibility.

BPF framework provides a way to break HTML functionality into smaller files (called BPF plugins or simply plugins) and assemble all of them together on the client side. It also allows the plugins to refer to some other plugins creating a plugin hierarchy. Moreover, you can place JavaScript code dealing with the functionality provided by the plugin into the same HTML file and later call this functionality either during the composition or at a later stage. This was my attempt to imitate the WPF/Silverlight code-behind concept within HTML/JavaScript universe.

All of the BPF composition related functionality is located within Composite.js file and it relies heavily on JQuery.

Simple Plugin Sample

Let us take a look at the sample under SimpleCompositionSample project. HTML directory of this project contains two html files Index.html - the main file and APlugin.html - a file containing the plugin code. To start the application, right click on Index.html file within Solution Explorer and choose "View in Browser" option.

Here is the HTML code within Index.html file:

<div style="font-size:30px;color:red">This is the main Module</div>
<img id="busyIndicator" src="../Images/busy_indicator.gif" style="vertical-align:central;margin-left:50%" />

<!-- plugin will get into this div below -->
<div id="APluginContainer1"></div>

<!-- call plugin's function to change its color to 'blue' for the plugin above -->
<button id="changePluginColorButton1">Change 1st Plugin Text to Blue</button>

<!-- plugin will get into this div below -->
<div id="APluginContainer2" style="margin-top:40px"></div>

<!-- call plugin's function to change its color to 'blue' for the plugin above -->
<button id="changePluginColorButton2">Change 2nd Plugin Text to Blue</button>

The only text visible within main HTML file is "This is the main Module". There are two <div> tags for inserting plugin content: "APluginContainer1" and "APluginContainer2". Each one of them has a button under it that calls the plugin's code-behind to change the color of the text of the corresponding plugin instance to blue.

You may notice that the same plugin is being inserted into two places within the main module.

Here is the JavaScript code of Index.html:

// this event will fire after all the plugins and their
// descendant plugins are loaded
var compositionReady = new bpf.utils.EventBarrier();

// the composition ready event fires after everything all the plugins
// and their descendants have been loaded into the main module.
compositionReady.addSimpleEventHandler(function (success) {
    $("#busyIndicator").hide();

    $("#changePluginColorButton1").click(function () {

        // after changePluginColorButton1 is clicked, the
        // the function changeColorBackToBlue defined within the plugin
        // will be called and will only affect APluginContainer1
        bpf.control("#APluginContainer1").call("changeColorToBlue");
    });

    // after changePluginColorButton2 is clicked, the
    // the function changeColorBackToBlue defined within the plugin
    // will be called and will only affect APluginContainer2
    $("#changePluginColorButton2").click(function () {
        bpf.control("#APluginContainer2").call("changeColorToBlue");
    })
});

// gets the plugin from file APlugin.html and inserts it into the 
// element pointed to by the selector "#APluginContainer1"
bpf.cmpst.getPlugin("APlugin.html", null, "#APluginContainer1", compositionReady);

// gets the plugin from file APlugin.html and inserts it into the 
// element pointed to by the selector "#APluginContainer2"
bpf.cmpst.getPlugin("APlugin.html", null, "#APluginContainer2", compositionReady);  

The two bpf.cmpst.getPlugin calls at the bottom, load the plugin into the main module inserting it into two different places within the DOM. The compositionReady event handler is called after all the plugins and their descendants are loaded into the main module. It hides the "busyIndicator" and it assigns the callback to the buttons' events calling changeColorToBlue function of the plugin's code=behind:

bpf.control("#APluginContainer1").call("changeColorToBlue");

The function bpf.control(jquery-selector) returns the code-behind for the plugin contained within the element pointed to by the jquery-selector passed to it. We use the function call() created by the BPF framework to call the code-behind's method. The function's first argument is the name of the method you want to call. If the method has some input arguments, you can add them after the method name.

Now, let us take a look at the plugin code:

<div id="aPlugin" style="font-size:25px">
    This is a plugin
</div>

<!--script tag should be marked by data-type="script-interface" in
    order for it to be recognized as containing the plugin's 'code-behind' -->
<script type="text/javascript" data-type="script-interface">
    (function () {
        // this function returns the plugin's 'code-behind' - 
        // a JSON object consisting the functions. Two function names are 
        // reserved "preRender" and "postRender" - the first executes 
        // before the plugin is inserted into the parent module's DOM
        // the second executes after the plugin is inserted into the parent's
        // module dome. 
        // other functions can be called at any later stage - at the developer's will
        // 'this' object for every function except for preRender contains the parameters
        // of the element into which this plugin attached. The most important parameter is
        // currentDOM - the DOM of the parent element.
        // All the JQuery selectors acting within the plugin should start with
        // this.currentDOM.find(... to make sure that we are only modifying our instance
        // of the plugin and not some other instances.
        // The other important field within "this" object is "codeBehind". It gives
        // access to the code-behind function returned below.
        // Note that all the function names should be in quotes in order for eval method
        // to work at the moment when the plugin is inserted into the plugin cache.
        return {
            "postRender": function (compositionReadyEventBarrier) {
                // change color to blue
                this.currentDOM.find("#aPlugin").css("color", "green");
            },
            "changeColorToBlue": function () {
                // change the color to blue
                this.currentDOM.find("#aPlugin").css("color", "blue");
            }
        };
    })();
</script> 

The HTML of the plugin simply shows text "This is a plugin".

The <script> tag marked with data-type="script-interface" attribute is considered to contain the plugin's code-behind. There should only be one <script> attribute like that. The rest of the script attributes can be added to the plugin in order to enhance intellisense, but will be removed when the plugin is loaded into the main module. All the JavaScript references contained in <script> required for the plugins to work should be placed in the main module.

The code-behind <script> tag contains an anonymous function that returns a JSON object defining various methods. There are two predefined code-behind methods that are called (if they exist) when the plugin is loaded into the main module: preRender and postRender - the first one is called before and the second one after the plugin is loaded.

All the code-behind methods except for preRender receive a special object as their this variable. This object contains currentDOM field which is a JQuery DOM object for the element within the parent module into which the plugin was attached.

Important Note: if you want to find a DOM element belonging to a plugin within the code-behind you should do it via a call to this.currentDOM.find(selector) function, not by using $(selector). The reason is that if the same plugin is inserted in multiple places within the main module, unless you use this.currentDOM.find method, you'll probably find multiple instances of the same plugin instead of that particular instance, so that all of your modification will be applied to multiple instances instead of the particular instance you need. BTW, note that that since our plugin has and id: id="aPlugin", and the plugin is inserted twice into the main module, you'll have two different elements with the same id within the main module. It seemed to be working fine in my experience due to the fact that I was always using this.currentDOM.find to resolve the instance. If you feel that it is wrong (after all it is the ABCs of the HTML that there should be no two elements with the same id within the DOM), you can avoid using ids within plugins and use unique class names instead.

The postRender function changes the color of the text of the plugin to green once the plugin is inserted into the main module.

Code-behind's method changeColorToBlue changes the color of the plugin's text to blue.

Here is how we call the changeColorToBlue function from the JavaScript code of the main module (file Index.html):

bpf.control("#APluginContainer1").call("changeColorToBlue"); 

When you run the application you will get the text for both instances of the plugin colored in green. When you click the button under the corresponding plugin instance that plugin instance will turn blue, while the other stays the same color. Here is the image of the application after the top plugin instance's button was clicked:

Image 11

The text of the top plugin instance changed to blue, while the text of the bottom plugin instance stayed green.

Chain of the Plugins Composition Sample

ChainOfPluginsCompositionSample solution contains a sample where the main module loads a plugin, which, in turn load another plugin we call it sub-plugin. All the HTML files of the sample are located under HTML folder. Index.html is the file containing the main module. You can start the application by right clicking on it and choosing "View in Browser" option. Here is the image of the application once it starts:

Image 12

The buttons change the color of the text of the plugin and sub-plugin correspondingly.

The code of Index.html file does not contain anything new in comparison to the previous sample. Most "interesting" code is located within APlugin.html file:

<div id="aPlugin" style="font-size:25px">
    This is a plugin

    <div id="subPluginContainer"></div>

    <Button id="changePluginColorButton">Change Plugin Color to Blue</Button>
    <Button id="changeSubPluginColorButton">Change SUB Plugin Color to Black</Button>
</div>
<script src="../Scripts/jquery-1.8.3.js"></script>
<script src="../Scripts/BPF/bpf.js"></script>
<script type="text/javascript" data-type="script-interface">
    (function () {
        return {
            "postRender": function (compositionReadyEventBarrier) {
                var self = this;

                // change color to green
                self.currentDOM.find("#aPlugin").css("color", "green");

                // create a child event barrier - it will fire
                // once the sub-plugin and all of its
                // descendants are loaded.
                // And the parent event barrier won't fire until it fires.
                var subCompositionReady = 
                    compositionReadyEventBarrier.createChildEventBarrier();

                subCompositionReady.addSimpleEventHandler(function () {
                    // changes the color of the plugin itself to blue.
                    $("#changePluginColorButton").click(function () {
                        self.currentDOM.find("#aPlugin").css("color", "blue");
                    });

                    // on changeSubPluginColorButton click call the sub-plugin's
                    // method changeColorToBlack
                    $("#changeSubPluginColorButton").click(function () {
                        bpf.control("#subPluginContainer", self).call("changeColorToBlack");
                    });
                });

                // get sub-plugin note that you need to pass 'this' object
                // as the second argument to getPlugin function
                bpf.cmpst.getPlugin("ASubPlugin.html", self, "#subPluginContainer", subCompositionReady);
            }
        };
    })();
</script>

Note that when we call getPlugin() function within the plugin, we pass self ( which is the same as this variable) as the second argument, not null as we do in the main module. Also note that in order to get access within a plugin to the code-behind object of the sub-plugin, we need to pass self as the second argument to the bpf.control() function: bpf.control("#subPluginContainer", self).call("changeColorToBlack");.

Composition and Navigation Sample

CompositionAndNavigationSample solution shows how to combine BPF composition and navigation. We create two tabs at the main module level. We also have tabs within a plugin. The plugin is added to the content of one of the tabs within the main module. We use the bpf.nav.Node objects create a navigation node around the plugin's tabs and connect it as a child of the main module's node structure. Here is the main modules code within Index.html file:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <link href="../Content/themes/base/jquery-ui.css" rel="stylesheet" />
    <title></title>
</head>
<body>
    <!-- links to be converted to tabs -->
    <ul id="pageTabs">
        <li><a href="#page1">Page1</a></li>
        <li><a href="#page2">Page2</a></li>
    </ul>

    
    <!-- page 1 message colored in red -->
    <div id="page1" style="color:red" >
        This is page 1

        <div id="pluginContainer"></div>
    </div>

    <!-- page 2 message colored in blue -->
    <div id="page2" style="color:blue">This is page 2</div>
</body>
</html>
<script src="../Scripts/jquery-1.8.3.min.js"></script>
<script src="../Scripts/jquery-ui-1.9.2.min.js"></script>
<script src="../Scripts/BPF/bpf.js"></script>

<script>
    $(document).ready(function () {

        // create the tabs, store a reference to the tabs.
        var topLevelTabs = $("body").tabs();

        // create the top level bpf.nav.Node object around the tabs.
        var topLevelNode = bpf.nav.getJQTabsNode(topLevelTabs);

        var compositionReady = new bpf.utils.EventBarrier();

        compositionReady.addSimpleEventHandler(function () {
            // this function is called after all the plugins are loaded (rendered)
            
            // we call "connectToParentNode" function on the plugin
            // passing the topLevelNode to it, in order to connect
            // the plugin's tabs to the bpf navigation framework.
            bpf.control("#pluginContainer").call("connectToParentNode", topLevelNode);

            // connect the navigation framework with the URL's hash
            bpf.nav.connectToUrlHash(topLevelNode);

            // update the current URL
            bpf.nav.setKeySegmentToHash(topLevelNode);
        });

        // get the plugin
        bpf.cmpst.getPlugin("APlugin.html", null, "#pluginContainer", compositionReady);
    });
</script>  

To connect the plugin's navigation node to that of the main module, we call function connectToParentNode on the plugin's code-behind, passing the parent node (topLevelNode) as a parameter to it:

bpf.control("#pluginContainer").call("connectToParentNode", topLevelNode);

Here is the plugin's code (from APlugin.html file):

 <div id="subTabPlugin">
    <ul id="subTabs">
        <li><a href="#subPage1">SUB Page1</a></li>
        <li><a href="#subPage2">SUB Page2</a></li>
    </ul>

    
    <!-- page 1 message colored in red -->
    <div id="subPage1" style="color:green" >This is SUB page 1</div>

    <!-- page 2 message colored in blue -->
    <div id="subPage2" style="color:purple">This is SUB page 2</div>
</div>
<script src="../Scripts/jquery-1.8.3.min.js"></script>
<script src="../Scripts/jquery-ui-1.9.2.min.js"></script>
<script src="../Scripts/BPF/bpf.js"></script>
<script type="text/javascript" data-type="script-interface">
    (function () {
        var pluginTabs;

        return {
            "postRender": function (compositionReadyEventBarrier) {
                // create tabs within the plugin and store the
                // tab object to be used later
                pluginTabs = this.currentDOM.find("#subTabPlugin").tabs();
            },
            "connectToParentNode": function (parentNode) {
                // create the navigation node at the plugin level and 
                // connect it with the parent navigation node.
                bpf.nav.addJQTabsChild(parentNode, "page1", pluginTabs);
            }
        };
    })();
</script> 

The method postRender creates the tabs out of the hyperlinks and stores the reference to them within the plugin by calling pluginTabs = this.currentDOM.find("#subTabPlugin").tabs();.

Connecting to the parent's navigation node is taken care of by the method connectToParentNode. This method is called from the main module. (Of course we could have connected the navigation nodes within postRender function but we wanted to demonstrate more composition functionality by creating a separate method connectToParentNode and calling it from the main module).

Parameterized Plugins

ParameterizedPluginSample shows how to create instances of the same plugin that differ in their looks. Just like in SimpleCompositionSample, two instances of the same plugin are inserted into the main module contained within Index.html file:

// gets the plugin from file APlugin.html and inserts it into the 
// element pointed to by the selector "#APluginContainer1"
bpf.cmpst.getPlugin(
    "APlugin.html",
    null,
    "#APluginContainer1",
    compositionReady,
    {
        fontSize: "50px",
        color: "blue"
    }
);

// gets the plugin from file APlugin.html and inserts it into the 
// element pointed to by the selector "#APluginContainer2"
bpf.cmpst.getPlugin(
    "APlugin.html",
    null,
    "#APluginContainer2",
    compositionReady,
    {                      // pass the object containing postRenderArguments
        fontSize: "20px", 
        color: "green"
    }
);  

The last argument to bpf.cmpst.getPlugin function is the JSON object which can be accessed from within postRender function of the plugin's code-behind via this.postRenderArguments object. Here is the code-behind of the plugin:

 {
    "postRender": function (compositionReadyEventBarrier) {
        var fontSize = this.postRenderArguments.fontSize; // get font size from the arguments
        var color = this.postRenderArguments.color; // get color from the arguments

        this.currentDOM.find("#aPlugin").css("font-size", fontSize);
        this.currentDOM.find("#aPlugin").css("color", color);
    }  
}

As a result we have the same plugin text displayed with different font-size and color:

Image 13

The top plugin instance has font size of 50px and blue color, while the bottom one - 20px and green color.

Generic Silverlight Plugin Sample

Here I describe a generic BPF plugin for Silverlight applications.

Silverlight apps are usually packaged as one file with .xap extension. In order to embed a Silverlight app into a page, you have to have the page reference Silverlight.js file (which comes from Microsoft). You also have to write the following HTML code within your HTML application:

<object type="application/x-silverlight-2" 
        style="display: block; text-align: center; width: 100%; height: 100%">
    <param name="source" value="../ClientBin/MySilverlightApp.xap" /> <!-- url to Silverlight app -->
    <param name="onError" value="onSilverlightError" />
    <param name="background" value="white" />
    <param name="minRuntimeVersion" value="5.0.61118.0" />
    <param name="autoUpgrade" value="true" />
    <a href="http://go.microsoft.com/fwlink/?LinkID=149156&v=5.0.61118.0" style="text-decoration: none">
        <img src="http://go.microsoft.com/fwlink/?LinkId=161376" alt="Get Microsoft Silverlight"
            style="border-style: none" />
    </a>
</object>  

This is a large chunk of HTML code and my idea was to create a BPF parameterized plugin for re-using it in multiple places within HTML applications. One of the parameters of the BPF plugin will be, of course, the Silverlight app's URL. Other parameters might be needed for positioning the application, e.g. width, height, margins.

You would probably want to know why Silverlight plugin is different from any other BPF plugin. Why can't we simply create a parameterized plugin using the information from the previous subsection? My answer is "redirect your question to Microsoft". The <object> tag behaves in a very strange way in conjunction with JQuery on IE8 and IE9 - trying to use JQuery to parse the DOM of the object tag totally mangles it. Besides, from my experience, the Silverlight app URL has to be set before the HTML code is inserted into the main module - IE won't change the Silverlight application when the Silverlight URL changes if the code has already been inserted. In fact it is because of these issues that I added preRender function to the code-behind to be called before the plugin is added to the main module.

The code for this sample is located under SilverlighPluginSample solution. Here is what you get if you run the application - (hey, after a week of toiling on this article I can allow myself a bit of self promotion - AWebPros.com is my company):

Image 14

To get around the issues with Silverlight plugin that I describe above, we change the plugin code substituting <object> tag for <myobject>. Also in order to place the URL parameter in the correct place, we use "___XAPFilePlaceHolder___" string as a placeholder. The substitution of these strings by the correct ones takes place within preRender method, before the code is inserted into the main module. We get the HTML code of the Silverlight plugin by using this.getDownloadedHtml() function (which, within preRender method indeed returns the full plugin HTML code).

Here is the code within SilverlightContainerPlugin.html file:

<div class="slContainerDiv">
    <myobject type="application/x-silverlight-2" 
            style="display: block; text-align: center; width: 100%; height: 100%">
        <param name="source" value="___XAPFilePlaceHolder___" />
        <param name="onError" value="onSilverlightError" />
        <param name="background" value="white" />
        <param name="minRuntimeVersion" value="5.0.61118.0" />
        <param name="autoUpgrade" value="true" />
        <a href="http://go.microsoft.com/fwlink/?LinkID=149156&v=5.0.61118.0" style="text-decoration: none">
            <img src="http://go.microsoft.com/fwlink/?LinkId=161376" alt="Get Microsoft Silverlight"
                style="border-style: none" />
        </a>
    </myobject>
</div>
<script src="../Scripts/jquery-1.8.2.js"></script>
<script type="text/javascript" data-type="script-interface">
    (function () {
        return {
            "preRender": function (preRenderArgs) {
                
                // get original html of the plugin
                var originalHtml = this.getDownloadedHtml()

                // replace "myobject" string with "object" and 
                // ___XAPFilePlaceHolder___ with the Silverlight app url parameter.
                var modifiedHtml = originalHtml.replace(/myobject/gi, "object").replace("___XAPFilePlaceHolder___", preRenderArgs.slAppUrl);

                // replace the HTML code of the plugin
                this.setDownloadedHtml(modifiedHtml);
            },
            "postRender": function () {
                var args = this.postRenderArguments;

                var slContainerDiv = this.currentDOM.find(".slContainerDiv");

                // position the plugin
                if (args) {
                    slContainerDiv.css("margin-top", args.marginTop);
                    slContainerDiv.css("margin-bottom", args.marginBottom);
                    slContainerDiv.css("width", args.width);
                    slContainerDiv.css("height", args.height);
                }
            },
        }
    })();
</script>  

Here is how the plugin is inserted within the main module:

bpf.cmpst.getPlugin(
    "SilverlightContainerPlugin.html",
    this,
    "#AWebProsLogo",
    compositionReady,
    {   // post-render args
        marginTop: "auto",
        marginBottom: "auto",
        width: "150px",
        height: "150px"
    },
    {   // pre-render args
        slAppUrl: "../ClientBin/AnimatedBanner.xap"
    }
);  

This plugin is not part of BPF library (which consists only of JavaScript files) but it can be very useful and as such it is published on github.com/npolyak/bpf/tree/master/BPF/UsefulBPFPlugins

SPA Server Communications Using ASP.NET MVC

Here we show how SPA can communicate with the ASP.NET MVC server. Unlike the previous two sections, this section does not contain anything new - it is simply a tutorial providing a number of different examples of SPA communicating with the server.

Simple GET Request Sample Returning String from the Server

As was mentioned in Introduction, once the SPA is running, it should only load string or JSON data for the application and not the HTML code. A very simple example of an SPA sending and getting some string data from an ASP server can be found under SPAServerStringCommunications solution.

To start the solution, right mouse button click on Index.html file and choose "View in Browser". Here is how the application looks after it is started:

Image 15

After you enter your name into the editable area and click "Get Server String" button the browser will send your name to the server and receive a string "Hello <yourname>" from the server and display above the button in red:

Image 16

SPAServerStringCommunications project was created as ASP.NET MVC 4 empty project. After the project was created, I removed Model and Views folders and added a "Hello" controller to the Controllers folder. I also removed all the scripts from the Scripts folder and installed JQuery as a NuGet package.

HelloControler contains only one method GetHelloMessage that takes name argument and returns string "Hello" + name wrapped in Json function:

public ActionResult GetHelloMessage(string name)
{
    // AllowGet flag states that the method can be accessed
    // by GET HTTP request.
    return Json("Hello " + name, JsonRequestBehavior.AllowGet);
}

In this case Json function simply wraps a string and so, the string is returned to the server.

The relative URL to GetHelloMessage function is obtained by concatenating the controller name, forward slash and the function name: "/hello/gethellomessage".

The project's HTML/JavaScript client is located within Index.html file. Here is the HTML part of the client code:

<body>
    <div>Enter name</div>

    <!-- name input -->
    <input id="nameInput" type="text" name="value" style="width:200px" />

    <!-- div to display the text returning from the server -->
    <div id="serverText" style="min-height:50px;color:red;margin-top:10px"></div>

    <!-- button to trigger communication with the server -->
    <button id="getHelloStringButton">Get Server String</button>
</body>

And here is the JavaScript:

$(document).ready(function () {
    $("#getHelloStringButton").click(function () { // on button click

        // get name value
        var name = nameInput.value;

        // send query string to the server.
        // /hello/gethellomessag is the relative url
        // second argument is the query string
        // 3rd argument is the function called on successful reply from the server
        $.get("/hello/gethellomessage", "name=" + name, function (returnedMessage) {

            // write the text coming fromt he server into
            // serverText element.
            $("#serverText").text(returnedMessage);
        });
    });
});

JavaScript code uses JQuery's $.get function to send a GET request to relative url "/hello/gethellomessage" and to call a function to insert whatever come from the server into serverText element. The structure of the query (which is part of the GET request from the client) is "name=<value-that-you've-entered>". The name within the query will map into name argument of the controller function and the value will become the value of the name argument of the controller function, so no extra parsing is necessary on the server side.

In general, when a GET query has multiple arguments, the query should be built as name1=<val1>&name2=<val1>... and on the server side the controller function arguments should have the same argument names as the GET query. In that case, parsing will happen automatically and the values of the arguments will be the same as the values within the query.

GETting a Complex Object from the Server

In the previous sample, we sent a string to the server to get a string back. SPAComplexObjectGetter project returns a JSON object with some structure to it in response to a GET HTTP request. To run the project, open it in Visual Studio right mouse click on Index.html file in Solution Explorer and choose "View in Browser". Here is what you will see after clicking button "Get Cinema Info":

Image 17

Unlike the previous sample, SPAComplexObjectGetter project has a non-empty Models folder with two classes in it: Cinema and Movie. Each Movie has a Title and a Year of the release. Cinema has Name, Description and a collection of Movie objects called Movies. Cinema's default constructor creates a Cinema object and populates it with some data including two Movie objects. The purpose of this sample is to show how this cinema information can be requested by the client, transferred to it from the server in JSON format and displayed on the client side.

"Controllers" folder contains DataController.cs file with only one method:

public ActionResult GetCinema()
{
    // create a cinema object populated with data
    Cinema cinema = new Cinema();

    // send it in JSON form back to the client
    return Json(cinema, JsonRequestBehavior.AllowGet);
}

The relative URL to call method GetCinema within DataController is "/data/getcinema".

Now let us look at the client code within Index.html file. HTML code is very simple consisting of a div to add the cinema info to and of a button for triggering the exchange with the server:

<body>
    <div id="cinemaInfo" style="margin-bottom:20px"></div>

    <Button id="getCinemaInfoButton">Get Cinema Info</Button>
</body>	

JavaScript code is more complex, but just like the code of the previous sample, it uses JQuery function $.get to send GET request to the server and process whatever information is returned from the server:

$(document).ready(function () {
    $("#getCinemaInfoButton").click(function () { // on button click

        // send query string to the server.
        // /hello/gethellomessag is the relative url
        // second argument an empty string since our query does not have parameters
        // 3rd argument is the function called on successful reply from the server
        $.get("/data/getcinema", "", function (cinema) {

            // clear the info displayed from 
            // the previous click (if any).
            $("#cinemaInfo").contents().remove();

            // add cinema information to "cinemaInfo" div element
            $("#cinemaInfo").append("Name: " + cinema.Name);
            $("#cinemaInfo").append("<div></div>");
            $("#cinemaInfo").append("Description: " + cinema.Description);
            $("#cinemaInfo").append("<div style='margin-top:5px'></div>");
            $("#cinemaInfo").append("Movies:");
            $("#cinemaInfo").append("<div></div>");
            // add information about individual movies
            for (var idx = 0; idx < cinema.Movies.length; idx++) {
                var movie = cinema.Movies[idx];

                $("#cinemaInfo").append
                (
                    "    " +
                    "Title: " + movie.Title +
                    ",    " +
                    "year: " + movie.Year
                    );
                $("#cinemaInfo").append("<div></div>");
            }
        });
    });
});

The query part of the function $.get is empty, since our GET request does not have any parameters.

The object returned from the server is represented by variable cinema which is an input variable to the function called on GET request success. Within that function we form the text and append it to the "cinemaInfo" div tag. The returned cinema object has fields Name and Description as well as an array of movie objects called Movies. Each movie object has Title and Year fields. In fact, the returned JSON object mimics the server side C# object of type Cinema.

You can view the JSON object returned from the server by opening a browser e.g. Chrome and typing the URL to the controller function GetCinema: "http://localhost:29863/data/getcinema". You will see the returned JSON string in the browser.

POSTing Complex Data Object to the Server

GET requests presented in the two previous samples build the query as part of the URL and as such GET requests are perfect when an object you want to send a set of names and values to the server. When a complex JSON object needs to be transferred from the client to the server, POST request should be utilized.

In this sub-section we will consider the same Cinema model as in the previous one, only the model will be created on the client, sent to the server for modifications, returned by the server back to the client and displayed on the client.

This sub-section's sample is located under SPA_POSTingObjectToServer solution.

The server has the same model classes: Cinema and Movie, but Cinema's default constructor is empty since the data is coming from the client. DataController's method ChangeCinemaData changes the data coming from the client by adding another movie "Anne of Planet Mars" to the collection of the movies. Then it returns the modified Cinema object back to the client.

To cover the case of the server failure, I also made the server request fail every other time. To mark the failure, the server changes the Response's StatusCode and returns "serverError" as a string. Here is the server code:

static bool evenRequest = true;

// url=/data/changecinemadata
public ActionResult ChangeCinemaData(Cinema cinema)
{
    evenRequest = !evenRequest;

    // report error on every even request
    if (evenRequest)
    {
        Response.StatusCode = (int)System.Net.HttpStatusCode.InternalServerError;

        return Json("serverError");
    }

    // add movie "Anne of Planet Mars"  
    // to Cinema data coming from the client 
    // and return the modified Cinema data
    // back to the client
    cinema.Movies.Add
    (
        new Movie
        {
            Title = "Anne of Planet Mars",
            Year = 3000
        }
    );

    return Json(cinema);
}

Note, that the server will interpret the data coming from the client as an object of type Cinema as long as the object has correct structure.

The client code is located in Index.html file. HTML consist of two div tags - one for the visual representation of the original Cinema object - the other one for holding the visual representation of the modified Cinema object returned from the server. There is also a button placed between them to trigger the exchange with the server. The visual representation of the server reply is colored in red.

<body>
    <!-- Here we place the original cinema data-->
    <div id="originalCinemaInfo" style="margin-bottom:20px"></div>

    <!-- clicking this button sends the cinema object to the server
         and places the result returning from the server under 
         serverCinemaInfo tag -->
    <Button id="changeCinemaInfoFromServerButton">Change Cinema Info via Server</Button>

    <div style="color:blue;margin-top:20px">Reply from the Server:</div>

    <!-- here we create a visual representation for Cinema object 
         coming from the server -->
    <div id="serverCinemaInfo" style="margin-bottom:20px;color:Red">
        
    </div>
</body>	

Here is the JavaScript code:

$(document).ready(function () {
    // we create a Cinema object on the client
    var cinema = {
        Name: "Mohawk Mall Cinema",
        Description: "An OK Cinema",

        Movies: [
            {
                Title: "Anne of Green Gables",
                Year: 1985
            },
            {
                Title: "Anne of Avonlea",
                Year: 1987
            }
        ]
    };

    // display the visual representation of the original 
    // cinema object
    displayCinemaInfo($("#originalCinemaInfo"), cinema);

    var cinemaJsonString = JSON.stringify(cinema);

    $("#changeCinemaInfoButton").click(function () {
        $.ajax({
            type: "POST",
            dataType: "json",
            contentType: "application/json; charset=utf-8;",
            url: "/data/changecinemadata",
            data: cinemaJsonString,
            success: function (changedCinemaFromServer) {

                // display the visual representation of the server modified 
                // cinema object
                displayCinemaInfo($("#serverCinemaInfo"), changedCinemaFromServer);
            },
            error: function (resultFromServer) {

                // clear the previous contet of serverCinemaInfo element
                $("#serverCinemaInfo").contents().remove();

                // display the error message
                $("#serverCinemaInfo").append("Server Error");
            }
        });
    });
});

We use generic $.ajax method to send a POST request to the server. This method allows us to also specify the function to be called in case of a server error as well as the content type for the request.

The data we send within the message body is our Cinema JSON object turned into string by JSON.stringify function. Some older browsers still do not support this function and because of that, the reference to json2.min.js file was added. This file came from NuGet installation of the json2 package.

Here is how the SPA looks after successful reply from the server:

Image 18

and here is how it looks in case of failure:

Image 19

Summary

This article talks about the Single Page Applications (SPAs). We define the SPA and present numerous SPA samples. All the information and functionality within the article is something I wished I knew and had several months ago when I just started building the SPAs.

This article introduces a new JavaScript library called BPF (Browser Presentation Foundation). This library is used for two purposes:

  • Navigation - enabling to navigate your SPA using browser URLs and navigation buttons.
  • Composition - showing how one can use BPF library to compose the SPA from different HTML pages on the client.
Eventually, I hope that the BPF library will include a lot of other functionality.

At the end of the article, I talk about how the SPA applications contact the server.

Acknowledgment

I was introduced to SPA by John Papa's presentation on Pluralsight at Single Page Apps with HTML5, Web API, Knockout and jQuery. Since then I am building web sites as SPAs. My warm thanks to him and other Pluralsighters.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect AWebPros
United States United States
I am a software architect and a developer with great passion for new engineering solutions and finding and applying design patterns.

I am passionate about learning new ways of building software and sharing my knowledge with others.

I worked with many various languages including C#, Java and C++.

I fell in love with WPF (and later Silverlight) at first sight. After Microsoft killed Silverlight, I was distraught until I found Avalonia - a great multiplatform package for building UI on Windows, Linux, Mac as well as within browsers (using WASM) and for mobile platforms.

I have my Ph.D. from RPI.

here is my linkedin profile

Comments and Discussions

 
GeneralMy vote of 5 Pin
Rajesh Pillai25-Dec-14 3:01
Rajesh Pillai25-Dec-14 3:01 
Nice work. Thanks for the share.
GeneralRe: My vote of 5 Pin
Nick Polyak25-Dec-14 4:53
mvaNick Polyak25-Dec-14 4:53 
GeneralMy vote of 5 Pin
Antonio Ripa5-Jan-14 23:38
Antonio Ripa5-Jan-14 23:38 
GeneralRe: My vote of 5 Pin
Nick Polyak1-Mar-14 14:18
mvaNick Polyak1-Mar-14 14:18 
GeneralMy vote of 5 Pin
csharpbd23-Apr-13 20:24
professionalcsharpbd23-Apr-13 20:24 
GeneralRe: My vote of 5 Pin
Nick Polyak28-Apr-13 16:13
mvaNick Polyak28-Apr-13 16:13 
Questionvote of 5 Pin
mkalos27-Feb-13 23:36
professionalmkalos27-Feb-13 23:36 
AnswerRe: vote of 5 Pin
Nick Polyak28-Feb-13 13:37
mvaNick Polyak28-Feb-13 13:37 
Questionwell done! Pin
Member 97680196-Feb-13 6:31
Member 97680196-Feb-13 6:31 
AnswerRe: well done! Pin
Nick Polyak28-Feb-13 13:37
mvaNick Polyak28-Feb-13 13:37 
GeneralMy vote of 5 Pin
Paul Newton 200628-Jan-13 9:56
Paul Newton 200628-Jan-13 9:56 
GeneralRe: My vote of 5 Pin
Nick Polyak28-Feb-13 13:37
mvaNick Polyak28-Feb-13 13:37 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA15-Jan-13 6:55
professionalȘtefan-Mihai MOGA15-Jan-13 6:55 
GeneralRe: My vote of 5 Pin
Nick Polyak15-Jan-13 10:36
mvaNick Polyak15-Jan-13 10:36 
GeneralMy vote of 5 Pin
Savalia Manoj M26-Dec-12 20:23
Savalia Manoj M26-Dec-12 20:23 
GeneralRe: My vote of 5 Pin
Nick Polyak27-Dec-12 5:16
mvaNick Polyak27-Dec-12 5:16 
QuestionNice idea, but is it good for a complicated application? Pin
southsouth21-Dec-12 16:37
southsouth21-Dec-12 16:37 
AnswerRe: Nice idea, but is it good for a complicated application? Pin
Nick Polyak22-Dec-12 12:10
mvaNick Polyak22-Dec-12 12:10 
GeneralRe: Nice idea, but is it good for a complicated application? Pin
Dewey25-Dec-12 11:56
Dewey25-Dec-12 11:56 
QuestionA strong vote of 5 Pin
Dewey21-Dec-12 0:08
Dewey21-Dec-12 0:08 
AnswerRe: A strong vote of 5 Pin
Nick Polyak21-Dec-12 0:15
mvaNick Polyak21-Dec-12 0:15 
GeneralMy vote of 4 Pin
arunj.basic19-Dec-12 20:30
arunj.basic19-Dec-12 20:30 
GeneralRe: My vote of 4 Pin
Nick Polyak20-Dec-12 0:06
mvaNick Polyak20-Dec-12 0:06 
Questiongood article Pin
zSegundo19-Dec-12 8:34
zSegundo19-Dec-12 8:34 
AnswerRe: good article Pin
Nick Polyak19-Dec-12 8:36
mvaNick Polyak19-Dec-12 8:36 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.