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:
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. fromhttp://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 id
s 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:
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 linebpf.nav.setKeySegmentToHash(tabNode)
sets the initial URL to match the one obtained frombpf.nav.Node
s 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:
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 thebpf.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.”:
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 bybpf.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 samebpf.nav.NodeBase
class. bpf.nav.ProductNode
functionality as will be discussed below. This inheritance relationship is shown at the diagram below:
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.
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 functiongetTotalHash()
– 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:
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:
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 functionssetSelectedKeySegmentsRecursive
, 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:
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);
Ray Yagubyan
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:
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:
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 asthis
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 functionconnectToParentNode
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:
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:
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):
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
Ray Yagubyan
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:
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:
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 intoserverText
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”:
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 aCinema
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 byJSON.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:
and here is how it looks in case of failure:
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.
Thank you