共用方式為


最佳化 WAN 的自訂網頁組件

**摘要:**學習自訂網頁組件時對頻寬影響降至最低的技巧,包含一般樣式建議,以及使用其他方法從 SharePoint 清單擷取及轉譯資料的特定資訊與程式碼範例 (24 列印頁面)。

Steve Peschka Microsoft Corporation

2008 年 1 月

**適用於:**Microsoft Office SharePoint Server 2007、Windows SharePoint Services 3.0、ASP.NET AJAX 1.0

內容

  • 簡介 WAN 的最佳化自訂網頁組件

  • 重複使用內建樣式或建立自訂樣式

  • 儲存狀態

  • 顯示資料網頁組件的最大化效能

  • 總結

  • 其他資源

簡介 WAN 的最佳化自訂網頁組件

開發要使用在高延遲或低頻寬網站的自訂網頁組件時,必須將焦點放在建立該類型環境頁面時使用的相同一般設計原則。應該致力於設計可減少來回伺服器的組件,及在網路中傳送的資料量。本文將說明可使用來達成設計目標的數種技巧。

重複使用內建樣式或建立自訂樣式

網頁組件發出 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 的樣式可確保不需要下載額外的樣式支援。

若組件必須要有自訂樣式,可考慮使用 能與 Bolb 快取同時使用的自訂樣式表。若您將其儲存於文件庫,樣式表會有與其有關的 Cacheability 指令,如此一來,在最初的點閱頁面後即無須再次下載。例如,相較於使用內嵌樣式,這樣對網站造成的影響較小 (因為每次轉譯該組件時,此內嵌樣式會在網路中傳輸)。

儲存狀態

網頁組件可能需要追蹤如使用者、要求及資料來源這類的資訊。有數種可供儲存狀態的選項,每個選項的說明不在本文的範圍內。但是一般而言,有兩種常用的方法可與網頁組件併用:ViewState 和伺服器快取。在低頻寬或高度延遲的環境中,若可能請避免使用 ViewState,因為這會在下載和發生任何回傳時,新增頁面的內容。這可套用到涉及在網路中傳輸資料的其他狀態形式,例如查詢字串、隱藏的欄位及 Cookie。

使用伺服器 Cache 類別讓您可在伺服器層級上儲存狀態資訊。使用伺服器快取的缺點為不是真的要當作每個使用者狀態機制使用 (雖然依情況的不同,可以讓使用者狀態機制如此運作)。此外,伺服器陣列的所有前端網頁伺服器均無法複製快取資訊。若您的組件是依呈現的狀態資訊,不管使用者要求 最後指向哪個前端網頁伺服器,那麼伺服器快取不是最好的選擇。

在此案例中,另一個選擇是使用 [工作階段狀態]。[工作階段狀態] 的預設是關閉,但在伺服器陣列中啟動 Microsoft Office InfoPath Forms Services (IPFS) 時,這個狀態會被啟用。啟用時,使用 Microsoft SQL Server 追蹤狀態,這也就是表示不論哪個前端網頁伺服器收到 HTTP 要求,皆可以使用工作階段狀態值。工作階段狀態的缺點是資料會保留在記憶體中,直到資料移除或到期為止。若沒有小心管理,在工作階段狀態儲存的大量資料集會因此降低伺服器效能。因為這些限制,除非絕對必要,否則不建議使用工作階段狀態。

也可嘗試編輯 Web 應用程式的 web.config 檔案,將網站所有頁面的 ViewState 設定為關閉。其包含具有 enableViewState 屬性的 pages 元素。若對頁面中 ViewState 的大小有特別顧慮,可嘗試將此屬性設定為 false (預設為 true)。如果您這樣做的話,需要測試網站及所有功能,確保這些能夠正常運作,因為部份控制項和網頁組件可能需要 ViewState 為啟動。

若您正在開發網頁組件,且需要使用類似 ViewState 功能,但不確定是否能夠使用在頁面,您可使用 [控制項] 狀態替代 (這是 ASP.NET 2.0 的新功能)。簡而言之,需要執行下列動作:

  1. 初始化期間呼叫 Page.RegisterRequiresControlState 可登錄控制項狀態。

  2. 覆寫 LoadControlState 方法和 SaveControlState 方法。

  3. 手動管理對應到控制項狀態的物件集合中屬於您的部份。

顯示資料網頁組件的最大化效能

若您的網頁組件將用以顯示資料,可嘗試數種方式將一般使用者的效能經驗發揮至極限。一般而言,在伺服器通訊所需要的次數和每項要求所擷取的資料量之間必須取得平衡。

提供轉譯限制

發出資料列的控制項可包含屬性,讓管理員控制顯示列數。根據控制所要使用的延遲性和頻寬,可允許調高或調低每個頁面要求中所轉譯的資料量。這也會影響檢視所有資料所需的要求數。

若一般使用者可設定傳回列數的屬性,可考慮新增限制,以免讓使用者的選擇壓垮網路。

使用內嵌 XML 資料島

顯示資料的另一個替代方案是使用內嵌 XML 資料島,並在用戶端執行轉譯。使用此方案將此所有將顯示的資料會以 XML 資料島形式發出到頁面上。用戶端指令碼是實際用在轉譯頁面的資料,且負責使用 XML DOM 從資料島擷取及顯示資料。

如此一來,您可使用單一要求來擷取所有資料,因此最小化來回伺服器通訊的次數。但是,下載的大小會明顯地變大,所以初始頁面的載入時間會加長。此外,若使用於頁面中的其他控制項造成回傳,便會每次發生此大量資料下載。此技巧最適用於確定不會發生類似狀況的案例,否則請將此技巧視為選用的資料擷取方法。例如,建立公用屬性以追蹤是否需要下載所有資料,讓網站管理員可控制該行為。

下列是網頁組件如何完成這項工作的簡單範例,包含從清單讀取所有的內容、以 XML 發出到頁面,以及使用用戶端 ECMAScript (JScript, JavaScript) 來轉譯及分頁資料。

注意

本文所列的程式碼並非代表 Microsoft 最佳程式碼實作。程式碼已經過簡化,以簡化範例;若要在產品環境中使用,需要變更程式碼元素。

在名稱為 連絡人 的網站中,取得其清單的內容後,網頁組件會覆寫 Render 方法,並進行啟動。

// 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)。為下載而設定的程式碼會在網頁組件的 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. 網頁組件的資料

範例網頁組件的第一頁

圖 2. 網頁組件的資料

網頁組件資料的第二頁

使用用戶端指令碼連線至 Web 服務

管理網路傳送資料量的另一個選項是使用用戶端指令碼連線至 Web 服務,以擷取資料。這個方法允許控制項擷取所需的資料,而不必回傳整個頁面。這不僅可讓使用者能有更好的體驗,而且因為不需要將整個頁面傳送到伺服器,及再傳送回擷取資料,以致可減輕網路負載。可使用 XmlHttp 元件直接擷取資料,或使用 Microsoft AJAX Library 1.0 (功能包裝住 XmlHttp) 擷取資料。

雖然 XmlHttp 和 ASP.NET AJAX 代表兩種高階的選項,可容易一起使用 ASP.NET AJAX 和自訂 Web 服務顯露 SharePoint 清單資料。技術上雖可使用 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 服務元件。然後 Web 服務會「註冊」至 SharePoint 產品及技術,如此一來,可透過 _vti_bin 目錄和所有內建的 Web 服務存取這項服務。更新主版頁面,讓主版頁面包含 ASP.NET AJAX ScriptManager 控制項和自訂 Web 服務的宣告標籤。然後撰寫網頁組件以產生用戶端指令碼,再透過自訂 Web 服務的 ASP.NET AJAX 元件擷取資料,並將此資料顯示在頁面中。

安裝 AJAX

首先,必須將 ASP.NET AJAX 1.0 二進位檔安裝到您的 SharePoint 服務。您可造訪 ASP.NET AJAX 網站 (英文) 進行下載。您必須在伺服器陣列中的每一個前端網頁伺服器進行安裝。

安裝 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;

程式碼的第一個部份會取得包含資料的網站、網頁和清單的參照。

// 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];

接著,會依據分頁方向、起始識別碼及結果集大小建立查詢語意。

// 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。此外,這完全是網頁組件的分頁支援。若要簡化排列,會將資料擷取到 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 服務

顯示使用在 ASP.NET AJAX 啟用網頁組件的自訂 Web 服務需要進行兩種設定。一種是設定讓 SharePoint 產品及技術知道 Web 服務,並可在 SharePoint Web 應用程式中進行呼叫。此作業包含數個高階步驟需要您執行以下動作:

  1. 將 Web 服務的程式碼後置類別建置到另一個組件,再將其登錄至全域組件快取。

  2. 產生及編輯靜態探索檔和 Web 服務描述語言 (WSDL) 檔。

  3. 部署 Web 服務檔案到 _vti_bin 目錄。

完成所有前述動作需要執行數個步驟。幸運的是,已經有精準的文章說明如何進行這項作業。如需完整的詳細資訊,請參閱 逐步解說:建立自訂 Web 服務 (英文)

將 Web 服務整合到 SharePoint _vti_bin 目錄之後,您必須修改主版頁面,新增 ASP.NET AJAX <ScriptManager> 標籤。在 <ScriptManager> 標籤內,定義使用中的自訂 Web 服務的「服務」進入點;在此範例中,自訂 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 資料建立自訂網頁組件

現在需要自訂網頁組件產生 ASP.NET AJAX 用戶端指令碼,以便從自訂 Web 服務中擷取資料。網頁組件本身幾乎不包含伺服器端邏輯;程式碼區塊包含在 ECMAScript (Jscript, JavaScript) 檔案中,而網頁組件會將其新增為 Web 資源 (WebResource.axd 檔案)。

第一步是要產生使用者介面頁面上所需的所有 HTML。有兩種基本方法能夠從網頁組件產生 HTML。較簡單的方法是將標籤直接寫為字串;較複雜但安全的方法是使用 ASP.NET 類別庫。利用相對較簡單的 HTML,通常可較快速且簡單地直接發出字串。在本範例中使用的 HTML 稍微複雜一些。其中包含資料、導覽控制項和「請稍候」介面元素的三個主要 <div> 標籤。「請稍候」<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();

其次,網頁組件會新增數個隱藏的欄位。分頁資料時,這些欄位可用以追蹤狀態。

// 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");

最後,登錄啟動指令碼,如此一來,載入頁面時,網頁組件將會擷取資料的第一個頁面。

建立啟動指令碼,以於載入頁面時呼叫我們的資料載入方法。

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

用以擷取資料的 EMCAScript (Jscript, JavaScript) 是在 OnPreRender 事件中登錄。如 XML 島網頁組件中說明的相同程序 (新增指令碼為內嵌資源,並將其登錄在 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 檔案可產生「請稍候」介面。在這個範例中,所使用的動畫 GIF 是包含在 SharePoint 產品及技術,所以對使用者來說較為熟悉。「請稍候」介面看起來如下。

圖 3.「請稍候」介面

網頁組件資料擷取訊息

所有的資料擷取和使用者介面管理都是由用戶端指令碼處理。JScript 或 JavaScript 會開始變更介面,隱藏包含清單資料和分頁控制項的 DIV 元素,及顯示「請稍候」介面。然後使用隱藏的欄位來收集 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 服務方法的所有參數之外,在結尾要新增三個其他參數。資料傳回時 (OnComplete)、若呼叫逾時 (OnTimeOut) 或發生錯誤 (OnError),這時會呼叫在 JScript 或 JavaScript 中所代表的函數。在此範例部份中,只有直接傳回到 DIV 元素 (通常為資料顯示所在) 的資料,OnTimeOut 函數和 OnError 函數才會轉譯該資訊。

OnComplete 函數是三個之中唯一必要的參數,其為 JScript 函數,看起來如下所示。

function OnComplete(arg)
{
  …
}

在 arg 參數包含 Web 服務方法呼叫的傳回值;在此範例中是包含 XML 的字串。此專案中,Web 服務傳回 XML,其是相同格式並用於 XML 島網頁組件。因此,列舉資料及在頁面上將此資料轉譯的程式碼是幾乎相同。首先會建立 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 島網頁組件的唯一不同點在於第一個和最後一個項目的 ID是儲存在 startID 和 end ID 隱藏欄位中,以支援控制項的分頁功能。網頁組件擷取資料之後,在適當的 DIV 元素中轉譯擷取的資料,並允許在內文中往上一頁或下一頁。圖 4 和圖 5 顯示資料的最前面兩頁。

圖 4. 資料的第一頁

AJAX 網頁組件的第一頁

圖 5. 資料的第二頁

範例 AJAX 網頁組件 - 第二頁

使用 JSON 建立網頁組件

在先前的範例中,XML 會從 Web 服務方法傳回,然後使用 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 類別定義如下。

[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 的版本看起來完全一樣。該組件支援正向或反向分頁資料。首次載入資料,以及使用者按一下「上一頁」或「下一頁」分頁連結時,會顯示「請稍候」介面。

測量結果

在低頻寬或高度延遲的網路中,像這樣的方法對於所使用的網路資源有非常正面的影響。撰寫第二個網頁組件,如此可從相同的清單發出相同的資料。但是,所有資料是由伺服器上的網頁組件產生,再以 HTML 寫到頁面。如此一來,每次按一下「上一頁」或「下一頁」連結時,便會強制回傳到伺服器,使整個頁面傳送回用戶端。值得注意的還有,若選擇使用 ASP.NET AJAX UpdatePanel 控制項作為替代,同樣的程序也會發生。頁面的所有表單變數都會回傳到伺服器,而整個頁面會依要求傳送回來。但是,只在 UpdatePanel 中包含的部份頁面會更新。圖 6 顯示是以 Fiddler 擷取在按一下第二個網頁組件上的「下一頁」連結時的快照。

圖 6. 顯示在第二個網頁組件上按一下「下一頁」連結的要求

網頁組件的 Fiddler 結果

Fiddler 擷取顯示執行回傳式轉譯網路中傳送總數 79,424 個位元組的清單資料的要求及回應。此外,圖 7 顯示藉由利用 XML,經由自訂 Web 服務,使用 ASP.NET AJAX 啟用的網頁組件擷取相同資料的 Fiddler 擷取圖。

圖 7 顯示藉由利用 XML,網頁組件經由自訂 Web 服務擷取相同資料

自訂 Web 服務的 Fiddler 結果

雖然擷取相同的清單資料,但網路只傳送 1973 個位元組。結果造成極大的不同,聰明使用此方法可大幅減少網路流量。但是,產生最小裝載所使用的 Web 服務方法是使用 JSON 來傳回 Records 類別 (如圖 8 所示)。

圖 8. 顯示使用 JSON 傳回「Records」類別

JSON 的 Fiddler 結果

總結

使用 JSON 可針對一個要求在網路傳送的總裝載減少到 1817 個位元組,也就是在組件回傳一整個頁面來擷取及分頁資料時的要求大小減少達 98 %。也因此能夠刪減用以發出資料的 ECMAScript (JScript, JavaScript) 大小,並在此過程中簡化程式碼。

雖然開發像這樣的方法比較複雜,但若網站的頻寬或延遲受限,此方法會是好的選擇,可助您改善效能及一般使用者的經驗。

其他資源

如需詳細資訊,請參閱下列資源:

下載本書

本主題隨附於下列可下載的叢書中,以便於閱讀與列印:

請參閱 Office SharePoint Server 2007 可下載的內容 (英文) 上提供的完整叢書清單。