为 WAN 环境优化自定义 Web 部件

摘要:学习一些方法,以便在自定义 Web 部件中最大程度地减少对带宽的影响,包括有关样式的一般建议和特定的信息,以及通过各种备选方法来检索和呈现 SharePoint 列表中的数据的代码示例。(24 个打印页面)

Steve Peschka Microsoft Corporation

2008 年 1 月

适用于:Microsoft Office SharePoint Server 2007、Windows SharePoint Services 3.0、ASP.NET AJAX 1.0

目录

  • 为 WAN 优化自定义 Web 部件的简介

  • 重复使用内置样式或创建自定义样式

  • 存储状态

  • 最大程度地提高显示数据的 Web 部件的性能

  • 结束语

  • 其他资源

为 WAN 优化自定义 Web 部件的简介

在开发将在高延迟或低带宽的网站中使用的自定义 Web 部件时,需要关注在为此类环境创建网页时运用的一般设计原则。您应该努力设计出这样的部件:最大程度地减少往返服务器的行程和通过网络发送的数据。您可以使用本文讨论的几种方法来实现这些设计目标。

重复使用内置样式或创建自定义样式

在 Web 部件发出 HTML 时,使用 Microsoft Office SharePoint Server 2007 和 Windows SharePoint Services 3.0 样式表内置的样式类:core.css。(在本文中,Office SharePoint Server 和 Windows SharePoint Services 统称为 Microsoft SharePoint 产品和技术)。通过重复使用这些样式,可以最大程度地减少对网页的影响,因为网页无需只是为了支持您的部件而另外下载一个样式表。此外,在用户初次访问网站后,core.css 文件就已下载到用户计算机的缓存中。通过使用作为 core.css 一部分的样式,可以确保无需另外下载样式表就能支持样式。

如果确实需要在部件上使用自定义样式,则考虑使用可与 Blob 缓存一起使用的自定义样式表。如果您将此样式表存储在文档库中,则它可能具有与之关联的可缓存性指令,这样在初次访问网页之后便无需下载此样式表。这与使用其他样式(例如嵌入式样式)相比,将会减轻对网站的影响(如果使用嵌入式样式,则每次呈现部件时都会通过网络传输样式)。

存储状态

Web 部件可能需要跟踪诸如用户、请求和数据源等信息。在存储状态时您有几种选择,本文不具体介绍这些选择。但一般来说,可以将两种常见的选择用于 Web 部件:ViewState 和服务器 Cache。在低带宽或高延迟的环境中,尽可能避免使用 ViewState,因为它会向所下载的网页和作为任何回发的网页添加内容。这适用于也涉及到通过网络传输数据(例如查询字符串、隐藏的字段和 cookie)的其他状态形式。

使用服务器 Cache 类允许您在服务器级别存储状态信息。使用服务器 Cache 的缺点是,它实际上并未设计为用作每用户状态机制(尽管可根据情况使它以这种方式工作)。此外,缓存信息不会复制到服务器场中的所有前端 Web 服务器上。如果不管用户请求最终命中的是哪台前端 Web 服务器,该状态信息存在与否对您的部件起着决定性的作用,那么,服务器 Cache 并不是一种好的选择。

在这种情况下,另一种选择是使用会话状态。默认情况下会停用会话状态,但当您在服务器场中激活 Microsoft Office InfoPath Forms Services (IPFS) 时会启用此状态。启用它时,它使用 Microsoft SQL Server 来跟踪状态,这意味着不管哪台前端 Web 服务器接收 HTTP 请求,都可以使用会话状态值。会话状态的缺点是,数据一直保留在内存中,直到被移除或过期为止。因此,如果管理不当,以会话状态存储的大型数据集可能会降低服务器性能。由于有这些限制,因此,除非绝对必要,否则建议您不要使用会话状态。

您也可以通过编辑 Web 应用程序的 web.config 文件,尝试为网站中的所有网页将 ViewState 设置为停用。此文件包含一个 pages 元素,而此元素具有一个 enableViewState 属性。如果您非常关心网页中的 ViewState 的大小,可以尝试将此属性设置为 false(默认情况下它设置为 true)。如果这样做,则需要全面测试您的网站和所有功能,以确保它正常工作,因为某些控件和 Web 部件可能希望 ViewState 处于启用状态。

如果正在开发 Web 部件并需要使用类似于 ViewState 的方法,但是不确定它在网页中是否可用,则可以改为使用控件状态,这是 ASP.NET 2.0 中的新方法。简单地说,您需要执行下列操作:

  1. 在初始化过程中通过调用 Page.RegisterRequiresControlState 来注册控件状态。

  2. 重写 LoadControlState 方法和 SaveControlState 方法。

  3. 手动管理映射到控件状态的对象集合部分。

最大程度地提高显示数据的 Web 部件的性能

如果 Web 部件用于显示数据,则可以通过多种方式尝试让最终用户体验到尽可能最高的性能。一般来说,您必须在需要访问服务器多少次以及应为一个请求检索多少数据之间取得平衡。

提供呈现限制

对于发出多行数据的控件,要包含一个属性,以允许管理员控制显示的行数。这样可以根据将使用控件的环境的延迟和带宽,灵活地增加或减少在每个网页请求中呈现的数据量。这还可以影响查看所有数据所需的请求的数量。

如果返回的行数是一个可以由最终用户设置的属性,请考虑添加约束条件,使某个用户的选择不会使网络瘫痪。

使用内联 XML 数据岛

显示数据的另一种备选方法是使用内联 XML 数据岛,并在客户端上执行呈现过程。通过使用此方法,要显示的所有数据都作为 XML 数据岛发到网页。客户端脚本用于在网页中实际呈现数据,并且它负责使用 XML DOM 来检索和显示数据岛中的数据。

通过这种方式,可以在单个请求中检索所有数据,从而最大程度地减少往返服务器的行程数量。但是,下载大小将明显变大,因此,初始网页加载时间会变长。此外,如果在使用了其他会导致回发的控件的网页中使用此方法,则它每次都会强制下载这样大的数据。如果您知道不会发生此问题,则此方法最合适,否则请将其用作可选的数据检索方法。例如,创建一个公共属性,以跟踪是否应下载所有数据,以便网站管理员可以控制此行为。

下面是一个相对简单的示例,说明这样做的 Web 部件;此部件读取列表中的所有内容,将此列表作为 XML 发到网页,然后使用客户端的 ECMAScript(JScript、JavaScript)来呈现和分页数据。

备注

本文中的代码并不代表 Microsoft 的最佳编码方法。这些代码经过精简,以便简化练习;需要更改代码中的某些元素才能在生产环境中使用。

此 Web 部件重写 Render 方法,并且首先获取名为 Contacts 的网站中的列表内容。

// Get the current Web site.
SPWeb curWeb = SPContext.Current.Web;

// Get the list; LISTNAME is a constant defined as "Contacts".
SPList theList = curWeb.Lists[LISTNAME];

// Ensure the list was found.
if (theList != null)
{
…
}

获得了对此列表的引用后,使用 StringBuilder 对象来创建将发到网页的 XML 数据岛。

// stringbuilder to create the XML island
System.Text.StringBuilder sb = new System.Text.StringBuilder(4096);

// Create the island header.
sb.Append("<XML ID='spList'><Items>");

// Enumerate through each item.
SPListItemCollection theItems = theList.Items;

foreach (SPListItem oneItem in theItems)
{
   // Add the tag for an item.
   sb.Append("<Item ");

   // Put the attributes in a separate try block.
   try
   {
      sb.Append("Name='" + oneItem["First Name"] + "' ");
      sb.Append("Company='" + oneItem["Company"] + "' ");
      sb.Append("City='" + oneItem["City"] + "' ");
   }
   catch
   {
      // Ignore.
   }

   // Close out the item.
   sb.Append("/>");
}

// Close off the XML island.
sb.Append("</Items></XML>");

// Write out the XML island; writer is the 
// HtmlTextWriter parameter to the method.
writer.Write(sb.ToString());

将 XML 添加到网页后,您还需要一个显示数据的元素。为满足这一要求,向网页添加一个简单的 <div> 标记。

// Add a <div> tag - this is where we'll output the data.
writer.Write("<div id='wpClientData'></div>");

您需要的下一段接口将用于分页数据。在此例中,将两个链接按钮添加到网页中。要注意的两件事是 OnClientClick 属性和 Attributes 集合。OnClientClick 属性设置为使用一个自定义 ECMAScript(JScript、JavaScript)函数,此函数编写为在客户端上显示数据。

Attributes 集合用于为 LinkButton 设置导航 URL。在此例中,我们想将 LinkButton 呈现为超链接,以便用户获得此项目可以单击的反馈,而且我们可以在用户单击它时执行某个操作。在此例中,# 链接用作导航 URL,原因是我们并不想真的导航到任何地方;我们只想呈现一个链接,并捕获其单击时间。

// Add the paging links.
LinkButton btn = new LinkButton();
btn.Text = "<< Prev";
btn.Attributes.Add("href", "#");
btn.OnClientClick = "renderPartData('PREV');";
btn.RenderControl(writer);

writer.Write("&nbsp;");

btn = new LinkButton();
btn.Text = "Next >>";
btn.Attributes.Add("href", "#");
btn.OnClientClick = "renderPartData('NEXT');";
btn.RenderControl(writer);

接下来,我们向网页添加一些隐藏的字段,以跟踪控件的分页情况。在此例中,我们想跟踪网页大小(一次显示多少个记录)、当前记录编号和记录总数。

// Add fields to track the number of items to see in a page and 
//current item number.  PageSize is a web part property that can be set 
//to control the number of rows returned 
Page.ClientScript.RegisterHiddenField("pageSize", PageSize);
Page.ClientScript.RegisterHiddenField("curPage", "-4");
Page.ClientScript.RegisterHiddenField("totalItems",
   theItems.Count.ToString());

在所有元素均已存在的情况下,Render 方法的最后一步是注册一个启动脚本,此脚本执行 javascript 以呈现数据。

// Create a startup script to call our dataload method when the page 
//loads
this.Page.ClientScript.RegisterStartupScript(this.GetType(), JSCR_START,
   "renderPartData();", true);

此 javascript 本身包含在项目中的一个名为 Scripts.js 的独立文件中。它被配置为嵌入的资源,并且作为 Web 资源(例如 webresource.axd)发送到网页。配置它以供下载的代码在 Web 部件的 OnPreRender 事件中运行。它首先调用 IsClientScriptIncludeRegistered 方法进行检查,以确保尚未将脚本引用添加到网页中;如果未添加,则将它注册为网页的 include。

// Register our JScript resource.
if (!Page.ClientScript.IsClientScriptIncludeRegistered(JSCR_NAME))
   Page.ClientScript.RegisterClientScriptInclude(this.GetType(),
JSCR_NAME, Page.ClientScript.GetWebResourceUrl(this.GetType(),
"Microsoft.IW.Scripts.js"));

此方法还要求您在项目的 AssemblyInfo.cs 类中注册 Web 资源。

[assembly: WebResource("Microsoft.IW.Scripts.js", "text/javascript")]

呈现网页时,它包含一个类似以下所示的链接。

<script src="/WebResource.axd?d=DVBLfJiBYH_yZDWAphRaGQ2&amp;t=633198061688768475" 
type="text/javascript"></script>

ECMAScript(JScript、JavaScript)读取 XML 并呈现数据。它首先确定分页界限:要显示的第一个和最后一个记录的记录编号。了解到需要显示的记录后,它使用相当简单的方法在客户端上创建 MSXML 的实例。

function createXdoc()
{
   ets the most current version of MSXML on the client.
   var theVersions = ["Msxml2.DOMDocument.5.0", 
      "Msxml2.DOMDocument.4.0", 
      "MSXML2.DOMDocument.3.0", 
      "MSXML2.DOMDocument", 
      "Microsoft.XmlDom"];

   for (var i = 0; i < theVersions.length; i++)
   {
      try
      {
         var oDoc = new ActiveXObject(theVersions[i]);
         return oDoc;
      }
      catch (theError)
      {
         // Ignore.
      }
   }
}

使用变量来存储 XML 数据,以及存储对 DIV 元素(此元素将输出结果)的引用。

// Get the XML.
var xData = document.getElementById("spList").innerHTML;
   
// Get the target.
var target = document.getElementById("wpClientData");

接下来,将数据从 MSXML 加载到 DOMDocument,然后选择 Item 元素的列表。

// Get the XML DomDocument.  
var xDoc = createXdoc();
   
// Validate that a document was created.
if (xDoc == null)
{
target.innerHTML = "<font color='red'>A required system component 
(MSXML) is not installed on your computer.</font>";
   return;
}
   
// Load the XML from the island into the document.
xDoc.async = false;
xDoc.loadXML(xData);
   
// Check for parsing errors.
if (xDoc.parseError.errorCode != 0) 
{
var xErr = xDoc.parseError;
target.innerHTML = "<font color='red'>The following error was 
encountered while loading the data for your selection: " + 
xErr.reason + "</font>";
   return;
}
   
// Get the items.  
var xNodes;
xDoc.setProperty("SelectionLanguage", "XPath");
xNodes = xDoc.documentElement.selectNodes("/Items/Item");  

既然选定了节点,现在可以为所需的特定网页集合枚举结果,并在网页上呈现结果。

// Check for data.
if (xNodes.length == 0)
   target.innerHTML = "<font color='red'>No data was found.</font>";
else
{
   // Create a table to render the data.
   output = "<table style='width:250px;'><tr><td>";
   output += "<table class='ms-toolbar'><tr>" +
      "<td style='width:75px;'>Name</td>";
   output += "<td style='width:75px;'>Company</td>";
   output += "<td style='width:75px;'>City</td></tr></table>
</td></tr>";
   output += "<tr><td><table>";
      
   // Cycle through all the data; startID and endID represent
   // the bounds of the page.
   for (var i = (parseInt(startID) - 1); i < parseInt(endID); i++)
   {
      // Create a new row.
      output += "<tr>";
                  
      // Add each cell.
      output += "<td style='width:75px;'>" + 
xNodes(i).getAttribute("Name") +
         "</td>";
      output += "<td style='width:75px;'>" +
         xNodes(i).getAttribute("Company") + "</td>";
      output += "<td style='width:75px;'>" + xNodes(i).getAttribute("City") +
         "</td>";
         
      // Close the row tag.
      output += "</tr>";
   }
      
   // Close the table tag.
   output += "</table>
</td></tr></table>";
      
   // Show the page parameters.
   output += "Records " + startID + " to " + endID + " of " + ti;
      
   // Plug the output into the document.
   target.innerHTML = output;
}

完成后,部件在客户端上显示记录,并提供一种无需建立另一个往返服务器的行程就能分页这些记录的机制。图 1 和图 2 显示带有两个不同数据网页的部件。

图 1. 带有数据的 Web 部件

示例 Web 部件的首页

图 2. 带有数据的 Web 部件

Web 部件数据的第二页

使用连接到 Web 服务的客户端脚本

用于管理通过网络发送的数据量的另一种方法是,使用连接到 Web 服务的客户端脚本来检索数据。此方法使控件不必回发整个网页就能检索所需的数据。这不仅给用户带来了更好的体验,而且还减轻了网络负载,原因是不用向服务器发送整个网页,然后再次回发整个网页以检索数据。可以直接使用 XmlHttp 组件来检索数据,也可以使用封装了 XmlHttp 功能的 Microsoft AJAX Library 1.0 来检索数据。

虽然 XmlHttp 和 ASP.NET AJAX 代表两种高级方法,但您可能会发现,最简单的方法是:将 ASP.NET AJAX 与公开 SharePoint 列表数据的自定义 Web 服务结合使用。尽管在技术上可以将 ASP.NET AJAX 库与基于 SOAP 的 Web 服务(例如 SharePoint 产品和技术内置的那些 Web 服务)一起使用,但这样做复杂得多,而且未提供 ASP.NET AJAX 样式的 Web 服务的任何其他好处,例如较小的有效负载以及对 JavaScript Object Notation (JSON) 的支持。

SharePoint 产品和技术提供多个 Web 服务,可供您用来显示数据。Lists Web 服务允许您从 SharePoint 产品和技术中的列表和库检索表格数据。可以使用它来呈现列表中包含的数据和指向列表项(例如文档或图像)的链接。Search Web 服务允许您搜索 SharePoint 产品和技术中包含的内容正文,以及正在爬网的任何其他外部源。通过仔细构造元数据驱动的查询,还可以使用它从一个或多个 SharePoint 列表中检索经过筛选的一组数据。

下面的示例演示如何创建一个自定义 Web 服务,以便从 SharePoint 产品和技术中检索列表数据。此 Web 服务类通过 System.Web.Script.Services.ScriptService 属性进行批注,这允许您使用随 ASP.NET AJAX 提供的客户端 Web 服务组件。然后,向 SharePoint 产品和技术“注册”此 Web 服务,以便可通过 _vti_bin 目录(包含所有内置的 Web 服务)访问它。母版页获得更新,以包含 ASP.NET AJAX ScriptManager 控件和此自定义 Web 服务的声明性标记。然后,编写一个 Web 部件,此部件生成客户端脚本,以便通过此自定义 Web 服务中的 ASP.NET AJAX 组件来检索数据,并在网页中显示这些数据。

安装 AJAX

首先,必须在 SharePoint 服务器上安装 ASP.NET AJAX 1.0 二进制文件。可以从 ASP.NET AJAX 网站(该链接可能指向英文页面)下载这些文件。您必须在服务器场中的每台前端 Web 服务器上安装这些文件。

安装 ASP.NET AJAX 后,您必须为将要使用 ASP.NET AJAX 的每个 Web 应用程序更新 web.config 文件。这个过程比较长;有关完整的逐步说明,请参阅 Mike Ammerlan 的博客文章将 ASP.NET AJAX 与 SharePoint 集成(该链接可能指向英文页面)

创建自定义 Web 服务

创建用以检索数据的自定义 Web 服务是必要的,因为它使您可以将 System.Web.Script.Services.ScriptService 应用于您的 Web 服务类,这使此类能够与 ASP.NET AJAX Web 服务框架一起使用。在此例中,开发了一个相对简单的 Web 服务类,以根据基本的参数检索列表数据。

此 Web 服务类包含对 System.Web.Extensions 类的引用,以便启用 ASP.NET AJAX 支持。在添加该引用后,向此类添加 using (Microsoft Visual C#) 或 Imports (Microsoft Visual Basic) 语句。

using System.Web.Script.Services;

然后,使用 ScriptService 属性来修饰此类,以便 ASP.NET AJAX Web 服务框架可以直接使用它。它包含一个默认的无参数构造函数,以便 ASP.NET AJAX 可以序列化此类。

namespace Microsoft.IW
{
   [ScriptService]
   public class AjaxDataWebService : WebService
   {

      public AjaxDataWebService()
      {
         // Default constructor
   }

在第一个示例中,Web 服务只包含一个返回字符串的方法。此字符串实际是将在客户端上使用的 XML。此方法的签名定义如下。

[WebMethod]
public string GetListData(string webUrl, string listName, int 
startingID, 
int pageSize, string[] fieldList, string direction)

// webUrl: URL to the Web site that contains the list
// listName: name of the list (such as "Contacts")
// startingID: used for paging data - which items to get
// pageSize: how many items to return
// fieldList: an array of fields to retrieve for each item
// direction: flag to indicate page forward or backward

StringBuilder ret = new StringBuilder(2048);
DataTable res = null;
string camlDir = string.Empty;
string camlSort = string.Empty;

代码的第一部分获取对网站的引用,并获取 Web 和包含数据的列表。

// Try getting the site.
using (SPSite theSite = new SPSite(webUrl))
{
   // Get the Web at the site URL.
   using (SPWeb theWeb = theSite.OpenWeb())
   {
      // Try getting the list.
      SPList theList = theWeb.Lists[listName];

接下来,根据分页方向、起始 ID 和结果集的大小创建查询语义。

// Use the direction to determine if we're going up or down.
// If we're going down, then sort it in descending order
// so that we go 20,19,18... for example, instead of 1,2,3; otherwise
// each time you paged backward you would always get the first
// page of records.
if (direction == "NEXT")
{
   camlDir = "<Gt>";
   camlSort = "TRUE";
}
else
{
   camlDir = "<Lt>";
   camlSort = "FALSE";
}

// Create the query where clause.
string where = "<Where>" + camlDir + "<FieldRef Name='ID'/>" +
   "<Value Type='Number'>" + startingID + "</Value>" +
   camlDir.Replace("<", "</") + "</Where>" +
   "<OrderBy><FieldRef Name='ID' Ascending='" + camlSort +
"'/></OrderBy>";

// Plug in the where clause.
qry.Query = where;

// Set the page size.
qry.RowLimit = (uint)pageSize;

// Create the view fields.
StringBuilder viewFields = new StringBuilder(1024);
foreach (string oneField in fieldList)
{
   // Add everything but the ID field; we’re doing the ID field 
   // differently because we need to include it for paging, 
   // but we can’t include it more than once because it would
   // result in the XML that is returned being invalid. So it
   // is special-cased here to make sure it is only added once.
   if (string.Compare(oneField, "id", true) != 0)
      viewFields.Append("<FieldRef Name='" + oneField + "'/>");
}

// Now plug in the ID.
viewFields.Append("<FieldRef Name='ID'/>");

// Set the fields to return.
qry.ViewFields = viewFields.ToString();

配置了查询后,下一步是执行查询。当返回结果时,再次检查分页方向。如果向后分页,则必须再次颠倒结果的顺序,以便它们按从小到大的 ID 显示在网页上。这同样仅仅是为了 Web 部件中的分页支持。为了简化排序,将数据检索到一个 ADO.NET DataTable 对象中。在检索了数据并对数据进行适当排序后,枚举每一行以创建从方法调用返回的 XML。

// Execute the query.
res = theList.GetItems(qry).GetDataTable();

// If we are going backward, we need to reorder the items so that
// the next and previous buttons work as expected; this puts it back 
// in 18,19,20 order so that the next or previous are based on 18
// (in this example) rather than 20.
if (direction == "PREV")
   res.DefaultView.Sort = "ID ASC";

// Create the root of the data.
ret.Append("<Items Count='" + theList.ItemCount + "'>");

// Enumerate results.
foreach (DataRowView dr in res.DefaultView)
{
   // Add the open tag.
   ret.Append("<Item ");

   // Add the ID.
   ret.Append(" ID='" + dr["ID"].ToString() + "' ");

   // Add each attribute.
   foreach (string oneField in fieldList)
   {
      // Add everything but the ID field.
      if (string.Compare(oneField, "id", true) != 0)
         ret.Append(oneField + "='" + dr[oneField].ToString() +
"' ");
   }

   // Add the closing tag for the item.
   ret.Append("/>");
}

// Add the closing tag.
ret.Append("</Items>");

当然,上面的所有代码都位于一个 try…catch 块中;在 finally 块中,我们释放与 DataTable 对象关联的资源,然后返回已创建的 XML。

finally
{
   // release the datatable resources
   if (res != null)
      res.Dispose();
   res = null;
}

return ret.ToString();

注册自定义 Web 服务

公开自定义 Web 服务以便在启用了 ASP.NET AJAX 的 Web 部件中使用它需要两种类型的配置。一种类型是配置此 Web 服务,以便 SharePoint 产品和技术了解它,并可以在 SharePoint Web 应用程序的上下文中调用它。此类型涉及到几个步骤,您大致需要执行下列操作:

  1. 将 Web 服务的代码隐藏类构建到一个单独的程序集中,并在全局程序集缓存中注册它。

  2. 生成并编辑静态发现文件和 Web Services 描述语言 (WSDL) 文件。

  3. 将 Web 服务文件部署到 _vti_bin 目录。

需要执行几个步骤才能完成上述所有操作。幸运的是,您可以找到一篇说明性文章,它说明了具体的步骤。有关完整的详细信息,请参阅演练:创建自定义 Web 服务

将 Web 服务集成到 SharePoint _vti_bin 目录后,您必须修改母版页,以添加一个 ASP.NET AJAX <ScriptManager> 标记。在 <ScriptManager> 标记内,为我们使用的自定义 Web 服务定义一个 Services 入口点;在此示例中,自定义 Web 服务名为 ListData.asmx。下面是添加到母版页的完整标记。

<asp:ScriptManager runat="server" ID="ScriptManager1">
   <Services>
      <asp:ServiceReference Path="_vti_bin/ListData.asmx" />
   </Services>
</asp:ScriptManager>

由于配置了 Web 服务,使得可以从 SharePoint _vti_bin 目录中调用它,而且向母版页添加了 <ScriptManager> 标记(包含对自定义 Web 服务的引用),因此,ASP.NET AJAX Web 服务客户端组件现在可以与之通信。

使用 XML 数据创建自定义 Web 部件

现在,我们需要一个自定义 Web 部件以生成 ASP.NET AJAX 客户端脚本,此脚本通过自定义 Web 服务检索数据。此 Web 部件本身几乎没有包含任何服务器端逻辑;大量的代码包含在此 Web 部件作为 Web 资源添加的一个 ECMAScript(Jscript、JavaScript)文件(WebResource.axd 文件)中。

第一步是生成在用户界面的网页中所需的所有 HTML。可以使用两种基本方法从 Web 部件生成 HTML。简单的方法是直接写出字符串形式的标记;更复杂但更安全的方法是使用 ASP.NET 类库。对于相对简单的 HTML,直接写出字符串通常更快和更容易。此例中使用的 HTML 稍微复杂一些。它包含三个对应于数据、导航控件和“please wait”界面元素的主 <div> 标记。“please wait”<div> 标记本身包含两个嵌套的 <div> 标记,用于正确定位部件中的图像和文本。基于这些较为复杂的 HTML 要求,使用了 ASP.NET 类库来生成 HTML,如下面的代码中所示。

// Add all the UI that is used to render the data.
// <div id='dataDiv' style='display:inline;'></div>
writer.AddAttribute(HtmlTextWriterAttribute.Id, "dataDiv");
writer.AddAttribute(HtmlTextWriterAttribute.Style, "display:inline;");
writer.RenderBeginTag(HtmlTextWriterTag.Div);
writer.RenderEndTag();

// Add a <div> tag to hold the navigation buttons.
// <div id='navDiv' style='display:inline;'>
writer.AddAttribute(HtmlTextWriterAttribute.Id, "navDiv");
writer.AddAttribute(HtmlTextWriterAttribute.Style, "display:inline;");
writer.RenderBeginTag(HtmlTextWriterTag.Div);

// Add the paging links inside the navigation <div> tag.
LinkButton btn = new LinkButton();
btn.Text = "<< Prev";
btn.Attributes.Add("href", "#");
btn.OnClientClick = "GetAjaxData('PREV');";
btn.RenderControl(writer);

writer.Write("&nbsp;");

btn = new LinkButton();
btn.Text = "Next >>";
btn.Attributes.Add("href", "#");
btn.OnClientClick = "GetAjaxData('NEXT');";
btn.RenderControl(writer);

// Close out the navigation <div> tag.
writer.RenderEndTag();

// Write the "please wait" <div> tag.
// <div id='waitDiv' style='display:none;'>
writer.AddAttribute(HtmlTextWriterAttribute.Id, "waitDiv");
writer.AddAttribute(HtmlTextWriterAttribute.Style, "display:inline;");
writer.RenderBeginTag(HtmlTextWriterTag.Div);

// Write the <div> tag to hold the "please wait" image. 
// <div style='float:left;'>
writer.AddAttribute(HtmlTextWriterAttribute.Style, "float:left;");
writer.RenderBeginTag(HtmlTextWriterTag.Div);

// Write the animated GIF tag.
// <img src='_layouts/images/gears_an.gif' alt='Please wait...'/>
writer.AddAttribute(HtmlTextWriterAttribute.Src, 
   "_layouts/images/gears_an.gif");
writer.AddAttribute(HtmlTextWriterAttribute.Alt, "Please wait...");
writer.RenderBeginTag(HtmlTextWriterTag.Img);
writer.RenderEndTag();

// Close the <div> tag for the image.
writer.RenderEndTag();

// Write the <div> tag for the text that goes next to the image.
// <div style='float:left;margin-top:22px;margin-left:10px;'>
writer.AddAttribute(HtmlTextWriterAttribute.Style,
   "float:left;margin-top:22px;margin-left:10px;");
writer.RenderBeginTag(HtmlTextWriterTag.Div);

// Write a header tag.
writer.RenderBeginTag(HtmlTextWriterTag.H4);
            
// Write the text.
writer.Write("Please wait while your data is being retrieved.");

// Close the header tag.
writer.RenderEndTag();

// Close the <div> tag for the text.
writer.RenderEndTag();

// Close the <div> tag for all of the "please wait" UI.
writer.RenderEndTag();

接下来,Web 部件添加几个隐藏的字段,用来在分页数据时跟踪状态。

// Add fields to keep track of number of items to see in a page and 
//current item number. PageSize is a web part property that can be set 
//to control the number of rows returned
Page.ClientScript.RegisterHiddenField("siteUrl", 
SPContext.Current.Web.Url);
Page.ClientScript.RegisterHiddenField("listName", "Contacts");
Page.ClientScript.RegisterHiddenField("pageSize", PageSize);
Page.ClientScript.RegisterHiddenField("totalItems", "-1");
Page.ClientScript.RegisterHiddenField("startID", "-1");
Page.ClientScript.RegisterHiddenField("endID", "-1");

最后,注册一个启动脚本,以便在网页加载时 Web 部件检索数据的第一页:

创建启动脚本以便在网页加载时调用我们的数据加载方法。

if (!Page.ClientScript.IsStartupScriptRegistered(JSCR_START)) 
Page.ClientScript.RegisterStartupScript(this.GetType(), 
JSCR_START, "GetAjaxData('NEXT');", true);

用于检索数据的 ECMAScript(Jscript、JavaScript)在 OnPreRender 事件中注册。前面说明了为 XML 数据岛 Web 部件添加作为嵌入资源的脚本并在 AssemblyInfo.cs 文件中注册它的过程,这里也使用此过程。ECMAScript 文件的注册如下所示。

// Register our JScript resource.
if (!Page.ClientScript.IsClientScriptIncludeRegistered(JSCR_NAME))
Page.ClientScript.RegisterClientScriptInclude(this.GetType(),
JSCR_NAME, Page.ClientScript.GetWebResourceUrl(this.GetType(),
"Microsoft.IW.ajaxdata.js"));

利用所创建的 HTML,JScript 或 JavaScript 文件可以在以下情况中生成“please wait”界面:网页加载并且请求数据,以及每次因为用户单击了“Next”或“Prev”链接而检索新一页的数据。在此例中,所使用的动画 GIF 是 SharePoint 产品和技术附带的,因此用户对它很熟悉。“please wait”界面将如下所示。

图 3.“Please wait”界面

Web 部件数据检索消息

所有数据检索和用户界面管理均由客户端脚本处理。JScript 或 JavaScript 首先更改界面以隐藏包含列表数据和分页控件的 DIV 元素,再显示“please wait”界面。然后,它使用隐藏的字段为 Web 服务方法的参数收集信息,再使用 ASP.NET AJAX 框架来调用自定义 Web 服务方法。

// This is declared as a global var but could have also been output
// by the Web Part into a hidden field; notice that the InternalName 
// for the list fields must be used.
var fields = new Array("FirstName", "Company", "WorkCity");

// Get the vars containing the data we're going to use.
var url = document.getElementById("siteUrl").value;
var list = document.getElementById("listName").value;
var ps = document.getElementById("pageSize").value;
var ti = document.getElementById("totalItems").value;
var startID = document.getElementById("startID").value;
var endID = document.getElementById("endID").value;
// Some code here to determine the startID for the page.

// Make the call to get the data.
ret = Microsoft.IW.AjaxDataWebService.GetListData(url, list, startID, 
ps, 
   fields, dir, OnComplete, OnTimeOut, OnError);
return true; 

通过 ASP.NET AJAX 来调用 Web 服务与传统的基于 SOAP 的 Web 服务有些不同。首先要注意的是,在调用 Web 服务方法时,必须使用完全限定类名。另一个不同的地方是,除了为 Web 服务方法提供所有参数之外,还在这些参数的末尾添加了三个额外的参数。它们代表 JScript 或 JavaScript 中的函数,分别在以下情况中调用:在返回数据时 (OnComplete)、如果调用超时 (OnTimeOut) 或如果出错 (OnError)。在此示例部件中,OnTimeOut 函数和 OnError 函数只是呈现了一些信息,这些信息直接返回到通常显示数据的 DIV 元素。

OnComplete 函数是这三个参数中唯一的必需参数,而且它只是一个如下所示的 JScript 函数。

function OnComplete(arg)
{
  …
}

arg 参数包含 Web 服务方法调用中的返回值;在此例中,它是一个包含 XML 的字符串。对于此项目,Web 服务返回的 XML 在格式上与 XML 数据岛 Web 部件相同。因此,用于枚举数据并将其呈现在网页上的代码几乎是相同的。它首先创建一个 MSXML DOMDocument,然后确认返回了某些有效的 XML。

// Get the XML DOMDocument. 
var xDoc = createXdoc();
   
// Validate that a document was created.
if (xDoc == null)
{
target.innerHTML = "<font color='red'>A required system component 
(MSXML) is not installed on your computer.</font>";
   return;
}
   
// Load the XML from the Web service method into the document.
xDoc.async = false;
xDoc.loadXML(arg);
   
// Check for parsing errors.
if (xDoc.parseError.errorCode != 0) 
{
   var xErr = xDoc.parseError;
target.innerHTML = "<font color='red'>The following error was 
encountered while loading the data for your selection: " + 
xErr.reason + "</font>";
   return;
}
   
// Get all the items.  
var xNodes;
xDoc.setProperty("SelectionLanguage", "XPath");
xNodes = xDoc.documentElement.selectNodes("/Items/Item");  
   
// Check for errors.
if (xNodes.length == 0)
   target.innerHTML = "<font color='red'>No data was found.</font>";
else
{
   // Code in here is virtually identical to XML island code.
}

此处的呈现代码与 XML 数据岛 Web 部件不同之处仅在于:第一项和最后一项的 ID 分别存储在 startID 和 endID 隐藏字段中,以便支持控件的分页功能。在 Web 部件检索数据后,它在适当的 DIV 元素中呈现数据,并允许您向前或向后分页内容。图 4 和图 5 显示数据的前两页。

图 4. 第一页的数据

AJAX Web 部件的首页

图 5. 第二页的数据

示例 AJAX Web 部件 - 第二页

使用 JSON 创建自定义 Web 部件

在前面的示例中,从 Web 服务方法返回了 XML,然后使用 XPath 和 MSXML DOMDocument 来读取内容。但是,ASP.NET AJAX 最强大的功能之一是它能够通过 JavaScript Object Notation (JSON) 来使用数据。这允许客户端开发人员使用对象和属性来访问数据,而不是处理更复杂的 XML。

为说明这一点,创建了第二个 Web 方法以返回一个自定义类;其方法签名如以下代码所示。

[WebMethod]
public Records GetListDataJSON(string webUrl, string listName, int 
startingID, int pageSize, string[] fieldList, string direction)

Records 类是一个自定义类,开发它是为了保存返回的数据;其定义如以下代码所示。

[Serializable()]
public class Records
{
   public int Count = 0;
   public int ItemCount = 0;
   public List<Record> Items = new List<Record>();

   public Records()
   {
      // Default constructor.
   }
}

在 Records 类中,Count 属性表示找到的项目总数,而 ItemCount 属性表示在此特定的调用中返回的项目数。这些属性用于在客户端上分页数据。实际显示的数据包含在 Items 属性中,这是 Record 项目的列表。Record 类的定义如下。

[Serializable()]
public class Record
{
   public SerializableDictionary<string, string> Item = 
      new SerializableDictionary<string, string>();

   public Record()
   {
      // Default constructor.
   }
}

Record 类只有一个属性,即支持序列化的自定义词典类型。Microsoft .NET Framework 2.0 中默认的 Dictionary 类不支持序列化,而 JSON 在默认情况下会序列化所有返回的数据。在此例中,需要一个 Dictionary 类型的类,以便不必将各个属性名称映射到此类,而是可以将它们作为键/值对添加。例如,myValuePair.Item.Add(someKey, someValue)。

备注

本文不介绍自定义词典类;但是,这里所用的类基于在 Paul Welter 的博客中介绍的工作,其内容公布在 XML 序列化通用词典(该链接可能指向英文页面)一文中。

此 Web 方法检索数据的方式与 XML 版本相同。它通过使用下面的代码创建方法的返回值。

Records ret = new Records();
DataTable res = null;

…
// Method is called to retrieve and sort data, and get total number of 
//items.
…

// Set the count of total and returned items.
ret.Count = myInternalWebServiceVariableThatTracksNumItems;
ret.ItemCount = res.Count;

// Enumerate results.
if (res != null)
{
   foreach (DataRowView dr in res)
   {
      // Create a new record.
      Record rec = new Record();

      // Add the ID.
      rec.Item.Add("ID", dr["ID"].ToString());

      // Add each attribute.
      foreach (string oneField in fieldList)
      {
         // Add everything but the ID field.
         if (string.Compare(oneField, "id", true) != 0)
            rec.Item.Add(oneField, dr[oneField].ToString());
      }

      // Add the record to the collection.
      ret.Items.Add(rec);
   }
}
return ret;

既然此方法返回一个类,我们就可以使用此类的属性在客户端脚本中枚举数据。我们也不用再创建 MSXML DOMDocument 来枚举结果,因而我们的客户端脚本变得简单了许多。呈现详细信息的实际代码如下所示。

// Will hold our output.
var output;
   
// Check for data. Count is a property on the Records class.
if (arg.Count == 0)
   target.innerHTML = "<font color='red'>No data was found.</font>";
else
{
   // Store the total items.
   ti.value = arg.Count;
      
   // Create a table to render the data. Straight
   // HTML goes here.
   …   
      
   // Cycle through all the data. ItemCount is a 
   // property of the Records class.
   for (var i = 0; i < arg.ItemCount; i++)
   {
      // Store page data for the first and last row.
      if (i == 0) 
         startID.value = arg.Items[i].Item["ID"];
      if (i == (arg.ItemCount - 1)) 
         endID.value = arg.Items[i].Item["ID"];
            
      // Create a new row.
      output += "<tr>";
                  
   // Add each cell.
      output += "<td style='width:75px;'>" + 
         arg.Items[i].Item["FirstName"] + "</td>";
      output += "<td style='width:75px;'>" + 
         arg.Items[i].Item["Company"] + "</td>";
      output += "<td style='width:75px;'>" + 
         arg.Items[i].Item["WorkCity"] + "</td>";
         
      // Close the row tag.
      output += "</tr>";
   }

   // The final HTML goes here to close up the TABLE tags.
   …
   
   // Plug the output into the document.
   target.innerHTML = output;
}

用户界面看起来与使用 XML 的版本完全一样。此部件支持向前和向后分页数据。当数据初次加载,以及当用户单击“Next”或“Prev”分页链接时,它会显示“please wait”界面。

测量结果

在低带宽或高延迟的网络中,像这样的解决方案可以对所使用的网络资源产生非常积极的影响。我们编写了第二个 Web 部件,以便从同一个列表发出完全相同的数据。但是,所有数据均在服务器上的 Web 部件中生成,然后作为 HTML 写入网页。结果,用户每次单击“Prev”或“Next”链接时,它会强制回发到服务器,并导致将整个网页发送回客户端。很重要的是,还要注意到如果我们选择了改用 ASP.NET AJAX UpdatePanel 控件,则将发生同样的过程。网页的所有表单变量均回发到服务器,并且通过请求发送回整个网页。但是,只会更新在 UpdatePanel 中包含的网页部分。图 6 显示了在单击第二个 Web 部件上的“Next”链接后由 Fiddler 捕获的请求快照。

图 6. 单击第二个 Web 部件上的“Next”链接所产生的请求

Web 部件的 Fiddler 结果

Fiddler 捕获的快照表明,以回发样式呈现列表数据所产生的请求和响应通过网络总共发送了 79,424 个字节。另一方面,图 7 显示在以下情况中 Fiddler 捕获的快照:使用已启用 ASP.NET AJAX 的 Web 部件,通过自定义 Web 服务使用 XML 来检索相同的数据。

图 7. Web 部件通过自定义 Web 服务使用 XML 检索相同的数据

自定义 Web 服务的 Fiddler 结果

检索了相同的列表数据,但是通过网络仅发送了 1973 个字节。这是一个巨大的差异,因此,巧妙地使用此方法可以大幅减少网络流量。但是,在所有有效负载中,最小的有效负载是通过使用 Web 服务方法(它使用 JSON 来返回 Records 类)生成的,如图 8 中所示。

图 8. 使用了 JSON 来返回 Records 类

JSON 的 Fiddler 结果

结束语

通过使用 JSON,我们可以将通过网络为一个请求发送的总有效负载减少到 1817 个字节,也就是说,对于执行完整的网页回发以检索和分页数据的部件,为其减少了 98% 的请求大小。我们还可以减少用于枚举数据的 ECMAScript(JScript、JavaScript)的大小,而且在此过程中也同时简化了代码。

虽然开发这样一个解决方案较为复杂,但如果您的网站的带宽或延迟受到限制,则此方法不失为一个较好的选择,它能帮助改善性能和最终用户的体验。

其他资源

有关详细信息,请参阅以下资源:

下载此书籍

本主题包含在以下可下载书籍内,以方便您阅读和打印:

有关可下载书籍的完整列表,请参阅 Office SharePoint Server 2007 的可下载书籍