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

异步加载和显示页面内容,并支持完全回发

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (68投票s)

2008年2月20日

Ms-PL

9分钟阅读

viewsIcon

616813

downloadIcon

4332

一个具有更少通信开销和更高性能的 AJAX UpdatePanel。

引言

我对 Acoustic 的文章《用户控件的增量页面显示模式》[^] 非常感兴趣。他构建了一个用于部分内容真正异步加载的解决方案。他的解决方案很好,但有一个缺点:他无法响应回发。因此,控件是静态的。它们只能显示初始内容,用户无法与控件进行交互。我决定编写一个全新的控件,可以支持以下功能:

  • 开发人员甚至无需编写一行 JavaScript。
  • 真正异步加载用户控件(*.ascx)。
  • 用户应该能够与用户控件进行交互。
  • 更新时无需渲染整个页面,就像 AJAX UpdatePanel 所做的那样。为了节省服务器资源,只需渲染用户控件。
  • 应该只有一个小的通信开销。这意味着只有属于用户控件的 viewstate 的一部分才会被传输回服务器。AJAX UpdatePanel 会将整个页面以及所有控件的 viewstate 传输回服务器,这可能是一个很大的开销。
  • 该控件应支持一些小功能,例如每隔 n 毫秒自动更新。

使用场景

您可以使用 PartialUpdatePanel 而不是 AJAX UpdatePanel 的示例场景可能包括:

  • 页面中需要回发支持但不需要整个页面环境信息的独立区域(例如,具有分页功能的数据列表,用户可以浏览新闻、信息流、邮件等)。
  • 当您的控件需要完成长时间操作时,向用户提供反馈。在这种情况下,使用渲染方法为“Clientside”的 PartialUpdatePanel。周围的页面将显示带有等待消息。用户将获得反馈,知道正在进行某项操作,需要等待。

基本概念

整个控件包括服务器端控件、客户端脚本部分和一个 HttpHandler。ASCX 控件的渲染场景在下图中有描述。

  1. ASPX Page 包含一个 PartialUpdatePanel,其 UserControlPath 属性设置为 ASCX 控件。

    <iucon:PartialUpdatePanel runat="server" ID="Panel1"
    
                 UserControlPath="~/PostBackSample.ascx">
        <ErrorTemplate>
            Unable to refresh content
        </ErrorTemplate>
    
        <LoadingTemplate>
            <div style="margin-left: 84px; margin-top: 10px;">
    
              <asp:Image ID="Image1" runat="server"
                         ImageUrl="~/images/loading.gif" />
    
            </div>
            <div style="text-align: center">
    
                Updating...
            </div>
        </LoadingTemplate>
    </iucon:PartialUpdatePanel>
  2. 页面加载时,一些 JavaScript 调用 PartialUpdatePanelHandler。它通过 HTTP-POST 发送用户控件的路径以及其他数据,例如控件的 viewstate

    // create request
    var request = new Sys.Net.WebRequest();
    request.set_url('PartialUpdatePanelLoader.ashx');
    request.set_httpVerb('POST');
    request.set_body(this._createRequestBody(eventTarget, eventArgument));
    request.set_userContext(this);
    request.add_completed(this._loadingComplete);
  3. 用户控件需要一个 Page 对象来进行渲染。这项工作由 PanelHostPage 完成。

    // this code is part of PartialUpdatePanelHandler
    if (context.Request.Form["__USERCONTROLPATH"] != null)
    {
        PanelHostPage page = new PanelHostPage(
           context.Request.Form["__USERCONTROLPATH"],
           context.Request.Form["__CONTROLCLIENTID"]);
    
          ((IHttpHandler)page).ProcessRequest(context);
    
        context.Response.Clear();
          context.Response.Write(page.GetHtmlContent());
    }
  4. 用户控件通常通过将其添加到 PageControls 集合中来渲染。

    protected override void CreateChildControls()
    {
        // Load Control
        if (_controlPath != null)
            _mainForm.Controls.Add(LoadControl(ResolveUrl(_controlPath)));
    
        base.CreateChildControls();
    }
  5. 页面输出被发送回 HttpHandler

  6. 内容被传输到客户端,并通过 DOM 操作插入到活动的 HTML 文档中。

    contentPanel.innerHTML = sender.get_responseData();

就是这样。

视图状态(ViewState)

PartialUpdatePanel 的内容被像整个页面一样处理。因此,它有自己的 viewstate,存储在一个隐藏字段中。隐藏字段被传输到 HttpHandler 并由 PanelHostPage 加载。加载、序列化和反序列化 viewstate 的逻辑在 PanelHostPage 的两个重写方法中完成。

private string _pageViewState;

protected override object LoadPageStateFromPersistenceMedium()
{
    PartialPageStatePersister persister =
            PageStatePersister as PartialPageStatePersister;
    persister.PageState = _pageViewState;
    persister.Load();

    return new Pair(persister.ControlState, persister.ViewState);
}

protected override void SavePageStateToPersistenceMedium(object state)
{
    PartialPageStatePersister pageStatePersister =
            this.PageStatePersister as PartialPageStatePersister;
    if (state is Pair)
    {
        Pair pair = (Pair)state;
        pageStatePersister.ControlState = pair.First;
        pageStatePersister.ViewState = pair.Second;
    }
    else
    {
        pageStatePersister.ViewState = state;
    }
    pageStatePersister.Save();

    _pageViewState = HttpUtility.UrlEncode(pageStatePersister.PageState);
}

自定义 PageStatePersister PartialPageStatePersister 使用 LosFormatter 正确加载和存储 ViewStateControlState

_pageViewState 的内容再次以隐藏输入字段的形式传输回客户端。

事件处理

在 ASP.NET 中,事件处理非常简单。例如,当用户单击按钮时,该按钮的 ClientID 会在 __EVENTTARGET 字段中传回页面。这个行为很容易在我们这里重现。我们所要做的就是为每个按钮添加 OnClientClick 调用,从那里调用一个加载部分内容的方法,并将事件源添加到传递给我们的 HttpHandler__EVENTTARGET 字段中。我们除此之外别无他事,其余的都交给 ASP.NET 事件管道处理。

使用控件

在使用该控件之前,您必须在 web.config 中注册 HttpHandler

<httpHandlers>
  <add verb="*" path="*.ashx" validate="false"

       type="iucon.web.Controls.PartialUpdatePanelHandler"/>
</httpHandlers>

还应在 appSettings 中添加一个值。请将该值更改为您自己的值!

 <appSettings>
    <add key="PartialUpdatePanel.EncryptionKey" value="k39#9sn1"/>
 </appSettings>

然后,只需在页面中添加一个 PartialUpdatePanel 并设置 UserControlPath 属性。在 LoadingTemplateErrorTemplate 中添加一些控件。当更新操作正在进行时,LoadingTemplate 的内容将变得可见。如果在更新过程中发生错误(例如,服务器超时),则显示 ErrorTemplate 的内容。如果将 AutoRefreshInterval 设置为大于 0 的值,面板将每 n 毫秒自动刷新一次。

还有一种机制可以通过 JavaScript 或服务器端代码与该控件进行通信。Parameters 是一个接受键值对的集合。当控件被渲染时,它们通过 HTTP-POST 传输到控件。以下示例显示了如何在 ASPX 页面中提供一些参数,通过 JavaScript 设置值,并在 ASCX 控件中借助 ParameterCollection 类读取它们。

<%-- PartialUpdatePanel with named parameters --%>
<iucon:PartialUpdatePanel runat="server" ID="PartialUpdatePanel4"

                          UserControlPath="~/ParameterSample.ascx">
    <Parameters>
        <iucon:Parameter Name="MyParameter" Value="Hello world" />

        <iucon:Parameter Name="Counter" Value="0" />

    </Parameters>
    <ErrorTemplate>

        Unable to refresh content
    </ErrorTemplate>

</iucon:PartialUpdatePanel>
<%-- Change the value of the parameter "Counter" and refresh the panel --%>

<script type="text/javascript">
    var counter = 0;

    function updateParameterSample()
    {
        $find("PartialUpdatePanel4").get_Parameters()["Counter"] = ++counter;
        $find("PartialUpdatePanel4").refresh();
    }

</script>

<input type="button" onclick="updateParameterSample(); return false;"

       value="Click to update panel with counter" />

ASCX 的代码隐藏从 ParameterCollection 读取参数值。

public partial class ParameterSample : System.Web.UI.UserControl
{
    protected void Page_Load(object sender, EventArgs e)
    {
        iucon.web.Controls.ParameterCollection parameters =
                  new iucon.web.Controls.ParameterCollection.Instance;

        Label1.Text = "Called " + parameters["Counter"] + " times";
    }
}

您也可以更改参数的值。它们将被传输回来并通过 JavaScript 访问。

InitialRenderBehaviour 属性控制控件是在正常页面渲染期间由服务器渲染(InitialRenderBehaviour.Serverside)。如果此属性设置为 InitialRenderBehaviour.Clientside,则当页面已传输到浏览器时,客户端会向服务器发送请求以渲染控件。这对于用户来说很有用,如果页面的一部分需要更长的时间来渲染,但用户可以先看到其他部分。他无需等待整个页面,而是可以在其他部分加载时与某些部分进行交互。InitialRenderBehaviour.None 会导致控件在通过 JScript 调用 $find("PartialUpdatePanel4").refresh(); 请求之前不进行渲染,如以上示例之一所示。在这种情况下,<InitialTemplate> 的内容会显示,直到发生刷新调用。

<iucon:PartialUpdatePanel runat="server" ID="PartialUpdatePanel1"
       UserControlPath="~/ExternalRefreshSample.ascx"
    InitialRenderBehaviour="None">
    <ErrorTemplate>

        Unable to refresh content
    </ErrorTemplate>
    <LoadingTemplate>
        Updating...
    </LoadingTemplate>
    <InitialTemplate>
        Nothing useful here until refresh() gets called
    </InitialTemplate>

</iucon:PartialUpdatePanel>

如果您想防止 LoadingTemplateContentTemplate 出现闪烁,您可以使用 DisplayLoadingAfter 属性设置一个以毫秒为单位的时间间隔。在超时之前,不会显示加载模板。如果您的 PartialUpdatePanel 刷新速度非常快,这很有用。

通过使用 RenderAfterPanel 属性,您可以按顺序加载内容。如果您的站点使用了例如 10 个 PartialUpdatePanel 并且 InitialRenderBehaviour 设置为 Clientside,您将同时向 Web 服务器发出 10 个请求。这可能会给服务器带来很大的负载。RenderAfterPanel 意味着在另一个加载完成后,当前 PartialUpdatePanel 不会被刷新。因此,内容是逐步加载的,服务器的负载会降低。

您可以从您的 UserControl 动态添加和运行 JavaScript 代码。只要将 addScriptTags 设置为 true,就可以支持 ScriptManager.RegisterStartupScriptScriptManager.RegisterClientScriptBlock 方法。以下代码显示了在托管在 PartialUpdatePanel 中的 UserControl 的按钮被单击时如何显示一个警告框。

protected void Button1_Click(object sender, EventArgs e)
{
    string script = string.Format("alert('{0}');", TextBox1.Text);

    ScriptManager.RegisterStartupScript(this, GetType(), "alert", script, true);
}

JavaScript 代码的内部传输和执行有点棘手。您不能简单地在 HTML 代码中添加一个 <script> 标签。然后,当 HTML 代码更新您文档的 DOM 树时,<script> 节点不会被执行。在内部,PartialUpdatePanel 将所有 JavaScript 片段传输到一个隐藏的 div 中。该 divScriptRenderer 类创建。更新文档中新渲染内容的 PartialUpdatePanel JavaScript 代码读取 div,使用 Sys._ScriptLoader.getInstance() 来执行传输的 JavaScript,最后,通过 DOM 操作从文档中删除 div

属性

EncryptUserControlPath 使用 DES 算法和 web.config 中定义的值加密您的 UserControl 的路径。
此属性默认设置为 true
注意:如果设置了 EncryptUserControlPath,则无法通过 JavaScript 更改 UserControlPath
UserControlPath 您的 UserControl 的虚拟路径。
AutoRefreshInterval 强制面板每 n 毫秒刷新一次。
DisplayLoadingAfter 在一个成功加载后渲染当前 UserControl
参数 本文中已描述 ;-)
InitialRenderBehaviour 设置初始渲染模式为 Serverside、Clientside 或 None。

AJAX 控件工具包支持

让 AJAX 控件工具包中的一些控件与 PartialUpdatePanel 一起工作是一项非常艰巨的任务。主要问题在于控件在运行时通过调用 $create()Sys.Application.add_init 中实例化自身。

Sys.Application.add_init(function() {
    $create(AjaxControlToolkit.AutoCompleteBehavior, {...}, null, null, $get(
        "PartialUpdatePanel7_myTextBox"));
);

现在,当部分回发完成时,相同的代码会再次运行,并且 $create 调用 Sys.Application.addComponent 函数会失败,因为组件已经被实例化了。

为了解决这个问题,我不得不进行一些非常粗糙的修改。在执行部分更新响应中的客户端脚本之前,我将 Sys.Application.addComponent 的函数引用更改为一个本地函数,该函数会检查组件是否已经被实例化。

_addComponent : function(component) {
    var id = component.get_id();
    if (typeof(Sys.Application._components[id]) === 'undefined')
        Sys.Application._components[id] = component;
}

这可以防止 addComponent 失败并抛出异常。当所有脚本成功运行后,原始函数指针会被恢复。

最后的话

如果您使用此控件,如果您能将您的项目 URL 发送给我,我将非常高兴,这样我就可以看到该控件的实际应用。还有……请不要忘记为这篇文章投票。

历史与更新

2010 年 9 月 13 日

1.8 版

  • .NET 4.0 现在可用。
2008 年 11 月 14 日

1.6 版

特别感谢 CodePlex 的 grown 为 PartialUpdatePanel 所做的出色工作!
  • 添加了用户控件路径的加密支持。
  • 添加了对自定义 ScriptManager 类型的支持(包括 ToolkitScriptManager)。
  • 添加了对 ToolkitScriptManager.CombineScriptsHandlerUrl 的支持。
  • 在运行时通过 JavaScript 更改 UserControlPath
  • 在往返期间在服务器端操作参数。
  • 修复了使用验证器和在 Clientside 模式下渲染的问题。
  • 修复了重新创建组件的错误。
2008 年 7 月 9 日

版本 1.5.2

  • 修复了支持 RadioButtons 状态的错误。
  • 添加了对全球化的支持。
  • 显示了扩展的异常信息。
2008 年 6 月 10 日

版本 1.5

  • 添加了对 ASP.NET AJAX 控件工具包中一些控件的支持。
  • 添加了对 ScriptManager.RegisterDataItem 的支持。
  • 添加了对 ScriptManager.RegisterClientScriptInclude 的支持。
  • 名为 ToolkitSample 的新示例展示了 PartialUpdatePanel 中的一些工具包控件。
2008 年 5 月 20 日

版本 1.4

  • 通过自定义 PageStatePersisterControlState 现在可以在回发之间正确传输。
  • 新属性 DisplayLoadingAfter
  • 新属性 RenderAfterPanel
  • 次要 bug 修复
2008 年 3 月 29 日

版本 1.3

  • 添加了对 ScriptManager.RegisterStartupScript 的支持,以便在部分回发后运行 JScript 代码。
  • 参数不再通过 HTTP-GET 传输,而是通过 HTTP-POST 传输。
  • 输出错误修复:在使用服务器端 InitialRenderMode 的控件时,<form> 标签被渲染了多次。
2008 年 2 月 28 日

版本 1.2.1

  • 次要 bug 修复:最初由服务器端渲染的控件的 Viewstate 在正常回发时没有被正确处理。
2008 年 2 月 27 日

版本 1.2

  • PartialUpdatePanel 添加了 InitialRenderBehaviour 属性。
  • PartialUpdatePanelHandler 添加了 IRequiresSessionState(感谢 aliascodeproject)。
  • PanelHostPage 中对 viewstate 进行 UrlEncode(感谢 Acoustic)。
2008 年 2 月 20 日 首次发布

该项目托管在 CodePlex [^] 上。那里的更新间隔更短。因此,如果您对此项目感兴趣,应定期访问该网站。

已知问题

ModalPopupExtender 仅在 FireFox 中运行良好。Internet Explorer 有一些我尚未解决的问题。因此,如果您想从 PartialUpdatePanel 中显示模态弹出窗口,请在父页面中创建 ModalPopupExtender。要显示对话框,请在 PartialUpdatePanel UserControl 的代码隐藏中调用 ScriptManager.RegisterStartupScript

示例

<script type="text/javascript">
function showModalPopupViaClient() {
    var modalPopupBehavior = $find('programmaticModalPopupBehavior');
    modalPopupBehavior.show();
}

function hideModalPopupViaClient() {
    var modalPopupBehavior = $find('programmaticModalPopupBehavior');
    modalPopupBehavior.hide();
}
</script>

<asp:Button runat="server" ID="hiddenTargetControlForModalPopup" style="display:none"/>

<ajaxToolkit:ModalPopupExtender runat="server" ID="programmaticModalPopup"
    BehaviorID="programmaticModalPopupBehavior"
    TargetControlID="hiddenTargetControlForModalPopup"
    PopupControlID="programmaticPopup"
    BackgroundCssClass="modalBackground"

    DropShadow="True"
    PopupDragHandleControlID="programmaticPopupDragHandle"
    RepositionMode="RepositionOnWindowScroll" >
</ajaxToolkit:ModalPopupExtender>
<asp:Panel runat="server" CssClass="modalPopup" ID="programmaticPopup"

    style="display:none;width:350px;padding:10px">
<asp:Panel runat="Server" ID="programmaticPopupDragHandle"
    Style="cursor: move;background-color:#DDDDDD;border:solid 1px Gray;
    color:Black;text-align:center;">
    ModalPopup shown and hidden in code


</asp:Panel>

<iucon:PartialUpdatePanel runat="server" ID="PartialUpdatePanel4"
    UserControlPath="~/ToolkitSample.ascx" />

You can now use this sample to see how to use ModalPopup with an invisible TargetControl.
The ModalPopupExtender
this popup is attached to has a hidden target control. The popup is hidden
<asp:LinkButton runat="server" ID="hideModalPopupViaServer" Text="on the server side
    in code behind" OnClick="hideModalPopupViaServer_Click" /> and

<a id="hideModalPopupViaClientButton" href="#">on the client in script</a>.

<br />
</asp:Panel>

您的 PartialUpdatePanel UserControl 中的代码隐藏将如下所示:

public partial class ToolkitSample : System.Web.UI.UserControl
{
    protected void Page_Load(object sender, EventArgs e)
    {
        ScriptManager.RegisterStartupScript(this, GetType(), "showPopup",
            "showModalPopupViaClient()", true);
    }
}

这将执行 showPopup,然后在部分 PostBack 后显示模态弹出窗口。

© . All rights reserved.