I’ve been playing around recently with my webserver – I recently switched from a hosted ASP.NET service to a virtual dedicated server, so I’m getting a chance to play with having full control over my server for the first time.  I spent some time setting up logging and statistics on the server using IIS’s logging feature and some 3rd party log-crunching software.  Having that logging data is invaluable – among other things, it helps me know which pages folks are interested in and gives me insight into whether it’s too hard to reach certain sections of my page.  With the addition of navigation controls in the Silverlight 3 Beta SDK, having a logging solution that cooperates with my web server seems only prudent!

Tim Heuer has a great blog post from December about using event tracking with Google Analytics for Silverlight applications.  My impression is that this approach would work well with the new navigation controls, but I was looking for a simple solution that would add entries to my IIS logs straight from the Silverlight application (without having to add any javascript or use the HTML bridge).

The issue at hand is that the navigation controls use the URI fragment (text after the “#” sign in the URL) to determine which Page to navigate to in a Frame control.  As a result (and rightly so), deeplinks into the Silverlight control or navigation that occurs within the Silverlight control never round-trip to the server, so there is no way for the server to log their occurrence.

Hence, my approach is fairly straightforward: make an HTTP request back to the hosting server that it will log any time my Frame is navigated.

My requirements for the experiment:

  • Reuse the built-in logging features of my web server (IIS 7, but I imagine this would work more broadly)
  • Ensure that unique URIs within my application are logged individually (so query strings, custom URIs, etc. are not lost in the logged data)
  • Avoid limiting the application by requiring the HTML bridge to be accessible or requiring additional files to be added to my website (such as javascript files or additional ASP.NET Pages)

It turns out this isn’t so difficult to do!  I started with the Silverlight Navigation Application project template that ships with the Silverlight 3 Beta Tools for Visual Studio that came out at MIX.  This project template gets me set up with a Frame, some Pages, and some buttons that cause the Frame to navigate – everything a newborn navigation application needs to grow big and strong!

I began by handling the Frame control’s “Navigated” event in MainPage.xaml:

<navigation:Frame x:Name="Frame" Source="/Views/HomePage.xaml"
                  Navigated="Frame_Navigated"
                  HorizontalContentAlignment="Stretch"
                  VerticalContentAlignment="Stretch"
                  Padding="15,10,15,10"
                  Background="White"/>

With the easy part out of the way (who knew?), I started playing with WebRequest and WebClient until I came up with something that seemed to meet my needs:

private void Frame_Navigated(object sender, NavigationEventArgs e)
{
    Uri uri = new Uri(Application.Current.Host.Source.ToString() + "?nav=" + Uri.EscapeDataString(e.Uri.ToString()));
    WebRequest wc = WebRequest.Create(uri);
    wc.Method = "POST";
    wc.BeginGetResponse((res) =>
    {
        WebResponse wr = wc.EndGetResponse(res);
    }, this);
}

I’ll walk through this line-by-line and explain my thinking:

Uri uri = new Uri(Application.Current.Host.Source.ToString() + "?nav=" + Uri.EscapeDataString(e.Uri.ToString()));

Here, I needed to come up with a file I knew would be present on the server and that isn’t likely to have any semantics that I’ll be overriding by making a request.  In addition, I wanted to choose a file that would uniquely identify the Silverlight application that is making the request.  It seemed only logical, then to use the XAP file for my Silverlight app for this purpose!  Next, I added a query string that would be sent down to the server that would uniquely identify the URI that the Frame is using.  The result, if the source URI is “/Views/HomePage.xaml” (as is shown in the XAML), the resulting URI is: “http://yourservername.com/yourSilverlightApp.xap?nav=%2FViews%2FHomePage.xaml”.  Ok, ok, it’s not pretty (thanks to the encoding of the URI), but it does the trick.  The query string here never gets used, but it does get sent to the server and logged, which leaves me with exactly the traces I was looking for!

Next:

WebRequest wc = WebRequest.Create(uri);
wc.Method = "POST";

The important takeaway from these two lines is that I used the HTTP POST method.  I spent a bunch of time trying to use GET to make the logging happen, but it had two critical drawbacks:

  • Every request would re-download the XAP, which was far more data than I wanted to transfer just to get logging going
  • WebRequest and WebClient both use the browser’s cache (and I couldn’t find a workaround that didn’t involve modifying the query string, which would have scuttled my approach), so repeat-visits to the same Page in the Silverlight application didn’t ever actually reach the server and get logged

Finally:

wc.BeginGetResponse((res) =>
{
    WebResponse wr = wc.EndGetResponse(res);
}, this);

This just shoots off the web request.  There’s nothing particularly special here aside from noting that the response is entirely ignored.

 

And that’s it!

 

Here’s the proof, straight from my IIS logs (in W3C format… IP’s redacted):

2009-05-13 04:41:06 POST /Samples/LoggedSilverlightNavigation/ClientBin/LoggedSilverlightNavigation.xap nav=%2FViews%2FAboutPage.xaml - <IP Redacted> HTTP/1.1 Mozilla/4.0+(compatible;+MSIE+8.0;+Windows+NT+6.1;+WOW64;+Trident/4.0;+SLCC2;+.NET+CLR+2.0.50727;+.NET+CLR+3.5.30729;+.NET+CLR+3.0.30729;+Media+Center+PC+6.0) - 405 1496

2009-05-13 04:41:06 GET / feed=rss2 - <IP Redacted> HTTP/1.1 Windows-RSS-Platform/2.0+(MSIE+8.0;+Windows+NT+6.1) - 304 475

2009-05-13 04:41:06 POST /Samples/LoggedSilverlightNavigation/ClientBin/LoggedSilverlightNavigation.xap nav=%2FViews%2FHomePage.xaml - <IP Redacted> HTTP/1.1 Mozilla/4.0+(compatible;+MSIE+8.0;+Windows+NT+6.1;+WOW64;+Trident/4.0;+SLCC2;+.NET+CLR+2.0.50727;+.NET+CLR+3.5.30729;+.NET+CLR+3.0.30729;+Media+Center+PC+6.0) - 405 1496

2009-05-13 04:41:08 POST /Samples/LoggedSilverlightNavigation/ClientBin/LoggedSilverlightNavigation.xap nav=%2FViews%2FAboutPage.xaml - <IP Redacted> HTTP/1.1 Mozilla/4.0+(compatible;+MSIE+8.0;+Windows+NT+6.1;+WOW64;+Trident/4.0;+SLCC2;+.NET+CLR+2.0.50727;+.NET+CLR+3.5.30729;+.NET+CLR+3.0.30729;+Media+Center+PC+6.0) - 405 1496

2009-05-13 04:41:08 POST /Samples/LoggedSilverlightNavigation/ClientBin/LoggedSilverlightNavigation.xap nav=%2FViews%2FHomePage.xaml - <IP Redacted> HTTP/1.1 Mozilla/4.0+(compatible;+MSIE+8.0;+Windows+NT+6.1;+WOW64;+Trident/4.0;+SLCC2;+.NET+CLR+2.0.50727;+.NET+CLR+3.5.30729;+.NET+CLR+3.0.30729;+Media+Center+PC+6.0) - 405 1496

And more proof, from my 3rd party stats tool (ignore the large transfer sizes… they’re artifacts of my testing the GET method, which transferred a few hundred KB with each request):

AWStats Screenshot showing LoggedSilverlightNavigation.xap being queried 

If you’re curious to download the code (it’s not much more than what you’ve already seen!), you can find it here: LoggedSilverlightNavigation.zip

There’s definitely still room for improvement, though.  One thing I’d like to get working eventually is setting the Referrer header on the HTTP request to a coherent value so that I can track how people get from page to page in my applications – but this is a good first step.

I hope you find that helpful!  If anyone has any other suggestions/tips/tricks for navigation, feel free to let me know!  This was just the result of my experimentation, so if you’ve got a better way, feel free to comment!

 

P.S. Still no correct responses to my Easter Egg hunt from my last post!  Don’t give up!  I’ll post the answer later this week.

P.P.S. Mark Monster has a great post from a few months ago on his blog about tracking Silverlight support in Google Analytics.  I imagine you could use a similar technique to accomplish the type of logging I do here.