领先技术:使用 ASP.NET 部分呈现功能进行 AJAX 编程 By Dino Esposito

领先技术
使用 ASP.NET 部分呈现功能进行 AJAX 编程
Dino Esposito

代码下载位置: CuttingEdge2008_08.exe (470 KB)
在线浏览代码
AJAX 的核心是 XMLHttpRequest 对象。AJAX 提供的用户体验机会取决于此对象在多个平台的大量浏览器上的可用性。自 2004 年以来已发生了很多事情,那一年,组件供应商首次开始展示 AJAX 应用程序,但在核心方面,如果不能使用 XMLHttpRequest 对象执行带外调用,就不可能有 AJAX。
随着 AJAX 应用程序复杂性的提高,开发人员逐渐意识到要通过一种经济有效的方式构建新一代 Web 应用程序,只靠简单的脚本驱动的带外调用是不够的。因此,对功能更加强大的工具集的需求不断增长,开发人员希望这些工具能够将 AJAX 功能添加到页面和应用程序,同时使用与传统的 ASP.NET 相同的开发模式。因此,简言之,要构建 AJAX 站点,开发人员不仅需要能够简单地调用 XMLHttpRequest 对象来提取数据,能够手动制作 JavaScript 功能来操作文档对象模型 (DOM),还需要一些其他功能。
在本月的专栏中,我将介绍一种针对利用 ASP.NET 部分呈现引擎的 AJAX 的实用方法。稍后您将看到,AJAX 需要在应用程序性能和开发人员效率之间寻求平衡。实际的 AJAX 站点并不完全是使用部分呈现或 XMLHttpRequest 对象的手动脚本构建的,它们需要的是功能强大的混合技术,这些技术通常可以通过自定义控件很好地合成。

带有菜单的页面
AJAX 的主要功用是最小化要重新加载的完整页面数目。部分呈现和手动脚本驱动调用都允许您提取服务器端数据,并避免整页刷新。对于许多内容类型而言,后端服务和 DOM 操作可能已经足够了。但如果您必须支持导航,会怎样呢?
传统的超链接破坏了 AJAX 的神奇功能,它会告知浏览器请求另一个 URL。结果,当前页面被冻结,直到下载完新的 HTML 块。当新数据到达时,该页面关闭,然后完全重绘浏览器的客户端。
位于母版页上的顶级链接可向用户指示站点的不同区域,这些链接可能确实会像传统超链接一样实现。在这种情况下,重新加载整个页面可能是可以接受的,但具体取决于用户的期望。
每次构建菜单时,都需要选择如何处理用户单击。您可以为每个菜单项分配一个 URL,也可以只是回发到同一页面。从 AJAX 角度看,您是在选择关闭当前页面、加载全新页面还是从服务器异步加载某些新内容。
请看以下代码片段,它显示了 ASP.NET 菜单控件的片段:
<asp:Menu runat="server" ID="Menu1">
<asp:MenuItem Text="Products" Value="Products">
    <asp:MenuItem Text="By price" NavigateUrl="..." /> 
    <asp:MenuItem Text="All" Value="Products-All" /> 
</asp:MenuItem>
...
</asp:Menu> 
“产品”菜单项的第一个 MenuItem 子元素起到的是 NavigateUrl 属性的作用。此属性可获取或设置单击菜单项时 URL 导航到的目标位置。第二个 MenuItem 元素起到的是 Value 属性的作用。Value 属性用于存储有关菜单项的其他数据,将被传递给 MenuItem 的回发事件。同一级别的每个菜单项都必须拥有唯一的 Value 属性值。当未指定显式导航 URL 时,单击菜单项会导致传统的回发。在服务器上,您只需处理 MenuItemClick 事件和 Value 属性的内容。
显然,如果您选择使用 Value 属性的方法,就可以使用 AJAX 技术并避免整页重新载入。此类型页面的整体模型是一个单页界面。
单页界面减少了重新加载的数量并消除了闪烁,因此可减轻用户界面的负担。然而,单页界面也意味着您的应用程序使用的 URL 区别很小,从而使来自搜索引擎的支持减弱。单页界面也意味着开发团队需要编写的页面减少,但页面内容更加丰富。此类方法还可以减少团队内部的并行操作。

导航 URL
当菜单将用户引入全新页面时,实际上您不需要使用“菜单”控件来实现解决方案。理论上,有超链接列表就已经足够了。虽然届时会有许多 DHTML 和 AJAX 菜单框架可用,但到最后,它们的动画、图形和提供的预定义外观会不同。从功能上讲,这些菜单只是超链接的集合,这些超链接由浏览器通过非 AJAX 方式本地处理。
通常,总页数多达数百的网站可能是针对少数入口页设置的,这些入口页随后会将用户引入不同的子网站。因此,主页只需要指向这些子网站的链接。在这种情况下,就需要导航,而不必非得用 AJAX。但是如果需要将内容拉至当前显示的页面,又该如何呢?您要如何组织此内容才能最大程度地提高应用程序的性能和团队的工作效率?

异步回发
上个月,我演示了如何使用 Windows ® Communication Foundation (WCF) 服务将原始数据或 HTML 返回到客户端。两种解决方案都各有优缺点。发送原始数据可优化带宽,但需要您使用 JavaScript 在此客户端上实现一些 DOM 操作逻辑。发送服务器生成的 HTML 会增加移动的数据量,但它允许您在服务器上维护大多数呈现逻辑。
这两种方法均属于所谓的“真正的”AJAX 解决方案类别,在此类别中基于双层模型 — Web 浏览器和服务层明确设计了应用程序。涉及用户界面的任何状态和逻辑均由客户端维护并控制;不需要任何视图状态或回发。
若要返回只读标记,则服务器生成的 HTML 最有用。如果您需要返回静态数据网格,或返回包含所选客户或发票的一些相关信息的面板,它将非常适合。如果您需要打开一个充满控件的交互面板,并且这些控件会触发事件并需要处理程序,它的吸引力就小得多了。在纯 AJAX 方法中,显示的标记(在客户端生成或在服务器上生成)必须包含 JavaScript 函数调用。
上个月,我使用服务实现了 HTML 消息模式和浏览器端模板模式。但您可能已经注意到,我的示例并非真正具有交互效果。事实证明这两种方法适用于部分情况,而非全部。我的示例服务返回了股票报价,但最终网格无法分页。例如,要支持分页,我必须插入指向 JavaScript 功能的超链接,然后确保下载了引用的 JavaScript。

菜单和部分呈现
图 1 显示了带有顶级菜单的示例页,允许用户在应用程序功能集中导航。此菜单已使用 ASP.NET 菜单控件创建,如 图 2 所示。您可以看到,所有菜单项都没有指定 NavigateUrl 属性,这个属性会将每个菜单项指向一个物理 URL,这就可能会指向一个完全不同的页面。指定 Value 属性后,每当用户选择任一菜单项后,都会发生回发,并且 MenuItemClick 事件将被引发至该页面:
void Menu1_MenuItemClick(object sender, MenuEventArgs e)
{
    LoadContent(e.Item.Value);
}
图 1 带有顶级菜单的 ASP.NET AJAX 页(单击图像可查看大图)
MenuEventArgs 类的功能类似于 Item 属性,表示单击的菜单项。MenuItem 对象的 Value 属性会通知事件处理程序有关单击项的标识的信息。通过使用本地 LoadContent 功能,此页可以动态加载所需内容。但是回发如何实现?
ASP.NET 菜单控件完全支持部分呈现。要放弃回发并顺利加载新内容,您只需设置 ScriptManager 控件并插入一个 UpdatePanel 控件,如下所示:
<asp:UpdatePanel runat="server" ID="UpdatePanel1" 
          UpdateMode="Conditional">
    <ContentTemplate>    
       ...
    </ContentTemplate>
    <Triggers>
        <asp:AsyncPostBackTrigger ControlID="Menu1" 
            EventName="MenuItemClick" />
    </Triggers>
</asp:UpdatePanel>
可更新的内容将绑定到菜单的 MenuItemClick。通过 UpdateMode 属性,面板也会进行配置以适应特定于控件的更新。这意味着,仅当用户单击菜单项时,才会刷新可更新的区域。
在大多数的 Web 应用程序中,您都是使用菜单将用户引向站点或页面的不同功能区域。使用不同的 ASPX 页面实现特定功能是非常有帮助的,原因有很多。它可以保持代码的清晰,并且更易于维护和测试。但更重要的是,它允许您将不同功能的实现分配给团队中的不同开发人员。这就可以并行执行一些开发任务,从而提高生产率。
单页界面是典型的 AJAX 模式,它建议您在每次触发事件时都会重排用户界面的应用程序中,创建一个主页。单页界面模型非常适用于最小化回发,但当面对团队的并行开发任务时,就需要谨慎一些。您应该将其视为基于插件概念的一个页面体系结构。换句话说,页面会使用一个约定的接口定义占位符,借助此接口可快速轻松地插入动态加载的组件。

提供占位符
在 ASP.NET 中,实现此面向页面体系结构的插件的最简单方法,就是使用 PlaceHolder 控件和 ASCX 用户控件定义按需组件。定义可更新区域的代码如下:
<asp:UpdatePanel runat="server" ID="UpdatePanel1" 
    UpdateMode="Conditional">
    <ContentTemplate>    
        <asp:PlaceHolder runat="server" ID="PlaceHolder1"
            EnableViewState="false" />
    </ContentTemplate>
    <Triggers>
        <asp:AsyncPostBackTrigger ControlID="Menu1" 
            EventName="MenuItemClick" />
    </Triggers>
</asp:UpdatePanel>
您将每个菜单项与一个 ASCX 用户控件关联,并在每次单击项目时将其加载到占位符中。 图 3 显示了上述 LoadContent 函数的源代码,您从 MenuItemClick 事件处理程序中调用该函数。
Page 类上的 LoadControl 方法采用 ASP.NET 用户控件的 URL,并将它作为由 UserControl 派生的类的实例加载到内存中。如果控件加载正确,此方法会将其添加到 PlaceHolder 的 Controls 集合。请注意,ASP.NET PlaceHolder 控件不输出任何标记,因此不必担心使用它会扰乱您的用户界面标记。
在包含动态和交互内容的 AJAX 站点上下文中使用 ASCX 用户控件不会减少您同时开发多个内容块的机会 — ASCX 控件是一种 ASP.NET 页,可以独立于站点的其余部分开发。

动态加载的控件
在 ASP.NET 中,需要稍微注意一下动态加载的控件,因为它们的行为会跨过回发。假设您创建了一个包含可分页客户网格的控件,然后将这个用户控件加载到占位符中(请参见 图 4)。在您单击底部的一个超链接导航到新页面之前,一切工作正常。单击后,此页将部分刷新,但是用户控件的全部内容都会消失。为什么呢?
图 4 动态加载到页面的交互式控件(单击图像可查看大图)
在 ASP.NET 中,每个页面请求都被视为独立于此前或此后的任何其他请求。因此要创建该页面类的全新实例,为每个传入请求服务。此新页面包含在 ASPX 源代码中静态引用的所有服务器控件的全新实例。
但动态添加的控件会怎样呢?只有页面中的代码使用一些永久机制跟踪动态控件时,页面才会知道它们。页面视图状态是此类型信息的优良存储媒体。在 图 3 中,我将 TrackUserControl 属性添加到了页面类,以提醒当前显示的是哪个用户控件。
在 Page_Load 事件中,您使用 TrackUserControl 属性的内容指明必须手动重新加载哪个用户控件,才能完全还原页面中上次包含的控件集合:
void Page_Load(object sender, EventArgs e)
{
    if (!IsPostingFromMenu() && IsPostBack)
        ReloadContent();
}
注意,回发的原因可能是用户触发了显示的用户控件中的某个操作,也可能是用户单击了其他菜单项。Page_Load 中的 if 语句可确定用户是否从菜单回发。如果是,则不会引发任何操作,因为必须加载一些新内容;否则,将重新加载跟踪的用户控件,以便其处理回发事件:
void ReloadContent()
{ 
    UserControl uc = null;
    try
    {
        uc = this.LoadControl(TrackedUserControl) as UserControl;
    }
    catch
    {
    }
    if (uc != null)
        Placeholder1.Controls.Add(uc);
}
动态添加的用户控件的视图状态会如何呢?已经向其传递 HTML 元素的数据又会如何呢?当为用户提供页面时,静态添加的控件和动态添加的控件之间没什么差异。当提交表单时,用户输入的任何内容(例如,输入字段中的文本)都将自动打包到 HTTP 请求。因此,在服务器上,动态加载的控件使用的任何信息都是可用的。然而,您应该知道,当触发页面的 Init 事件时,此类控件尚不可用。这是因为页面的控件树只使用静态引用的控件填充。
直到该页面的作者向树中添加任何动态引用的控件。不过,页面作者可能需要从永久存储媒体中读取有关所添加控件的信息。如果此信息存储在会话或缓存中,则可以通过 Init 事件处理程序安全地访问它。如果该信息存储在视图状态袋中,就必须等待 Load 事件。事实上,在 ASP.NET 页面生命周期中,视图状态是在 Init 和 Load 事件之间进行解压缩和处理的。
在此阶段,还会检查已发布的所有数据并将其映射到现有控件。但是专门用于动态添加的控件的数据又如何呢?基本上,已发布的数据是在 Page__Load 事件之前或之后处理的。在第一轮中未找到任何匹配控件的数据将缓存,并在 Load 事件之后再次处理。
在 Load 事件中,开发人员应该检查上次动态添加的是哪些控件,并在页面控件树中重新加载它们。恰好足够,Controls 集合的 Add 方法可使用在视图状态下找到的控件的任何数据来自动更新该控件的状态。

哪个控件进行了回发?
从外部资源加载其部分内容的页面需要了解引发回发的原因。在 图 4 所示的示例代码中,页面会回发,原因可能是用户从菜单中选择了给定元素,也可能是用户在使用该用户界面上的其他组件时触发了回发。
在 ASP.NET 中,没有哪个属性能够告诉您是哪个控件导致的回发。不过,发布控件的 ID 却不难找到。如果用户通过单击“提交”按钮进行回发,则该控件的 ID 将在 HTTP 请求中列出。如果在 HTTP 请求的主体中不存在任何按钮控件,则用户将通过与“链接”按钮或自动回发控件交互来进行回发。
无论如何,回发源的 ID 都位于 __EVENTTARGET 隐藏字段中。在 ASP.NET AJAX 中,当使用部分呈现时,事情就会简化,因为 ScriptManager 控件发布了一个自定义属性 AsyncPostBackSourceElementID。以下代码是生成 图 4 的代码的摘录,它展示了如何确定用户是单击菜单项还是通过另一个控件回发到服务器的:
bool IsPostingFromMenu()
{
    ScriptManager sm = ScriptManager.GetCurrent(this);
    string ctlID = sm.AsyncPostBackSourceElementID;
    Control c = this.FindControl(ctlID);
    if (c == null)
        return false;

    return (c.ID == "Menu1");
}
当用户重复单击菜单时,可能会再次加载给定的 ASCX 用户控件。加载用户控件意味着要下载控件标记并走完服务器生命周期。然而,不断重新加载用户控件可能会对性能产生负面影响,因此您可以使用一项 ASP.NET 功能(如输出缓存)来帮助提升页面的整体性能。

缓存用户控件的输出内容
要创建向站点提供某些内容的用户控件,成本可能非常昂贵,因为这需要一个或多个数据库调用并可能需要几百万个 CPU 循环。既然这样,那为什么还要每秒钟多次重新生成同样的用户控件,尤其是在内容并非在不断更改的情况下?
比较好的策略是创建一次用户控件,缓存其输出并为其指定最大期限。一旦用户控件的缓存快照过时,第一个传入请求就会以标准方式再次服务,再次运行控件的代码并缓存生成的标记。
ASP.NET 页面输出缓存功能允许您缓存页面和用户控件响应,这样无需执行代码即可满足后续请求,只要返回缓存的输出即可。要使用户控件可缓存,您可以在 ASCX 的源中声明 @OutputCache 属性,如以下代码所示。此代码片段会将该控件的输出缓存一分钟:
<% @OutputCache Duration="60" VaryByParam="None" %>
输出缓存是一项出色的功能,但是它距离成为万灵丹还很远。它只是一种使应用程序服务于更多页面并使用户控件更为快速的方法。它不一定能够使应用程序效率更高或更具可扩展性。不过,借助输出缓存,您一定可以降低服务器上的工作量,这是因为页面和资源都是缓存的下游项。
另外,输出缓存仅仅是匿名内容的一项可行选择。请求的缓存内容直接由 IIS 提供服务,并且从不将其传递到能够验证调用方身份的 ASP.NET 管道。
最后,回发的页面和用户控件需要一些额外工作。特别是,您需要指示 ASP.NET 保存输出的多个副本,这些副本之间会有一个或多个参数不同。为此,您需要在 @OutputCache 指令中使用 VaryByParam 属性。

侧重实用性
虽然部分呈现经常被认为是廉价的 AJAX,是为那些不能运用所需的能源创建“真正的”AJAX 应用程序的人保留的,但它当时是使用 ASP.NET 的开发人员的最佳选择之一。最后,请记住,AJAX 涉及由多种技术和模式的组合所带来的许多折衷和有效的 AJAX 结果。

请将您想向 Dino 询问的问题和提出的意见发送至 cutting@microsoft.com

Dino Esposito 目前是 IDesign 的架构师,也是《Programming ASP.NET 3.5 Core Reference》 的作者。Dino 定居于意大利,经常在世界各地的业内活动中发表演讲。您可加入他的博客,网址为 weblogs.asp.net/despos
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值