Cover

Silverlight ASP.NET AJAX = Good Times

August 18, 2007
No Comments.

Url: http://wildermuth.com/silverlight/silverlightaj…

I’ve been digging into the AJAX-Silverlight story a little more.  I wanted an example of something more business-like than what I have already crafted (the updating clock is an interesting demo but not very practical) so I decided to try and do a simple updating line graph.  This post is assuming you know a bit about ASP.NET AJAX and won’t detail every step I took in creating the application (e.g. like creating the ScriptManager).  Here’s what I wanted my chart to look like:

I created a simple XAML file to show my chart and created a Path object for each data line. I’ll get into how I am updating the data in a minute.  First thing I did was change the way I am loading the Silverlight object. With the standard template (and most examples), the creation of the Silverlight object is done inside the div tag that will house the Silverlight. I took the cue from Fritz Onion’s recent blog to use ASP.NET AJAX’s pageLoad function instead:

// Known Function Name in AJAX to be called once the page has completed loading
function pageLoad()
{
  createSilverlight($get("myChart"));
}

I changed the call to createSilverlight to accept the object to be hosted in and I am using the $get AJAX shortcut to retrieving an element. Great, I now have the Silverlight chart in the browser but it doesn’t show much yet.

My expectation was to use an AJAX callback to get new data to show in my chart.  I toyed with the idea of creating a web service and actually getting data but in the end it made more sense to just use a PageMethod.  Page methods are static (or shared in VB) methods of a page that can be called instead of creating a separate web service.  To enable page methods, you need to tell the ScriptManager:

<asp:ScriptManager ID="theScriptManager" runat="server" EnablePageMethods="true">
  <Scripts>
    <asp:ScriptReference Path="silverlight.js" />
    <asp:ScriptReference Path="scene.xaml.js" />
    <asp:ScriptReference Path="default.aspx.js" />
  </Scripts>
</asp:ScriptManager>

Now that I enabled the page methods, I could create a new page method (called GetStats).  Notice the [WebMethod] is still required, but you do not have to create a whole web service:

public partial class _Default : System.Web.UI.Page
{
  // ...
  
  [WebMethod]
  public static List<double> GetStats()
  {
    Random rnd = new Random();
    List<double> results = new List<double>(4);
    results.Add(rnd.Next(100));
    results.Add(rnd.Next(100));
    results.Add(rnd.Next(100));
    results.Add(rnd.Next(100));

    return results;
  }
}

Instead of getting real data, I am just creating four random (or pseudo-random) data points that I can return to the page.  The idea here is that I have four new points (one for each color).  I want to add a new data point to each of the line graphs over time.  So this method can be used to get a new set of data.

Now onto the Silverlight code.  I have a JScript class that is where most of the guts of the real work is being done.  First in my OnLoad event handler I am retrieving each of the lines (Paths) so I draw them later as we have new data. (Note, I am actually putting them in an array to make the code a bit more streamlined).  I am also grabbing the height of the control to use for calcuations later and setting the initial set of data (starting out with four zeros).  Finally, I am calling a function that will get the first set of data:

SilverlightChart.Scene.prototype =
{
  handleLoad: function(control, userContext, rootElement) 
  {
    this.control = control;
    this.result1 = rootElement.findName("result1");
    this.result2 = rootElement.findName("result2");
    this.result3 = rootElement.findName("result3");
    this.result4 = rootElement.findName("result4");
    this.results = [this.result1, this.result2, this.result3, this.result4];
    this.height = rootElement.height;
    this.data = [[0], [0], [0], [0]];
    this.getNewStats();
  },

The getNewStats function is interesting because all it does is call the page method (Note: that all page methods are created with a PageMethodnamespace to make calling them simple).  All web service calls are asynchronous so the parameter of the page method is a delegate to another function to call.  I am using the createDelegate call to allow me to use a function inside my JScript class for the callback.  If you were using simple JScript functions, you could just specify the name of the function:

getNewStats: function()
{
  PageMethods.GetStats(Silverlight.createDelegate(this, this.getStatsComplete));
},

Once the method is executed it calls our getStatsComplete function.  This funciton goes through and adds the new data (the resultis the data that you returned from the page method) to our array of data. Then it calls a function that actually calculates the look of each line.  Finally it uses setTimeout to act as a timer and call the getNewStats function again after two second (2000 milliseconds) have elapsed.  This allows our new chart to get new data approximately every two seconds and will continue to grow as long as the user stays on the page:

getStatsComplete: function(result)
{
  for (var x = 0; x < 4; x++)
  {
    this.data[x].push(result[x]);
  }
  this.calculateGraph();
  setTimeout(Silverlight.createDelegate(this, this.getNewStats), 2000);
},

The last two pieces of functionality are the calculateGraphand formatDatafunctions.  The calculateGraph function changes the entire data element of each of the lines to a new set of data for the Path object. This is inefficient but this is necessary because currently you cannot add elements to the children of a Path’s Data element. To get around this limitation we simply use the formatData function to build a string using the Path’s markup syntax to describe our particular line:

calculateGraph: function()
{
  for (var x = 0; x < 4; x++)
  {
    // Create by Creating the Data Block
    this.results[x].data = this.formatData(this.data[x]);  
    
    // Create by adding elements to the geometry *** DOESN'T WORK ****
    //this.results[x].data.children;	    
  }
},

The formatData function just takes a set of values (that we appended earlier) for a line and composes a complete data string. Of particular interest is that the Path’s Data syntax does *not* have to be pixel based.  In this case we are assuming all the data is zero to 100.  You should notice the beginning of every line starts with “M0, -100 M0,0”.  The “M” in the Path markup syntax says Move (not draw).  This forces our Path to be 100 high (going from -100 to 0).  We then add the value pair by moving them over 5 points at a time and using the new data as a negative value.  The idea here is that coordinates are normally top-down, but our chart wants to show data bottom up (zero at the bottom and 100 at the top) so we are just use negative values to accomplish this:

formatData: function(values)
{ 
  var result = "M0,-100 M0,0";
  for (var i = 1; i < values.length; i++)
  
    result = result + " L" + (i*5) + ",-" + values[i];
  }
  return result;
}

This code might seem odd because the size of the path is wierd.  When we have two points, we have 0,0 as a point and lets say 5,-50 as a point.  That would seem awfully small but in the example it shows the data as wide as the chart.  What gives?  The trick is that when we created the Paths in the XAML file, we told them to stretch (using the Stretch=“Fill” attribute).  By using a Width, Height and Stretch our Path’s actual data does not have to reflect pixels at all.  You use more traditional data points and just stretch them to the right size:

...
<Path x:Name="result1" Width="320" Height="240" 
      Stroke="#FFFF0000" Data="M0,-100 M0,0" Stretch="Fill" />
<Path x:Name="result2" Width="320" Height="240" 
      Stroke="#FF0000FF" Data="M0,-100 M0,0" Stretch="Fill" />
<Path x:Name="result3" Width="320" Height="240" 
      Stroke="#FF008000" Data="M0,-100 M0,0" Stretch="Fill" />
<Path x:Name="result4" Width="320" Height="240" 
      Stroke="#FFD000D0" Data="M0,-100 M0,0" Stretch="Fill" />
...

Let me know if you have any questions about this example and have fun playing with Silverlight and ASP.NET AJAX together!

You can download a clean version of the code to play with here.