はじめに
Periodic Refresh(定期リフレッシュ)パターンを使用するAjax対応コンポーネントは、サーバを定期的にポーリングして変更がないかどうかを確認します。この記事では、AjaxNotifierという名前のAjax対応カスタムコントロールの開発を通じて、Periodic Refreshパターンを用いた独自のAjax対応コントロールの開発方法を説明します。
AjaxNotifierは、サーバを定期的にポーリングして、最新の通知がポストされていないかどうかを確認し、その通知を図1のようなポップアップダイアログに表示します。このポップアップダイアログには、通知の送信元と通知の内容が示されます。
この処理のワークフローを以下に示します。
- AjaxNotifierは、DOMとJavaScriptを使用して、ユーザーに示されている最新の通知IDを取得します。通知IDには、通知の発行順序を特定するのに必要な情報がすべて含まれています(例えば、通知の作成日を示すタイムスタンプなど)。
- AjaxNotifierは、ASP.NET 2.0クライアントコールバックメカニズムを使用して、サーバに対して非同期コールバックを行い、最新の通知に関する情報が含まれるXMLドキュメントをダウンロードします。また、これまでにユーザーに表示した最新の通知IDをサーバに渡して、サーバが次の通知をクライアントに送り返せるようにします。
- 次に、AjaxNotifierは、XML、DOM、およびJavaScriptを使用して、必要なデータをXMLドキュメントから動的に取得し、ポップアップダイアログに表示します。
AjaxNotifierコントロールは、WebControl基本クラスから派生しており、ICallbackEventHandlerインターフェースを実装します。このコントロールは、WebControlクラスの次のメソッドをオーバーライドします。
- OnPreRender
- AddAttributesToRender
- TrackViewState
- SaveViewState
- LoadViewState
WebControlからの派生
リスト1に、OnPreRenderメソッドの実装を示します。
リスト1 OnPreRenderメソッド
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は、3つのスクリプトブロックを登録します。最初のスクリプトブロックは、「CustomComponents.AjaxNotifier.js」埋め込みリソースを参照します。
Page.ClientScript.RegisterClientScriptResource(typeof(AjaxNotifier),
"CustomComponents.AjaxNotifier.js");
埋め込みリソースについては、『Professional ASP.NET 2.0 Server Control and Component Development』(Wrox, July-2006, ISBN: 0-471-79350-7)の第26章「Developing Ajax-Enabled Controls and Components:Client-Side Functionality」で詳しく説明しています。「AjaxNotifier.js」スクリプトファイルには、AjaxNotifierコントロールが使用するJavaScript関数が含まれています。この記事では、このJavaScript関数について詳しく説明します。
2番目のスクリプトブロックには、DoCallbackというJavaScript関数のコードが含まれています。この関数は、ページのClientScriptプロパティのGetCallbackEventReferenceメソッドが返すJavaScriptコードを実行します。『Professional ASP.NET 2.0 Server Control and Component Development』(Wrox, July-2006, ISBN: 0-471-79350-7)の第27章「Developing Ajax-Enabled Controls and Components:Asynchronous Client Callback」で触れているように、このJavaScriptコードには、サーバへの非同期クライアントコールバックを行うJavaScript関数の呼び出しが含まれています。
string js = Page.ClientScript.GetCallbackEventReference(
this,
"GetNotificationId('"+ClientID+"')",
"AjaxNotifierCallback",
"'" + ClientID + "'", true);
string js2 = "function DoCallback () {" + js + ";}";
3番目のスクリプトブロックには、サーバへの非同期クライアントコールバックを行うJavaScript関数の呼び出しが含まれます。OnPreRenderがRegisterStartupScriptメソッドを使ってこのスクリプトを登録し、ページ下部のスクリプトをレンダリングするようページに要求していることに注目してください。これは、ページのロード直後に最初の非同期呼び出しが行われることを意味します。この重要性については後で説明します。
Page.ClientScript.RegisterStartupScript(typeof(AjaxNotifier),
typeof(AjaxNotifier).FullName + "WebDoCallback", js,true);
リスト2に、AjaxNotifierコントロールのAddAttributesToRenderメソッドのコードを示します。
リスト2 AddAttributesToRenderメソッド
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);
}
}
}
AjaxNotifierがポップアップ表示するダイアログは、ダイアログのリサイズ時にクライアントサイドで行われるすべての処理(例えばレンダリング、移動、リサイズ、フォント調整など)を自ら担当します。従って、このポップアップダイアログはサーバーコントロールではありません。第27章では、このポップアップダイアログのようなクライアントサイドコンポーネントのCSSスタイル属性をAjax対応コントロール自身の最上位プロパティとして公開する方法を説明しています。
AjaxNotifierは、前述した第27章のAjaxDetailsDialogが公開するのと同じ最上位プロパティを公開します。また、AjaxNotifierコントロールでは、TrackViewStateメソッド、SaveViewStateメソッド、およびLoadViewStateメソッドをオーバーライドすることで、これらの最上位プロパティの状態をページのポストバック間で管理しています。これについては、第27章で詳しく説明します。
ICallbackEventHandlerの実装
AjaxNotifierコントロールでは、ICallbackEventHandlerインターフェースを実装することで、ASP.NET 2.0のクライアントコールバックメカニズムを利用してサーバへの非同期クライアントコールバックを行います。リスト3に、RaiseCallbackEventメソッドおよびGetCallbackResultメソッドのコードを示します。
リスト3 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は、DataSourceIDという名前の文字列プロパティを公開します。ページ開発者は、これを必要な表形式データソースコントロールのIDプロパティの値に設定しなければなりません。リスト3に示すように、RaiseCallbackEventメソッドは、ASP.NETの表形式データソースコントロールAPIを使用して、一般的な方法でデータストアからデータを取得します。これについては、このガイドで既に詳しく説明しました。RaiseCallbackEventメソッドの引数には、現在のユーザーに示されている最新の通知のIDを渡します。RaiseCallbackEventメソッドで、このID値をSessionオブジェクトに格納していることに注目してください。この必要性を理解するために、AjaxNotifierコントロールを使ったページを見てみましょう(リスト4)。エンドユーザーがこのページにアクセスすると、図1のダイアログが表示されます。
リスト4 AjaxNotifierコントロールを使ったページ
<%@ 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>
リスト4のページでは、AjaxNotifierコントロールをSqlDataSourceコントロールにバインドしています。<asp:SqlDataSource>タグのSelectCommand属性には、次のSelectSQLステートメントが含まれています。
Select * From Notifications Where Id > @Id
このSQLステートメントには、@Idという名前のパラメータが含まれています。このパラメータの値は、現在のユーザーに示される最新の通知のIDです。リスト4に示したように、SqlDataSourceコントロールはSessionParameterを使用して、Sessionオブジェクトから通知IDを取得します。そのために、通知IDをSessionオブジェクトに格納しているのです。
<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オブジェクトだけではありません。この作業を行う他の方法については、『Professional ASP.NET 2.0 Server Control and Component Development』(Wrox, July-2006, ISBN: 0-471-79350-7)の第27章「Developing Ajax-Enabled Controls and Components:Asynchronous Client Callback」で説明しています。リスト3に示したように、RaiseCallbackEventメソッドは、SelectCallbackメソッドをSelectデータ操作のコールバックとして登録します。このSelectCallbackメソッドの実装をリスト5に示します。このメソッドの主な仕事は、取得したデータレコードを使用してXMLドキュメントを生成し、クライアントに送信することです。つまり、クライアントとサーバは、XML形式でデータをやり取りします。
リスト5 SelectCallbackメソッド
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();
}
}
従って、XMLドキュメントの構造または書式を決めることも、Ajax対応コントロールの開発者の仕事の1つとなります。リスト6に、AjaxNotifierコントロールがサポートするXMLドキュメントの例を示します。
リスト6 AjaxNotifierがサポートするXMLドキュメントの例
<notification>
<id>3</id>
<source>John</source>
<summary>We'll meet tomorrow morning</summary>
</notification>
他のすべてのXMLドキュメントと同じように、このXMLドキュメントも1つのドキュメント要素(<notification>)を持ち、この要素には、<id>、<source>、<summary>の3つの子要素が含まれています。
リスト5に示したように、SelectCallbackメソッドは、『Professional ASP.NET 2.0 Server Control and Component Development』(Wrox, July-2006, ISBN: 0-471-79350-7)の第24章「Developing Custom Role Providers、Modules、and Principals」で説明したXmlWriterストリーミングAPIを使用してXMLドキュメントを生成します。生成されたXMLドキュメントはStringWriterにロードされます。SelectCallbackメソッドは、まず、XmlWriterのWriteStartDocumentメソッドを呼び出してドキュメントの開始を通知し、XML宣言を生成します。
次に、WriteStartElementメソッドを呼び出して、<notification>ドキュメント要素の開始タグを書き出します(リスト6を参照)。
その後、列挙オブジェクトにアクセスし、取得したデータを一般的な方法で列挙できるようにします。
IEnumerator iter = data.GetEnumerator();
続いて、PropertyDescriptionCollectionコレクションを取得します。このコレクションには、取得したレコードのデータフィールドごとに1つのPropertyDescriptorオブジェクトが含まれています。
PropertyDescriptorCollection col =
TypeDescriptor.GetProperties(iter.Current);
次に、これらのPropertyDescriptorオブジェクトを反復処理し、<source>要素、<summary>要素、および<id>要素と、その内容を書き出します。
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());
GetNotificationId JavaScript関数の再確認
ここで、リスト1のコードに戻り、前に説明しなかった部分について見ていきます。リスト7に、該当部分を太字で示します。
リスト7 OnPreRenderメソッドの再確認
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);
}
リスト7の太字部分が示すように、GetNotificationIdJavaScript関数は、クライアントからサーバに渡される通知IDを特定する処理に使われています。この通知IDは、これまでの時点でユーザーに示されている最新の通知のIDです。GetNotificationIdJavaScript関数の実装は次のようになっています。
function GetNotificationId(ajaxNotifierId)
{
var ajaxNotifier = document.getElementById(ajaxNotifierId);
return ajaxNotifier.notificationId;
}
リスト7で見たように、OnPreRenderは、AjaxNotifierコントロールのClientIDプロパティの値をGetNotificationIdJavaScript関数に渡します。GetNotificationIdは、この値をdocument DOMオブジェクトのgetElementByIdメソッドに渡すことでAjaxNotifierコントロールの親HTML要素にアクセスし、親要素のnotificationId属性の値を返します。この属性が、現在のユーザーに示されている最新の通知のIDを表すカスタムHTML属性であることに注目してください。
リスト7の太字部分のコードが示すように、OnPreRenderは、クライアントコールバック要求のコールバックとしてAjaxNotifierCallbackJavaScript関数を登録します。この関数は、サーバ応答が到着すると自動的に呼び出されます。AjaxNotifierCallbackJavaScript関数の実装をリスト8に示します。
リスト8 AjaxNotifierCallbackメソッド
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);
}
AjaxNotifierCallback関数は、まず、第27章で説明したCreateXmlDocumentJavaScript関数を呼び出して、XMLストアを作成します。
var xmlDocument = CreateXmlDocument();
次に、サーバから受け取ったXMLドキュメントを、XMLストアにロードします。
xmlDocument.loadXML(result);
次に、XMLドキュメントのドキュメント要素にアクセスします。リスト6に示したように、このドキュメント要素は<notification>要素です。
var notification = xmlDocument.documentElement;
そして、ドキュメント要素の最初の子要素にアクセスします。リスト6に示したように、最初の子要素は<id>要素です。
var notificationId = notification.childNodes[0].text;
次に、AjaxNotifierCallbackはAjaxNotifierコントロールの親HTML要素にアクセスします。
var ajaxNotifier = document.getElementById(context);
AjaxNotifierコントロールの親HTML要素には、現在のユーザーに示されている最新の通知のIDを持つnotificationIdという名前のカスタム属性があることを思い出してください。AjaxNotifierCallbackは、サーバから受け取った通知のIDが、現在のユーザーに示されている最新の通知のIDと異なるかどうかを確認し、異なっている場合は、新しい通知IDをAjaxNotifierコントロールの親HTML要素のnotificationId属性に割り当てます。
ajaxNotifier.notificationId = notificationId;
その後、AjaxNotifierCallbackは、第26章で説明したInitializeDetailsPopupJavaScript関数を呼び出して、ポップアップダイアログを初期化します。
InitializeDetailsPopup(context);
次に、<notification>ドキュメント要素にある2番目と3番目の子要素の、開始タグと終了タグに挟まれているテキストコンテンツにアクセスします。リスト6を見るとわかるとおり、この2つの子要素は<source>要素と<summary>要素です。
var notificationSource = notification.childNodes[1].text;
var notificationSummary = notification.childNodes[2].text;
次に、AjaxNotifierCallbackは、新しい通知を表示するHTMLを生成します。
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>";
そして、DisplayDetailsPopup関数を呼び出して、詳細ポップアップダイアログにHTMLを表示します。
DisplayDetailsPopup (content);
最後に、setTimeoutJavaScript関数を呼び出します。
setTimeout(DoCallback,6000);
リスト1では、DoCallbackJavaScript関数が、サーバへの非同期クライアントコールバックを行うJavaScriptコードを実行し、ポストされた最新の通知が含まれるXMLドキュメントをダウンロードしています。AjaxNotifierCallbackJavaScript関数は、ページがロードされると自動的に呼び出されます。つまり、ユーザーがページをダウンロードすると、次のシーケンスが自動的にトリガされます。
- AjaxNotifierCallbackが呼び出される。
- AjaxNotifierCallbackが、ポストされた最新の通知を示すダイアログをポップアップ表示する。
- AjaxNotifierCallbackがsetTimeout関数を呼び出す。
setTimeout関数は、定期的にDoCallback関数を呼び出します。このDoCallback関数は、サーバに対してクライアントコールバック要求を行うJavaScriptコードを実行し、ポストされた最新の通知が含まれるXMLドキュメントをダウンロードします。サーバの応答が到着すると、JavaScriptコードはAjaxNotifierCallbackメソッドを呼び出し、このメソッドは前に説明した手順を繰り返します。
次のワークフローで、AjaxNotifierの動作を自分自身で確かめてみましょう。
- 『Professional ASP.NET 2.0 Server Controls and Component Development』のサンプルコードを取得して、第29章のコードファイルを含むアプリケーションをVisual Studio 2005で開きます。
- アプリケーションを実行し、「AjaxNotifier.aspx」ページにアクセスします。リスト4がこのページの内容です。AjaxNotifierが自動的にサーバへの非同期コールバックを行って最新の通知を取得し、図1のポップアップダイアログに表示することを確認してください。
- Server Explorerウィンドウに移動して、Notificationsという名前のデータベーステーブルにアクセスします。
- 新しい通知レコードをテーブルに追加します。AjaxNotifierが最新の通知を自動的に表示していることを確認してください。
このアプリケーションを最初に起動したときには、AjaxNotifierによってすべての通知が1つ1つ表示されます。これは、AjaxNotifierの現在の実装には、前回のセッションでユーザーに示された最新の通知のIDが格納されていないからです。そのため、アプリケーションを起動するたびに、この通知IDがゼロにリセットされます。この点は、ASP.NET 2.0のProfileオブジェクトに最新の通知のIDを格納することで簡単に修正できます。
ASP.NET、Webサービス、.NET テクノロジ、XMLテクノロジ、ADO.NET、C#、3Dコンピュータグラフィック、ヒューマンインターフェイス(HI)の有用性、および設計パターンを専門とするシニアソフトウェアエンジニア兼コンサルタント。業務の傍ら執筆活動も行う。オブジェクト指向分析、設計、プログラミングに関する経験は10年を超えており、.NET Framework、ADO.NET、ASP.NET、およびXMLテクノロジに関する記事を『Dr. Dobb's Journal』、『asp.netPRO』、『Microsoft MSDN Online』など業界最大手の専門誌に多数寄稿している。