可能你觉得callback很弱,AJAX才够强。其实网上大多数callback的示例代码都是不太正确的(包括MSDN)。这里提供了一种不同的使用callback的方法。只用 很少的javascript 就实现了一个联级下拉框。你会发现: 轻量级的callback其实也很好用。
在这里我有两个DropDownList,ddlCategory和ddlProduct。要求ddlCategory变化后ddlProduct无刷新的填充新的项目。
要使用Callback首先要继承 ICallbackEventHandler 接口:
正式版的 ICallbackEventHandler 要实现以下两个方法:
void RaiseCallbackEvent ( string eventArgument)
eventArgument 现在改为由 RaiseCallbackEvent 接收,而不是由 GetCallbackResult 直接接受了。目的是为了让你可以在 RaiseCallbackEvent 中做一些初始化操作,这点在编写支持callback的控件时特别有用,有兴趣的话你可以参考 GridView 和 DetailView 中 RaiseCallbackEvent 的代码。在这里我只使用最简单的方式,把 eventArgument 存到一个私有成员中:
protected virtual void RaiseCallbackEvent( string eventArgument)
{
this ._callbackEventArgument = eventArgument;
}
在客户端触发callback需要使用到 GetCallbackEventReference ,正式版中的 GetCallbackEventReference 位于 Page.ClientScript 下。 ClientScrip t是2.0中 Page 的一个新增成员,专门用于处理客户端教本(javascript),它是一个实例化的ClientScriptManager。
function CallServer(arg, context)
{
<%= ClientScript.GetCallbackEventReference(this, "arg", "ReceiveServerData", "context")%> ;
}
function ReceiveServerData(result, context)
{
...
}
</ script >
{
ddlCategory.Attributes.Add( " onchange " , " CallServer(....) " );
}
在这里我使用了一个javascript函数 CallServer 来包装callback的触发,当然你也可把它直接挂到onchange或其他客户端事件上。不过用一个函数来包装的话,可以很方便的在callback前后做一些其他操作,下面我就会用到。
不知道你有没有发觉,我们传给callback两个参数: arg 和 context ,但是 RaiseCallbackEvent 只得到一个(arg),另一个参数 context 会给原封不动的传给 ReceiveServerDate 。这个 context 到底有什么用呢?甚至连MSDN里的代码也没有很正确的使用这个参数。可能你觉得callback很弱,只传入一个string(arg)传出一个string(result),还要编写大量的javascript代码才能实现想要的功能。其实,只要正确使用上面那个 context 参数就可以用很少的javascript实现很理想的功能。
首先,我们拆分一下 arg ,把我们要调用的服务端方法放进去:
然后用反射在服务器端调用这个方法(FillProduct):
{
string [] parts = _callbackEventArgument.Split( ' | ' );
return this.GetType().GetMethod(parts[0]).Invoke(this, new objcet[]{parts[1]}) ;
}
我们来看看FillProduct会返回些什么:
{
ddlCategory.SelectedValue = categoryID;
ddlProduct.DataBind();
StringWriter writer1 = new StringWriter(CultureInfo.InvariantCulture);
HtmlTextWriter writer2 = new HtmlTextWriter(writer1);
ddlProduct.RenderControl(writer2);
writer2.Flush();
writer2.Close();
return writer1.ToString();
}
你可以看到,我把需要更新的ddlProduct整个重新Render后传回来了,也就是说要用新生成的ddlProduct的HTML替换原来的ddlProduct的HTML。怎么做到这一点呢? context 参数要出马了:
<script type="text/javascript">
function CallServer(arg, context)
{
context.innerHTML = "Loading";
<%= ClientScript.GetCallbackEventReference(this, "arg", "ReceiveServerData", "context")%>;
}
function ReceiveServerData(result, context)
{
context.innerHTML = result;
}
</script>
.....
<asp:DropDownList ID="ddlCategory" runat="server" DataSourceID="SqlDataSource1"
DataTextField="CategoryName" DataValueField="CategoryID" AppendDataBoundItems="True" >
<asp:ListItem Value="">- Select Category -</asp:ListItem>
</asp:DropDownList>
<span id="_span1">
<asp:DropDownList ID="ddlProduct" runat="server" DataSourceID="SqlDataSource2"
DataTextField="ProductName" AppendDataBoundItems="True">
<asp:ListItem Value="">- Select Product -</asp:ListItem>
</asp:DropDownList>
</span>
{
ddlCategory.Attributes.Add( " onchange " , " CallServer('FillProduct|'+this.value, _span1) " );
}
原来我把要更新的ddlProduct放在_span1里,context就是用来传递这个_span1的。只要用新生成的HTML填充这个_span1,ddlProduct的更新就OK了。这样我们就很轻松的完成了一个无刷新的联级下拉框。
需要注意的是,要在页面提交(postback)后取得这个ddlProduct的值,需要使用
Request.Form[ddlProduct.UniqueID]
因为viewstate并没有被更新。
在下面的完整代码里我还做了如下两件事,以提高代码的复用性:
- 对_callbackEventArgument稍加处理,以便调用任意多个参数的方法(FillProduct只有一个参数)
- 提取了FillProduct中Rander Control的那部分,以便于其他方法也可以使用。(这项操作用VS2005的Refactor很容易就完成了
其实你可以把这两部分整理到一个CallbackHelp类中,这样复用性就更高了。
Enjoy it.
<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
< html xmlns ="http://www.w3.org/1999/xhtml" >
< script type ="text/javascript" >
function CallServer(arg, context)
{
context.innerHTML = " Loading " ;
<%= ClientScript.GetCallbackEventReference( this , " arg " , " ReceiveServerData " , " context " ) %> ;
}
function ReceiveServerData(result, context)
{
context.innerHTML = result;
}
</ script >
< head runat ="server" >
< title > Callback </ title >
</ head >
< body >
< form id ="form1" runat ="server" >
< div >
< asp:SqlDataSource ID ="SqlDataSource1" runat ="server"
ConnectionString ="<%$ ConnectionStrings:NORTHWNDConnectionString1 %>"
SelectCommand ="SELECT [CategoryID], [CategoryName] FROM [Categories]" >
</ asp:SqlDataSource >
< asp:SqlDataSource ID ="SqlDataSource2" runat ="server"
ConnectionString ="<%$ ConnectionStrings:NORTHWNDConnectionString1 %>"
SelectCommand ="SELECT [ProductID], [ProductName] FROM [Products] WHERE ([CategoryID] = @CategoryID)" >
< SelectParameters >
< asp:ControlParameter ControlID ="ddlCategory" Name ="CategoryID"
PropertyName ="SelectedValue" />
</ SelectParameters >
</ asp:SqlDataSource >
< div id ="_div1" runat ="server" >
< asp:DropDownList ID ="ddlCategory" runat ="server"
DataSourceID ="SqlDataSource1" DataTextField ="CategoryName"
DataValueField ="CategoryID" AppendDataBoundItems ="True" >
< asp:ListItem Value ="" > - Select Category - </ asp:ListItem >
</ asp:DropDownList >
< span id ="_span1" >
< asp:DropDownList ID ="ddlProduct" runat ="server"
DataSourceID ="SqlDataSource2" DataTextField ="ProductName"
AppendDataBoundItems ="True" >
< asp:ListItem Value ="" > - Select Product - </ asp:ListItem >
</ asp:DropDownList >
</ span >< span id ="_span2" >
< asp:Button ID ="btnBuy" runat ="server" Text ="Buy" Enabled ="false" OnClick ="btnBuy_Click" />
< br />
</ span >
</ div >
< asp:Label ID ="Label1" runat ="server" ></ asp:Label >
</ div >
</ form >
</ body >
</ html >
using System.IO;
using System.Collections;
using System.Globalization;
using System.Reflection;
using System.Web;
using System.Web.UI;
public partial class Callback : Page, ICallbackEventHandler
{
private string _callbackEventArgument;
protected void Page_Load(object sender, EventArgs e)
{
ddlCategory.Attributes.Add("onchange", "CallServer('FillProduct|'+this.value,_span1)");
ddlProduct.Attributes.Add("onchange", "CallServer('ShowBuy|'+this.value,_span2)");
}
#region ICallbackEventHandler Members
public string GetCallbackResult()
{
string[] parts = _callbackEventArgument.Split('|');
object[] args = null;
string result = "";
if (parts.Length > 1)
{
args = new object[parts.Length - 1];
Array.Copy(parts, 1, args, 0, args.Length);
}
MethodInfo method = this.GetType().GetMethod(parts[0]);
if (method != null)
{
result = (string)method.Invoke(this, args);
}
return result;
}
public void RaiseCallbackEvent(string eventArgument)
{
_eventArgument = eventArgument;
}
void ICallbackEventHandler.RaiseCallbackEvent(string eventArgument)
{
this.RaiseCallbackEvent(eventArgument);
}
string ICallbackEventHandler.GetCallbackResult()
{
return this.GetCallbackResult();
}
#endregion
public string FillProduct(string categoryID)
{
ddlCategory.SelectedValue = categoryID;
ddlProduct.DataBind();
return RenderControl(ddlProduct);
}
public string ShowBuy(string ProductID)
{
btnBuy.Enabled = !string.IsNullOrEmpty(ProductID);
return RenderControl(btnBuy);
}
protected void btnBuy_Click(object sender, EventArgs e)
{
_div1.Visible = false;
Label1.Text = "Buy: " + Request.Form[ddlProduct.UniqueID];
}
private string RenderControl(Control control)
{
StringWriter writer1 = new StringWriter(CultureInfo.InvariantCulture);
HtmlTextWriter writer2 = new HtmlTextWriter(writer1);
control.RenderControl(writer2);
writer2.Flush();
writer2.Close();
return writer1.ToString();
}
}