Hello World
Spiga

Tip:在使用AjaxControlTookit的控件时响应事件

2007-07-11 22:31 by 老赵, 8980 visits

背景

在《分清ASP.NET AJAX中的Extender和Behavior模型》一文中,我谈到了使用AjaxControlTookit中控件的关键是客户端的各Behavior组件。微软官方推出的示例都太过于重视演示效果,而忽略了实际使用中的问题——市场需要吧,要让技术看上去吸引人,这么做无可厚非。但是作为一个实际开发产品的程序员,如果要灵活地使用AjaxControlTookit中的组件,则必须了解该如何在客户端操作各Behvaior对象的各个成员(属性、方法和事件等)。

在《分》文中,我举了一个操作ModalPopupBehavir对象的例子,这样显示和隐藏模态窗口的功能就局限于在服务器端ModalPopupExtender中指定的控件了。但是在这篇文章中,我想着重讲述一下在客户端操作那些组件事件的一些方法和技巧。因为,服务器端那些Extender对于客户端Behavior组件的事件支持并没有我们期望得那么良好。

Extender组件对于Behavior属性的支持

为什么AjaxControlToolkit中的控件使用起来那么方便?因为我们有了那么多的Extender。有了丰富的Extender控件,开发人员就可以在服务器端进行编程——例如设置各种属性,最终Extender会在页面中写入一些脚本用于创建客户端的Behavior对象。例如如果我们要使用ModalPopupExtender,最简单的情况下我们只需要在Extender中指定TargetControlID和PopupControlID就可以了,如下:

<asp:Button ID="Button1" runat="server" Text="Button" />
<asp:Panel ID="Panel1" runat="server" Height="50px" Width="125px">
    Hello World!
</asp:Panel>

<ajaxToolkit:ModalPopupExtender runat="server" TargetControlID="Button1" PopupControlID="Panel1" />

一行多余的代码页不要写,Extender会自动在页面上引入所需的脚本文件,并生成如下的客户端代码(格式有所改变):

Sys.Application.add_init(function()
{
    $create(
        AjaxControlToolkit.ModalPopupBehavior,
        {
            "PopupControlID":"Panel1",
            "dynamicServicePath" : "/Default.aspx",
            "id":"ctl02"
        },
        null,
        null,
        $get("Button1"));
});

于是,在客户端的init阶段就会创建客户端的对象(init阶段在window.onload事件执行之后才会开始,因此如果页面上的资源需要大量时间才能完成加载的话,用户会发现Behavior的效果会很晚才出现——不过我们先不关心这个)。一般来说,页面上有多少个Extender,客户端就会出现多少个类似的语句。在$create方法的第二个参数是一个在页面上用JSON表示的对象,这是需要设置的Behavior对象的属性集合。例如在上面的例子中,ModalPopupBehavior对象的PopupControlID属性会被设置为"Panel"。

有过AjaxControlToolkit中控件开发经验的朋友一定知道,会被输出到客户端作为Behavior属性的那些Extender的属性都被标记了ExtenderControlPropertyAttribute,例如在ModalPopupExtender中就有如下的成员定义:

[ExtenderControlProperty]
public string PopupControlID
{
    get { ... }
    set { ... }
}

这样,AjaxControlToolkit中定义的基础类库就会把这个属性的值作为客户端Behavior的值,并将其输出至客户端——控件开发人员和控件使用者双方皆大欢喜。

有人说:我要响应事件

客户端的Behavior除了属性之外还有各种事件——例如CalendarBehavior的shown事件,它会在日历被打开之后触发。利用事件我们就可以在合适的时候执行特定的代码,获得丰富的功能。在服务器端标记属性然后输出至客户端早已众人皆知,那么我们可以在服务器端指定客户端的事件吗?

以前不行,不过现在已经可以了。在上一个版本AjaxControlToolkit中新增了和属性差不多的支持功能。现在AjaxControlToolkit中包含了一个ExtenderControlEventAttribute类,它的作用就是在服务器端标记某个属性,使它与某个客户端的事件对应。这样我们就可以在服务器端设置客户端某个事件的响应方法,我们只需要保证那个方法存在并且逻辑正确即可。

例如,我们这次使用CaledarExtender作为示例:

<asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
<ajaxToolkit:CalendarExtender runat="server" TargetControlID="TextBox1"
    OnClientShown="onShownHandler"/>

<textarea id="TraceConsole" cols="50" rows="10"></textarea>

<script language="javascript" type="text/javascript">
    function onShownHandler(sender, args)
    {
        Sys.Debug.trace("Calendar shown.");
    }
</script>

目前我在服务器端的CalendarExtender中指定了OnClientShown属性,于是在页面上就出现了如下的JavaScript代码:

Sys.Application.add_init(function()
{
    $create(
        AjaxControlToolkit.CalendarBehavior,
        {"id":"ctl02"}, 
        {"shown":onShownHandler}, 
        null, 
        $get("TextBox1"));
});

$create方法的第三个参数是响应事件方法的集合,其中onShownHandler被作为shown事件的响应方法。因此,在每次Calendar被展开之后onShownHandler方法都会被执行,于是在页面上的TextArea中就会显示出“Calendar shown.”的字样。

服务器端OnClientShown属性和客户端shown事件的映射关系是由CalendarExtender中以下代码决定的:

[DefaultValue("")]
[ExtenderControlEvent]
[ClientPropertyName("shown")]
public virtual string OnClientShown
{
    get { ... }
    set { ... }
}

ExtenderControlEventAttribute标记表明该属性将会映射到客户端Behavior的某个事件,而ClientPropertyNameAttribute标记则会接受一个参数,表明具体是哪个事件(如果不标记的话,则使用和属性同名的事件)。

一切就是这么简单……

……只可惜并非每个Extender都那么友好

例如,HoverMenuExtender。话说HoverMenuBehavior也有事件,例如showing和shown。可惜的是,由于ExtenderControlEventAttribute类和它的功能出现的比较晚,早期版本中出现的控件都没有这个功能。可能在新发布的AjaxControlToolkit中会慢慢地为它们补充这些功能,不过现在还是要响应Behavior的事件啊,是不?

在《分》文中,我演示了如何在客户端操作单个ModalPopupBehavior,虽然没有使用事件,但是我们既然可以操作那个Behavior对象了,为它添加一个Event Handler难道还不简单吗?不过似乎有朋友结合基于模板反复的控件(例如Repeater)与AjaxControlTookit中控件的时遇到了阻碍——这下子出现了多个Behavior对象,又该如何设置每一个的BehaviorID,又该如何操作每一个Behavior对象呢?要知道,Repeater在页面上最终会反复多少次可能也无法轻易得知(或使用)啊。

例如,我们结合Repeater控件和HoverMenuExtender来使用:

<asp:Repeater ID="Repeater1" runat="server">
    <ItemTemplate>
        <a id="link" runat="server">Hover on me!</a>
        <asp:Panel runat="server" ID="tooltip" style="display:none;
                border:solid 1px black; padding:5px; background-color:Yellow; width:100px">
            Loading...
        </asp:Panel>
        <ajaxToolkit:HoverMenuExtender runat="server" ID="hoverMenu"
            TargetControlID="link" PopupControlID="tooltip" PopupPosition="Right" />
    </ItemTemplate>
    <SeparatorTemplate>
        <hr />
    </SeparatorTemplate>
</asp:Repeater>

根据上面的代码,当鼠标指向每个<a />元素时,将会出现一个浮动的标签,其中显示Loading字样。 但是如果我们要在每个区域显示出来之后访问一个WebService方法,并将得到的结果显示在浮动的区域内,又该怎么做呢?很显然,我们必须响应每个HoverMenuBehavior对象的shown事件。于是我们可以这样做:

<script language="javascript" type="text/javascript">
    function onShownHandler(sender, args)
    {
        HoverMenuService.GetContent(onSuccess, null, sender.get_popupElement());
    }

    function onSuccess(result, tooltip)
    {
        tooltip.innerHTML = result;
    }
</script>

<asp:Repeater ID="Repeater1" runat="server">
    <ItemTemplate>
        <a id="link" runat="server">Hover on me!</a>
        <asp:Panel runat="server" ID="tooltip"
	        style="display:none; border:solid 1px black; padding:5px; background-color:Yellow; width:100px;">
            Loading...
        </asp:Panel>
        <ajaxToolkit:HoverMenuExtender runat="server" ID="hoverMenu"
            TargetControlID="link" PopupControlID="tooltip" PopupPosition="Right" />
        
        <script language="javascript" type="text/javascript">
            Sys.Application.add_load(function(sender, args)
            {
                if (args.get_isPartialLoad()) return;
                var behaviorId = '<%# (Container.FindControl("hoverMenu") as HoverMenuExtender).BehaviorID %>';
                $find(behaviorId).add_shown(onShownHandler);
            })
        </script>
    </ItemTemplate>
    <SeparatorTemplate>
        <hr />
    </SeparatorTemplate>
</asp:Repeater>

我这里使用的是相对最容易的做法。我们既然可以在页面上反复DOM元素,自然也可以反复脚本代码。在ItemTemplate中的脚本代码很容易,其关键在于通过每一项的Container来找到其中的HoverMenuExtender,获得其BehaviorID,接着通过BehaviorID找到HoverMenuBehavior对象,并为它们响应shown事件(值得注意的是,添加事件响应方法是在load阶段中进行,因为那些Behavior对象是在init阶段中创建的)。每当鼠标滑过某个<a />元素时,其对应的HoverMenuBehavior对象的shown事件将被触发,onShownHandler方法会被执行,而其第一个参数sender即为当前触发事件的HoverMenuBehavior对象。于是我们调用HoverMenuService的GetContent方法用于获得填充内容。

还是看看最后在页面上生成的代码。有时候对于开发人员来说,直接阅读代码会更容易理解一些东西:

<a id="Repeater1_ctl00_link">Hover on me!</a>
<div id="Repeater1_ctl00_tooltip" style="...">
    Loading...
</div>
<script language="javascript" type="text/javascript">
    Sys.Application.add_load(function(sender, args)
    {
        if (args.get_isPartialLoad()) return;
        var behaviorId = 'Repeater1_ctl00_hoverMenu';
        $find(behaviorId).add_shown(onShownHandler);
    })
</script>

<hr />

<a id="Repeater1_ctl02_link">Hover on me!</a>
<div id="Repeater1_ctl02_tooltip" style="...">
    Loading...
</div>
<script language="javascript" type="text/javascript">
    Sys.Application.add_load(function(sender, args)
    {
        if (args.get_isPartialLoad()) return;
        var behaviorId = 'Repeater1_ctl02_hoverMenu';
        $find(behaviorId).add_shown(onShownHandler);
    })
</script>
...

不知是从AjaxControlToolkit的哪个版本开始,如果不设置Extender的BehaviorID,就会把Extender控件的ClientID作为BehaviorID了。根据Microsoft AJAX Library创建Component的规则,如果Component在创建时不提供ID,那么就不会在Sys.Application的某个集合中保持那个Component对象的引用,也就是说我们无法通过$find方法来获得它了。如果我们不需要在客户端操作那个Component对象,这并没有任何影响。而如果真的需要操作时,也只需在Extender中将BehaviorID设为特定的值即可。目前自动分配BehaviorID的做法,我估计就是为了解决类似Repeater这样的控件中使用Extender的情况,因为在这种情况下,我们不能在模板中指定Extender的BehaviorID,因为这样就会在客户端创建相同ID的Component——这是不允许做法。当然,我们还是有解决方法的,例如我们可以响应Repeater的ItemDataBound事件,然后在事件每次被触发时为当前Extender的BehaviorID设置唯一的值即可。

结论

AjaxControlTookit中控件的关键在于客户端的Behavior对象,Behavior对象大都有丰富的属性,方法和事件。在目前的版本中,为某些Behavior添加Event Handler并非一件很容易的事情。相信今后的AjaxControlToolkit版本中将会为一些较早的Extender补充对于客户端事件的支持,让我们拭目以待吧,只是眼下我们还需要使用一些技巧来实现功能了。不过,我们完全可以自己修改AjaxControlToolkit的代码,又何必等到官方的发布呢?

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

18 条回复

  1. Clingingboy
    *.*.*.*
    链接

    Clingingboy 2007-07-11 23:27:00

    有时感觉这种开发复杂化.就是为了封装.我还喜欢基于prototype的.简单.

    比如拖拉效果ResizableControl.页面上1-2个还可以了.但我同时还需要移动的效果.最大的问题在于,我页面上有100个以上的对象需要拖拉.这个东西就派不上用场了.只能做个参考了.

    而且这么个拖拉效果控件的Behavior对象代码量居然有将近600行(虽然有很多注释).我们是否应该思考下,寻找更简单的方式。想偷懒下。

  2. 老赵
    admin
    链接

    老赵 2007-07-12 00:13:00

    @Clingingboy
    怎么说呢,使用时还是需要优化的,microsoft ajax library搭的架子有些大。不过ajaxcontroltookit也提供了release版本,优化了脚本文件的体积。

  3. e1931_music[未注册用户]
    *.*.*.*
    链接

    e1931_music[未注册用户] 2007-07-12 22:18:00

    老兄 只能做个参考了 呵呵

  4. Cat Chen
    *.*.*.*
    链接

    Cat Chen 2007-07-13 12:34:00

    其实我也觉得Control Toolkit应该在前面加上Demo这个字眼,呵呵……因为demo的性质要远大于实用性。那么多的Extender,其实很多只能作为参考,最终还是需要按照自己的需求来定制开发Extender。

  5. 老赵
    admin
    链接

    老赵 2007-07-13 13:47:00

    @Cat Chen
    其实一部分够灵活,一部分就很一般了。

  6. 怪怪[未注册用户]
    *.*.*.*
    链接

    怪怪[未注册用户] 2007-07-14 23:30:00

    ModalPopup这个控件在使用上也有些问题, 比如他出现的方位这块设计的就不是特别好. 另外BehaiverBase这个基类(鸡肋?)也没啥意思. 这块我给彻底重写了. 首先抹掉BehaiverBase鸡肋,还有类层次里面所有涉及和服务器打交到的部分. 因为对另一部分应用, 那点功能根本用不上, 白白影响带宽和执行效率. 另外面向对象设计的重要一条原则是, 组合比继承好; 更何况对于职责来说, 跟服务器打交道的部分, 不应该在一个界面行为的超类中实现. 第二, 在一般性Popup中把方位换成了上中下, 左中右, 内外, 几个设定共同决定的方式, 这样使用更灵活而且代码反而更短. 然后ModalPopup继承Popup, 不过这个我在考虑是不是也拆散成一个MaskBehaiver和一个PopupBehaiver, 这样Mask还可以用在别处并且和其它的类组合, 而不用每个需要类似效果的单有自己的Mask行为代码. 接下来在ModalPopup上通过继承出来的子类组合那些负责数据传输或从其它JS对象获取数据职责的不同的类, 形成用于不同目的的ModalPopup. 如果不需要Mask, 则那些搞数据的类可以结合进和Popup继承下来的子类. 其实是不是非得用子类结合也可以商榷, 子类中组合数据获取和显示, 在Popup/ModalPopup的这个例子中, 对于我来说, 有一个子类相当于一个快捷方式, 如果用于特殊的而不是广泛的目的, 子类都不是必要的. 微软的程序员还是难改框架设计者的思维, 哪怕在这些小玩意儿上.

    至于服务器端, 我这种方式由于JS类多(而不是大而全的类), 封装起来得做点无聊的重复性工作. 不过比如你一个界面就是需要这些操作, 使用上那些写在aspx里的控件markup数量也不会更多, 只是种类变多了,有智能提示的开发环境这种牺牲可以忽略不计. 现在没时间, 否则应该统一一下, 做一个专门的代码生成工具解决那些繁文缛节的工作.

    还是做个不可靠的承诺, 这些我重新搞过的类, 有空会都放出来..,这些玩意儿没必要大家写了一遍又一遍的, 还不如共同改进. 等我的工作状态调整到每天能空闲出20%的时间, 老赵看看能不能合作一下, 由我来提供例子,由你来帮我写文章? 文章算咱们共同的成果, 因为看这意思, 我短时间内不可能有空创作了...

  7. 怪怪[未注册用户]
    *.*.*.*
    链接

    怪怪[未注册用户] 2007-07-14 23:36:00

    或者这样, 到年底, Orcas正式出了, 确认不会再框架上有大变化, 也确定哪些东西不需要咱们自己开发了. 然后干脆把AJAX Extension写个开源复刻版, 服务器端从ScriptManager起, 客户端除了核心运行库, 重新组合和优化一遍.

    我有意愿做这个事, 也愿意把我的工作拿出来起头, 不过欠缺一个负责组织的人.

  8. 老赵
    admin
    链接

    老赵 2007-07-14 23:50:00

    @怪怪
    没有问题。不过你说的“开源复刻”版在版权上可以会有问题,按照微软公布的协议似乎不能修改和重新发布?我再去看看。

  9. xpengfee[未注册用户]
    *.*.*.*
    链接

    xpengfee[未注册用户] 2007-07-16 10:19:00

    老赵,正文的字怎么那么小?太费眼了

  10. 怪怪[未注册用户]
    *.*.*.*
    链接

    怪怪[未注册用户] 2007-07-17 19:43:00

    不能修改不至于吧? 重新发布的问题确实没考虑过. 不过我觉得这种工作总得有人做, php/ror那边有大量的人在prototype.js等包的基础上做这种工作, 已经成了一种优势了, 你看国内外最近新建的那些web2.0, 很多JS操作多的网站, 全都是直接引用了别人的JS完事. 如果MS这边有版权的问题, 那干脆改个名. 只要改动够大, 总不能说有些地方你这么写了我就不能这么写.

  11. 老赵
    admin
    链接

    老赵 2007-07-17 21:02:00

    @怪怪
    嗯,要尽量避免有版权纠纷。
    prototype是允许别人修改的,这一点和ASP.NET AJAX不同。

  12. 怪怪[未注册用户]
    *.*.*.*
    链接

    怪怪[未注册用户] 2007-07-17 21:52:00

    所以我的想法是, ASP.NET AJAX客户端Runtime不动, 但是服务器端的一些类, 干脆重新写, 反正Copy他的已有代码片断, 改吧改吧就不犯法了, 咱们论证以后, 肯定能拿出一个更灵活和有效的结构. 至于AJAX Control Toolkit这样的东西, 肯定是整个重新写, 但是它已经写好的, 而且是合理的那些代码, 我估计一样Copy以后改吧改吧也不犯法了就, 否则我自己手写, 也不见得在很多局部就和它不一样啊.

  13. zz[未注册用户]
    *.*.*.*
    链接

    zz[未注册用户] 2007-09-06 13:25:00

    .net

  14. wedding dress[未注册用户]
    *.*.*.*
    链接

    wedding dress[未注册用户] 2007-09-22 09:44:00

    高手就是高手,值得我们学习,收藏!

  15. 老汤[未注册用户]
    *.*.*.*
    链接

    老汤[未注册用户] 2008-01-09 10:04:00

    【我的理想:“让外国人看中国人写的技术书籍和文章”。】理想够高、远。加油啊!!!如果能让外国人用中国的操作系统就是理想状态了。
    在硬件上,不知谁可以发此豪言壮语【我的理想:“让外国人用中国人做的计算机”。】

  16. 破釜沉舟[未注册用户]
    *.*.*.*
    链接

    破釜沉舟[未注册用户] 2008-06-06 11:27:00

    赵老师你好:
    用了modalpopup有一段时间了,总是有这样那样的问题。现在又有了新问题,请赵老师帮忙解决。我想让modalpopup弹出的panel显示它所在的页面的一些数据。能实现吗?在这先谢谢了!

  17. 老赵
    admin
    链接

    老赵 2008-06-06 16:55:00

    --引用--------------------------------------------------
    破釜沉舟: 赵老师你好:
    用了modalpopup有一段时间了,总是有这样那样的问题。现在又有了新问题,请赵老师帮忙解决。我想让modalpopup弹出的panel显示它所在的页面的一些数据。能实现吗?在这先谢谢了!
    --------------------------------------------------------
    既然是JS编程,基本上怎么都是可以的。

  18. 破釜沉舟[未注册用户]
    *.*.*.*
    链接

    破釜沉舟[未注册用户] 2008-06-07 10:44:00

    赵老师:
    谢谢您百忙之中解答我的问题!工作忙多注意身体啊!祝您端午节快乐!

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我