次の方法で共有


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

目次

  • カスタム Web パーツの WAN 向け最適化の概要

  • 組み込みスタイルを再利用する、またはカスタム スタイルを作成する

  • 状態を保存する

  • データを表示する Web パーツのパフォーマンスを最大にする

  • まとめ

  • 参考資料

カスタム Web パーツの WAN 向け最適化の概要

待機時間が長い、または帯域幅の低いサイトで使用するカスタム 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 の 2 つがあります。低帯域幅、または待機時間が長い環境では、可能な限り 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) に設定してみることができます。ViewState をオフにする場合は、サイトおよびすべての機能を徹底的にテストし、正しく動作することを確認する必要があります。一部のコントロールや Web パーツでは、ViewState がオンであることを想定している可能性があります。

Web パーツを開発していて、ViewState のような機能を使用する必要があり、ページでそれを使用できるかどうかが不明な場合は、代わりに ASP.NET 2.0 の新機能であるコントロール ステートを使用できます。つまり、以下を行う必要があります。

  1. 初期化している間に Page.RegisterRequiresControlState を呼び出して、コントロール ステートに登録します。

  2. LoadControlState メソッドと SaveControlState メソッドをオーバーライドします。

  3. オブジェクト コレクションの中でコントロール ステートにマップされる部分を手動で管理します。

データを表示する Web パーツのパフォーマンスを最大にする

Web パーツをデータの表示に使用する場合は、エンドユーザーが実感するパフォーマンスを最大限高めるために試すことが可能な方法がいくつかあります。通常、サーバーに対して必要なトリップの数と、1 回の要求で取得する必要のあるデータの量とのバランスについて考慮する必要があります。

表示の制限を設ける

データの行を生成するコントロールについては、表示される行数を管理者が制御できるようにするプロパティを組み込みます。コントロールが使用される待機時間と帯域幅に応じて、各ページ要求で表示されるデータの量を柔軟に増やしたり減らしたりできます。また、すべてのデータを表示するために必要な要求の数も制御できます。

返される行数がエンドユーザーでも設定できるプロパティの場合は、1 人のユーザーの設定によってネットワークが占有されないように制限を設けることを検討します。

インライン XML データ アイランドを使用する

データを表示するもう 1 つの方法は、インライン XML データ アイランドを使用し、クライアント側で表示を実行することです。この方法では、表示されるすべてのデータは、XML データ アイランドとしてページに生成されます。ページでの実際のデータ表示はクライアント側スクリプトが行い、XML DOM を使用してアイランドからデータを取得して表示します。

この方法を使用すると、1 回の要求ですべてのデータを取得できるので、サーバーへのラウンドトリップの数が最小になります。ただし、ダウンロード サイズは確実に大きくなるので、ページの初期読み込みの時間は長くなります。また、ポストバックが発生する他のコントロールが使用されているページでこの方法を使用すると、毎回この大きいデータのダウンロードが強制されます。この手法は、このようなことが発生しないことがわかっているシナリオに使用するのが最適であり、そうでない場合はオプションのデータ取得方法として使用します。たとえば、すべてのデータをダウンロードする必要があるかどうかを追跡するためのパブリック プロパティを作成し、サイトの管理者が動作を制御できるようにします。

これを行う比較的簡単な Web パーツの例を次に示します。この例では、リストからすべてのコンテンツを読み取り、XML としてページに生成し、クライアント側の ECMAScript (JScript、JavaScript) を使用してデータの表示とページ移動を行っています。

注意

この記事のコードは、Microsoft が勧める最善のコーディング手法を示すことを目的としたものではありません。コードは簡単に試すことができるように単純化してあります。運用環境で使用するには、コードの要素を変更する必要があります。

この Web パーツは、Render メソッドをオーバーライドし、最初に Contacts という名前の Web サイトにあるリストのコンテンツを取得します。

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

インターフェイスで次に必要になるのは、データのページ移動を行う部分です。この例では、2 つのリンク ボタンをページに追加します。注意が必要なのは、OnClientClick プロパティと Attributes コレクションの 2 つです。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. は、2 つの異なるページのデータを示しています。

図 1. データが表示されている Web パーツ

サンプル Web パーツの先頭ページ

図 2. データが表示されている Web パーツ

Web パーツ データの 2 ページ目

Web サービスに接続するクライアント側スクリプトを使用する

ネットワーク上を送信されるデータの量を管理するもう 1 つの方法は、Web サービスに接続してデータを取得するクライアント側スクリプトを使用するものです。この方法では、コントロールでページ全体をポストバックすることなく必要なデータを取得できます。この方法は、ユーザーにとって操作性がよくなるだけでなく、データを取得するためにページ全体をサーバーに送信して再度戻してもらう必要がないので、ネットワークの負荷も軽くなります。データの取得は、XmlHttp コンポーネントで直接行うことも、XmlHttp の機能をラップする Microsoft AJAX Library 1.0 を使用して行うこともできます。

XmlHttp と ASP.NET AJAX はどちらも高いレベルのオプションですが、SharePoint リスト データを公開するカスタム Web サービスと組み合わせて ASP.NET AJAX を使用するのが最も簡単であると考えられます。技術的には、ASP.NET AJAX ライブラリと SOAP ベースの Web サービス (SharePoint 製品とテクノロジに組み込まれているような Web サービス) を使用することもできますが、実行手順は非常に複雑になります。小さいペイロード サイズ、JavaScript Object Notation (JSON) のサポートなど、ASP.NET AJAX スタイルの Web サービスを使用することによるメリットもありません。

SharePoint 製品とテクノロジでは、データの表示に使用できる複数の Web サービスが提供されています。Lists Web サービスを使用すると、SharePoint 製品とテクノロジのリストおよびライブラリから表形式のデータを取得できます。これを使用すると、リストに含まれるデータと、ドキュメント、画像など、リスト内のアイテムへのリンクを表示できます。Search Web サービスでは、SharePoint 製品とテクノロジに含まれるコンテンツの本体、およびクロールされている他の外部ソースを検索できます。メタデータ駆動のクエリを綿密に作成することで、1 つ以上の SharePoint リストからフィルタ処理済みのデータ セットを取得することもできます。

次の例では、カスタム Web サービスを作成して SharePoint 製品とテクノロジからリスト データを取得する方法を示します。Web サービス クラスには System.Web.Script.Services.ScriptService 属性で注釈が付いているので、ASP.NET AJAX で提供されるクライアント側 Web サービス コンポーネントを使用できます。その後 Web サービスは SharePoint 製品とテクノロジに "登録" され、すべての組み込み Web サービスで _vti_bin ディレクトリを介してアクセスできるようになります。マスタ ページを更新し、ASP.NET AJAX ScriptManager コントロールおよびカスタム Web サービスの宣言タグを組み込みます。それから、ASP.NET AJAX コンポーネント経由でカスタム Web サービスからデータを取得してページに表示するクライアント側スクリプトを生成する Web パーツを作成します。

AJAX をインストールする

最初に、ASP.NET AJAX 1.0 バイナリを SharePoint サーバーにインストールする必要があります。バイナリは、ASP.NET AJAX のサイト (英語)からダウンロードできます。ファーム内の各フロントエンド Web サーバーにこれをインストールする必要があります。

ASP.NET AJAX をインストールした後、ASP.NET AJAX を使用する予定の各 Web アプリケーションの web.config ファイルを更新する必要があります。これは少し時間のかかる作業になります。詳細な手順については、Mike Ammerlan が投稿しているブログ「Integrating ASP.NET AJAX with SharePoint (英語)」を参照してください。

カスタム Web サービスを作成する

データを取得するためにカスタム Web サービスを作成する必要があるのは、System.Web.Script.Services.ScriptService を Web サービス クラスに適用できるようにするためであり、それによって ASP.NET AJAX Web サービス フレームワークで Web サービスを使用できるようになります。この例では、基本パラメータに基づいてリスト データを取得する比較的簡単な Web サービス クラスを作成しました。

Web サービス クラスには、ASP.NET AJAX のサポートを有効にするために、System.Web.Extensions クラスへの参照を組み込みます。その参照を追加した後、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 サービスには文字列を返すメソッドが 1 つだけ含まれます。この文字列は、実際には、クライアントで使用される 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 サービスを登録する

ASP.NET AJAX 対応の Web パーツで使用するためにカスタム Web パーツを公開するには、2 種類の構成が必要です。そのうちの 1 種類の構成では、SharePoint 製品とテクノロジが Web サービスについて認識し、SharePoint Web アプリケーションのコンテキストで Web サービスを呼び出すことができるようにします。これには、次に示す高いレベルでの手順がいくつか伴います。

  1. Web サービス用の分離コード クラスを別のアセンブリとして作成し、それをグローバル アセンブリ キャッシュに登録します。

  2. スタティック探索ファイルおよび WSDL (Web サービス記述言語) ファイルを生成して編集します。

  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 サービス クライアント コンポーネントは、この Web サービスと通信できます。

XML データを使用してカスタム Web パーツを作成する

次に、カスタム Web パーツで、カスタム Web サービスからデータを取得するための ASP.NET AJAX クライアントスクリプトを生成する必要があります。Web パーツ自体には、サーバー側ロジックはほとんど含まれません。コードの大部分は、Web パーツが Web リソース (WebResource.axd ファイル) として追加する ECMAScript (Jscript、JavaScript) ファイルに含まれます。

最初に、ページのユーザー インターフェイスで必要なすべての HTML を生成します。Web パーツから HTML を生成するには、基本的に 2 つの方法があります。簡単なのは、文字列としてタグを直接書き出す方法です。ASP.NET クラス ライブラリを使用する方法は、これより複雑になりますが安全性は増します。比較的単純な HTML の場合は、通常、単に文字列を生成する方が時間がかからず簡単です。この例で使用する HTML は、少し複雑です。データに関する主要な 3 つの <div> タグ、ナビゲーション コントロール、および "please wait" (お待ちください) というインターフェイス要素が含まれます。"please wait" <div> タグには、パーツ内の画像とテキストを正確に配置するために、2 つの <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);

データの取得に使用する EMCAScript (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 ファイルが、ページが読み込まれてデータが要求されているとき、およびユーザーが [Next] または [Prev] のリンクをクリックしたために新しいページのデータが取得されるたびに、"please wait" インターフェイスを生成できます。この例で使用するアニメーション化された 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 サービスのメソッドを呼び出すときには完全修飾クラス名を使用するということです。もう 1 つの相違点は、Web サービス メソッドには、すべてのパラメータを提供するだけでなく、最後に 3 つのパラメータを追加することです。これらは JScript または JavaScript の関数を表し、データが返されるとき (OnComplete)、呼び出しがタイムアウトした場合 (OnTimeOut)、およびエラーが発生した場合 (OnError) に呼び出されます。このサンプルのパーツでは、OnTimeOut 関数と OnError 関数は、返された情報を、正常時にデータが表示される DIV 要素に直接表示するだけです。

OnComplete 関数は 3 つのうちで唯一の必須パラメータであり、次のような 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. は、最初の 2 ページのデータを示しています。

図 4. 1 ページ目のデータ

AJAX Web パーツの先頭ページ

図 5. 2 ページ目のデータ

サンプル AJAX Web パーツ - 2 ページ目

JSON を使用して Web パーツを作成する

前の例では、XML が Web サービス メソッドから返された後は、XPath および MSXML DOMDocument を使用してコンテンツを読み取っていました。これに対し、ASP.NET AJAX の最も強力な機能の 1 つは、JavaScript Object Notation (JSON) を使用してデータを使用できることです。これにより、クライアント側の開発者は、複雑な XML を操作するのではなく、オブジェクトとプロパティを使用してデータにアクセスできます。

これを示すために、カスタム クラスを返す 2 番目の 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 クラスのプロパティは 1 つだけで、シリアル化をサポートするカスタム ディクショナリ型です。Microsoft .NET Framework 2.0 の既定の Dictionary クラスはシリアル化をサポートせず、JSON は既定ですべての戻りデータをシリアル化します。この例では、個別のプロパティ名をクラスにマップする必要がないように、Dictionary 型のクラスが必要です。代わりに、キーと値のペアとして追加してもかまいません。たとえば、myValuePair.Item.Add(someKey, someValue) のようになります。

注意

ここでは、カスタム ディクショナリ クラスについては説明しません。ただし、使用したクラスは、「XML Serializable Generic Dictionary (英語)」に投稿されている Paul Welter のブログで説明されている作業が基になっています。

この 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" インターフェイスを表示します。

結果を評価する

低帯域幅または待機時間が長いネットワークでは、このようなソリューションを使用することで、使用されるネットワーク リソースには非常によい影響があります。2 番目の Web パーツは、同じリストからまったく同じデータを生成するように作成されました。ただし、すべてのデータはサーバー上の Web パーツで生成された後、HTML としてページに書き込まれます。その結果、[Prev] または [Next] リンクがクリックされるたびに、強制的にサーバーにポストバックし、ページ全体がクライアントに返送されます。代わりに ASP.NET AJAX の UpdatePanel コントロールを使用しても、同じプロセスが発生することに注意することが重要です。ページのすべてのフォーム変数がサーバーにポストバックされて、ページ全体が要求から送り返されます。ただし、UpdatePanel に含まれるページの部分のみが更新されます。図 6. は、この 2 番目の Web パーツで [Next] リンクをクリックした場合の要求のスナップショットを、Fiddler でキャプチャしたものです。

図 6. 2 番目の Web パーツで [Next] リンクをクリックした場合の要求

Web パーツに対する Fiddler の結果

この Fiddler のキャプチャでは、リスト データのポストバック スタイルの表示による要求と応答では、全部で 79,424 バイトがネットワークを介して送信されたことがわかります。一方、図 7. は、ASP.NET AJAX 対応の Web パーツを使用し、XML を使用してカスタム Web サービスを介して同じデータを取得した場合の Fiddler でのキャプチャを示しています。

図 7. Web パーツが XML を使用してカスタム Web サービス経由で同じデータを取得した場合

カスタム Web サービスに対する Fiddler の結果

取得したのは同じリスト データですが、ネットワークを介して送信されたのは 1,973 バイトだけです。これは大きな差であり、この方法を適切に使用するとネットワーク トラフィックが大幅に減少する可能性があります。ただし、すべての中で最小のペイロードが生成されたのは、図 8. で示すように、JSON を使用する Web サービスのメソッドを使用して Records クラスを返した場合でした。

図 8. JSON を使用して Records クラスを返した場合

JSON に対する Fiddler の結果

まとめ

JSON を使用すると、1 回の要求に対してネットワーク経由で送信されるペイロードの総量を 1,817 バイトまで減らすことができ、これは、データの取得とページ移動のためにページ全体をポストバックするパーツの場合の要求サイズに対して 98% の削減になります。また、データの列挙に使用する ECMAScript (JScript、JavaScript) のサイズも小さくなり、コードも簡単になりました。

このようなソリューションの方が開発は複雑ですが、帯域幅または待機時間に制約のあるサイトの場合は、パフォーマンスとユーザー操作性の改善に役立つ良い選択肢となる可能性のある方法です。

参考資料

詳細については、以下のリソースを参照してください。

このドキュメントをダウンロードする

このトピックは、簡単に読んだり印刷したりできるように、次のダウンロード可能なドキュメントに収められています。

入手可能なドキュメントの詳細な一覧については、「Office SharePoint Server 2007 のダウンロード可能なブック」を参照してください。