Share via


Otimizando Web Parts personalizadas para a WAN

Atualizado em: 2009-04-23

Resumo: aprender técnicas para minimizar o impacto na largura de banda em Web Parts personalizadas, incluindo sugestões gerais de estilo e exemplos de códigos e informações específicas para meios alternativos de recuperar e processar dados de listas do SharePoint. (24 páginas impressas)

Steve Peschka, Microsoft Corporation

Janeiro de 2008

Aplica-se a: Microsoft Office SharePoint Server 2007, Windows SharePoint Services 3.0, ASP.NET AJAX 1.0

Conteúdo

  • Introdução à otimização de Web Parts personalizadas para WAN

  • Reutilizando estilos internos ou criando estilos personalizados

  • Armazenando estado

  • Maximizando o desempenho de Web Parts que exibem dados

  • Conclusão

  • Recursos adicionais

Introdução à otimização de Web Parts personalizadas para WAN

O desenvolvimento de Web Parts personalizadas que serão usadas em sites com alta latência ou com baixa largura de banda requer um foco nos mesmos princípios de design gerais que são usados ao criar páginas para esse tipo de ambiente. Você deve fazer um esforço para criar partes que minimizem as viagens de ida e volta para o servidor e a quantidade de dados enviados na rede. Este artigo aborda várias técnicas que podem ser empregadas para atender a essas metas de design.

Reutilizando estilos internos ou criando estilos personalizados

Quando a Web Part emitir HTML, use classes de estilos que sejam internas da folha de estilos do Microsoft Office SharePoint Server 2007 e do Windows SharePoint Services 3.0: core.css. (Neste artigo, o Office SharePoint Server e o Windows SharePoint Services são denominados coletivamente como Produtos e Tecnologias do Microsoft SharePoint). Ao reutilizar esses estilos, você pode minimizar o impacto na página, porque ela não precisará baixar uma folha de estilos adicional apenas para oferecer suporte à sua parte. Além disso, após a visita inicial ao site, o usuário já terá o arquivo core.css baixado em seu cache. Ao usar estilos que fazem parte do core.css, você garante que não sejam necessários downloads adicionais para suporte ao estilo.

Se você precisar de estilos personalizados na sua parte, considere utilizar uma folha de estilos personalizada que possa ser usada com o cache blob. Se for armazenada em uma biblioteca de documentos, a folha de estilos poderá ter uma diretiva de capacidade de cache associada para que ela não precise ser baixada após a visita inicial à página. Isso terá um impacto menor no site do que o uso de estilos embutidos, por exemplo, que seriam transmitidos na rede sempre que a parte fosse processada.

Armazenando estado

É possível que as Web Parts precisem controlar informações como o usuário, a solicitação e a fonte de dados. Há várias opções para armazenar estado; a descrição de cada uma está fora do escopo deste artigo. Entretanto, em geral, há duas opções comuns que podem ser usadas com Web Parts: ViewState e Cache do servidor. Em um ambiente de baixa largura de banda ou altamente latente, se possível, evite usar a opção ViewState, pois ela adiciona conteúdo à página no download e como qualquer postback. Isso se aplica a outras formas de estado que também envolvam a transmissão de dados na rede, como sequências de consultas, campos ocultos e cookies.

O uso da classe Cache do servidor permite que você armazene informações de estado no nível do servidor. A desvantagem de usar o Cache do servidor é que ele não foi projetado para ser usado como um mecanismo de estado por usuário (embora, dependendo das circunstâncias, ele possa funcionar dessa forma). Além disso, as informações do cache não são replicadas em todos os servidores Web front-end no farm. O Cache do servidor não é uma boa opção se a sua parte depende das informações de estado presentes, seja qual for o servidor Web front-end que acabar recebendo uma solicitação de usuário.

Neste cenário, outra opção é usar o Estado da Sessão. O Estado da Sessão está desativado por padrão, mas é habilitado quando você ativa o Microsoft Office InfoPath Forms Services (IPFS) em um farm. Quando ele é habilitado, usa o Microsoft SQL Server para controlar o estado, o que significa que os valores de estado de sessão podem ser usados independentemente de qual servidor Web front-end recebe a solicitação HTTP. A desvantagem do estado da sessão é que os dados permanecem na memória até serem removidos ou expirarem. Grandes conjuntos de dados armazenados no estado da sessão podem, portanto, reduzir o desempenho do servidor se não forem gerenciados com cuidado. Devido a essas restrições, não é recomendável o uso do estado da sessão, a menos que seja absolutamente necessário.

Você também pode tentar definir ViewState como desativada em todas as páginas no site editando o arquivo web.config em um aplicativo Web. Ele contém um elemento pages que possui um atributo enableViewState. Se você tiver uma grande preocupação com o tamanho de ViewState na página, poderá tentar definir esse atributo como false (ele é true por padrão). Se você fizer isso, precisará testar completamente o site e todos os seus recursos para garantir que ele funcione adequadamente, porque alguns controles e Web Parts podem esperar que ViewState esteja ativada.

Se você estiver desenvolvendo uma Web Part e precisar usar algo similar a ViewState, mas não tiver certeza se estará disponível em uma página, poderá usar o Estado de controle, que é novo no ASP.NET 2.0. Em suma, você precisaria fazer o seguinte:

  1. Registrar-se no estado de controle chamando Page.RegisterRequiresControlState durante a inicialização.

  2. Substituir os métodos LoadControlState e SaveControlState.

  3. Gerenciar manualmente sua parte do conjunto de objetos mapeada para o estado de controle.

Maximizando o desempenho de Web Parts que exibem dados

Se a sua Web Part for usada para exibir dados, você poderá tentar melhorar a experiência dos usuários finais em relação ao desempenho de diversas maneiras. Em geral, você precisa obter um equilíbrio entre o número necessário de viagens ao servidor e a quantidade de dados que deve ser recuperada para uma solicitação.

Fornecendo limites de processamento

Para controles que emitem linhas de dados, inclua uma propriedade que permita que um administrador controle quantas linhas são exibidas. Dependendo da latência e da largura de banda nas quais o controle será usado, esse recurso permitirá uma flexibilidade para aumentar ou diminuir a quantidade de dados processados em cada solicitação de página, e também pode impactar o número de solicitações necessárias para exibir todos os dados.

Se o número de linhas retornadas for uma propriedade que possa ser definida por usuários finais, considere a adição de restrições para que as opções de um usuário não saturem a rede.

Usando ilhas de dados XML embutidos

Outra alternativa para exibir dados é usar uma ilha de dados XML embutidos e realizar o processamento do lado do cliente. Com essa abordagem, todos os dados exibidos são emitidos para a página como uma ilha de dados XML. O script do lado do cliente é usado para processar os dados na página e é responsável pelo uso do DOM XML para recuperar e exibir os dados da ilha.

Ao fazer isso, você pode recuperar todos os dados em uma única solicitação para que o número de viagens de ida e volta ao servidor seja reduzido. Entretanto, o tamanho de download, obviamente, será maior e, por consequência, o carregamento da página inicial levará mais tempo. Além disso, se for usado em uma página em que se utilizam outros controles que causem postbacks, ele forçará que esse grande download de dados ocorra toda vez. Essa técnica é mais adequada quando você sabe que isso não vai ocorrer ou quando você a utiliza como um método de recuperação de dados opcional. Por exemplo, crie uma propriedade pública para rastrear se todos os dados devem ser baixados para que um administrador de site possa controlar o comportamento.

Veja a seguir um exemplo relativamente simples de uma Web Part que lê todo o conteúdo de uma lista, emite-o para uma página como XML e usa o ECMAScript do lado do cliente (JScript, JavaScript) para processar e percorrer dados da página.

Dica

O código neste artigo não tem intenção de representar as práticas recomendadas de codificação da Microsoft. O código foi simplificado para facilitar o exercício; os elementos do código precisariam ser alterados para um ambiente de produção.

A Web Part substitui o método Render e começa obtendo o conteúdo de uma lista no site denominado Contatos.

// 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)
{
…
}

Depois que a referência à lista é obtida, um objeto StringBuilder é usado para criar a ilha de dados XML que será emitida para a página.

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

Com o XML adicionado à página, você também precisa de um elemento no qual exibir os dados. Para esse requisito, uma simples marca <div> é adicionada à página.

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

O próximo bloco de interface que você precisa será usado para percorrer dados da página. Nesse caso, dois botões de link são adicionados à página. Duas coisas a serem observadas são: a propriedade OnClientClick e o conjunto Attributes. A propriedade OnClientClick é definida para usar uma função ECMAScript (JScript, JavaScript) personalizada desenvolvida para exibir os dados no cliente.

O conjunto Attributes é usado para definir a URL de navegação para o LinkButton. Nesse caso, nosso objetivo é que o LinkButton seja processado como um hiperlink para que o usuário veja um item clicável, e para que nós possamos realizar alguma ação no momento em que ele for clicado. Nesse caso, o link # é usado como a URL de navegação, pois, na verdade, não queremos navegar para nenhum lugar, queremos apenas processar um link e capturar quando ele for clicado.

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

Em seguida, adicionaremos alguns campos ocultos à página para acompanhar a paginação do controle. Nesse caso, queremos rastrear o tamanho da página (quantos registros mostrados por vez), o número atual de registros e o número total de registros.

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

Com todos os elementos presentes, a última etapa do método Render é registrar um script de inicialização que execute o javascript para processar os dados.

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

O próprio javascript fica em um arquivo separado no projeto denominado Scripts.js. Ele é configurado para ser um recurso incorporado e enviado para a página como recurso da Web (por exemplo, webresource.axd). O código que o configura para download é executado no evento OnPreRender da Web Part. Primeiro, ele confere se a referência do script ainda não foi adicionada à página chamando o método IsClientScriptIncludeRegistered e, caso não tenha sido, ele a registra como include na página.

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

Este método também requer que você registre o recurso da Web na classe AssemblyInfo.cs do projeto.

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

Quando a página é processada, ela inclui um link com a seguinte aparência.

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

O ECMAScript (JScript, JavaScript) lê o XML e processa os dados. Ele primeiro determina os limites de paginação: o número do primeiro e do último registro a ser exibido. Depois de saber os registros que precisa exibir, ele usa um método razoavelmente simples para criar uma instância do MSXML no cliente.

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.
      }
   }
}

As variáveis são usadas para armazenar os dados XML e uma referência ao elemento DIV, onde os resultados serão apresentados.

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

Depois, os dados são carregados no DOMDocument do MSXML e, em seguida, a lista dos elementos Item é selecionada.

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

Agora que os nós estão selecionados, os resultados podem ser enumerados para o conjunto específico de páginas necessário e processados na página.

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

Quando estiver concluída, a parte exibirá os registros no cliente e fornecerá um mecanismo para paginar por eles sem fazer outra viagem de ida e volta ao servidor. A Figura 1 e a Figura 2 mostram a parte com duas páginas diferentes de dados.

Figura 1. Web Part com dados

Primeira página de exemplo de Web Part

Figura 2. Web Part com dados

Segunda página de dados de Web Part

Usando script do lado do cliente que se conecta aos serviços Web

Outra opção para gerenciar a quantidade de dados enviada pela rede é usar um script do lado do cliente que se conecte aos serviços Web para recuperar dados. Esse método permite que um controle recupere os dados necessários sem precisar fazer o postback de toda a página. Isso não apenas é uma experiência melhor para o usuário, mas coloca uma carga mais leve na rede, pois a página inteira não vai e volta do servidor para recuperar os dados. A recuperação de dados pode ser feita com o componente XmlHttp diretamente ou com o Microsoft AJAX Library 1.0, que envolve a funcionalidade em torno do XmlHttp.

Embora o XmlHttp e o ASP.NET AJAX representem duas opções de nível superior, é provável que você ache mais fácil usar o ASP.NET AJAX junto com um serviço Web personalizado que exponha os dados da lista do SharePoint. Embora seja tecnicamente possível usar as bibliotecas do ASP.NET AJAX com um serviço Web baseado em SOAP (como os serviços Web internos dos Produtos e Tecnologias do SharePoint), isso é consideravelmente mais complicado de se fazer e não oferece nenhum dos benefícios adicionais dos serviços Web do estilo ASP.NET AJAX, como tamanhos de carga menores e suporte para JavaScript Object Notation (JSON).

Os Produtos e Tecnologias do SharePoint fornecem vários serviços Web que podem ser usados para exibir dados. O serviço Web Lists permite que você recupere dados tabulares de listas e bibliotecas nos Produtos e Tecnologias do SharePoint. Você pode utilizá-lo para processar os dados de listas e links para itens em uma lista, como documentos ou imagens. O serviço Web Search permite que você pesquise no corpo do conteúdo nos Produtos e Tecnologias do SharePoint e em quaisquer outras fontes externas que estejam sendo rastreadas. Com consultas orientadas por metadados cuidadosamente construídas, ele também pode ser usado para recuperar um conjunto filtrado de dados de uma ou mais listas do SharePoint.

O exemplo a seguir demonstra a criação de um serviço Web personalizado para recuperar dados de lista dos Produtos e Tecnologias do SharePoint. A classe do serviço Web é anotada com o atributo System.Web.Script.Services.ScriptService, que permite que você use os componentes do serviço Web do lado do cliente que são fornecidos com o ASP.NET AJAX. O serviço Web é então "registrado" com os Produtos e Tecnologias do SharePoint para que possa ser acessado pelo diretório _vti_bin com todos os serviços Web internos. A página mestra é atualizada para incluir o controle ScriptManager do ASP.NET AJAX e as marcas declarativas do serviço Web personalizado. Em seguida, é desenvolvida uma Web Part que gera o script do lado do cliente para recuperar dados através dos componentes do ASP.NET AJAX a partir do serviço Web personalizado e exibi-los na página.

Instalando o AJAX

Primeiro, você deve instalar os binários do ASP.NET AJAX 1.0 nos servidores do SharePoint. Você pode baixá-los do ASP.NET AJAX site (em inglês) . Eles devem ser instalados em cada servidor Web front-end no farm.

Após instalar o ASP.NET AJAX, você deve atualizar o arquivo web.config para cada aplicativo Web em que o ASP.NET AJAX será usado. Isso é um exercício um pouco extenso; para obter instruções passo a passo, consulte a postagem no blog de Mike Ammerlan sobre a integração do ASP.NET AJAX com o SharePoint (em inglês).

Criando um serviço Web personalizado

A criação de um serviço Web personalizado para recuperar os dados é necessária porque permite que você aplique o System.Web.Script.Services.ScriptService à classe de serviço Web, possibilitando que ele seja usado com a estrutura do serviço Web do ASP.NET AJAX. Nesse caso, uma classe de serviço Web relativamente simples foi desenvolvida para recuperar dados de lista com base em parâmetros básicos.

A classe de serviço Web inclui uma referência à classe System.Web.Extensions para habilitar o suporte ao ASP.NET AJAX. Depois que essa referência for adicionada, uma instrução using (Microsoft Visual C#) ou Imports (Microsoft Visual Basic) é adicionada à classe.

using System.Web.Script.Services;

A classe é então decorada com o atributo ScriptService para que possa ser consumida diretamente pela estrutura dos serviços Web do ASP.NET AJAX. Ele inclui um construtor padrão sem parâmetros para que a classe possa ser serializada pelo ASP.NET AJAX.

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

      public AjaxDataWebService()
      {
         // Default constructor
   }

No primeiro exemplo, o serviço Web contém somente um método, que retorna uma cadeia de caracteres. A cadeia de caracteres, na verdade, é o XML que será consumido no cliente. A assinatura do método é definida da maneira a seguir.

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

A primeira parte do código obtém uma referência ao site, Web e uma lista que contém os dados.

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

Em seguida, a semântica da consulta é criada com base na direção da paginação, na ID de início e no tamanho do conjunto de resultados.

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

Com a consulta configurada, a próxima etapa é executar. Quando os resultados forem retornados, a direção da paginação é verificada novamente. Se houver paginação regressiva, então os resultados deverão ter sua ordem invertida novamente para que seja exibida da menor para a maior ID na página. Novamente, esse recurso é simplesmente para suporte à paginação na Web Part. Para simplificar a ordenação, os dados são recuperados em um objeto DataTable ADO.NET. Depois que os dados forem recuperados e classificados adequadamente, cada linha será enumerada para criar o XML retornado da chamada do método.

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

Todo o código anterior está em um bloco try…catch, obviamente; no bloco finally, nós liberamos os recursos associados ao objeto DataTable e depois retornamos o XML que foi criado.

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

return ret.ToString();

Registrando o serviço Web personalizado

Expor o serviço Web personalizado para uso na Web Part habilitada para ASP.NET AJAX requer dois tipos de configuração. Um tipo configura o serviço Web para que os Produtos e Tecnologias do SharePoint o reconheçam e possam chamá-lo no contexto de um aplicativo Web do SharePoint. Isso envolve várias etapas que, em um nível superior, exigem estes procedimentos:

  1. Criar a classe code-behind do serviço Web em um assembly separado e registrá-la no cache de assembly global.

  2. Gerar e editar um arquivo de descoberta estático e um arquivo WSDL (Web Services Description Language).

  3. Implantar os arquivos de serviço Web no diretório _vti_bin.

Várias etapas são necessárias para concluir todas as ações anteriores. Felizmente, já existe um artigo disponível que descreve como fazer isso. Para obter detalhes completos, consulte Passo a passo: criando um serviço Web personalizado.

Após integrar o serviço Web ao diretório _vti_bin do SharePoint, você deve modificar a página mestra para adicionar uma marca <ScriptManager> do ASP.NET AJAX. Na marca <ScriptManager>, defina um ponto de entrada de Serviços para o serviço Web personalizado que estamos usando; neste exemplo, o serviço Web personalizado é denominado ListData.asmx. Veja a seguir a marca completa que é adicionada à página mestra.

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

Com o serviço Web configurado para que possa ser chamado a partir do diretório _vti_bin do SharePoint e a marca <ScriptManager> adicionada à página mestra com uma referência ao serviço Web personalizado, os componentes cliente do serviço Web do ASP.NET AJAX agora podem se comunicar com ele.

Criando uma Web Part personalizada usando dados XML

Neste momento, precisamos de uma Web Part personalizada para gerar o script cliente do ASP.NET AJAX para recuperar dados do serviço Web personalizado. A própria Web Part não tem quase nenhuma lógica do lado do servidor; grande parte do código está em um arquivo ECMAScript (Jscript, JavaScript) que a Web Part adiciona como recurso da Web (arquivo WebResource.axd).

A primeira etapa é gerar todo o HTML necessário na página para a interface do usuário. Há duas maneiras básicas de gerar HTML de uma Web Part. A forma simples é desenvolver as marcas diretamente como cadeias de caracteres; uma forma mais complicada, porém mais segura, é usar as bibliotecas de classes do ASP.NET. Com o HTML relativamente simples, geralmente é mais rápido e fácil emitir apenas as cadeias de caracteres. O HTML usado neste caso é um pouco mais complicado. Ele contém três marcas <div> principais para os dados, controles de navegação e elementos de interface “aguarde”. A marca “aguarde”<div> contém duas marcas <div> aninhadas nela para posicionar adequadamente a imagem e o texto dentro da parte. Com base nesses requisitos de HTML mais complicados, as bibliotecas de classes do ASP.NET foram usadas para gerar o HTML, conforme mostrado no código a seguir.

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

Em seguida, a Web Part adiciona vários campos ocultos que são usados para rastrear o estado ao paginar os dados.

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

Finalmente, um script de inicialização é registrado para que quando a página for carregada, a Web Part recupere a primeira página de dados:

Crie um script de inicialização para chamar o método de carregamento de dados quando a página for carregada.

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

O EMCAScript (Jscript, JavaScript) usado para recuperar os dados é registrado no evento OnPreRender. O mesmo processo de adicionar o script como um recurso incorporado e registrá-lo no arquivo AssemblyInfo.cs, conforme foi descrito para a Web Part de ilha de XML, também foi usado aqui. O registro do arquivo ECMAScript tem esta aparência.

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

Com o HTML que foi criado, o arquivo JScript ou JavaScript pode gerar uma interface “aguarde” quando a página for carregada e os dados forem solicitados, e sempre que uma nova página de dados for recuperada, porque o usuário clicou nos links Avançar ou Anterior. Nesse caso, o GIF animado usado é um dos incluídos nos Produtos e Tecnologias do SharePoint, por isso, os usuários estarão familiarizados com ele. A interface “aguarde” tem esta aparência.

Figura 3. Interface "aguarde"

Mensagem de recuperação de dados de Web Part

O gerenciamento da interface do usuário e recuperação de dados é manipulado pelo script do lado do cliente. O JScript ou o JavaScript começa alterando a interface para ocultar os elementos DIV que contêm os controles de paginação e os dados de lista e mostrando a interface “aguarde”. Depois, ele usa os campos ocultos para coletar as informações para os parâmetros do método do serviço Web e usa a estrutura do ASP.NET AJAX para chamar o método do serviço Web personalizado.

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

Chamar um serviço Web pelo ASP.NET AJAX é um pouco diferente dos serviços Web tradicionais baseados em SOAP. A primeira coisa a ser observada é a necessidade de usar o nome de classe totalmente qualificado ao chamar um método de serviço Web. A outra diferença é que, além de fornecer todos os parâmetros para o método do serviço Web, três parâmetros adicionais são incluídos no final. Eles representam funções em JScript ou JavaScript que serão chamadas quando os dados forem retornados (OnComplete), se a chamada atingir o tempo limite (OnTimeOut) ou se houver um erro (OnError). Neste exemplo de parte, a função OnTimeOut e OnError processam apenas as informações retornadas diretamente para o elemento DIV onde os dados são exibidos normalmente.

A função OnComplete é o único parâmetro obrigatório dos três e é apenas uma função JScript que tem esta aparência.

function OnComplete(arg)
{
  …
}

O parâmetro arg contêm o valor de retorno da chamada do método do serviço Web; neste caso, é uma cadeia de caracteres contendo XML. Para este projeto, o serviço Web retorna o XML no mesmo formato usado na Web Part de ilha de XML. Assim, o código para enumerar os dados e processá-los na página é quase idêntico. Ele primeiro cria um DOMDocument MSXML e verifica se algum XML válido foi retornado.

// 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.
}

O código de processamento aqui é diferente da Web Part de ilha de XML somente porque as IDs do primeiro e do último item são armazenadas nos campos ocultos startID e endID para oferecer suporte à funcionalidade de paginação do controle. Depois que a Web Part recuperar os dados, ela os processa no elemento DIV apropriado e permite voltar e avançar pelo conteúdo da página. A Figura 4 e a Figura 5 mostram as duas primeiras páginas de dados.

Figura 4. Primeira página de dados

Primeira página de Web Part AJAX

Figura 5. Segunda página de dados

Exemplo de Web Part AJAX - segunda página

Criando uma Web Part personalizada usando JSON

No exemplo anterior, o XML foi retornado do método do serviço Web e depois o XPath e um DOMDocument MSXML foi usado para ler o conteúdo. Entretanto, um dos recursos mais avançados do ASP.NET AJAX é sua capacidade de consumir dados usando JSON (JavaScript Object Notation). Isso permite que o desenvolvedor do lado do cliente trabalhe com objetos e propriedades para acessar dados, em vez de manipular um XML mais complicado.

Para demonstrar essa situação, foi criado um segundo método da Web que retorna uma classe personalizada; a assinatura do método tem a aparência similar ao código a seguir.

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

Records é uma classe personalizada que foi desenvolvida para armazenar os dados de retorno; sua definição tem a aparência similar ao código a seguir.

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

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

Na classe Records, a propriedade Count refere-se ao número total de itens encontrados, e a propriedade ItemCount refere-se ao número de itens retornados nessa chamada em particular. Essas propriedades são usadas para paginar os dados do lado do cliente. Os dados reais exibidos estão na propriedade Items, que é uma lista de itens de Registro. A classe Record é definida da maneira a seguir.

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

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

A classe Record possui apenas uma propriedade, que é um tipo de dicionário personalizado que oferece suporte à serialização. A classe padrão Dictionary no Microsoft .NET Framework 2.0 não oferece suporte à serialização, e o JSON, por padrão, serializa todos os dados de retorno. Nesse caso, uma classe do tipo Dictionary é necessária para que nomes de propriedades individuais não tenham que ser mapeados para a classe. Em vez disso, eles podem ser adicionados como um par chave/valor. Por exemplo, myValuePair.Item.Add(someKey, someValue).

Dica

A descrição da classe de dicionário personalizada está fora do escopo deste artigo; entretanto, a classe usada foi baseada no trabalho descrito na postagem do blog de Paul Welter no XML Serializable Generic Dictionary (em inglês) .

O método da Web funciona de forma idêntica à versão do XML na forma como ele recupera os dados. Ele cria o valor de retorno para o método usando o código a seguir.

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;

Agora que o método está retornando uma classe, podemos enumerar os dados no script do lado do cliente usando as propriedades da classe. Não é preciso mais criar um DOMDocument MSXML para enumerar os resultados, e o script do lado do cliente torna-se mais simples. O código real para processar os detalhes tem esta aparência.

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

A interface do usuário tem a mesma aparência da versão que usa XML. A parte permite avançar e voltar pelos dados das páginas. Ela mostra a interface “aguarde” quando os dados são carregados pela primeira vez e quando um usuário clica nos links de paginação Avançar ou Anterior.

Medindo os resultados

Em uma rede de baixa largura de banda ou alta latência, uma solução como esta pode ter um impacto muito positivo nos recursos de rede que são consumidos. Foi desenvolvida uma segunda Web Part que emitia exatamente os mesmos dados da mesma lista. Entretanto, todos os dados foram gerados na Web Part no servidor e depois gravados como HTML na página. Como resultado, sempre que os links Anterior ou Avançar eram clicados, ela forçava um postback para o servidor fazendo com que toda a página fosse retornada ao cliente. É importante observar também que se tivéssemos optado por usar um controle UpdatePanel do ASP.NET AJAX, o mesmo processo ocorreria. Todas as variáveis do formulário da página são postados de volta no servidor, e a página inteira é retornada da solicitação. Entretanto, somente a parte da página contida em UpdatePanel é atualizada. A Figura 6 mostra um instantâneo da solicitação ao clicar no link Avançar nessa segunda Web Part conforme capturado por Fiddler.

Figura 6. Solicitação ao clicar no link Avançar na segunda Web Part

Resultados do Fiddler para Web Part

A captura de Fiddler indica que a solicitação e a resposta de um processamento feito no estilo de postback dos dados da lista enviou um total de 79.424 bytes através da rede. Como alternativa, a Figura 7 mostra uma captura de Fiddler quando a Web Part habilitada para ASP.NET AJAX foi usada para recuperar os mesmos dados através do serviço Web personalizado usando XML.

Figura 7. A Web Part recuperou os mesmos dados através do serviço Web personalizado usando XML

Resultados do Fiddler para serviço Web personalizado

Os mesmos dados da lista foram recuperados, mas apenas 1973 bytes foram enviados através da rede. Essa é uma diferença bem grande, e o uso inteligente desta metodologia pode reduzir de forma significativa o tráfego na rede. A menor carga de todas, entretanto, foi gerada usando o método do serviço Web que usa o JSON para retornar a classe Records, conforme mostrado na Figura 8.

Figura 8. JSON usado para retornar a classe Records

Resultados do Fiddler para JSON

Conclusão

Ao usar o JSON, foi possível reduzir a 1817 bytes a carga total de uma solicitação enviada por conexão, o que é uma redução de 98% do tamanho da solicitação da parte que faz um postback de página inteira para recuperar e percorrer dados da página. Também houve uma redução do tamanho do ECMAScript (JScript, JavaScript) usado para enumerar os dados e uma simplificação do código no processo.

Embora seja mais complicado desenvolver uma solução como esta, se o seu site tiver restrição de largura de banda ou latência, esta abordagem pode ser uma boa opção para ajudar a melhorar o desempenho e a experiência do usuário final.

Recursos adicionais

Para obter mais informações, consulte os seguintes recursos:

Baixar este manual

Para facilitar a leitura e a impressão, este tópico está incluído no seguinte manual que pode ser baixado:

Consulte a lista completa de manuais disponíveis na página de download de conteúdo do Office SharePoint Server 2007.