有必要指出,执行回调时目标页面实际上开始执行修改的生命周期。大多数控件事件不会执行,但 Page.Load 和 Page.Init 事件处理程序会执行。Page.IsPostBack 属性将返回 true,但检查 Page.IsCallback 属性可区分回调还是回传。
当前客户端回调实现的唯一问题是编程接口还非常简单,特别是它要求只能使用字符串交换信息。现在 ASP.NET 的趋势是使用回调功能把动态功能构建到动态控件里,而不是直接在页面中使用它们。
自定义控件里的客户端回调
把客户端回调整合到页面需要不少工作。然而,更好的选择是使用它们构建富控件,然后可以在多个页面里使用它们。最妙的是,不必设计底层回调基础设施就可以获得 Windows 风格的响应性。
使用动态回调构建的控件类型没有限定,但很多控件只使用回调刷新它们用户界面的一部分(如 TreeView)。你可以巧妙的创建一个自动提供这个功能的容器控件。
基本思想是创建一个自 Panel 继承的新控件。这个面板包含要刷新的内容。在特定的时刻,js 事件被执行,使面板执行一次回调。这时,面板会引发一个服务器端事件以通知代码。你响应该事件并调整面板中的任意控件。事件结束时,面板获得自身内容的新 HTML 并返回它。客户端脚本只需通过一点点 DHTML 把当前面板的内容替换成新 HTML。
这个控件为如何把 Ajax 技术整合到自定义控件提供了一个直观的示例。不过,在真实的 Web 应用程序里,你不太可能会用 DynamicPanel,这是因为 ASP.NET AJAX 有一个叫做 UpdatePanel 的类似的但更强大的控件。关于 ASP.NET AJAX 的技术我会在后续的博客中介绍。
1. DynamicPanel
第一步是从 Panel 派生并实现 ICallbackEventHandler 接口。作为 ICallbackEventHandler 的一部分,DynamicPanel 要实现 Raise_CallbackEvent() 和 GetCallbackResult() 方法。它是一个两步的过程。首先,DynamicPanel 要激发一个事件通知页面,页面处理这个事件并执行适当的修改。其次,DynamicPanel 要为它的内容呈现 HTML,然后它才能把它的信息(包括它的客户端 ID)返回给客户端网页。
public class DynamicPanel : Panel, ICallbackEventHandler
{
public EventHandler Refresh;
public void RaiseCallbackEvent(string eventArgument)
{
// Fire an event to notify the client a refresh has been requested.
if (Refresh != null)
{
Refresh(this, EventArgs.Empty);
}
}
public string GetCallbackResult()
{
// Prepare the next response that will be sent back to the page.
EnsureChildControls();
using (StreamWriter sw = new StreamWriter())
{
using (HtmlTextWriter writer = new HtmlTextWriter(sw))
{
writer.Write(this.ClientID + "_");
this.RenderContents(writer);
}
return sw.ToString();
}
}
}
这段客户端脚本代码找到页面上的面板后用新的 HTML 替换它的内容:
<script type='text/javascript'>
function RefreshPanel(result, context)
{
if (result != '')
{
var separator = result.indexOf('_');
var elementName = result.substr(0, separator);
var panel = document.getElementById(elementName);
panel.innerHTML = result.substr(separator+1);
}
}
</script>
不过,不必在使用面板的每个页面上硬编码这段脚本,这是没必要的。可以在 DynamicPanel.OnInit() 方法中通过编程注册它:
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
string script =
@"<script type='text/javascript'>
function RefreshPanel(result, context)
{
if (result != '')
{
var separator = result.indexOf('_');
var elementName = result.substr(0, separator);
var panel = document.getElementById(elementName);
panel.innerHTML = result.substr(separator+1);
}
}
</script>";
Page.ClientScript.RegisterClientScriptBlock(this.GetType(),
"RefreshPanel", script);
}
这就完成了 DynamicPanel 的基础。不过,这个例子有一个很大的限制:页面没有办法触发回天并刷新面板。也就是说,要由你的代码获取回调引用并把它插入页面。
幸运的是,可以创建另一个和 DynamicPanel 一起使用的控件来简化这一过程。例如,可以创建一个 DynamicPanelRefreshLink,它在被单击时自动触发关联面板的刷新。
实现这个解决方案的第一步是重构 DynamicPanel 并实现 ICallbackContainer 接口。这个接口能让 DynamicPanel 提供回调引用,而不再强调用户浏览页面。为了实现 ICallbackContainer,你要提供返回引用的 GetCallbackScript() 方法。Panel 会依赖页面,确保指定其自身为回调目标,而作为客户端脚本的 RefreshPanel() 将会处理响应。
public string GetCallbackScript(IButtonControl buttonControl, string argument)
{
return Page.ClientScript.GetCallbackEventReference(
this,"","RefreshPanel","null",true);
}
2. DynamicPanelRefreshLink
现在准备实现更简单的刷新按钮。可以设置一个 PanelID 属性指定它要关联的面板,最后在呈现自身时,使用 FindControl() 方法找到关联的 DynamicPanel 控件并把回调脚本引用添加到 onclick 特性上:
public class DynamicPanelRefreshLink: LinkButton
{
public string PanelID
{
get { return (string)ViewState["PanelID"]; }
set { ViewState["PanelID"] = value; }
}
protected override void AddAttributesToRender(HtmlTextWriter writer)
{
DynamicPanel pnl = Page.FindControl(PanelID) as DynamicPanel;
if (pnl!=null)
{
writer.AddAttribute("onclick", pnl.GetCallbackScript(this, ""));
}
base.AddAttributesToRender(writer);
}
}
3. 客户端页面
创建一个简单的文本页面,添加上面的2个控件,设置 DynamicPanelRefreshLink.PanelID 属性;然后往面板中添加一些内容和控件,最后为 DynamicPanel.Refresh 事件添加一个处理程序,使用这个程序修改面板中控件的内容或格式。
运行这个页面你会发现现在不必回传页面即可刷新面板。