Introduction
In this article I’m going to bring together a set of technologies that makes it very easy to create HTML5 based mobile content.
- ASP.Net
- jQuery Mobile
- Ajax enabled WCF service
- D3.js
This article is partly about using the traditional ASP.Net WebForms architecture to create a mobile web site using jQuery Mobile, but it’s mostly about creating a starting point for using D3.js with Ajax enabled WCF Services.
The end result is a Server CPU Meter with animated updates every other second.
My previous article D3.js crash course[^] shows how to use D3.js to create graphical elements.
Setting the scene
Before we start, we need to set things up in a way that allows us to use the latest versions of jQuery and jQuery Mobile. By default ASP.Net will load an older version of jQuery using the default templates for WebForms – so we’re going to make the script manager load version 1.9.1 of jQuery.
ASP.Net provides an assembly level attribute that allows us to register a static method that will be called long before the Application_Start method of your Global class.
[assembly: PreApplicationStartMethod(typeof(Harlinn.d3js.Part2.Web.ScriptInitializer), "Initialize")]
The attribute tells ASP.Net that we want the static Initialize
method of the Harlinn.d3js.Part2.Web.ScriptInitializer
class to be called as early as possible.
public class ScriptInitializer
{
const string jqueryVersionString = "1.9.1";
const string jqueryMobileVersionString = "1.3.0";
public static void Initialize()
{
ScriptManager.ScriptResourceMapping.AddDefinition("jquery",
new ScriptResourceDefinition
{
Path = "~/Scripts/jquery-" + jqueryVersionString + ".min.js",
DebugPath = "~/Scripts/jquery-" + jqueryVersionString + ".js",
CdnPath = "http://ajax.aspnetcdn.com/ajax/jQuery/jquery-" +
jqueryVersionString + ".min.js",
CdnDebugPath = "http://ajax.aspnetcdn.com/ajax/jQuery/jquery-" +
jqueryVersionString + ".js",
CdnSupportsSecureConnection = true,
LoadSuccessExpression = "window.jQuery"
});
By registering jQuery in this manner, we get to enjoy the magic provided by the ScriptManager, like:
- Automatic debug/release support – The ScriptManager will use the compressed minified scripts when the application is deployed to production, and use the ‘normal’ scripts during development.
- Content Delivery Network (CDN) support with fallback to your own server in case the CDN server is down, of for some other reason is unable to deliver the scripts to our application.
ScriptManager.ScriptResourceMapping.AddDefinition("jquery.mobile",
new ScriptResourceDefinition
{
Path = "~/Scripts/jquery.mobile-" + jqueryMobileVersionString + ".min.js",
DebugPath = "~/Scripts/jquery.mobile-" + jqueryMobileVersionString + ".js",
CdnPath = "http://ajax.aspnetcdn.com/ajax/jquery.mobile/" +
jqueryMobileVersionString + "/jquery.mobile-" +
jqueryMobileVersionString + ".min.js",
CdnDebugPath = "http://ajax.aspnetcdn.com/ajax/jquery.mobile/" +
jqueryMobileVersionString + "/jquery.mobile-" +
jqueryMobileVersionString + ".js",
CdnSupportsSecureConnection = true
});
}
}
We can now reference the registered scripts from the ScriptManager without getting errors due to multiple loads of jQuery.
<asp:ScriptManager ID="scriptManager" runat="server">
<Scripts>
<asp:ScriptReference Name="jquery" />
<asp:ScriptReference Name="jquery.mobile" />
</Scripts>
</asp:ScriptManager>
The Master Page
ASP.Net master pages is one of the nicer features of ASP.Net as they provide a mechanism that allows you to define elements that you want on all of the pages on your site.
When we talk about a Page in jQuery Mobile, we’re not necessarily talking about a single html document, as jQuery Mobile allows us to define several pages inside a single html document.
Inside the <body> tag, each "page" in the document is identified with an element (usually a div) with the data-role="page"
attribute:
<div data-role="page">
...
</div>
The page is the main unit of interaction in jQuery Mobile, and it's used to group content into logical views that can be animated in and out of view with page transitions. A html document may start with a single "page" and the AJAX navigation system will load additional pages on demand into the DOM as the user navigate around. Alternatively, a html document can be built with multiple "pages" inside it and jQuery Mobile will transition between these local views with no need to request content from the server.
Any valid HTML markup can be used within a "page", but a typical page in jQuery Mobile will have immediate child divs with data-roles of "header", "content", and "footer".
<div data-role="page">
<div data-role="header">...</div>
<div data-role="content">...</div>
<div data-role="footer">...</div>
</div>
Now, armed with this information, we’ll create our initial master page using a mix of familiar asp.net controls and jQuery Mobile elements:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
We add the meta viewport tag in the head to specify how the browser should display the page zoom level and dimensions. When this isn't set a mobile browser will often use a "virtual" page width of around 900 pixels to make the browser work well with sites written for a desktop browser.
<link rel="stylesheet" runat="server" href="~/Content/jquery.mobile-1.3.0.min.css" />
<script src="http://d3js.org/d3.v3.min.js"></script>
Since we wanted to replace the default implementation of jQuery we did that with the help of the PreApplicationStartMethod attribute, but there is nothing that prevents us from pulling in scripts using the <script> tag.
<asp:ContentPlaceHolder ID="head" runat="server">
</asp:ContentPlaceHolder>
</head>
<body data-theme="a">
<form id="form1" runat="server">
<asp:ScriptManager ID="scriptManager" runat="server">
<Scripts>
<asp:ScriptReference Name="jquery" />
<asp:ScriptReference Name="jquery.mobile" />
</Scripts>
<Services>
<asp:ServiceReference Path="~/Services/DataService.svc" />
</Services>
</asp:ScriptManager>
The ScriptManager contains a collection of ServiceReference elements in the Services collection, and this incredibly cool feature makes the ScriptManager generate javascript code that allows our HTML5 application to retrieve data from the registered Ajax enabled WCF service with incredible ease.
Here starts our jQuery Mobile "page" definition:
<div data-role="page" data-theme="a">
Following the usual jQuery Mobile convention, we start by defining the "header" element
<div data-role="header" data-theme="f" data-position="fixed">
<h1 id="heading" runat="server" >Harlinn D3.js - Part 2</h1>
<a href="#nav-panel" data-iconpos="notext" data-icon="bars">Menu</a>
</div>
Note that the header element includes a link to the following panel, which provides navigation between pages.
<div id="nav-panel" data-role="panel" data-theme="a" data-position-fixed="true">
<ul class="nav-search" data-role="listview" data-theme="a">
<li data-icon="delete"><a href="#" data-rel="close">Close menu</a></li>
<li>
<asp:HyperLink ID="homeLink" runat="server"
NavigateUrl="~/Default.aspx" Text="Home" />
</li>
<li>
<asp:HyperLink ID="introductionLink" runat="server"
NavigateUrl="~/Introduction.aspx" Text="Introduction" />
</li>
</ul>
</div>
The content div includes a ContentPlaceHolder for the contents of the page:
<div data-role="content" >
<asp:ContentPlaceHolder ID="ContentPlaceHolderContent" runat="server">
</asp:ContentPlaceHolder>
</div>
And so does the footer div:
<div data-role="footer" data-theme="f" data-position="fixed">
<asp:ContentPlaceHolder ID="ContentPlaceHolderFooter" runat="server">
</asp:ContentPlaceHolder>
</div>
</div>
</form>
</body>
</html>
Ajax enabled WCF Service
The Ajax enabled WCF service, is particularly useful when used together with the ScriptManager.
Visual Studio 2012 even provides intellisense for the Ajax enabled WCF service, greatly simplifying the development of HTML5 based applications that relies on the WCF service to provide access to the data it requires.
[ServiceContract(Namespace = "Harlinn.d3js",Name="DataService")]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class DataService
{
[OperationContract]
public List<string> Get5LinesOfText()
{
List<string> result = new List<string>(new string[]{
"Line 1",
"Line 2",
"Line 3",
"Line 4",
"Line 5",
});
return result;
}
}
Initially the WCF service only has one method, Get5LinesOfText, which unsurprisingly returns 5 lines of text. We will extend the service as required, but for now we keep things really simple.
It’s really important that you note the name of the Namespace and Name as specified for the ServiceContract attribute, since this specifies the naming elements that will be used to generate the javascript object that the ScriptManager automagically inserts into our html document.
We now have a reasonable starting point for creating mobile content in a relatively straight forward manner.
5 Lines of Text
It's time to create our first WebForm based on the template we've defined for our jQuery Mobile application:
We give the page an appropriate name, Introduction.aspx, and select the masterpage:
Visual studio 2012 locates the ContentPlaceHolder elements in the master page, and generates Content elements that are linked to each of the ContentPlaceHolder elements:
<%@ Page Title="" Language="C#" MasterPageFile="~/TopLevel.Master" AutoEventWireup="true"
CodeBehind="Introduction.aspx.cs" Inherits="Harlinn.d3js.Part2.Web.Introduction" %>
<asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolderContent" runat="server">
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolderFooter" runat="server">
</asp:Content>
We now set the the Title attribute of the Page to "Introduction", and insert
<asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolderFooter" runat="server">
<h4>I'm a footer</h4>
</asp:Content>
something into the footer of the page.
We will usually use D3 to create new DOM elements such as lines, circles, rectangles, or other visual forms that represent our data, but D3 can equally well be used to create other DOM elements, such as paragraphs inside a div:
<div id="paragraphs">
</div>
So, now that we have the "paragraphs" div, we will retrieve 5 lines of text from our Ajax enabled WCF serivice:
<script type="text/javascript">
Harlinn.d3js.DataService.Get5LinesOfText(function (data) {
We've now retrieved 5 lines of text from the DataService, so we use D3 to select the "paragraphs" div we just created using the standard CSS selector syntax:
d3.select("#paragraphs")
Then we use D3 to create a selection containing all the existing paragraph elements under the selected div - which will result in an empty selection.
.selectAll("p")
Then we use D3's selection.data()[^] method to join the the data we have retrieved from the DateService to the selection
.data(data)
Next we call selection.enter()[^] which returns placeholder nodes for each data element for which no corresponding existing DOM element was found in the current selection.
.enter()
Now that we have a selection representing the data elements that are not bound to a DOM element, we call selection.append()[^] to create a paragraph element "p" for each element in the selection returned by enter()
.
.append("p")
D3's selection.text()[^] allows us to specify the contents of the created elements. When we pass a callback function D3 will call our callback with the respective item from the data array as the argument, which in this case is the text we want to put into the paragraph element.
.text(function (d) { return d; });
});
</script>
It's time to hit F5 and take a look at what we've created:
The CPU Meter
At this point we can try to create something useful, like a CPU meter.
We’ll accomplish this by combining code generated by CIMTool, which you'll find here: CIMTool for Windows Management Instrumentation - Part 3[^] with a slightly modified version of Mike Bostocks Bullet Charts[^].
We'll start by having a look at the data used to drive Mikes example:
[
{"title":"Revenue",
"subtitle":"US$, in thousands",
"ranges":[150,225,300],
"measures":[220,270],
"markers":[250]},
{"title":"Profit",
"subtitle":"%",
"ranges":[20,25,30],
"measures":[21,23],
"markers":[26]},
{"title":"Order Size",
"subtitle":"US$, average",
"ranges":[350,500,600],
"measures":[100,320],
"markers":[550]},
{"title":"New Customers",
"subtitle":"count",
"ranges":[1400,2000,2500],
"measures":[1000,1650],
"markers":[2100]},
{"title":"Satisfaction",
"subtitle":"out of 5",
"ranges":[3.5,4.25,5],
"measures":[3.2,4.7],
"markers":[4.4]}
]
If we want to reuse as much of Mikes code as possible, it will probably be a good idea to create a class that will be serialized to JSON that follows the same format:
[DataContract]
public class Bullet
{
public Bullet()
{
ranges_ = new List<int>(new int[] {75,100});
measures_ = new List<int>();
markers_ = new List<int>();
}
[DataMember]
public string title
{
get
{
return title_;
}
set
{
title_ = value;
}
}
[DataMember]
public string subtitle
{
get
{
return subtitle_;
}
set
{
subtitle_ = value;
}
}
[DataMember]
public List<int> ranges
{
get
{
return ranges_;
}
set
{
ranges_ = value;
}
}
[DataMember]
public List<int> measures
{
get
{
return measures_;
}
set
{
measures_ = value;
}
}
[DataMember]
public List<int> markers
{
get
{
return markers_;
}
set
{
markers_ = value;
}
}
}
Armed with the above class, we'll be able to serve up data from our DataService in the expected format, so we'll add a new method that does just that:
[OperationContract]
public List<Bullet> GetPerCPUData()
{
List<Bullet> result = new List<Bullet>();
string queryString = "select * from Win32_PerfFormattedData_PerfOS_Processor";
ManagementScope scope = ManagementScope;
ObjectQuery query = new ObjectQuery(queryString);
ManagementObjectSearcher searcher = new ManagementObjectSearcher(scope,query);
using (searcher)
{
var elements = searcher.Get();
using (elements)
{
foreach (ManagementObject managementObject in elements)
{
Win32PerfFormattedDataPerfOSProcessor processor =
new Win32PerfFormattedDataPerfOSProcessor(managementObject, false);
Bullet perCPUData = new Bullet()
{
title = "CPU " + processor.Name,
subtitle="%"
};
if(processor.PercentProcessorTime.HasValue)
{
perCPUData.markers.Add(
Convert.ToInt32(processor.PercentProcessorTime.Value) );
}
if (processor.PercentPrivilegedTime.HasValue)
{
perCPUData.measures.Add(
Convert.ToInt32(processor.PercentPrivilegedTime.Value));
}
if (processor.PercentUserTime.HasValue)
{
perCPUData.measures.Add(
Convert.ToInt32(processor.PercentUserTime.Value));
}
if (processor.PercentInterruptTime.HasValue)
{
perCPUData.measures.Add(
Convert.ToInt32(processor.PercentInterruptTime.Value));
}
result.Add(perCPUData);
}
}
}
return result;
}
The method uses a class Win32PerfFormattedDataPerfOSProcessor
that's generated by CIMTool to decode the information contained in the ManagementObject.
We're now ready the create a web form based on our master page, CPUMeterForm.aspx.
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolderContent" runat="server">
<script src="Scripts/bullet.js"></script>
bullet.js is an unmodified version of Mikes code, but we need to make a few changes to the style since our background is pretty close to black:
<style>
.bullet { font: 10px sans-serif;fill:whitesmoke; }
.bullet .marker { stroke: #000; stroke-width: 2px; }
.bullet .tick line { stroke: #666; stroke-width: .5px; }
.bullet .range.s0 { fill: #eee; }
.bullet .range.s1 { fill: #ddd; }
.bullet .range.s2 { fill: #ccc; }
.bullet .measure.s0 { fill: lightsteelblue; }
.bullet .measure.s1 { fill: steelblue; }
.bullet .title { font-size: 14px; font-weight: bold; fill:whitesmoke; }
.bullet .subtitle { fill: #999; }
</style>
Mikes original code adds elements to the body of the page, but we want to put everything inside a div:
<div id="panel">
</div>
At the beginning of our script we set up the size of each bullet chart:
<script>
var margin = { top: 5, right: 40, bottom: 20, left: 40 },
width = 320 - margin.left - margin.right,
height = 50 - margin.top - margin.bottom;
var chart = d3.bullet()
.width(width)
.height(height);
Now we're ready to fetch the data from the DataService:
Harlinn.d3js.DataService.GetPerCPUData( function (data) {
Since the GetPerCPUData
method returns data in the same format Mike used in his example, we can just reuse his code:
var svg = d3.select("#panel").selectAll("svg")
.data(data)
.enter().append("svg")
.attr("class", "bullet")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(chart);
var title = svg.append("g")
.style("text-anchor", "end")
.attr("transform", "translate(-6," + height / 2 + ")");
title.append("text")
.attr("class", "title")
.text(function (d) { return d.title; });
title.append("text")
.attr("class", "subtitle")
.attr("dy", "1em")
.text(function (d) { return d.subtitle; });
});
</script>
</asp:Content>
Many of Mikes visualizations are based on data provided in the JSON format, and by ensuring that our data is served in a similar format we can create stunning visualizations with just a little effort.
So head over to the D3.js Gallery page[^] - Mike has created D3.js and many stunning visualizations that you can learn from. D3.js is released under a BSD license, so we're free to use it in commercial applications - just remember the copyright notice and the disclaimer.
CPU Meter take II
By making some small changes to the script we'll enable updates of the CPU Meter every other second:
<script>
var svg = null;
var interval = 2000;
var margin = { top: 5, right: 40, bottom: 20, left: 40 },
width = 320 - margin.left - margin.right,
height = 50 - margin.top - margin.bottom;
var chart = d3.bullet()
.width(width)
.height(height);
The UpdatePerCPUData function updates the data in the Bullet Charts:
function UpdatePerCPUData() {
Harlinn.d3js.DataService.GetPerCPUData(function (data) {
svg.datum(function (d, i) {
d.ranges = data[i].ranges;
d.measures = data[i].measures;
d.markers = data[i].markers;
return d;
}).call(chart.duration(1000));
setTimeout(UpdatePerCPUData, interval);
});
}
While the RenderPerCPUData function is resposible for redering the Bullet Charts when the user first visits the page.
function RenderPerCPUData() {
Harlinn.d3js.DataService.GetPerCPUData(function (data) {
d3.select("#panel").text("");
svg = d3.select("#panel").selectAll("svg")
.data(data)
.enter().append("svg")
.attr("class", "bullet")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(chart);
var title = svg.append("g")
.style("text-anchor", "end")
.attr("transform", "translate(-6," + height / 2 + ")");
title.append("text")
.attr("class", "title")
.text(function (d) { return d.title; });
title.append("text")
.attr("class", "subtitle")
.attr("dy", "1em")
.text(function (d) { return d.subtitle; });
setTimeout(UpdatePerCPUData, interval);
});
}
RenderPerCPUData();
</script>
The end result is a pleasing visualization, where changes to the CPU load causes an animated transition between old and new values.
Concluding remarks
The major thing I wanted to demonstrate in this article was a single line of code:
Harlinn.d3js.DataService.Get5LinesOfText(function (data) {
Creating an Ajax enabled WCF service makes retrieving data into an HTML5 application so easy it’s almost silly – or rather it’s brilliant, and unless you have particularly demanding requirements, it’s perhaps a bit silly not to use this technology.
Depending on response and interest I’ll extend this article to cover a few simple KPIs.
History
- 4. of March 2013 - Initial posting
- 5. of March 2013 - Periodic update of the CPU Meter every other second.
- 27. of December 2013 - Upgraded the solution to use jQuery 2.0.3 and jQuery Mobile 1.4.0