Оптимизация настраиваемых веб-частей для территориально-распределенной сети

Краткий отчет: способы обучения минимизации воздействия на пропускную способность в настраиваемых веб-частях, в том числе общие рекомендации по стилю, специальные сведения и примеры кодов для альтернативных средств, предназначенных для извлечения данных из списков SharePoint и их представления. (24 печатных страницы)

Корпорация Майкрософт, Стив Пешка (Steve Peschka)

Январь 2008 г.

Применимо к: Microsoft Office SharePoint Server 2007, Windows SharePoint Services 3.0, ASP.NET AJAX 1.0

Содержание

  • Введение в оптимизацию настраиваемых веб-частей для территориально-распределеной сети

  • Повторное использование встроенных стилей или создание настраиваемых стилей

  • Хранение сведений о состоянии

  • Максимальное увеличение производительности веб-частей, отображающих данные

  • Заключение

  • Дополнительные ресурсы

Введение в оптимизацию настраиваемых веб-частей для территориально-распределеной сети

При разработке настраиваемых веб-частей, предназначенных для использования в сайтах с большим временем задержки или низкой пропускной способностью, в основе лежат те же общие принципы разработки, используемые при создании страниц для такого типа среды. Необходимо приложить усилия для разработки веб-частей, которые будут сводить к минимуму как обмен пакетами "туда и обратно", так и объем данных пересылаемых по сети. В данной статье обсуждается несколько методов, позволяющих соответствовать перечисленным целям при разработке.

Повторное использование встроенных стилей или создание настраиваемых стилей

Когда веб-часть создает 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. При хранении в библиотеке документов таблица стилей может обладать директивой возможности кэширования, связанной с ней так, что она не нуждается в последующей загрузке после первого посещения страницы. Это уменьшит нагрузку на сайт, в отличие от использования встроенных стилей, которые бы передавались по сети каждый раз при представлении веб-части.

Хранение сведений о состоянии

Веб-части могут отслеживать такие сведения как пользователя, запрос и источник данных. Существует несколько вариантов состояния хранения; описание каждого варианта выходит за рамки данной статьи. Однако можно выделить два наиболее распространенных варианта, используемых с веб-частями: ViewState и сервер класса Cache. В среде с низкой пропускной способностью или большим временем задержки следует по возможности избегать использования варианта ViewState, поскольку он добавляет содержимое к странице как при загрузке, так и обратной передаче. Этот вариант применяется с другими формами состояния хранения, которые также участвуют в передаче данных по сети, как, например, строки запроса, скрытые поля и файлы cookie.

Использование сервера класса Cache позволяет хранить сведения о состоянии на уровне сервера. Недостаток использования такого сервера заключается в том, что он в действительности не предназначен для использования в качестве механизма для отслеживания состояния каждого пользователя (хотя, в зависимости от обстоятельств, его можно заставить работать таким образом). Кроме того, сведения о кэше не дублируются на всех интерфейсных веб-серверах фермы. Если веб-часть зависит от наличия этих сведений о состоянии, независимо от того, на какой интерфейсный веб-сервер пользователь отправил запрос, это говорит о том, что сервер класса Cache не является лучшим вариантом.

В этом сценарии должен использоваться другой вариант: состояние сеанса. Вариант "Состояние сеанса" выключен по умолчанию, но он включается при активации в ферме служб форм Microsoft Office InfoPath (IPFS). Когда этот вариант включен, он использует Microsoft SQL Server для отслеживания состояния, что означает, что состояние сеанса можно использовать независимо от того, какой интерфейсный веб-сервер получает HTTP-запрос. Недостаток данного варианта заключается в том, что данные остаются в памяти до тех пор, пока они не будут удалены или срок их действия не истечет. Вследствие этого большие наборы данных, которые хранятся в состоянии сеанса, могут снизить производительность сервера при неграмотной организации управления. Из-за этих ограничений не рекомендуется использовать состояние сеанса, пока в этом не возникнет явная необходимость.

Также можно попробовать отключить вариант ViewState для всех страниц сайта, изменив для веб-приложения файл web.config. Он содержит элемент pages с атрибутом enableViewState. Если вы беспокоитесь о размере 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) для отображения данных на странице.

Примечание

Код, представленный в этой статье, не предназначен для представления лучших рекомендаций по кодированию Майкрософт. Этот код упрощает выполнение; элементы кода потребуется изменить для производственной среды.

Веб-часть отменяет метод 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 используется для настройки URL-адреса перехода для LinkButton. При этом требуется, чтобы 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. Он настраивается, чтобы быть встроенным ресурсом, и отправляется на страницу в качестве веб-ресурса (например, 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 для проекта.

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

Далее данные загружаются в DOMDocument из MSXML, а затем выбирается список элементов 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. Веб-часть с данными

Вторая страница образца веб-части

Использование клиентского сценария, который подключается к веб-службам

Еще одним способом управления данными, пересылаемыми по сети, является использование клиентского сценария, который подключается к веб-службам с целью извлечения данных. Этот способ позволяет управлять извлечением нужных данных, избегая необходимости возвращать страницу целиком на сервер. Это не только удобнее для пользователя, но также уменьшает нагрузку на сеть, поскольку целая страница не отправляется обратно на сервер, а возвращается для извлечения данных. Извлечь данные можно с помощью компонента XmlHttp напрямую или Microsoft AJAX Library 1.0, который сосредотачивает функции вокруг XmlHttp.

Несмотря на то, что XmlHttp и ASP.NET AJAX представляют два варианта верхнего уровня, скорее всего окажется самым простым использовать ASP.NET AJAX в сочетании с настраиваемой веб-службой, предоставляющей данные списка SharePoint. Хотя с технической точки зрения использование библиотек ASP.NET AJAX вместе с веб-службой на базе SOAP (наподобие веб-служб, встроенных в продукты и технологии SharePoint) является возможным, в действительности выполнить это гораздо сложнее и, кроме того, это не предоставляет никаких дополнительных преимуществ веб-служб ASP.NET стиля AJAX, таких как уменьшение нагрузки и поддержка JavaScript Object Notation (JSON).

Продукты и технологии SharePoint предоставляют несколько веб-служб, которые можно использовать для отображения данных. Веб-служба Lists позволяет извлекать табличные данные из списков и библиотек продуктов и технологий SharePoint. Можно использовать ее для отображения данных, содержащихся в списках и ссылках к элементам списка, таким как документы или графические изображения. Веб-служба Search позволяет производить поиск в пределах содержимого, содержащегося в продуктах и технологиях SharePoint и любых других внешних источниках, подвергающихся обходу. Данную службу можно также использовать с аккуратно созданными запросами под управлением метаданных с целью извлечения отфильтрованных наборов данных из одного или нескольких списков SharePoint.

В следующем примере показан процесс создания настраиваемой веб-службы для извлечения списковых данных из продуктов и технологий SharePoint. Класс веб-службы обозначен атрибутом System.Web.Script.Services.ScriptService, который позволяет использовать компоненты клиентской веб-службы, предоставленные ASP.NET AJAX. Затем веб-служба "регистрируется" в продуктах и технологиях SharePoint, чтобы иметь доступ через каталог_vti_bin ко всем встроенным веб-службам. Главная страница обновляется и получает элемент управления ASP.NET AJAX ScriptManager, а также описательные теги для настраиваемой веб-службы. Затем веб-служба записывается, что приводит к созданию клиентского сценария для извлечения данных из настраиваемой веб-службы посредством компонентов ASP.NET AJAX и для их отображения на странице.

Установка AJAX

Сначала необходимо установить двоичные файлы ASP.NET AJAX 1.0 на серверах SharePoint. Их можно загрузить с сайта ASP.NET AJAX. Их необходимо установить на каждом интерфейсном веб-сервере фермы.

После этого идет установка ASP.NET AJAX, необходимо обновить файл web.config для каждого веб-приложения, в котором будет использоваться ASP.NET AJAX. Это длительный процесс; подробные пошаговые инструкции см. в записи блога Майка Аммерлана (Mike Ammerlan) в разделе Интеграция ASP.NET AJAX с SharePoint.

Создание настраиваемой веб-службы

Создание настраиваемой веб-службы для извлечения данных необходимо, поскольку она дает возможность применять System.Web.Script.Services.ScriptService к вашему классу веб-сервера, благодаря чему его можно использовать с инфраструктурой веб-службы ASP.NET AJAX. При этом создается относительно простой класс веб-службы для извлечения списковых данных на основе базовых параметров.

Этот класс веб-службы включает ссылку на класс System.Web.Extensions, что делает возможным поддержку ASP.NET AJAX. После добавления этой ссылки к классу добавляется инструкция using (Microsoft Visual C#) или Imports (Microsoft Visual Basic).

using System.Web.Script.Services;

Затем к классу добавляется атрибут ScriptService, чтобы им можно было пользоваться напрямую посредством инфраструктуры веб-службы ASP.NET AJAX. По умолчанию она содержит конструктор без параметров, чтобы класс можно было сериализовать с помощью ASP.NET AJAX.

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

      public AjaxDataWebService()
      {
         // Default constructor
   }

В первом примере веб-служба содержит только один метод, который возвращает строку. Строка в действительности является языком 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();

После настройки запроса следующим шагом является выполнение. После того, как результаты будут возвращены, снова выполняется проверка направления разбиения на страницы. При организации разбиения на страницы в обратном направлении порядок представления результатов должен быть снова изменен в обратную сторону, чтобы на странице они отображали идентификатор от меньшего к большему. Это опять же делается только для поддержки разбиения на страницы в веб-части. Чтобы упростить процесс упорядочивания, данные извлекаются в объект DataTable ADO.NET. После того, как данные будут извлечены и отсортированы должным образом, каждая строка перечисляется с целью создания 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();

Регистрация настраиваемой веб-службы

Отображение настраиваемой веб-службы для использования в веб-части, активируемой ASP.NET AJAX, требует конфигурации двух типов. Один тип для ее настройки таким образом, чтобы продукты и технологии SharePoint знали о веб-службе и могли вызвать ее в контексте веб-приложения SharePoint. Это включает несколько этапов, которые на верхнем уровне требуют выполнения следующих действий:

  1. Создать класс с фоновым кодом для веб-службы в отдельной сборке и зарегистрировать его в глобальном кэше сборок.

  2. Создать и изменить файл статического обнаружения и файл языка определения веб-служб (WSDL).

  3. Развернуть файлы веб-службы в каталоге _vti_bin.

Для выполнения всех предыдущих действий необходимо несколько шагов. К счастью, уже существует статья с рекомендациями, где описываются эти шаги. Подробнее об этом см. в разделе Пошаговое руководство: создание настраиваемой веб-службы.

После интеграции веб-службы с каталогом SharePoint _vti_bin необходимо изменить главную страницу и добавить тег ASP.NET AJAX <ScriptManager>. Внутри тега <ScriptManager> следует определить точку входа служб для настраиваемой веб-службы, которая используется; в данном примере настраиваемая веб-служба имеет имя ListData.asmx. Далее полный тег добавляется к главной странице.

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

Когда веб-служба настроена таким образом, что ее можно вызвать из каталога SharePoint _vti_bin, и тег <ScriptManager> добавлен к главной странице со ссылкой на настраиваемую веб-службу, клиентские компоненты веб-службы ASP.NET AJAX теперь могут взаимодействовать с ней.

Создание настраиваемой веб-части с помощью данных XML

Теперь требуется, чтобы настраиваемая веб-часть создала клиентский сценарий ASP.NET AJAX для извлечения данных из настраиваемой веб-службы. Сама веб-часть практически не содержит серверной логики; массив кода содержится в файле ECMAScript (Jscript, JavaScript), который добавляется веб-частью в качестве веб-ресурса (файл 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. Здесь также используется процесс добавления сценария в качестве встроенного ресурса и его регистрация в файле AssemblyInfo.cs, который был описан для веб-части острова XML. Регистрация файла 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, в которых содержатся списковые данные и элементы управления разбиением на страницы, и отображения интерфейса типа "Пожалуйста, подождите". Затем сценарий использует скрытые поля, чтобы собрать информацию для параметров метода веб-службы, и использует инфраструктуру ASP.NET AJAX, чтобы вызвать метод настраиваемой веб-службы.

// 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 отличается от традиционных веб-служб на базе SOAP. Первое, что следует отметить, это то, что при вызове метода веб-службы необходимо использовать полное имя класса. Следующее отличие состоит в том, что, помимо предоставленных параметров для метода веб-службы, в конце добавляются еще три дополнительных параметра. Они представляют функции в сценарии JScript или JavaScript, которые будут вызываться при возвращении данных (OnComplete) в случае окончания времени вызова (OnTimeOut) или в случае ошибки (OnError). В этой части примера обе функции OnTimeOut и OnError только отображают данные, возвращенные напрямую в элемент DIV, где обычно данные и отображаются.

Функция OnComplete является единственным обязательным параметром из трех, а также является всего лишь функцией сценария JScript, которая выглядит следующим образом.

function OnComplete(arg)
{
  …
}

Параметр arg содержит значение, возвращаемое из вызова метода веб-службы; в этом случае это строка, содержащая XML. Для данного проекта веб-служба возвращает 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 только тем, что идентификаторы первого и последнего элементов хранятся в скрытых полях startID и endID, чтобы поддерживать функцию разбиения на страницы элемента управления. После извлечения веб-частью данных она отображает их в соответствующем элементе DIV и позволяет проводить разбиение содержимого на страницы в обе стороны. На рисунках 4 и 5 показаны первые две страницы данных.

Рисунок 4. Первая страница данных

Первая страница веб-части AJAX

Рисунок 5. Вторая страница данных

Образец веб-части AJAX — вторая страница

Создание настраиваемой веб-части с помощью JSON

В предыдущем примере XML возвращался из метода веб-службы, и затем XPath и MSXML DOMDocument использовались для считывания содержимого. Однако одной из самых сильных функций ASP.NET AJAX является способность извлекать данные с помощью JavaScript Object Notation (JSON). Это позволяет клиентскому разработчику работать с объектами и свойствами для получения доступа к данным, избавляя его от работы с более сложным XML.

Чтобы продемонстрировать это, был создан второй веб-метод, который возвращает настраиваемый класс; подпись этого метода похожа на следующий код.

[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 имеет только одно свойство, которое является типом вспомогательного словаря, который поддерживает сериализацию. Класс Dictionary, задаваемый по умолчанию в Microsoft .NET Framework 2.0, не поддерживает сериализацию, а JSON по умолчанию поддерживает сериализацию всех возвращаемых данных. В этом случае требуется класс типа Dictionary, чтобы избежать необходимости сопоставления индивидуальных имен свойств в классе. Вместо этого их можно добавить в качестве пары значений или ключей. Например, myValuePair.Item.Add(someKey, someValue).

Примечание

Описание класса вспомогательного словаря не включено в данную статью; однако используемый класс основывался на работе, описанной в записи блога Пола Велтера (Paul Welter) в разделе Универсальный сериализуемый словарь XML.

Веб-метод работает таким же образом, как и 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 показан сбор данных, выполненный Fiddler при использовании веб-части, активируемой ASP.NET AJAX, с целью извлечения тех же данных посредством настраиваемой веб-службы с помощью XML.

Рисунок 7. Веб-часть извлекает те же данные посредством настраиваемой веб-службы с помощью XML

Результаты Fiddler для пользовательской веб-службы

При извлечении тех же списковых данных по сети было передано всего 1973 байт. Это огромная разница, и умелое использование этого метода может значительно сократить сетевой трафик. Однако наименьшая нагрузка создается при использования метода веб-службы, прибегающего к помощи JSON для возвращения класса Records, что отображено на рисунке 8.

Рисунок 8. Использование JSON для возвращения класса "Records"

Результаты Fiddler для JSON

Заключение

Благодаря использованию JSON удалось сократить общую нагрузку на сеть до одного запроса размером 1817 байт, и такое сокращение в процентном соотношении составляет 98% размера запроса для веб-части, возвращающей страницу целиком для извлечения данных и их разбиения на страницы. Также удалось уменьшить размер сценария ECMAScript (JScript, JavaScript), используемого для перечисления данных, и упростить код в ходе выполнения.

Несмотря на то, что разработать решение, подобное данному, не так уж просто, и к тому же если функционирование сайта ограничено пропускной способностью или задержками, этот подход может быть хорошим вариантом для повышения производительности и усовершенствования условий работы конечных пользователей.

Дополнительные ресурсы

Дополнительные сведения см. в следующих документах:

Загрузка этой книги

Для упрощения чтения и печати эта тема включена в следующую загружаемую книгу:

См. полный список доступных книг на веб-сайте Загружаемые книги для Office SharePoint Server 2007.