Example image of Digg Spy.

I’ve recently noticed the real-time “spy” feature popping up on more and more sites. Though it’s often a huge time waster, I can’t help but love the feature. It’s a great example of AJAX leveraged to do what it does best. It struck me that an ASP.NET AJAX implementation would be an excellent use of page methods for efficiency and __doPostBack() to trigger an UpdatePanel refresh. So, I decided to put together a proof of concept, using the ASP.NET AJAX framework.

To create a fully functional example, several things need to be done:

  • Choose a data source to “spy” on.
  • Build an interface to add rows, for testing.
  • Display that data in a row-based format.
  • Create a method to find the most recent row of data.
  • Use that method to asynchronously monitor row updates.
  • Refresh stale data when additions are detected.

Choosing a data source.

Many data sources lend themselves to this sort of monitoring, such as database queries, web services, event logs, and just about any other form of ordered, row-based data. What most of them have in common is that you should try to use a caching layer between the actual data source and the polling mechanism.

So, we won’t worry about underlying data store, and use the .NET Cache directly. In real implementation, simply extend the cache update code to also save to your data layer if appropriate.

For this example, we’ll spy on a date sorted list of blog post titles. To initialize an example schema and some test data on first run, I used this code:

protected void Page_Load(object sender, EventArgs e)
{
  // If no data has been cached yet, generate
  //  test data for purposes of demonstration.
  if (Cache["Headlines"] == null)
  {
    // Create a DataTable.
    DataTable dt = new DataTable("Headlines");
 
    // Add schema for the article example.
    dt.Columns.Add("Date", typeof(DateTime));
    dt.Columns.Add("Title", typeof(string));
 
    // Populate the test data.
    dt.Rows.Add(new object[] {DateTime.Now, 
       "CSS style as AJAX progress indicator"});
    dt.Rows.Add(new object[] {DateTime.Now.AddDays(-1.25), 
       "AJAX, file downloads, and IFRAMEs"});
    dt.Rows.Add(new object[] {DateTime.Now.AddDays(-2), 
       "Easily refresh an UpdatePanel, using JavaScript" });
 
    // Cache the initialized DataTable.
    Cache["Headlines"] = dt;
  }
}

If the cache is empty, this populates it with some initial data. In your application, replace the row adds with an initial call to your data source, and cache that instead.

An interface to the data

For testing purposes, we’ll need an interface to manipulate the cache.

AddArticle.aspx:

Title: <asp:TextBox runat="server" ID="ArticleTitle" />
<asp:Button runat="server" ID="Add" Text="Add" OnClick="Add_Click" />

AddArticle.aspx.cs:

protected void Add_Click(object sender, EventArgs e)
{
  // Retrieve the cached DataTable.
  DataTable dt = (DataTable)Cache["Headlines"];
 
  // Add a row for the posted title.
  dt.Rows.Add(new object[] {DateTime.Now, ArticleTitle.Text});
 
  // Re-cache the updated DataTable.
  Cache["Headlines"] = dt;
}

This allows us to add new articles to the cached DataTable. Depending on where your data comes from, you might not need this at all. If you do control input of the data in this manner, make sure that you also save the additions to your underlying data store in this step.

Displaying the data

EncosiaSpy!To complete the demo site, we need a display of the cached articles in our system.

Default.aspx:

<asp:ScriptManager runat="server" EnablePageMethods="true" />
<asp:UpdatePanel runat="server" ID="up1" OnLoad="up1_Load">
  <ContentTemplate>
    <asp:GridView runat="server" ID="Headlines" />
    <asp:HiddenField runat="server" ID="LatestDisplayTick" />
  </ContentTemplate>
</asp:UpdatePanel>
protected void up1_Load(object sender, EventArgs e)
{
  // Retrieve the cached DataTable.
  DataTable dt = (DataTable)Cache["Headlines"];
 
  // Set the hidden field's value to the
  //  latest headline's "tick".
  LatestDisplayTick.Value = GetLatestHeadlineTick().ToString();
 
  Headlines.DataSource = dt;
  Headlines.DataBind();
}

This will display a GridView table of all the article dates and titles in the cache. Not quite the aesthetics of DiggSpy, but it’ll work.

Since I plan to use a page method for polling, I went ahead and enabled them on the ScriptManager.

I also added a hidden field to the page that embeds the latest headline’s DateTime in “ticks”. Doing this will give us an easy way to make comparisons without having to worry about DateTime parsing in client script. By embedding it in the updated content, we help ensure that our data is always well in sync.

Finding the most recent row

Now, it’s time to create the GetLatestHeadlineTick() method in Default.aspx.cs, used in the preceding display code. This will also be the page method that we use on the client to poll for changes.

[WebMethod]
public static long GetLatestHeadlineTick()
{
  // Retrieve the cached DataTable.
  DataTable dt = (DataTable)HttpContext.Current.Cache["Headlines"];
 
  // Sort by date and find the latest article.
  DataRow row = dt.Select("", "Date DESC")[0];
 
  // Return that article's timestamp, in ticks.
  return ((DateTime)row["Date"]).Ticks;
}

Again, I’m converting the DateTime to Ticks so that it’s an easy to compare quantity. You could just as easily use the ID from an identity field, or a non-temporal natural key. Whatever makes sense for your data. Just keep in mind that numeric sequence comparisons are easiest.

Note the explicit cache reference. That’s necessary since the page method must be a static method.

Monitoring cache data on the client

Finally, we need client script to poll the page method and respond accordingly. I added this as a ScriptReference to the ScriptManager in Default.aspx:

function Check()
{
  // Call the static page method.
  PageMethods.GetLatestHeadlineTick(OnSucceeded, OnFailed);
}
 
function OnSucceeded(result, userContext, methodName)
{
  // Parse the page method's result and the embedded
  //  hidden value to integers for comparison.
  var LatestTick = parseInt(result);
  var LatestDisplayTick = parseInt($get('LatestDisplayTick').value); 
 
  // If the page method's return value is larger than 
  //  the embedded latest display tick, refresh the panel.
  if (LatestTick > LatestDisplayTick)
    __doPostBack('UpdatePanel1', '');
  // Else, check again in five seconds.
  else
    setTimeout("Check()", 5000);
}
 
// Stub to make the page method call happy.
function OnFailed(error, userContext, methodName) {}
 
function pageLoad()
{
  // On initial load and partial postbacks, 
  //  check for newer articles in five seconds.
  setTimeout("Check()", 5000);
}

In a nutshell, this checks GetLatestHeadlineTick() every 5 seconds, compares it to the embedded LatestTick in the UpdatePanel, and triggers an UpdatePanel refresh if the UpdatePanel’s data has become stale.

If you open Default.aspx in one window and AddArticle.aspx in another, adding an article title from AddArticle will cause Default.aspx’s GridView to automatically refresh itself. If no updates are added, no partial postbacks occur.

This could be accomplished by using a Timer control to constantly refresh the UpdatePanel. However, by polling a light-weight page method, we can afford to poll for updates much more frequently and/or support many more concurrent users than if we were triggering a full partial postback on every polling. There are orders of magnitude between the performance of UpdatePanels and page methods.

Room for improvement (always)

I think this example is fairly compelling, but there’s always room for improvement.

A pause/resume feature, like the one on DiggSpy would be easy to implement and definitely be handy. That could be implemented with two or three lines of client script.

You should also add error handing in OnFailed(). Ideally, this should have some more robust error handling. Maybe retrying with a longer interval, if the request timed out, assuming an overloaded server. Or, perhaps keeping a counter of how many failures in a row have occurred and displaying a user friendly message about the situation after a certain threshold.

I used a GridView for brevity, but I think it would be more ideal to use a repeater to output divs. It would be lighter weight than the GridView and lend itself to dynamic updating via DOM manipulation.

That DOM manipulation would be another nice improvement. Rather than running a full partial postback to refresh the display, another page method could be created that returns a JSON object with articles more recent than a given tick. Then, those new article divs could be added to the display less abruptly, via fade, slide, or some other client side pizazz.

It’s surprisingly fun to play with and watch in action. Give it a try yourself:

Download Source