65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET - UpdatePanelProgressExtender

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.69/5 (8投票s)

2009年7月20日

CPOL

14分钟阅读

viewsIcon

67820

downloadIcon

3665

一个ASP.NET扩展控件,用于在UpdatePanel控件更新时,在UpdatePanel上显示基于模板的进度消息覆盖层。这是一种很好的Web 2.0方式,让用户知道他们的长时间请求正在处理中。

UpdatePanel正在更新,并且在此过程中使用UpdatePanelProcessExtender控件临时被阻止的屏幕截图

引言

上周,我正在为我的一位客户的软件平台开发一个信用卡支付模块。正如大多数开发者可能知道的那样,授权信用卡支付可能需要一些时间,比如10到15秒。由于我不想让客户等待一个看似根本没有加载的页面,我决定需要一些方法来清楚地向客户表明他们的请求正在处理中,同时阻止他们因为不耐烦而反复点击按钮。

简而言之,我需要一种控件,可以在UpdatePanel更新时显示一个漂亮的“Web 2.0动画加载图标”,同时锁定UI的相关部分。由于我没有找到任何现成的方法令人满意,所以我创建了UpdatePanelProcessExtender控件。UpdatePanelProcessExtender控件在UpdatePanel更新时会阻止它,并在UpdatePanel上显示一个基于模板的消息覆盖层。与我在网上找到的大多数解决方案相反,UpdatePanelProcessExtender控件仅在它绑定的UpdatePanel更新时起作用,而不仅仅是任何UpdatePanel更新时。

在本文中,我将首先解释如何使用UpdatePanelProcessExtender控件,以便那些想立即使用UpdatePanelProcessExtender控件的人。在操作指南之后,我将讨论UpdatePanelProcessExtender控件的工作原理以及我在开发UpdatePanelProcessExtender控件时遇到的一些问题。

使用UpdatePanelProcessExtender控件

依赖项和要求

简而言之

  • .NET Framework 2.0 或兼容版本 (3.0, 3.5)
  • 已启用ASP.NET AJAX的网站/应用程序
  • ASP.NET AJAX Control Toolkit 版本 20229
  • jQuery(嵌入在程序集中)
  • jQuery blockUI 插件(嵌入在程序集中)
  • Visual Studio 2005 Express C# 用于构建(免费)
  • 已在FireFox 3.5、Internet Explorer 7 - 8和Google Chrome 1.x中测试并运行

UpdatePanelProcessExtender控件是ASP.NET AJAX UpdatePanel控件的一个扩展控件,它依赖于ASP.NET AJAX Control Toolkit、jQuery和jQuery blockUI插件。jQuery和jQuery blockUI插件已嵌入在程序集中,因此您不一定需要自己包含它们。

由于UpdatePanelProcessExtender控件扩展了UpdatePanel控件,因此您只能在已启用AJAX的网站/应用程序中使用UpdatePanelProcessExtender控件。此外,UpdatePanelProcessExtender控件依赖于ASP.NET AJAX Control Toolkit。我假设大多数开发已启用AJAX的ASP.NET网站/应用程序的开发人员已经在使用ASP.NET AJAX Control Toolkit。UpdatePanelProcessExtender控件的预编译版本依赖于ASP.NET AJAX Control Toolkit的20229版本,这是与.NET Framework 2.0兼容的最后一个版本。

使用代码

为了快速使用UpdatePanelProcessExtender控件,请按照以下步骤操作。对于有经验的开发人员,您可以像使用任何其他控件扩展器一样使用UpdatePanelProcessExtender控件。

  1. 下载UpdatePanelProcessExtender控件的预编译二进制文件,并将其放入ASP.NET网站/应用程序的/Bin文件夹中。这等同于添加对UpdatePanelProcessExtender控件预编译二进制文件的引用。
  2. 在您要使用它的ASP.NET页面上注册程序集。为此,请在页面顶部,在<%@ Page %>指令下方添加以下代码,如下所示:
  3. <%@ Page Language="C#" AutoEventWireup="true" 
        CodeFile="Default.aspx.cs" Inherits="_Default" 
        Title="UpdatePanelProgressExtender examples" %>
    
    <%@ Register Assembly="UpdatePanelProgress" 
      Namespace="Vereyon.Web.UI" TagPrefix="cc1" %>
  4. 假设您已经在网页上放置了一个UpdatPanel控件,请将UpdatePanel包裹在一个<div>元素中,并将UpdatePanel的内容包裹在一个<div>元素中,如下所示。此步骤并非严格必需,但我认为这是控制UpdatePanel大小的最佳方法。外部包装<div>元素是一个指定边框和边距的好地方。确保内部包装<div>元素使您的UpdatePanel精确地拉伸到您想要的大小。如果您需要,内部包装<div>元素是指定内边距的好地方。
  5. <div class="yourStyle">
        <asp:UpdatePanel ID="yourUpdatePanelId" 
                 runat="server" UpdateMode="Conditional">
            <ContentTemplate>
                <div class="yourContentStyle">
                    <!-- Your content -->
                </div>
            </ContentTemplate>
            <Triggers>
                <!-- Your triggers -->
            </Triggers>
        </asp:UpdatePanel>
    </div>
  6. UpdatePanel控件下方添加以下代码,以便放置UpdatePanelProgressExtender控件。请确保将UpdatePanelProgressExtender控件的TargetControlId属性设置为您要它作用的UpdatePanel的ID。
  7. <cc1:UpdatePanelProgressExtender ID="UpdatePanelProgressExtender2" 
              runat="server" Mode="Panel" 
              TargetControlID="UpdatePanel2" CssClass="progressMessage">
        <ProgressTemplate>
            <img src="Images/ajax-loader.gif" alt="Loading" />
            <br />
            Please wait while your request is being processed...
        </ProgressTemplate>
    </cc1:UpdatePanelProgressExtender>
  8. 根据需要修改<ProgressTemplate>模板内容。上面的代码是文章中包含的示例网站的代码。当UpdatePanel正在更新时,<ProgressTemplate>模板的内容将被渲染在UpdatePanel之上。
  9. (可选)在您的ScriptManager控件下方添加一个脚本块,以便重置jQuery blockUI样式表,如下所示:
  10. <script type="text/javascript">
    //<![CDATA[
    $.blockUI.defaults.css = {}; 
    //]]>
    </script>
  11. (可选)将以下CSS声明添加到您的样式表中,以样式化覆盖层:
  12. div.blockMsg {
        width:  60%;
        top:    30%;
        left:   20%;
        text-align: center;
        background-color: #fff;
        border: 3px solid #aaa;
        padding: 0;
        color: #0000;
    }
  13. 现在,您应该发现您的UpdatePanel在更新时会被很好地阻止,并显示您定义的消息。

有关更详细且可运行的示例,请下载本文顶部提供的示例网站,该网站应已完全准备好运行。

包含的示例和源代码

包含的示例网站显示了两个带有基本描述和更新按钮的UpdatePanel。请注意,只有实际正在更新的UpdatePanel会被阻止,而不是像使用UpdateProgress控件那样阻止所有UpdatePanel

源代码已完全文档化,服务器端代码应该易于理解。客户端JavaScript代码可能对初学者来说有点令人望而生畏,因为它使用了委托和一些“半技巧”来使代码正常工作。然而,JavaScript代码也已文档化,因此应该相当容易理解。

开发

现在我将讨论我在过去两天开发UpdatePanelProcessExtender控件时遇到的一些问题和挑战。

设计目标

  • 找到一种(相当)健壮且可重用的方法来通知用户他们的请求正在处理中,同时防止他们在变得不耐烦时“反复点击按钮”。
  • 仅阻止实际正在更新的UpdatePanel。这与标准的ASP.NET UpdateProgress控件等其他实现相反。
  • 保持尽可能多的设计自由度,允许自定义消息。

服务器端代码

UpdatePanelProgressExtender控件是一个非常基础的扩展控件,服务器端没有真正的魔力。只是将一些属性推送到客户端JavaScript类。<ProgressMessage>模板内容在初始化时在子控件中实例化,并在一个具有控件ClientID<div>元素中自动隐藏。

protected override void Render(HtmlTextWriter writer)
{
    // Render the progress message container div with it's properties.
    writer.WriteBeginTag("div");
    writer.WriteAttribute("id", ClientID);
    writer.WriteAttribute("class", CssClass);
    writer.WriteAttribute("style", "display: none;");
    writer.Write(HtmlTextWriter.TagRightChar);

    // Render the contents of this UpdatePanelProgressExtender
    // (i.e. the template instance).
    base.Render(writer);

    writer.WriteEndTag("div");
}

检测哪个UpdatePanel正在更新

由于我明确只想阻止实际正在更新的UpdatePanel,所以我必须找到一种方法来检测导致UpdatePanelProgressExtender实例正在处理的UpdatePanel发生AJAX回发的UpdatePanel。这个问题可能看起来有点模糊,要真正理解它,我们首先需要查看所有其他“UpdatePanel进度通知器”以及此实现的基本检查UpdatePanel“事件”的方法。在客户端组件初始化时,将执行以下JavaScript代码以监听实际上是AJAX Web请求的操作:

// Create event handlers
this._onBeginRequestHandler = Function.createDelegate(this, this._onBeginRequest);
this._onEndRequestHandler = Function.createDelegate(this, this._onEndRequest);
this._onUnblockHandler = Function.createDelegate(this, this._onUnblockElement);

// Bind event handlers to the PageRequestManager events.
Sys.WebForms.PageRequestManager.getInstance().add_beginRequest(this._onBeginRequestHandler);
Sys.WebForms.PageRequestManager.getInstance().add_endRequest(this._onEndRequestHandler);

这段代码的问题在于,this._onBeginRequestthis._onEndRequest方法会为每一个AJAX调用调用。这是因为只有一个PageRequestManager实例,所有AJAX调用都通过它进行。此时,有人可能会说这并没有什么真正的问题,因为必须有一种方法可以让UpdatePanel自己跟踪谁在发起请求,谁没有。

答案是肯定的,但Microsoft并没有公开这些属性。而且,虽然每个JavaScript程序员都应该知道,JavaScript并没有真正意义上的属性保护级别。对非官方API进行编程是有风险的,因为它们可能会被更改。因此,需要在使用之前测试API是否存在,以防止UpdatePanelProgressExtender控件将来失效。如果需要,回滚到更简单但正确的行为也很重要。我以以下方式实现的:

_onBeginRequest : function(sender, args) {

    // HACK: Attempt to determine if this request originated
    // from the update panel this blocker is bound to.
    // If not possible, just block the panel.
    if(sender._postBackSettings && sender._postBackSettings.panelID) {
        if(this._containsControl(this._element.id, sender._postBackSettings.panelID)) {
            this._bPanelKnown = true;
            this._block();
        }
    } else {
        this._bPanelKnown = false;
        this._block();
    }
},

如上面的代码所示,sender对象包含调用AJAX调用的UpdatePanel的ID。然而,代码首先确保sender._postBackSettings.panelID属性确实可用,如果不可用,则会优雅地降低其功能。

现在我们知道了调用AJAX调用的UpdatePanel的ID,我们必须检查这个ID是否与UpdatePanelProgressExtender控件实例所关联的UpdatPanel ID匹配。出于某种未知原因,sender._postBackSettings.panelID属性不包含UpdatePanel的纯DOM元素ID,而是包含一个具有不同名称分隔符的ID列表。无论如何,_containsControl()函数解决了这个问题。我将不解释这段代码,因为它相当直接。

_containsControl : function(controlId, controlList) {

    var aControls, el, elId;

    // Check if the specified element id (control id in asp.net)
    // is present in the passed list.
    // This is part of the hack for detecting which
    // update panel is updating. For some unknown
    // reason, the control id's in the control list use
    // the $-character as a name spacer, while
    // the control id's are rendered with an underscore
    // as name spacer. We thus need to convert
    // this using an expression...
    aControls = controlList.split("|");
    for(var i = 0; i < aControls.length; i++) {
        elId = aControls[i].replace(/\$/g, "_");
        if(elId == controlId)
            return true;
    }

    return false;
}

此时,UpdatePanelProgressExtender控件知道它绑定的UpdatePanel是否正在更新,并可以相应地采取行动。

防止进度消息丢失

当我最初开发UpdatePanelProcessExtender控件时,进度消息在Internet Explorer中可以完美地工作一次,然后就会丢失;也就是说,会显示一个空的进度消息。然而,在Firefox中,一切都运行得很好。现在,在大家开始抨击Internet Explorer之前,这个行为的原因实际上非常合乎逻辑,而且我老实说不知道基于DOM规范,人们应该期待哪种行为。

问题及其原因

首先,在弄清楚正确的UpdatePanel是否正在更新之后,就直接调用jQuery blockUI插件来阻止UpdatePanel<div>元素。jQuery blockUI插件基本上会将包含进度消息(即UpdatePanelProgressExtender控件的<ProgressTemplate>模板内容)的<div>元素从DOM中分离出来,同时将其在DOM中的位置和一个<div>元素的句柄保存起来。然后,jQuery blockUI插件将进度消息<div>元素放入UpdatePanel<div>元素中,并适当调整其大小以覆盖UpdatePanel <div>元素的内容。

问题发生在UpdatePanel从服务器接收响应时。我假设UpdatePanel只是用从服务器接收到的内容替换其<div>元素的innerHTML属性,或者类似的操作。此时,Internet Explorer似乎决定实际销毁UpdatePanel <div>元素中的所有内容,包括我们的进度消息。因此,DOM元素丢失了,或者至少是它的内容。然而,Firefox决定保持DOM元素完好无损,只是丢弃了它在UpdatePanel <div>元素中的图形表示和DOM位置。直到之后, PageRequestManager.beginRequest才被调用,这是客户端UpdatePanelProgressExtender控件正在监听的。然后,jQuery blockUI插件尝试解除对UpdatePanel的阻止,并将消息<div>元素恢复到其在DOM中的原始位置。在Firefox中,它完全成功,但在Internet Explorer中,它只恢复了一个空的<div>元素。

解决方案

防止消息<div>元素丢失的解决方案在于将其置于UpdatePanel内部之外的原则——正如我们在下面_block()函数中的JavaScript代码中所确定的那样,UpdatePanel的内部会被销毁:

// Wrap the update panel to block in an extra
// div container in order to maintain IE compatibility.
// This is required because in IE (don't known why
// not in FireFox) all content of the update panel 
// are lost when it updates, thus the jQuery blockUI
// extension is unable to restore the progress message for reuse.
this._elementParent = this._element.parentNode;
this._elementContainer = document.createElement("div");

this._elementParent.insertBefore(this._elementContainer, this._element);
this._elementParent.removeChild(this._element);
this._elementContainer.appendChild(this._element);

$(this._elementContainer).block(blockParams);

这段代码会动态地将UpdatePanel<div>元素包装在另一个<div>元素中,允许jQuery blockUI插件将其进度消息放置在新创建的<div>元素中,同时覆盖UpdatePanel <div>元素。由于进度消息<div>元素现在不再放置在UpdatePanel <div>元素内部,因此UpdatePanel可以在不丢失进度消息<div>元素的情况下更新其内容,从而避免在Internet Explorer中丢失。

当然,这种操作会颠倒过来,以在UpdatePanel使用下面的JavaScript代码在_onUnblockElement()函数中被解除阻止时规范DOM:

// Revert the operation performed in _block().
this._elementContainer.removeChild(this._element);
this._elementParent.insertBefore(this._element, this._elementContainer);
this._elementParent.removeChild(this._elementContainer);

控制UpdatePanel的大小

在回顾操作指南时,有些人可能会问:“为什么这个家伙要我把UpdatePanel包装在div中,并将其内容包装在另一个div中?!”嗯,猜猜怎么着,我将解释这一切。让我们先看看本文包含的示例网站中的第一个UpdatePanel是如何呈现的:

<h1>Element blocking example</h1>
<div class="updatePanel">
    <div id="UpdatePanel1">
        <div class="updatePanelContent">
            <br />
            The UpdatePanelProgressExtender bound to this UpdatePanel only blocks the content
            of this UpdatePanel when it is updating.<br />
            <br />
            <input type="submit" name="updateButton1" 
              value="Update panel" id="updateButton1" /><br />
            <br />
            Last update: 18-7-2009 16:34:03
        </div>
    </div>
</div>

内部包装<div>元素

我将首先处理内部包装<div>元素,即包裹UpdatePanel内部UpdatePanel内容的那个元素,因为它的功能相当简单。内部包装<div>元素实际上是可选的,但应该存在以将您的UpatePanel拉伸到所需的大小。这是必需的,因为UpdatePanel本身被渲染为一个<div>元素,如上面的代码所示。然而,不可能对UpdatePanel <div>元素应用任何样式。消息覆盖层的大小正好是覆盖UpdatePanel <div>元素。因此,内部包装<div>元素负责将UpdatePanel <div>元素拉伸到正确的大小。

外部包装<div>元素

外部包装<div>元素指定了UpdatePanel的实际大小。内部包装<div>元素负责将UpdatePanel完全拉伸以适应外部包装<div>元素。外部包装<div>元素也是指定控件边框(例如,使用CSS)的好地方。

理由

此时,这个内部包装<div>元素可能看起来是一个非常粗糙的解决方法,但我找不到更好的方法来实现这一点,除非专门扩展UpdatePanel控件以与UpdatePanelProgressExtender控件配合使用。

我认为,在保持完全控制的同时扩展UpdatePanel控件本身就有些繁琐。在ASP.NET页面中,可以非常容易地以任何您喜欢的方式修改包装<div>元素。此外,在我使用UpdatePanelProgressExtender控件的方式中,每个UpdatePanel都将被一个控制其大小的<div>元素包装,我几乎可以肯定,在几乎所有实际使用场景中都会是这样。如果UpdatePanel的大小是完全弹性的,那么当然可以自由地省略包装<div>元素。

局限性和已知问题

像几乎所有代码一样,如果不是全部的话,UpdatePanelProgressExtender控件代码并不完美。在这里,我将讨论一些局限性和已知问题,您在使用UpdatePanelProgressExtender控件的实际环境中可能需要或不需要注意。

ASP.NET AJAX Control Toolkit 依赖项

如前所述,UpdatePanelProgressExtender控件依赖于ASP.NET AJAX Control Toolkit。虽然我认为不太可能有人在不利用ASP.NET AJAX Control Toolkit的情况下开发已启用ASP.NET AJAX的网站,但可能需要移除此依赖项。为了摆脱对ASP.NET AJAX Control Toolkit的依赖,请在UpdatePanelProgressExtender类中实现System.Web.UI.IExtenderControl接口,这并不难。我没有这样做,因为我无论如何都在使用ASP.NET AJAX Control Toolkit,而且UpdatePanelProgressExtender控件实际上是一个大型控件库的一部分,该库以各种方式在很大程度上依赖于ASP.NET AJAX Control Toolkit。

嵌入的jQuery脚本

我已将jQuery和jQuery blockUI脚本嵌入到程序集中。虽然这有利于轻松部署,并防止在没有控件使用时加载脚本,但在更多控件依赖jQuery的环境中可能会很麻烦。我嵌入jQuery脚本的原因与ASP.NET AJAX Control Toolkit依赖项的原因大致相同。但是,您可能会发现通过从UpdatePanelProgressExtender类声明中删除以下代码来剥离嵌入式脚本很有用。别忘了您将不得不以某种方式加载jQuery脚本。

[RequiredScript(typeof(jQueryScripts))]

嵌套的UpdatePanels

我没有测试过UpdatePanelProgressExtender控件是否能与嵌套的UpatePanel一起工作。DOM元素引用很可能会丢失,所以我预计它不会起作用。为了让UpdatePanelProgressExtender控件与嵌套的UpdatePanel一起工作,人们很可能需要修改客户端代码。

附录

虽然我认为我已经开发了一个相当可行的解决方案来满足我大部分的设计目标,但代码至少有些“hacky”。然而,我认为这完全是可以避免的。在我看来,这对于JavaScript和AJAX开发来说一直是个问题。虽然jQuery等JavaScript/AJAX框架试图缓解这些问题,但一旦您想做一些更复杂的事情,事情就会很快变得棘手。

请随时评论。这是我第一次在CodeProject上发表文章,所以我可能没有把所有东西都写得正确和清晰。由于CodeProject上有一些非常、我说是非常棒的文章,它们在帮助我学习随机知识和解决特定问题方面都发挥了作用,所以我觉得把一些东西回馈回来会很好。当然,我将非常感谢任何提示和评论。

链接和鸣谢

历史

  • 2009/07/18 - 版本 1.0 - 初始发布。
  • 2009/07/19 - 版本 1.1 - 修复了一些拼写错误和包装div的说明。
  • 2009/08/24 - 版本 1.2 - 修复了使用UpdatePanel外部触发器时的错误。
© . All rights reserved.