`
az7772010
  • 浏览: 201920 次
  • 性别: Icon_minigender_1
  • 来自: 天津
社区版块
存档分类
最新评论

ASP.NET AJAX 4.0

阅读更多
数据驱动的 Web 应用程序的新 AJAX 支持
Bertrand Le Roy

代码下载位置: MSDN Code Gallery (188 KB)
在线浏览代码

本文基于 ASP.NET 的预发布版本撰写而成。文中的所有信息均有可能发生变更。

本文将介绍以下内容:
  • 服务器端数据操作
  • UpdatePanel 和客户端
  • 减少回发和负载
  • 客户端模板呈现
本文使用了以下技术:
ASP.NET AJAX 4.0
AJAX 之所以是一个激动人心的 Web 平台原因有多种。通过使用 AJAX,从前在服务器上执行的许多任务都改为在浏览器中执行,从而使与服务器之间的往返操作更少、带宽消耗更低而且 Web UI 响应更快更及时。尽管这些成果都归功于将大量工作转移到客户端,但对于希望自由支配服务器应用程序的所有功能和灵活性的许多开发人员而言,浏览器仍然不是首选环境。
到目前为止,解决方案使用的都是 UpdatePanel 控件,它允许开发人员在构建 AJAX 应用程序的同时仍保留服务器工具的完整阵列。但是,UpdatePanel 主要是从传统的回发模型派生而来 — 因此,UpdatePanel 请求仍是一个完整的回发。实际上,使用 UpdatePanel 时,整个表单(包括 ViewState)将被发送到服务器,几乎整个页面生命周期都在那里执行,并且呈现过程也在服务器上执行。显然,此方法会抵消迁移到 AJAX 的其中一个主要原因。此处唯一真正的节省是使用了 XmlHttpRequest 而非常规的 HTTP POST 请求,并且只有页面的更新部分和 ViewState 被发回客户端。因此,响应要少得多,但请求并没有减少。
纯 AJAX 方法的性能几乎在各方面都优于 UpdatePanel 方法。在纯 AJAX 解决方案中,呈现操作将在客户端上执行,而服务器仅返回数据(通常要比等效的 HTML 少很多)。此方法还可显著减少网络请求的数量:通过将数据存放在客户端上,将允许更多应用程序的 UI 逻辑在浏览器中运行。
但是,纯 AJAX 方法的主要问题是浏览器缺少将数据转换成 HTML 的工具。它只有以下两个粗糙的现成功能可执行此类操作:innerHTML,它可以用提供的 HTML 字符串替换元素的所有内容;以及速度较慢的“文档对象模型”(DOM) API,它们可对标记和属性进行操作(在抽象级别方面类似于 HtmlTextWriter)。
在本文中,我将展示一个使用典型回发编写的页面的三个迭代,然后使用 UpdatePanel 和纯 AJAX 来展示在服务器上所用的技术其性能在哪些情况下会优于客户端。前两个示例可使用公开提供的 ASP.NET 3.5 SP1 构建,而第三个版本将使用 ASP.NET 4.0 中的一些新客户端功能。

基于回发的主-详细信息页面
我所要构建的页面将显示某个列表中的产品,在选中后,它会在列表右侧的面板中显示该产品的详细描述。我将使用 AdventureWorks 示例数据库,它可从以下网址下载:go.microsoft.com/fwlink/?LinkId=124953。我只会使用 LINQ to SQL 创建一个基本数据层,因为数据层并非本文的重点所在。
首先,将 AdventureWorks .mdf 文件添加到应用程序的 App_Data 文件夹中。然后,添加一个新的 "LINQ to SQL classes" .dbml 文件,并将 Product、ProductPhoto 和 ProductProductPhoto 表和 vProductModelCatalogDesc<wbr>ription 视图从服务器资源管理器拖放到设计图面上。生成的数据层如<strong>图 1</strong> 所示。</wbr>
图 1 数据层体系结构(单击图像可查看大图)
此页面包含两个视图窗格,一个窗格显示产品列表,另一个窗格显示产品的详细信息。图 2 显示了呈现的页面。在对页面的第一个请求中,使用以下代码将产品列表绑定到数据:
复制代码
private void BindProductList() {
    ProductList.DataSource = from p in AdventureWorksContext.Products
                             where p.ProductSubcategoryID == 1 
                             //Mountain bikes
                             orderby p.Name
                             select p;
    ProductList.DataBind();
}
图 2 自行车列表和详细信息(单击图像可查看大图)
列表本身及其模板如图 3 所示。数据库的查询代码可在下载项目中找到,此代码非常简单;它只是在数据库中查询属于 Mountain Bike 类别的产品并将 ListView 控件与其绑定。当然,可使用数据源控件来实现相同的目的,但我发现代码方法更为灵活且更易于预测。如果您不只是设计-查看人员,则得到的结果可能会有所不同。
从数据集创建 HTML 标记的实际工作完全由 ListView 控件来实现,它非常方便而且非常省力。我需要做的全部工作只是在 LayoutTemplate 和 ItemTemplate 属性中为该 HTML 提供模板(参见图 3)。此模板将把产品列表呈现为无序列表中的链接(UL 和 LI 标记)。
ASP.NET<wbr>AJAX<wbr>4.0<wbr>图 3 产品的无序列表</wbr>
复制代码
<asp:ListView ID="ProductList" runat="server"
    DataKeyNames="ProductId"
    OnSelectedIndexChanging="ProductList_SelectedIndexChanging"
    OnSelectedIndexChanged="ProductList_SelectedIndexChanged">
    <LayoutTemplate>
        <ul ID="itemPlaceholderContainer<wbr>" runat="server"&gt;
            &lt;asp:PlaceHolder ID="itemPlaceholder" runat="server" /&gt;
        &lt;/ul&gt;
    &lt;/LayoutTemplate&gt;
    &lt;ItemTemplate&gt;
        &lt;li&gt;&lt;asp:LinkButton runat="server" ID="Select" CommandName="Select" 
                                         Text='&lt;%# eval_r("Name") %&gt;' /&gt;&lt;/li&gt;
    &lt;/ItemTemplate&gt;
&lt;/asp:ListView&gt;
</wbr>
请注意,链接本身并非普通的链接而是 LinkButton 控件,这意味着它们将回发到页面而非导航到其他页面。它们是具有按钮语义的有效链接。当然,在缺少 JavaScript 的情况下,可通过使用合适样式的常规 Button 控件来替代这些 LinkButton 控件,从而轻松提高页面的可访问性。
此处使用的 LinkButton 的关键功能不是将事件处理程序附加到每个按钮,而是将 CommandName 属性设为 "Select"。最终的结果就是在单击时,按钮会从控件树弹出命令,直到被理解它的控件处理为止。此功能非常强大,它允许任意 UI 元素将命令发送到父控件,而且它只需知道所需的命令和参数即可,其他的可以一概不管。正是它使得功能强大的数据控件(如 ListView)能够继续使开发人员对标记进行完全的控制。在我构建相同页面的纯 AJAX 版本时,您会看到它如何转换成类似的浏览器概念。
在这一阶段,我有一个无需编写任何代码即可进行选择的产品列表。通过使用 DataSource 控件和 ControlParameter 将列表的所选数据键值绑定到详细信息视图的所选数据键值,可继续这一无代码方法,但我选择通过代码来实现。接下来,我将处理列表的 SelectedIndexChanged 事件,并使用相关的产品 ID 来调用 BindProductDetails 方法:
复制代码
protected void ProductList_SelectedIndexChanged(object sender, 
                                                 EventArgs e) {
    var productId = (int)ProductList.SelectedDataKey.Value;
    BindProductDetails(productId);
}
BindProductDetails 在数据库中查询产品信息和照片,然后将它们绑定到详细信息视图中的对应控件。
照片由一个简单的处理程序提供,它会在数据库中查询图像字节并将其复制到响应的二进制流中(参见图 4)。此处理程序将被页面的三个版本分别用到。现在,我有一个由数据驱动的产品主-详细信息视图,它完全由强制性和声明性的服务器代码编写而成,但存在许多有待改进的地方。
ASP.NET<wbr>AJAX<wbr>4.0<wbr>图 4 获取产品照片</wbr>
复制代码
public void ProcessRequest (HttpContext context) {
    int id;
    if (int.TryParse(context.Request.QueryString["id"], out id)) {
        context.Response.ContentType = "image/gif";
        AdventureWorksDataContex<wbr>t dc = new AdventureWorksDataContex<wbr>t();
        var bytes = dc.ProductPhotos
            .Where(p =&gt; p.ProductPhotoID == id)
            .Single().LargePhoto;
        context.Response.OutputStream.Write(bytes.ToArray(), 0, bytes.Length);
    }
    else {
        throw new HttpException(404, "Image not found");
    }                
}
</wbr></wbr>
这是一个非常典型的 Web 窗体页面,在状态数量方面,它是有状态的。在对浏览器中的呈现源进行快速检查后,发现 ViewState 约为 4KB,原因是各个视图都记得各自的所有内部状态并在每个回发中都附带着它。
同时,页面并未显示有关当前状态的线索;无论在页面上执行何种操作,浏览器导航栏中的 URL 始终为 "1_WebForm.aspx"。如果用户为页面添加书签,他在最初看到的始终是没有任何产品详细信息的页面。
在这一简单示例中,可通过以下方法修复这一问题:将产品列表中的链接更改为常规链接而非 LinkButton 控件,然后使用到详细信息页面的普通导航替换选择回发(甚至可以关闭 ViewState 以在每次往返时节省约 4KB 两次)。如果您采用的是最近开发的 ASP.NET 模型视图控制器 (MVC) 库(参见 msdn.microsoft.com/magazine/cc337884),您可能已发现这是用来说明 MVC 作用的一个典型示例。
基于导航的方法还可以显著改善站点的可搜索性(这可以作为一篇全新文章的主题)。也就是说,典型的数据驱动应用程序要比这种简单示例复杂得多,而且普通的导航通常并不是构建 UI 流的正确方法。因此,即使是对这样一个简单的应用程序,我也会花费一些时间来说明如何转换回发和 Web 窗体概念以及如何通过 AJAX 改进它们。
回发和链接导航都有可能引起超出您预期的对用户体验的干扰:在回发和导航过程中,UI 都会被冻结,任何其他用户交互都无法执行,直到服务器使用新的内容做出响应为止,然后还需要呈现这些新内容以替换整个文档,有时可能会丢失一些细微的状态(如滚动位置)。
另一个问题是用户对“后退”按钮和历史记录的工作方式具有非常特定的预期。遗憾的是,在回发模型中,很难甚至根本无法控制进入历史记录的内容或者当用户按下“后退”、“前进”或“刷新”按钮时发生的情况。理想情况下,构成状态更改和创建历史记录点的内容应由开发人员来控制,但在回发应用程序中,几乎所有用户交互都会在浏览器历史记录中创建一个条目。

UpdatePanel 版本
改进此页面的一个简单方法是使用 UpdatePanel。当用户操作可能会导致回发时,UpdatePanel 允许您划分出要更改的页面部分。在这一非常简单的示例中,我要更新的页面区域是详细信息视图。要实现 UpdatePanel 部分更新,只需向页面添加一个 ScriptManager 控件即可(紧随 form 标记之后),如下所示:
复制代码
<asp:ScriptManager ID="ScriptManager1" runat="server"/>
还必须围绕详细信息视图添加 UpdatePanel 本身:
复制代码
<asp:UpdatePanel ID="UpdatePanel1" runat="server" RenderMode="Inline">
    <Triggers>
        <asp:AsyncPostBackTrigger ControlID="ProductList" 
            EventName="SelectedIndexChanged" />
    </Triggers>
    <ContentTemplate>
        <div class="float" id="productDetails">
            <fieldset>
            ...
            </fieldset>
        </div>
    </ContentTemplate>
</asp:UpdatePanel>
请注意,此 UpdatePanel 有一个触发器,它会监视产品列表的 SelectedIndexChanged 事件。当可能触发部分更新的所有控件本身都在 UpdatePanel 内部时,这一点并非必要;但此处产品列表应保持位于 UpdatePanel 外部,因为在其 SelectedIndexChanged 事件发生时其呈现不需要进行更新。如果不提供触发器,则会发生常规回发而非部分更新。并且,在使用 UpdatePanel 时,一定要记住为可能触发部分更新的所有回发控件都指定一个 ID。否则,可能会导致页面在没有明显原因的情况下使用常规回发。
这就是对具有 AJAX 外观的典型回发 Web 窗体进行转型所需的全部工作。但是,这似乎并未真正得到所希望的全部功能。其中的一个问题是失去了对“后退”按钮的支持。现在,如果用户在浏览了六辆自行车后返回,他将返回到他在您之前所访问的站点。在意识到这一点后,如果他按“前进”按钮,则会回到应用程序的默认状态(在本示例中,会进入一个未选中任何产品的产品列表)。
幸运的是,ASP.NET 3.5 SP1 提供了一种简单的方法使“后退”按钮支持恢复页面。现在,ScriptManager 有一个非常方便的 EnableHistory 属性、AddHistoryPoint 方法以及 Navigate 事件,通过它们的共同作用可使应用程序开发人员能够控制浏览器历史记录,而且远远超出常规回发所允许的范围。此功能不仅恢复了因使用 UpdatePanel 而失去的支持,并且在实现时采用了功能更为强大的格式。
与常规回发最大的差别在于,我现在可以完全决定在应用程序状态中构成更改的内容并筛选出我认为不太重要的用户交互。我还可以得到“添加书签”功能,并确保浏览器历史记录下拉列表中的条目具有可读性且具有实际意义。
要向页面添加历史记录管理,首先需要确定使用书签时用户可能希望保存的信息。在这里,只有一个相关的信息部分需要输入状态:即当前所选产品的 ID。
我需要截取将要更改该状态的所有事件。实际上,需要处理的唯一事件就是先前用作触发器的事件:即针对产品列表的 SelectedItemChanged 事件。我已经开始对该事件进行处理以重新绑定详细信息视图,现在只需添加一些代码以便在每次触发该事件时创建一个新的历史记录点即可:
复制代码
protected void ProductList_SelectedIndexChanged(object sender, 
                                                       EventArgs e) {
    var productId = (int)ProductList.SelectedDataKey.Value;
    var product = BindProductDetails(productId);
    if (ScriptManager1.IsInAsyncPostBack 
                                   && !ScriptManager1.IsNavigating) {
         ScriptManager1.AddHistoryPoint("product", 
           productId.ToString(), "AdventureWorks - " + product.Name);
    }
}
为确保不会由于用户导航回应用程序的先前状态而触发此事件,代码会检查以确保该请求是异步回发的一部分而不是导航操作的一部分。如果不执行此检查,我创建的新历史记录点会覆盖浏览器中先前可能已存在的所有前进历史记录。
完成此检查后,即可安全地调用 AddHistoryPoint 方法,此方法将传入我所关心的单个状态部分(即产品 ID),参数名称为 "product"。正如您将看到的,此名称就是要在修改后的 URL 中使用的名称。该值本身需要转型为字符串。可将历史记录状态视为另一种形式的查询字符串。我要向此方法提供的最后一部分信息是文档标题。这是改善用户体验的一个绝佳机会,因为用户将能够在历史记录导航下拉列表中看到有意义的信息,它们非常有助于用户浏览应用程序(参见图 5)。
图 5 历史记录下拉列表(单击图像可查看大图)
此状态得以维持的原因在于浏览器 URL 的哈希值(# 号后的部分,最初设计用于实现文档内导航)。之所以使用它作为存储介质,是因为它是无需实际离开页面及其 JavaScript 和 DOM 状态即可添加历史记录条目的唯一方式(因为只有当 URL 发生更改时,浏览器才允许添加另一个历史记录项)。随之而来的是将状态存储在 URL 中时遇到的限制,因为它只有有限的空间可供使用。有些浏览器可能会拒绝超过 1KB 的 URL。如果需要更大的空间,则可能说明您没有将相关的信息部分选作状态,因此需要进行重构。
请注意我是如何使用产品 ID 的(相对较小的数据部分),而不是使用完整的名称(它可能会更友好,但通常都比较大)。URL 中的空间不足可能还说明常规 Web 窗体和 ViewState 可能要比 AJAX 和历史记录更适合您的设计。
另一半难题是在浏览历史记录时,如何恢复刚刚保存的状态。为此我将使用 ScriptManager 来处理 Navigate 事件(参见图 6)。在此代码中,我首先处理没有状态的情形。这是指在回到 GET 请求后,返回页面的默认状态。如果您不知道状态实际是通过执行新回发来恢复的,则这一做法可能显得有点另类。在此示例中,回发的“之前”状态(即框架自动恢复的状态)是浏览器历史记录中的“之后”状态,因此我必须清除该恢复的状态而使用默认状态替代它。
ASP.NET<wbr>AJAX<wbr>4.0<wbr>图 6 处理 Navigate 事件</wbr>
复制代码
protected void ScriptManager_Navigate(object sender, HistoryEventArgs e) {
    var productIdString = e.State["product"];
    if (productIdString == null) {
        ProductList.SelectedIndex = -1;
        ProductDetails.DataSource = null;
        ProductDetails.DataBind();
        ProductModelDetails.DataSource = null;
        ProductModelDetails.DataBind();
        ProductPhotoList.DataSource = null;
        ProductPhotoList.DataBind();
        Page.Title = "AdventureWorks";
    }
    else {
        var productId = int.Parse(productIdString);
        var product = BindProductDetails(productId);
        ProductList.SelectedIndex = (
            from p in AdventureWorksContext.Products
            where p.ProductSubcategoryID == 1 // Mountain bikes
            orderby p.Name
            select p).ToList().IndexOf(product);
        BindProductList();
        Page.Title = "AdventureWorks - " + product.Name;
    }
}
其次,状态本身应被视为是用户提供的,因此应对其进行验证(我使用的方法是将其解析为整数)。最后,除了重置列表和详细信息状态以外,我还将在恢复状态时恢复页面的标题。
如果在进行了这些更改后再使用此页面,则每次选中一个产品后,浏览器中的 URL 都会发生改变,如下所示:
复制代码
http://MyServer/MSDNAjax/2_UpdatePanel.aspx#&&5YLQHC81D2
OEdJU/9ZBdHUip1qx3ooPKDhCLgKog<wbr>upQ=
</wbr>
它看起来很难看而且可读性不强,对吧?其原因是框架默认会认为用户提供的数据具有危险性,因此它会打乱状态以防篡改。然而,在许多情况下,开发人员会更愿意选择可读性较好而且不太危险的 URL,即使需要通过代码验证其状态并允许用户改动它。在某些情形中,可改动 URL 甚至被视为一件好事(MSDN 库即是一个极好的示例;它允许用户根据一个可预测的架构来构建自己的 URL,例如 msdn.microsoft.com/library/system.web.ui.scriptmanager.aspx,因为它可以使导航变得更加容易。
为允许此类情况发生,ScriptManager 公开了 EnableSecureHistoryState<wbr> 布尔属性。只需将其设为 false,URL 即会变得更加友好,如下所示:</wbr>
复制代码
http://MyServer/MSDNAjax/2_UpdatePanel.aspx#&&product=776
最终会得到一个更为流畅的页面,它不仅看上去类似于一个 AJAX 版本的 Web 窗体,而且还具有很多非常方便的附加功能,例如可添加书签以及优化处理“后退”按钮等。并且所有这一切都无需编写任何 JavaScript 代码即可实现。

纯 AJAX 版本
尽管非常类似于页面的 UpdatePanel 版本,但我仍会减少 ViewState 的权重。为了精简它,我要将更多的逻辑传输给客户端。为此,我需要编写一些有趣的 JavaScript。
您可能非常擅长使用 ASP.NET AJAX 3.5 SP1 编写纯 AJAX 版本,但获取数据并将其格式化为 HTML 可能会比较繁琐。可使用以下两种基本方法将数据转换成 HTML。
第一种方法(这也是大多数客户端模板引擎所采用的)是使用动态数据内容来连接改变静态模板内容的字符串。这看起来非常简单而且快速,因为它使用 innerHTML 作为与 DOM 交互的唯一方法。但是它也存在一些问题。
它无法很好处理的一件事就是防止注入攻击:如果想要通过连接字符串来生成 HTML,需要在使用之前编码所有数据。否则,将引号引入属性或将脚本标记引入文本节点(无论是恶意还是无意)都可能导致执行任意代码(这一点非常糟糕)。编码要比想像的更难,因为可能需要不同的编码算法,具体则取决于要注入的是文本属性、URL 属性还是文本节点。
模板引擎还需要表达式语言;尽管无需修改即可轻松注入普通数据字段,但那只是最简单的情形。通常需要应用格式字符串、组合多个字段,更为常见的情况是要在显示之前操控数据。可通过在将数据放入模板之前进行转型来实现这一目的,但如果将该功能内置到模板引擎中,则会更加简单有效。开始添加各种功能(如格式化)后,您很快就会发现您需要灵活的完整表达式语言。
如果能够交错代码和标记(如同在 ASP 中使用 <% %> 块那样),则可实现一些有趣的情形,如围绕 HTML 片段使用循环来重复标记,或使用简单的 if 语句来实现条件呈现。同样,此类情形也需要有一种真正可用的完整语言。
最后,HTML 只是故事的一半。AJAX 应用程序实际上与活动的内容相关,而不仅仅是对 DOM 的客户端更新。生成了 HTML 后,您仍然必须将事件挂接到元素中并附加控件和行为。您可在生成 HTML 之后通过代码实现此目的,但这样做会在 HTML(它会变得非常简单)和逻辑(它会变得非常复杂,而且需要了解模板的结构)之间产生令人讨厌的不对称现象。
换句话说,为了附加某个行为,您需要了解其附加位置,而这反过来又意味着在 HTML 模板标记结构中所做的任何更改同时也需要在激活它的代码中进行。使该耦合更松散的方法有多种,但较好的解决方案是创建模板引擎的内容激活部分。
可以从数据生成 HTML 的另一种方法是直接操控 DOM API,然后从节点创建元素、属性和文本节点。在开始时,从多方面考虑这似乎都是一种糟糕的选择。的确,它可以令那些标准的追随者感到非常满意,但由于某种奇怪的原因,它要比 innerHTML 慢很多。但它未得到普遍使用的主要原因是 DOM API 表现力不太强,而且生成的代码难以阅读,维护起来则更加困难 — 至少没有任何帮助和额外的抽象概念。有些工具包(如 jQuery)提供了极好的抽象并使整个过程变得更加有趣,但即使是使用此类工具,它也仍然相当困难(这也是 jQuery 有多个模板插件的原因)。
有些用户可能知道 Microsoft 已在 ASP.NET AJAX Futures 中提供了一个模板引擎,但它在设计方面的速度也比较慢而且操作复杂,我们希望它能做得更好一些。这一失败的初次尝试带来的好处是加深了我们对于不希望新版本出现的状况(即速度慢而且操作复杂)的了解。
开发团队测试了许多不同的设计(从字符串连接到完整的 DOM 操作)以找出适合于 ASP.NET AJAX 的新模板引擎,并且我们针对它们进行了性能、简易性和灵活性方面的评估。我们还针对它们需要避免发生的情形对其进行了对比。并不存在理想的解决方案,但我们选择的似乎更为折衷。
新引擎的原理非常简单:获取模板代码(包含 HTML、数据字段、表达式、声明性组件实例以及命令性代码),并且像变魔术一样将其自动转换为可创建等效 HTML 的 JavaScript。这看起来非常简单,实际上也确实如此(嗯,至少在浏览器怪异现象出现之前)。使用 DOM API 确实会导致性能受到影响,但如果我们在操作时多加注意、在 DOM 之外构建元素、尽可能少添加和晚添加元素,这样对性能的影响并不会太大,并且考虑到它所带来的令人惊讶的灵活性,还是非常值得做出这种牺牲的。字符串连接方法中的所有问题似乎都自然消失了。
注入攻击要对自身负责。使用代码创建文本节点和设置属性值时,无需进行编码,因为使用的 API 已经很安全。这与使用 SQL 参数和通过连接字符串来构建 SQL 类似。任何一个头脑清醒的人都不会再执行后者,为什么你要甘冒类似的风险呢?
现在来看,究竟谁需要新的表达式语言?我们已有一个答案:JavaScript。将模板标记转换为 JavaScript 代码时,除了将 JavaScript 表达式注入所生成的代码外,还有更简单的方法吗?
要实现最常见的模板开发任务,即单次单向注入数据字段(在服务器上表示为 "<%= 表达式 %>",在我们的系统中表示为 "{{ 表达式 }}"),我将使用常被忽视的一个 JavaScript 功能 — "with" 关键字。利用它,我不必依靠类似 "{{ dataItem.myField }}" 的表达式即可注入与模板实例相关的数据项字段。有了 "with" 关键字的帮助,您可围绕模板的生成代码添加 "with(dataItem) {…}" 之类的代码,以使数据项的所有成员都提升到模板函数的顶层范围,从而使表达式的注入与 "{{ myField }}" 一样简单。
可通过以下两种方法将行为注入到模板中。首先,从 itemCreated 事件编写 $attachEvent 和 $create 代码,或使用可从模板获取且引用最新创建元素的特殊 $element 变量内嵌到模板中。或者,也可以使用我们提供的声明性语法。例如,如果想向输入标记添加一个自动完成和水印行为,应编写类似于下面所示的代码:
复制代码
<body xmlns:sys="javascript:Sys"
 xmlns:
 xmlns:watermark="javascript:AjaxControlToolkit.extBoxWatermarkBehavior">
...
<input id="search" sys:attach="autocomplete,watermark"
 autocomplete:servicepath="SearchAutoComplete.asmx"
 watermark:watermarktext="Type your search terms here" />
在这里,我将使用 xmlns XHTML 命名空间声明为 HTML 或正文标记(或模板的父标记)上的每个声明行为注册前缀。这将使我能够以标准方式扩展 XHTML 标记,并且它类似于服务器代码的 @Register 指令。"xmlns:" 之后的部分是将要与每个行为或控件相关联的前缀。命名空间的 URL 使用 "javascript:" 协议来将前缀映射到特定的 JavaScript 类型。"sys" 命名空间是一个特殊的系统命名空间,应将其映射到 Sys 命名空间(这是 AJAX 中的根命名空间)。
实例化本身是通过特殊属性 sys:attach 实现的,它的值是那些需要实例化并要附加到元素的行为或控件的前缀列表(以逗号分隔)。然后,我可以为所有行为设置属性,而且不会与常规的 HTML 属性或相同元素上的其他行为发生冲突,因为它们都已通过命名空间很好地进行了区分。
引擎最吸引人的功能之一就是将模板编译成 JavaScript 代码的过程与真正的编译步骤非常类似。这意味着它只需针对每个模板执行一次操作即可,并且它还提供了提前执行多个任务而非每次实例化模板时都执行这些任务的机会。现在理论方面的内容已经介绍得够多了。应该如何将其应用到主/详细信息页面呢?

AJAX 版本模板
产品列表的模板非常简单:
复制代码
<ul id="productListTemplate" class="sys-template">
    <li>
        <a href="{{ String.format('3_Client.aspx?product={0}',
        ProductID) }}">{{ Name }}</a>
    </li>
</ul>
项目模板是其中包含简单链接的列表项。链接文本就是产品的名称 ("{{ Name }}"),而 href 属性是使用纯 JavaScript 通过产品 ID 构建的一个格式化字符串:
复制代码
"{{ String.format('5_Client.aspx?product={0}', ProductID) }}"
类 "sys-template" 是在页面的 CSS 中定义的,它可使模板在页面的初始呈现中隐藏起来。图 7 显示了这一简单模板编译后的代码。详细信息视图要复杂一些,它实际上包含一些内嵌代码(参见图 8)。我本来可以使用嵌套模板来呈现照片列表,但针对每个照片的标记来使用常规循环要简单一些。如果动态处理更改数据并将更改自动反映到标记中(这属于受支持的情况,但超出了本文的讨论范畴),则嵌套模板是有效的,但由于这里处理的是单向单次绑定,因此内嵌代码即完全够用。
ASP.NET<wbr>AJAX<wbr>4.0<wbr>图 7 模板编译后的代码</wbr>
复制代码
function(__containerElement, $dataItem, $parentContext, __instanceId) {
   var __context = {}, $component, __app = Sys.Application, 
      __creatingComponents = __app.get_isCreatingComponents(), 
      __components = [], __componentIndex, __e, __f, __topElements = [],
      __p = [__containerElement], $index = __instanceId, 
      $id = Sys.Preview.UI.Template._getIdFunction(__instanceId), 
      $element = __containerElement;
   Sys.Preview.UI.Template._contexts.push(__topElements);
   with(__context) { with($dataItem || {}) {
      $element=__p[1]=document.createElement_x('LI');
      __topElements.push($element);
      $element=__p[2]=document.createElement_x('A');
      $component = $element;
      __e = document.createAttribute('href');
      __e.nodeValue = String.format('5_Client.aspx?product={0}',
                                                       ProductID);
      $element.setAttributeNode(__e);
      __p[1].appendChild($element);
      __p[2].appendChild(document.createTextNode(Name));
      $element=__p[2];
      __p[1].appendChild(document.createTextNode(" "));
      $element=__p[1];
   } 
}
   for (var __i = 0, __l = __topElements.length; __i < __l; __i++) {
      __containerElement.appendChild(__topElements[__i]);
   }
Sys.Preview.UI.Template._contexts.pop();
 return new Sys.Preview.UI.TemplateResult(this, __containerElement, __topElements, __components);
}
ASP.NET<wbr>AJAX<wbr>4.0<wbr>图 8 显示详细信息</wbr>
复制代码
<div class="sys-template" id="productDetailsTemplate">
    <fieldset>
        <legend>{{ Name }} ({{ ProductNumber }}) 
            {{ String.format("{0:C}", ListPrice) }}</legend>
        <ul class="photoList">
            <!--* for (var i = 0; i < Photos.length; i++) { *-->
            <li><img src="{{ String.format('productphoto.ashx?id={0}',
                Photos[i]) }}" /></li>
            <!--* } *-->
        </ul>
        <table>
            <tr><td class="label">Summary:</td><td>{{ Summary }}</td></tr>
            <tr><td class="label">Experience:</td>
                <td>{{ RiderExperience }}</td></tr>
              ...
            <tr><td class="label">Style:</td><td>{{ Style }}</td></tr>
            <tr><td class="label">Wheel:</td><td>{{ Wheel }}</td></tr>
            <tr><td class="label">Maintenance:</td>
                <td>{{ MaintenanceDescription }}</td></tr>
        </table>
    </fieldset>
</div>
模板是在首次实例化时编译的,但它们是通过将模板标记的父元素作为构造函数参数来创建 "new Sys.Preview.UI.Template" 而完成的。模板本身是在网络调用的回调中实例化的(该回调将从服务器的 Web 服务中将数据带回):
复制代码
AdventureWorks.GetProducts(1 ,
  function(productArray) {
    renderProductList(productArray, productListTemplate);
    selectProduct(initialProductID, true);
});

function renderProductList(productArray) {
    var target = $get("productList");
    target.innerHTML = "";
    for (var i = 0, l = productArray.length; i < l; i++) {
        productListTemplate.createInstance(target, productArray[i]);
    }
}
在 ASP.NET 4.0 的发行版本中,这并非必须;DataView 组件会负责模板解析、编译和实例化。此应用程序中的大多数代码最终都会消失,但展示的内部工作原理却非常有用。它还说明了希望包括模板呈现的组件开发人员可能会如何使用此功能。

事件冒泡
对于用户单击其中一个产品时会显示相应的详细信息视图的代码,应该把它放在哪里呢?此代码与服务器端的代码类似,使用的都是事件冒泡;因此我可以针对列表中的所有链接编写一个单击事件处理程序(这样一来,我就可以在需要的时候向列表添加链接或从中删除链接,而不必创建新的处理程序或清理旧的处理程序)。以下代码显示了该处理程序。对于列表中的链接,其所有单击事件都会从列表本身冒出并在这里得到处理。"e.target" 是对实际被单击元素的引用;换句话说,它是链接,使我能够从 href 属性检索产品 ID 并选择相关的产品:
复制代码
$addHandler($get("productList"), "click", function(e) {
    var href = e.target.href;
    selectProduct(parseInt(href.substring(href.indexOf('=') + 1), 10));
    e.preventDefault();
    e.stopPropagation();
});
完成后,事件的默认操作(链接导航)将被取消而事件将无法再冒出。要实现此目的,可针对事件对象调用 W3C 标准 stopPropagation 和 preventDefault 方法(框架在所有浏览器中都提供,包括 Internet Explorer)。

管理后退按钮
仍然会从服务器端版本复制的唯一功能是历史记录。通过针对 ScriptManager 控件启用历史记录,可以同时启用客户端 API,它们完全等同于我之前使用过的服务器端 API 而且它们甚至可以同时使用(启用混合的客户端-服务器端状态管理)。
要创建历史记录点,可从与状态更改相对应的事件(在本例中,是指单击列表中的某个产品)中调用 Sys.Application.addHistoryPoint:
复制代码
Sys.Application.addHistoryPoint({product: productDetails.ProductID}, 
    "AdventureWorks - " + productDetails.Name);
相应地,状态会从 Sys.Application 的 "navigate" 事件中恢复。事件处理程序接收的 HistoryEventArgs 参数有一个 state 属性,利用它可以检索要恢复的产品:
复制代码
Sys.Application.add_navigate(function(sender, e) {
    var ProductID = parseInt(e.get_state()["product"], 10);
    selectProduct(ProductID, true);
});

最终产品
结果页面的行为方式与 UpdatePanel 版本非常类似,但在网络流量方面却不具可比性。选中某个产品时,UpdatePanel 版本会向服务器发送超过 4KB 的数据并且接收约 8KB。而纯 AJAX 版本仅发送 "{"productId":771}" 加上标准的 HTTP 头,接收的则是 2KB 的纯 JavaScript Object Notation (JSON) 数据。每次用户单击某个产品时都可节省约 10KB 的流量。
这只是计划随 ASP.NET 4.0 发布的众多激动人心的功能中的一个。请将您的看法通过以下网址与我们共享:go.microsoft.com/fwlink/?LinkId=126987

Bertrand Le Roy 博士是 Microsoft 负责 AJAX 的项目经理。他在这一领域做了五年的开发工作。他还在 OpenAjax 联盟中担任 Microsoft 的代表。
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics