使用AJAX实时显示数据
Display data updates in real-time with AJAX
http://encosia.com/2007/07/25/display-data-updates-in-real-time-with-ajax/
最近我注意到越来越多的网站上开始出现实时“spy”功能。虽然它经常要耗费大量的时间,但是我还是忍不住喜欢上这种功能。这是一个挖掘AJAX最大潜力的例子。它深深的吸引了我,这是一个使用为了效率使用web method的优秀的ASP.NET AJAX实现,它采用__doPostBack()来触发UpdatePanel更新。因此我决定使用ASP.NET AJAX框架来解释一下这些概念。(So, I decided to put together a proof of concept, using the ASP.NET AJAX framework.)
为了创建一个全概念的例子,需要做下面这些事情:
l 选择一个用来“spy”的数据源。
l 构建一个接口用来添加行以进行测试。
l 使用基于行的格式显示数据
l 创建一个方法找到最近的数据行
l 使用那个方法异步监视行的更新
l 当检测到加法(additions)时刷新废弃的数据。
选择数据源
许多数据源都可以用于这种监控,例如数据库查询,web服务,事件日志,以及其他任何有序的,基于行的数据。它们中大多数都有一个共同点就是你应该尝试在实际数据源和轮询机制之间使用一个缓存层。
因此,我们不要担心底层的数据存储,直接使用.NET Cache就可以了。在实际实现中,简单的对缓存更新代码进行扩展,使它在合适的时候也存储到你的数据层就可以了。
在实例中,我们将spy一个博客文章的有序列表。为了在首次运行时,就能够有一个发起实例和一些测试数据。
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;
}
}
如果缓存为空,那么它会产生一些初始的数据。在你的应用程序中,将添加的行替换为对你的数据源的首次调用,并将其缓存起来。
数据接口
为了测试的目的,我们需要一个操作缓存的接口。
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;
}
这允许我们添加一个新文章到缓存的DataTable中。依赖于你的数据来自哪里,你可能根本不需要这些。如果你使用这种方式控制数据的输入,确保在这一步中,你也将“添加的数据”(译者注:即刚添加的那行数据)也保存到底层的数据存储中。
显示数据
为了完成实例框架,我们需要将系统中缓存的文章显示出来。
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();
}
这将会在一个GridView表中显示缓存中所有的文章的日期和标题。虽然没有DiggSpy那么美观,但是起码它能工作。
由于我计划使用一个web方法来轮询,于是我使能了ScriptManager中的页面方法。
我也向页面中添加了一个隐藏域,它展现最新的文章的DateTime。这样我们可以使用一个简单的方法来比较而不必担心要在客户端脚本中进行DateTime解析。通过将它嵌入在更新的内容中,帮助确保我们的数据总是同步的。
寻找最近的行
现在是时候使用前面显示的代码向Default.aspx.cs中添加GetLastHeadlineTick()方法了。这也是一个web方法,我们在客户端使用它来轮询更新。
[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;
}
再次注意,我将DateTime转换成Ticks,这样比较值变得非常容易。You could just as easily use the ID from an identity field, or a non-temporal natural key.任何对你的数据来说有意义的东西。只需要牢记数字比较是最简单的。
注意显式的静态缓存引用。这是必须的,因为web方法必须是静态的。如果你需要执行非静态的操作,你仍然能实现它,但是你将需要将轮询web方法改为轮询一个单独的web服务。
在客户端监控缓存数据
最后,我们需要一些客户端脚本轮询web方法并做相应的响应。在Default.aspx中我将其作为一个ScriptReference添加到ScriptManager中:
function Check()
{
// Call the static page method.
PageMethods.GetLatestHeadlineTick(OnSucceeded, OnFailed);
}
function OnSucceeded(result, userContext, methodName)
{
// Parse the web method's result and the embedded
// hidden value to integers for comparison.
var LatestTick = parseInt(result);
var LatestDisplayTick = parseInt($get('LatestDisplayTick').value);
// If the web 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);
}
实际上,它每隔5秒调用一次GetLastHeadlineTick(),并与嵌入在UpdatePanel中的LatestTick进行比较,如果UpdatePanel的日期过期就触发一次UpdatePanel更新。
如果你在一个浏览器窗口中打开Default.aspx,而在另一个窗口中打开AddArticle.aspx,在AddArticle中添加一篇文章会导致Default中的GridView自动更新自己。如果没有添加文章,那么不会发生部分更新。
这也可以使用一个Timer控件定期更新UpdatePanel来完成同样的功能。然而,通过轮询一个轻量级的web方法,而不是每次轮询都触发一次完全部分更新,那么我们可以承受更频繁的轮询和更多的并发用户。这两种方法之间的差别是好几个数量级的。
改进的控件(总是)
我认为这个例子非常吸引人,但是还有改进的空间。
暂停/继续功能,就像DiggSpy中的一个也很容易实现,也很方便。只使用两到三行客户端脚本就能实现。
你也可以添加错误处理OnFailed()。理想情况下,应该有一些更加健壮的错误处理。当服务器负荷比较大,在请求超时时,间隔更长的时间再重试。或者,增加一个计数器,记录有新行出现时请求失败的次数,并在超出阀值时显示一条友好的提示。
我使用GridView是为了简单,但是我认为如果使用一个输出divs的repeater更好。它更轻巧,可以通过DOM操作来动态更新。
DOM操作将会是另一个好的改进。不是运行一次完全部分更新来更新显示,可以创建另一个web方法,它返回一个比给定tick更新的文章的JSON对象。然而,那些新文章的divs可以更优美的显示出来,例如通过渐变,幻灯,或者其他客户端技巧。