BLACK JACK

Get busy living, or get busy dying.
posts - 25, comments - 186, trackbacks - 30, articles - 2
可能你觉得callback很弱,AJAX才够强。其实网上大多数callback的示例代码都是不太正确的(包括MSDN)。这里提供了一种不同的使用callback的方法。只用很少的javascript就实现了一个联级下拉框。你会发现:轻量级的callback其实也很好用。


在这里我有两个DropDownList,ddlCategory和ddlProduct。要求ddlCategory变化后ddlProduct无刷新的填充新的项目。

要使用Callback首先要继承ICallbackEventHandler接口:
public partial class Callback : PageICallbackEventHandler
或:
<%@ Implements Interface="System.Web.UI.ICallbackEventHandler" %>

正式版的ICallbackEventHandler要实现以下两个方法:
string GetCallbackResult ()
void RaiseCallbackEvent (string eventArgument)

eventArgument现在改为由RaiseCallbackEvent接收,而不是由GetCallbackResult直接接受了。目的是为了让你可以在RaiseCallbackEvent中做一些初始化操作,这点在编写支持callback的控件时特别有用,有兴趣的话你可以参考GridViewDetailViewRaiseCallbackEvent的代码。在这里我只使用最简单的方式,把eventArgument存到一个私有成员中:
private string _callbackEventArgument;

protected virtual void RaiseCallbackEvent(string eventArgument)
{
      
this._callbackEventArgument = eventArgument;
}

在客户端触发callback需要使用到GetCallbackEventReference,正式版中的GetCallbackEventReference位于Page.ClientScript下。ClientScript是2.0中Page的一个新增成员,专门用于处理客户端教本(javascript),它是一个实例化的ClientScriptManager。
<script type="text/javascript">
   
function CallServer(arg, context)
   {

        
<%= ClientScript.GetCallbackEventReference(this"arg""ReceiveServerData""context")%>;
    }

    
function ReceiveServerData(result, context)
    {
        ...

    }
</script>

    protected void Page_Load(object sender, EventArgs e)
    {
        ddlCategory.Attributes.Add(
"onchange""CallServer(....)");
    }

在这里我使用了一个javascript函数CallServer来包装callback的触发,当然你也可把它直接挂到onchange或其他客户端事件上。不过用一个函数来包装的话,可以很方便的在callback前后做一些其他操作,下面我就会用到。

不知道你有没有发觉,我们传给callback两个参数:argcontext,但是RaiseCallbackEvent只得到一个(arg),另一个参数context会给原封不动的传给ReceiveServerDate。这个context到底有什么用呢?甚至连MSDN里的代码也没有很正确的使用这个参数。可能你觉得callback很弱,只传入一个string(arg)传出一个string(result),还要编写大量的javascript代码才能实现想要的功能。其实,只要正确使用上面那个context参数就可以用很少的javascript实现很理想的功能。


首先,我们拆分一下arg,把我们要调用的服务端方法放进去:
ddlCategory.Attributes.Add("onchange""CallServer('FillProduct|'+this.value, ...)");

然后用反射在服务器端调用这个方法(FillProduct):
    public string GetCallbackResult()
    {
        
string[] parts = _callbackEventArgument.Split('|');

        return this.GetType().GetMethod(parts[0]).Invoke(this, new objcet[]{parts[1]}) ;
    }

我们来看看FillProduct会返回些什么:
    public string FillProduct(string categoryID)
    {
        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>


    protected void Page_Load(object sender, EventArgs e)
    {
        ddlCategory.Attributes.Add(
"onchange""CallServer('FillProduct|'+this.value, _span1)");
    }


原来我把要更新的ddlProduct放在_span1里,context就是用来传递这个_span1的。只要用新生成的HTML填充这个_span1,ddlProduct的更新就OK了。这样我们就很轻松的完成了一个无刷新的联级下拉框。


需要注意的是,要在页面提交(postback)后取得这个ddlProduct的值,需要使用
Request.Form[ddlProduct.UniqueID]
因为viewstate并没有被更新。
更新:
或者可以把ddlProduct的EnableViewState改为False,然后就可以用ddlProduct.SelectedValue取值了。但是这样做在复杂的页面中可能会引起一些错误,要更具具体情况谨慎处理。



在下面的完整代码里我还做了如下两件事,以提高代码的复用性:

  1. 对_callbackEventArgument稍加处理,以便调用任意多个参数的方法(FillProduct只有一个参数)
  2. 提取了FillProduct中Rander Control的那部分,以便于其他方法也可以使用。(这项操作用VS2005的Refactor很容易就完成了

其实你可以把这两部分整理到一个CallbackHelp类中,这样复用性就更高了。
Enjoy it.

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Callback.aspx.cs" Inherits="Callback" %>

<!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;
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)
    
{
        _callbackEventArgument 
= 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();
    }

}

Feedback

#1楼   回复  引用  查看    

2005-11-27 10:23 by 高海东      
写的好
继续写关于2.0的文章

#2楼   回复  引用    

2005-11-27 12:00 by Web Hosting Review[未注册用户]
强啊!多谢分享。

#3楼   回复  引用  查看    

2005-11-28 08:27 by 一帆(老鼠粮仓之路)      
晕死,怎么写的这样好啊
先收藏啊

#4楼   回复  引用    

2005-11-28 09:30 by pantian
比我的好像还要复杂一些,过几天,我把我使用CallBack的方法也写上来看看。呵呵。

#5楼   回复  引用  查看    

2005-11-29 09:26 by 福星人      
好像意义不是很大,还是ajax方便。

#6楼   回复  引用  查看    

2005-11-29 22:46 by Boler Guo      
终于有一个系统分析这个callback的文章了~

#7楼   回复  引用  查看    

2005-12-02 20:14 by 阿不      
代码里出现了好几个编译时错误。

#8楼[楼主]   回复  引用  查看    

2005-12-02 22:19 by 出走的影子      
@阿不
是有一处错误,我已经修改好了。有些地方是博客园编辑器造成的,比如#region 的地方会多点东西。你自己去掉吧。

#9楼   回复  引用  查看    

2005-12-07 13:41 by 阿不      
是的.不过都是一些简单的错误,我是说如果不用编译都能写出正确率这么高的代码,很不错了.

#10楼   回复  引用    

2005-12-28 00:11 by efan[未注册用户]
感觉上确实不如AJAX方便:)

#11楼   回复  引用    

2006-01-16 12:09 by ajaxtest[未注册用户]
看看这个是否可以,编码方式还是asp.net方式,不过调用方式是ajax的.
http://tz.yndns.com/controldemo/ZeroAjaxDemo/index.htm
可以不用写脚本(自动封装好了)就可以实现这种联动效果了.
感觉方便一些.

#12楼   回复  引用    

2006-02-07 16:04 by iCaca[未注册用户]
貌似ajax对中文的处理不是很好吧

#13楼   回复  引用    

2006-02-28 22:08 by 锦瑟[未注册用户]
这东西重写innerhtml,要丢失ViewState,风险太大……类似这种级联菜单的刷新,还是多写点javascript代码实现保险。

#14楼   回复  引用    

2006-03-02 22:16 by nontrick[未注册用户]
我在有MultiView的页面使用,GetCallBackResult有执行并且返回傎,但是客户端的方法却未被调用,普通页面则正常,哪位高人指点一下,谢谢!还没注册成功,收藏先

#15楼   回复  引用    

2006-03-20 16:29 by wangxj[未注册用户]
不错呀,是这看到的最有实用价值的一个例子了,谢谢楼主。
期待有更好的例子。

#16楼   回复  引用    

2006-05-10 22:34 by wg[未注册用户]
好,为我打开了思路了,谢谢楼主了

#17楼   回复  引用  查看    

2006-11-15 15:32 by MS的明天      
有那么点意思

#18楼   回复  引用  查看    

2007-01-04 09:37 by Nina      
如果是1.0的呢,也要實現這樣的效果,當然更高一點,因為是要求每隔幾秒就刷新數據一次,但是頁面不閃爍。

#19楼   回复  引用    

2007-07-26 16:21 by 王帅[未注册用户]
asp 2.0的 callback就是用了ajax了,大家可以看它的js文件,在服务器端,也是了无新意的。

#20楼   回复  引用    

2007-09-15 15:04 by bolar[未注册用户]
如果把这个联动的两个dropdownlist 写成一个控件,好像就有问题了

#21楼   回复  引用    

2007-10-10 10:44 by iamybj@gmail.com[未注册用户]
关于这个asp.net 2.0的callback,我有一个建议,就是把
protected virtual void RaiseCallbackEvent(string eventArgument)
改为
protected virtual void RaiseCallbackEvent(string arg, string context)
这个context参数最好也传进来,因为可能需要根据context来执行不同的代码,你这里分解eventArgument参数,有点太麻烦,而且不大正规的样子。



发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 285363




相关文章:

相关链接: