Developing Ajax-Enabled Controls and Components: Periodic Refresh Pattern

Ajax-enabled components that use the Periodic Refresh pattern periodically poll the server for changes. This article develops an Ajax-enabled custom control named AjaxNotifier that will show you how to use the Periodic Refresh pattern to develop your own Ajax-enabled controls.

AjaxNotifier is an Ajax-enabled control that periodically polls the server for the latest posted notification and displays the notification in a pop-up dialog, shown in Figure 1. As this figure shows, the pop-up dialog exposes two pieces of information about a notification: the source of the notification (notifier) and the content of the notification.

Figure 1

Here is how the workflow goes:

  1. AjaxNotifier uses DOM and JavaScript to retrieve the latest notification ID that the user has seen. The notification IDs include the complete information needed to determine the order in which the notifications are issued. This could be a timestamp; that is, the creation date of the notification.
  2. AjaxNotifier uses the ASP.NET 2.0 client callback mechanism to make an asynchronous callback to the server to download the XML document that contains the information about the latest notification. The client-side code also passes the notification ID of the latest notification that the user has seen to the server so the server can send the next notification back to the client.
  3. AjaxNotifier then uses XML, DOM, and JavaScript to dynamically retrieve the required data from the XML document and to display the information in the pop-up dialog.

The AjaxNotifier control derives from the WebControl base class and implements the ICallbackEventHandler interface. The control overrides the following methods of the WebControl class:

  • OnPreRender
  • AddAttributesToRender
  • TrackViewState
  • SaveViewState
  • LoadViewState

Deriving from WebControl

Listing 1 presents the implementation of the OnPreRender method.

Listing 1: The OnPreRender method

protected override void OnPreRender(EventArgs e)
{
   DetermineRenderClientScript();
   if (renderClientScript)
   {
      string js = Page.ClientScript.GetCallbackEventReference(
                  this,
         "GetNotificationId('"+ClientID+"')",
         "AjaxNotifierCallback",
         "'" + ClientID + "'", true);
      string js2 = "function DoCallback () {" + js + ";}";

      Page.ClientScript.RegisterClientScriptResource
         (typeof(AjaxNotifier),
         "CustomComponents.AjaxNotifier.js");
      Page.ClientScript.RegisterClientScriptBlock(typeof(AjaxNotifier),
         typeof(AjaxNotifier).FullName + "DoCallback", js2, true);
      Page.ClientScript.RegisterStartupScript(typeof(AjaxNotifier),
         typeof(AjaxNotifier).FullName + "WebDoCallback", js,true);
   }
   base.OnPreRender(e);
}

OnPreRender registers three script blocks. The first script block references the CustomComponents.AjaxNotifier.js embedded resource:

Page.ClientScript.RegisterClientScriptResource(typeof(AjaxNotifier),
        "CustomComponents.AjaxNotifier.js");

Chapter 26 "Developing Ajax-Enabled Controls and Components: Client-Side Functionality" in the book Professional ASP.NET 2.0 Server Control and Component Development (Wrox, July-2006, ISBN: 0-471-79350-7) covers the embedded resources in detail. The AjaxNotifier.js script file contains the JavaScript functions that the AjaxNotifier control uses. In this article, I discuss these JavaScript functions in detail.

The second script block contains the code for the DoCallback JavaScript function. Notice that this function runs the JavaScript code that the GetCallbackEventReference method of the ClientScript property of the page returns. As covered in Chapter 27 "Developing Ajax-Enabled Controls and Components: Asynchronous Client Callback" in the book Professional ASP.NET 2.0 Server Control and Component Development (Wrox, July-2006, ISBN: 0-471-79350-7), this JavaScript code includes a call to a JavaScript function that contains the code that makes the asynchronous client callback to the server:

string js = Page.ClientScript.GetCallbackEventReference(
        this,
        "GetNotificationId('"+ClientID+"')",
        "AjaxNotifierCallback",
        "'" + ClientID + "'", true);
string js2 = "function DoCallback () {" + js + ";}";

The third script block includes a call to the JavaScript function that contains the code that makes the asynchronous client callback to the server. Notice that OnPreRender uses the RegisterStartupScript method to register this script to request the page to render the script at the bottom of the page. This means that the first asynchronous call is made right after the page is loaded. You'll see the significance of this later in this section.

    Page.ClientScript.RegisterStartupScript(typeof(AjaxNotifier),
    typeof(AjaxNotifier).FullName + "WebDoCallback", js,true);

Listing 2 contains the code for the AddAttributesToRender method of the AjaxNotifier control.

Listing 2: The AddAttributesToRender method
protected override void AddAttributesToRender(HtmlTextWriter writer)
{
  base.AddAttributesToRender(writer);

  if (renderClientScript)
  {
      CssStyleCollection col;
      writer.AddAttribute("notificationId", "0");
      if (dialogStyle != null)
      {
         col = dialogStyle.GetStyleAttributes(this);
         writer.AddAttribute("dialogStyle", col.Value);
      }
      if (headerStyle != null)
      {
         col = headerStyle.GetStyleAttributes(this);
         writer.AddAttribute("headerStyle", col.Value);
      }
      if (itemStyle != null)
      {
         col = itemStyle.GetStyleAttributes(this);
         writer.AddAttribute("itemStyle", col.Value);
      }
      if (alternatingItemStyle != null)
      {
         col = alternatingItemStyle.GetStyleAttributes(this);
         writer.AddAttribute("alternatingItemStyle", col.Value);
      }
   }
}

The dialog that the AjaxNotifier pops up handles everything on the client side including rendering, moving, resizing, and font-adjustment when the dialog is resized. Therefore, this pop-up dialog isn't a server control. Chapter 27 shows you how you can expose the CSS style attributes of a client-side component such as this pop-up dialog as top-level properties of the Ajax-enabled control itself.

AjaxNotifier exposes the same top-level properties that the AjaxDetailsDialog from the previously mentioned Chapter 27 exposes. The AjaxNotifier control also overrides the TrackViewState, SaveViewState, and LoadViewState methods to manage the states of these top-level properties across page postbacks. This is thoroughly discussed in Chapter 27 in the same book.

Developing Ajax-Enabled Controls and Components: Periodic Refresh Pattern

Implementing ICallbackEventHandler

The AjaxNotifier control implements the ICallbackEventHandler interface to use the ASP.NET 2.0 client callback mechanism to make its asynchronous client callbacks to the server. Listing 3 presents the code for the RaiseCallbackEvent and GetCallbackResult methods.

Listing 3: Implementing the methods of the ICallbackEventHandler
   protected virtual string GetCallbackResult()
   {
      return callbackResult;
   }
   protected virtual void RaiseCallbackEvent(string eventArgument)
   {
      IDataSource ds = (IDataSource)Page.FindControl(DataSourceID);
      int notificationId = int.Parse(eventArgument);
      if (notificationId < 0)
         notificationId = 1;
      Page.Session["NotificationId"] = notificationId;
      if (ds != null)
      {
         DataSourceView dv = ds.GetView(string.Empty);
         dv.Select(DataSourceSelectArguments.Empty, SelectCallback);
      }
   }

AjaxNotifier exposes a string property named DataSourceID that the page developer must set to the value of the ID property of the desired tabular data source control. As Listing 3 shows, the RaiseCallbackEvent method uses the ASP.NET tabular data source control API to retrieve the data from the underlying data store in generic fashion as thoroughly discussed earlier in the book. The argument of the RaiseCallbackEvent method contains the notification ID of the latest notification that the current user has seen. Notice that the RaiseCallbackEvent method stores this notification ID value in the Session object. To help you understand why this is necessary, consider the page shown in Listing 4 where the AjaxNotifier control is used. Figure 1 shows what the end users see when they access this page.

Listing 4: A page that uses the AjaxNotifier control
<%@ Page Language="C#" %>
<%@ Register TagPrefix="custom" Namespace="CustomComponents"
Assembly="CustomComponents" %>
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
   <form id="form1" runat="server">
      <custom:AjaxNotifier runat="server" DataSourceID="MySource"
      DialogStyle-BackColor="LightGoldenrodYellow"
      DialogStyle-BorderColor="Tan" DialogStyle-BorderWidth="1px"
      DialogStyle-CellPadding="2" DialogStyle-CellSpacing="0"
      DialogStyle-BorderStyle="Groove" DialogStyle-ForeColor="Black"
      DialogStyle-GridLines="None" HeaderStyle-BackColor="Tan"
      HeaderStyle-Font-Bold="True"
         AlternatingItemStyle-BackColor="PaleGoldenrod" />
      <asp:SqlDataSource runat="server" ID="MySource"
       ConnectionString="<%$ connectionStrings:
                         MySqlConnectionString %>"
       SelectCommand="Select * From Notifications Where Id > @Id">
          <SelectParameters>
             <asp:SessionParameter Name="Id"
                  SessionField="NotificationId"
                Type="Int32" />
          </SelectParameters>
      </asp:SqlDataSource>
   </form>
</body>
</html>

As Listing 4 shows, this page binds the AjaxNotifier control to a SqlDataSource control. Notice that the SelectCommand attribute of the <asp:SqlDataSource> tag contains the following Select SQL statement:

Select * From Notifications Where Id > @Id

This SQL statement contains a parameter named @Id whose value is the notification ID of the latest notification that the current user has seen. As Listing 4 shows, the SqlDataSource control uses a SessionParameter to retrieve the notification ID from the Session object. That is the reason that Listing 4 stores the notification ID in the Session object.

<asp:SqlDataSource runat="server" ID="MySource" 
 ConnectionString="<%$ connectionStrings:MySqlConnectionString %>"
 SelectCommand="Select * From Notifications Where Id > @Id">
   <SelectParameters>
     <asp:SessionParameter Name="Id" SessionField="NotificationId" 
      Type="Int32" />
   </SelectParameters>
</asp:SqlDataSource>

Session object is not the only available option to communicate data between a tabular data source control and posted data. Chapter 27 "Developing Ajax-Enabled Controls and Components: Asynchronous Client Callback" in the book Professional ASP.NET 2.0 Server Control and Component Development (Wrox, July-2006, ISBN: 0-471-79350-7) discusses another option to accomplish the same task. As Listing 3 shows, the RaiseCallbackEvent method registers the SelectCallback method as the callback for the Select data operation. Listing 5 presents the implementation of the SelectCallback method. The main responsibility of this method is to use the retrieved data record to generate the XML document that will then be sent to the client. In other words, the client and server exchange data in XML format.

Listing 5: The SelectCallback method
private void SelectCallback(IEnumerable data)
{
   using (StringWriter sw = new StringWriter())
   {
      using (XmlWriter xw = XmlWriter.Create(sw))
      {
         xw.WriteStartDocument();
         xw.WriteStartElement("notification");
         IEnumerator iter = data.GetEnumerator();
         if (iter.MoveNext())
         {
            PropertyDescriptorCollection col =
                    TypeDescriptor.GetProperties(iter.Current);
            foreach (PropertyDescriptor pd in col)
            {
               if (pd.Name == "Source")
                  xw.WriteElementString("source",
                              (string)pd.GetValue(iter.Current));
              else if (pd.Name == "Notification")
                 xw.WriteElementString("summary",
                              (string)pd.GetValue(iter.Current));
              else if (pd.Name == "Id")
                 xw.WriteElementString("id",
                              pd.GetValue(iter.Current).ToString());
            }
         }
         xw.WriteEndElement();
         xw.WriteEndDocument();
      }
      callbackResult = sw.ToString();
   }
}

Therefore, one of your responsibilities as an Ajax-enabled control developer is to decide on the structure or format of the XML document. Listing 6 presents an example of an XML document that the AjaxNotifier control supports.

Listing 6: Example of an XML document that AjaxNotifier supports
<notification>
   <id>3</id>
   <source>John</source>
   <summary>We'll meet tomorrow morning</summary>
</notification>

This XML document, like all XML documents, has a single document element, that is, <notification>, which contains three child elements: <id>, <source>, and <summary>.

As shown in Listing 5, the SelectCallback method uses the XmlWriter streaming API discussed in Chapter 24 "Developing Custom Role Providers, Modules, and Principals" in the book Professional ASP.NET 2.0 Server Control and Component Development (Wrox, July-2006, ISBN: 0-471-79350-7) to generate the XML document, which is then loaded into a StringWriter. The SelectCallback method first calls the WriteStartDocument method of the XmlWriter to signal the beginning of the document and to emit the XML declaration:

xw.WriteStartDocument();

It then calls the WriteStartElement method to write out the opening tag of the <notification> document element (see Listing 6):

xw.WriteStartDocument();

Next, it accesses the enumerator object that knows how to enumerate the retrieved data in generic fashion:

IEnumerator iter = data.GetEnumerator();

It then retrieves the PropertyDescriptionCollection collection that contains one PropertyDescriptor object for each datafield of the retrieved record:

PropertyDescriptorCollection col =
        TypeDescriptor.GetProperties(iter.Current);

Next, it iterates through these PropertyDescriptor objects to write out the <source>, <summary>, and <id> elements and their contents:

if (pd.Name == "Source")
   xw.WriteElementString("source",
   (string)pd.GetValue(iter.Current));
else if (pd.Name == "Notification")
   xw.WriteElementString("summary",
   (string)pd.GetValue(iter.Current));
else if (pd.Name == "Id")
   xw.WriteElementString("id",
   pd.GetValue(iter.Current).ToString());

Developing Ajax-Enabled Controls and Components: Periodic Refresh Pattern

Revisting the GetNotificationId JavaScript function

Now, revisit Listing 1 to discuss part of this code listing not yet covered, as highlighted in Listing 7.

Listing 7: The OnPreRender method revisited
protected override void OnPreRender(EventArgs e)
{
   DetermineRenderClientScript();
   if (renderClientScript)
   {
      string js = Page.ClientScript.GetCallbackEventReference(
               this,
               "GetNotificationId('"+ClientID+"')",
               "AjaxNotifierCallback",
               "'" + ClientID + "'", true);
      string js2 = "function DoCallback () {" + js + ";}";

      Page.ClientScript.RegisterClientScriptResource
               (typeof(AjaxNotifier),
               "CustomComponents.AjaxNotifier.js");
      Page.ClientScript.RegisterClientScriptBlock(typeof(AjaxNotifier),
               typeof(AjaxNotifier).FullName +
               "DoCallback", js2, true);
      Page.ClientScript.RegisterStartupScript(typeof(AjaxNotifier), 
               typeof(AjaxNotifier).FullName +
               "WebDoCallback", js,true);
   }
   base.OnPreRender(e);
}

As the highlighted portion of Listing 7 shows, the GetNotificationId JavaScript function is assigned to the task of determining the notification ID that the client passes to the server. Recall that this notification ID is the ID of the latest notification that the current user has seen. The following code listing presents the implementation of the GetNotificationId JavaScript function:

function GetNotificationId(ajaxNotifierId)
{
   var ajaxNotifier = document.getElementById(ajaxNotifierId);
   return ajaxNotifier.notificationId;
}

As the highlighted code in Listing 7 illustrates, OnPreRender passes the value of the ClientID property of the AjaxNotifier control to the GetNotificationId JavaScript function. As this code shows, GetNotificationId passes this value to the getElementById method of the document DOM object to access the containing HTML element of the AjaxNotifier control. GetNotificationId then returns the value of the notificationId attribute of the containing element. Notice that this attribute is a custom HTML attribute that holds the notification ID of the latest notification the current user has seen.

As the highlighted code in Listing 7 shows, OnPreRender registers the AjaxNotifierCallback JavaScript function as the callback for the client callback requests. This function is automatically called when the server response arrives. Listing 8 contains the implementation of the AjaxNotifierCallback JavaScript function.

The AjaxNotifierCallback function first calls the CreateXmlDocument JavaScript function discussed in the previously mentioned Chapter 27 to create an XML store:

var xmlDocument = CreateXmlDocument(); 

It then loads the XML document that it has received from the server to the XML store:

xmlDocument.loadXML(result);

Next, it accesses the document element of the XML document. As shown in Listing 6, the document element is the <notification> element:

var notification = xmlDocument.documentElement;

It then accesses the first child element of the document element. As shown in Listing 6, the first child element is the <id> element:

var notificationId = notification.childNodes[0].text;

Next, AjaxNotifierCallback accesses the containing HTML element of the AjaxNotifier control:

var ajaxNotifier = document.getElementById(context);

Recall that the containing HTML element of the AjaxNotifier control has a custom attribute named notificationId that holds the notification ID of the latest notification that the current user has seen. AjaxNotifierCallback checks whether the notification ID of the notification that it has received from the server is different from the notification ID of the latest notification that the user has seen. If so, it assigns the new notification ID to the notificationId attribute of the containing HTML element of the AjaxNotifier control:

ajaxNotifier.notificationId = notificationId;

AjaxNotifierCallback then calls the InitializeDetailsPopup JavaScript function discussed in the previously mentioned Chapter 26 to initialize the pop-up dialog:

InitializeDetailsPopup(context);

Next, it accesses the text content within the opening and closing tags of the second and third child elements of the <notification> document element. As shown in Listing 6, these two child elements are the <source> and <summary> elements:

var notificationSource = notification.childNodes[1].text;
var notificationSummary = notification.childNodes[2].text;

Next, AjaxNotifierCallback generates the HTML that displays the new notification:

var content = "<r>" +
                 "<td colspan='2'>" +
                    "<p><center><b>New Message</b></center></p>" +
                    "<p><b>From: </b>"+notificationSource+"</p>" +
                    "<p><b>Message:</b><br/>"+
                    notificationSummary+"</p>" +
                 "</td>" +
              "</r>";

It then calls the DisplayDetailsPopup function to display the HTML in the details pop-up dialog:

DisplayDetailsPopup (content);

Finally, AjaxNotifierCallback calls the setTimeout JavaScript function:

setTimeout(DoCallback,6000);

As shown in Listing 1, the DoCallback JavaScript function runs the JavaScript code that makes the asynchronous client callback to the server to download the XML document that contains the latest posted notification. The AjaxNotifierCallback JavaScript function is automatically called when the page is loaded. This means that when the user downloads the page, the following sequence is automatically triggered:

  1. AjaxNotifierCallback is called.
  2. AjaxNotifierCallback pops up the dialog that displays the latest posted notification.
  3. AjaxNotifierCallback calls the setTimeout function.

The setTimeout function periodically calls the DoCallback function, which in turn runs the JavaScript code that makes a client callback request to the server to download the XML document that contains the latest posted notification. When the server response arrives, this JavaScript code, in turn, calls the AjaxNotifierCallback method, which repeats the same steps discussed before.

Listing 8: The AjaxNotifierCallback Method
function AjaxNotifierCallback(result, context)
{
   var xmlDocument = CreateXmlDocument();
   xmlDocument.loadXML(result);

   var notification = xmlDocument.documentElement;
   if (notification.childNodes.length > 0)
   {
      var notificationId = notification.childNodes[0].text; 
      var ajaxNotifier = document.getElementById(context);
      if (notificationId != ajaxNotifier.notificationId)
      {
         ajaxNotifier.notificationId = notificationId;
         InitializeDetailsPopup(context);
         var notificationSource = notification.childNodes[1].text; 

         var notificationSummary = notification.childNodes[2].text; 

         var content = "<r>" +
            "<td colspan='2'>" +
               "<p><center><b>Notification</b></center></p>" +
               "<p><b>From: </b>"+notificationSource+"</p>" +
               "<p><b>Message:</b><br/>"+notificationSummary+"</p>" +
            "</td>" +
         "</r>";
         DisplayDetailsPopup (content);
      }
   }
   setTimeout(DoCallback,6000);
}

Try the following workflow to see for yourself how the AjaxNotifier works:

  1. Get the sample code files for the book Professional ASP.NET 2.0 Server Controls and Component Development and open the application that contains the code files for Chapter 29 in Visual Studio 2005.
  2. Run the application to access the AjaxNotifier.aspx page. Listing 4 shows the contents of this page. Notice that the AjaxNotifier automatically makes an asynchronous client callback to the server to retrieve the latest notification and displays the notification in the pop-up dialog as shown in Figure 1.
  3. Go to the Server Explorer window and access the database table named Notifications.
  4. Add a new notification record to the table. Notice that the AjaxNotifier automatically shows the latest notification.

When you first launch the application, AjaxNotifier displays all the notifications one after another because the current implementation of the AjaxNotifier doesn't store the notification ID of the latest notification that the user saw in the previous session. That's why AjaxNotifier resets this notification ID to zero every time you relaunch the application. You can easily fix this by storing the notification ID of the latest notification in the ASP.NET 2.0 Profile object.

This article is adapted from Professional ASP.NET 2.0 Server Control and Component Development by Dr. Shahram Khosravi (Wrox, 2006, ISBN: 0-471-79350-7), from Chapter 29, "Developing Ajax-Enabled Controls and Components: More Ajax Patterns."

Copyright 2007 by WROX. All rights reserved. Reproduced here by permission of the publisher.



About the Author

Shahram Khosravi

Shahram is a senior software engineer, consultant, author, and instructor specializing in ASP.NET, Web services, .NET technologies, XML technologies, ADO.NET, C#, 3D computer graphics, Human Interface (HI) usability, and design patterns. He has more than 10 years of experience in object-oriented analysis, design, and programming. Shahram has written articles on the .NET Framework, ADO.NET, ASP.NET, and XML technologies for industry leading magazines such as Dr. Dobb's Journal, asp.netPRO magazine, and Microsoft MSDN Online.

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Hybrid cloud platforms need to think in terms of sweet spots when it comes to application platform interface (API) integration. Cloud Velocity has taken a unique approach to tight integration with the API sweet spot; enough to support the agility of physical and virtual apps, including multi-tier environments and databases, while reducing capital and operating costs. Read this case study to learn how a global-level Fortune 1000 company was able to deploy an entire 6+ TB Oracle eCommerce stack in Amazon Web …

  • Cisco and Intel have harnessed flash memory technology and truly innovative system software to blast through the boundaries of today's I/O-bound server/storage architectures. See how they are bringing real-time responsiveness to data-intensive applications—for unmatched business advantage. Sponsored by Cisco and Intel® Partnering in Innovation

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds