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.
Here is how the workflow goes:
- 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.
- 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.
- 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.
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);
}
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.
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);
}
}
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>
Select * From Notifications Where Id > @Id
<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());
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);
}
function GetNotificationId(ajaxNotifierId)
{
var ajaxNotifier = document.getElementById(ajaxNotifierId);
return ajaxNotifier.notificationId;
}
var xmlDocument = CreateXmlDocument();
It then loads the XML document that it has received from the server to the XML store:
xmlDocument.loadXML(result);
var notification = xmlDocument.documentElement;
var notificationId = notification.childNodes[0].text;
Next, AjaxNotifierCallback accesses the containing HTML element of the AjaxNotifier control:
var ajaxNotifier = document.getElementById(context);
ajaxNotifier.notificationId = notificationId;
InitializeDetailsPopup(context);
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);
- AjaxNotifierCallback is called.
- AjaxNotifierCallback pops up the dialog that displays the latest posted notification.
- AjaxNotifierCallback calls the setTimeout function.
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:
- 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.
- 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.
- Go to the Server Explorer window and access the database table named Notifications.
- 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.
No comments:
Post a Comment