Rich Visualizations with Silverlight and Astoria

How we get here
I found myself wanting to have a nicer way of looking at a bunch of data I reference every now and then. Often I will whip up a quick HTML application to do this - it's ultimately a simple text file I can quickly edit and tweak, and the scripting environment allows me to access a lot of powerful objects.

The problem for me was that I needed something a bit more powerful than HTML. I have Office 2007 installed, and of course I've quickly learned to love the new conditional formatting stuff in it. I really wanted something along those lines for myself.

Turns out, Silverlight is a great fit for this. The Silverlight 1.0 Beta has great JavaScript integration, so weaving everything together took me very little effort. Ultimately I decided to turn the exercise into this blog post, and that's how we arrive here.

Of course I wanted to show data that is publicly available, and frankly I'm too lazy to do much parsing in JavaScript, so I turned to the Astoria services publicly available on https://astoria.mslivelabs.com/ for this.

So, let's build this bit by bit.

The skeleton
First thing is to create an simple text file, which we'll name products.hta. We can double-click this to launch the HTML application, and hit F5 in the browser to refresh it - extremely convenient! I'm going to keep this very simple, so I won't include the Silverlight helper .js files. Instead, this is what we start off with.

<html>
<head>
<title>Northwind Products Sample</title>
<style>
body {
background: #FFFFFF;
color: #444444;
font-family: Verdana;
font-size: 70%;
}
</style>
<!-- MY SCRIPT CODE COMES HERE -->
</head>

<body>
<h1>Northwind Products Sample</h1>
<div>
This sample shows a simple way to use JavaScript and the Silverlight 1.0 Beta
to query an Astoria service and render a list of product names with a background
bar indicating the relative unit price.
</div>

<!-- This inlines the XAML we want to start with. -->
<script type='text/xaml' id='xamlContent'><?xml version='1.0'?>
<Canvas
xmlns="
https://schemas.microsoft.com/client/2007 "
xmlns:x="
https://schemas.microsoft.com/winfx/2006/xaml ">
<Canvas>
<Rectangle Width='180' Height='32' Stroke='Black' StrokeThickness='2' Fill='White' />
<TextBlock Canvas.Left='8' FontSize='24' FontFamily='Verdana' x:Name='GetProductsButton'>Get Products</TextBlock>
</Canvas>
<Canvas x:Name='ProductsHost' Canvas.Top='32'>
</Canvas>
</Canvas>
</script>

<!-- This declares the Silverlight control directly. -->
<object
type="application/ag-plugin"
id="myControl"
width="400"
height="100">
<param name="source" value="#xamlContent" />
<param name="onLoad" value="onLoad" />
</object>

</body>
</html>

The head part of the document doesn't have much yet, but the body includes two interesting things: a script block of type 'text/xaml' which includes XAML, and an object tag of type 'application/ag-plugin'. This object tag declares our Silverlight client (note that I'm assuming you already have that installed), and wires the content to the text/xaml script and an onLoad event that we'll have to define next. So far, all we get is a simple text with a rectangle in the background - nothing too thrilling.

Handling Events
Let's start writing some code now. I want to wire up a click event on the text, and I could keep a reference to the top-level element as well, but I'll just keep refering to the control by ID in this sample. So we'll replace the "MY SCRIPT CODE COMES HERE" comment with this bit.

<script>
function onLoad(sender, eventArgs) {
// Wire up event handlers.
var button = sender.findName("GetProductsButton");
button.addEventListener("MouseLeftButtonDown", "GetProductsButtonMouseLeftButtonDown");
}

// ALL THE REST OF THE CODE COMES HERE
</script>

So, here we've found our "button", and wired up an additional event for it. Let's go implement that now.

function GetProductsButtonMouseLeftButtonDown(sender, eventArgs) {
var baseUrl = "
https://astoria.sandbox.live.com/northwind/northwind.rse ";
var oReq = new XMLHttpRequest;
oReq.onreadystatechange = function() {
if (oReq.readyState == 4) {
var responseText = oReq.responseText;
// Scrub for function calls - use a real safe eval replacement in production!
responseText = responseText.replace("(", "").replace(")", "");
var products = eval(responseText);
DisplayProducts(products);
}
};

var requestUrl = baseUrl + "/Products";
requestUrl += "?$orderby=ProductName";

  StartDownloadingProducts();

oReq.open("GET", requestUrl, true);
oReq.setRequestHeader("Accept", "application/json");
oReq.send();
}

I'm using the new IE7 syntax for creating an XMLHttpRequest. Because this is an HTA, I can be pretty sure that IE will be used. Let's take note of a few interesting things here:
- We have a base URL for the Northwind service hosted in the Astoria sandbox. We can then compose our request over this, by appending the resource container we want to query, and specifying the order we want the products to come down with.
- We specify application/json in the Accept HTTP header, so the service gives us a JSON reply instead of the default XML.
- In the onreadystatechange handler, we simply scrub a bit and eval() the response, which gives us an array of products.

There are also a couple of helper functions we're using to handle the UI part - let's look at those now.

Driving the Silverlight Control
The first function we have pending is StartDownloadingProducts, which we call before we issue the request.

function StartDownloadingProducts() {
var control = document.getElementById("myControl");
var productsHost = control.content.findName("ProductsHost");

  // Clear any content from previous requests.
productsHost.children.clear();

// Let user know we're doing a long-running request...
var xamlFragment = "<Canvas>";
xamlFragment += " <TextBlock Canvas.Left='8' FontSize='16' FontFamily='Verdana'>";
xamlFragment += "Downloading products - please wait...";
xamlFragment += " </TextBlock>";
xamlFragment += "</Canvas>";
productsHost.children.add(
control.content.createFromXaml(xamlFragment));
}

This simply clears the productsHost Canvas (in case we had the results of a previous request there), and puts up a message indicating that we're in the process of downloading the products.

Finally, this is the function that shows the products in the Silverlight control.

function DisplayProducts(products) {
var control = document.getElementById("myControl");
var productsHost = control.content.findName("ProductsHost");

var offset = 0;
var maxUnitPrice = 0;
var maxWidth = control.width - 16;
var itemHeight = 22;

// Calculate the max unit price, to scale all bars.
for (var i = 0; i < products.length; i++) {
if (products[i].UnitPrice > maxUnitPrice) {
maxUnitPrice = products[i].UnitPrice;
}
}

  // Clear any content from previous requests.
productsHost.children.clear();

  for (var i = 0; i < products.length; i++) {
var xamlFragment = "<Canvas Canvas.Top='" + offset + "'>";
// Add a rectangle first, to acts as a background bar.
xamlFragment += " <Rectangle Canvas.Left='4' Height='" + itemHeight + "' Stroke='#99229922'";
xamlFragment += " Width='" + ((products[i].UnitPrice / maxUnitPrice) * maxWidth) + "'>";
xamlFragment += " <Rectangle.Fill>";
xamlFragment += " <LinearGradientBrush StartPoint='0,0' EndPoint='1,0'>";
xamlFragment += " <GradientStop Color='#FF22AA22' Offset='0' />";
xamlFragment += " <GradientStop Color='#4422AA22' Offset='1' />";
xamlFragment += " </LinearGradientBrush>";
xamlFragment += " </Rectangle.Fill></Rectangle>";

// Add the name of the product.
xamlFragment += " <TextBlock Canvas.Left='8' FontSize='16' FontFamily='Verdana'>";
xamlFragment += products[i].ProductName;
xamlFragment += " </TextBlock>";

xamlFragment += "</Canvas>";
productsHost.children.add(
control.content.createFromXaml(xamlFragment));

offset += itemHeight;
}

 
  // Resize the Silverlight control to fit all its content.
  control.height = 32 + offset;
}

I start off by getting the control and the element within where I want to add blocks, I remove the content that might have been there (the 'Downloading products' message in this case), and then I generate a string with the XAML representation that I want. Note that I need to calculate the maximum price up-front and then set the width relative to that for the item background. After I've got the string, a call to createFromXaml and add do the trick. I end up by resizing the control, to make sure that all the UI I've just added is visible.

And that's really all there is to this sample. The output of clicking the Get Products button is shown below. Just let me know if these are interesting topics of discussion, and I'll expand some more on how to make these technologies do cool stuff.

Screenshot of Northwind Products sample

This posting is provided "AS IS" with no warranties, and confers no rights. Use of included script samples are subject to the terms specified at https://www.microsoft.com/info/cpyright.htm.