Compartir a través de


Optimización de elementos web personalizados para la WAN

Resumen: aprenda técnicas para minimizar el impacto del ancho de banda en los elementos web personalizados, incluidas sugerencias de estilo generales e información específica, así como ejemplos de código para medios alternativos para recuperar y representar datos de listas de SharePoint. (24 páginas impresas)

Steve Peschka, Microsoft Corporation

Enero de 2008

Se aplica a: Microsoft Office SharePoint Server 2007, Windows SharePoint Services 3.0, ASP.NET AJAX 1.0

Contenido

  • Introducción a la optimización de elementos web personalizados para la WAN

  • Volver a usar los estilos integrados o crear estilos personalizados

  • Estado de almacenamiento

  • Maximización del rendimiento de elementos web que muestran datos

  • Conclusión

  • Recursos adicionales

Introducción a la optimización de elementos web personalizados para la WAN

Para desarrollar elementos web personalizados que se usarán en sitios con una latencia alta o un ancho de banda bajo, es preciso centrarse en los mismos principios de diseño generales que se usan al crear páginas para ese tipo de entorno. Debe hacer un esfuerzo para diseñar elementos que minimicen ambos viajes de ida y vuelta al servidor, así como la cantidad de datos que se envían a través de la red. En este artículo se presentan varias técnicas que se pueden implementar para cumplir estos objetivos de diseño.

Volver a usar los estilos integrados o crear estilos personalizados

Cuando los elementos web emiten HTML, use las clases de estilo que se incluyen en las hojas de estilo de Microsoft Office SharePoint Server 2007 y Windows SharePoint Services 3.0: core.css. (En este artículo, Office SharePoint Server y Windows SharePoint Services se denominan de forma colectiva Productos y Tecnologías de Microsoft SharePoint). Al volver a usar esos estilos, se puede minimizar el impacto en la página, ya que la página no deberá descargar una hoja de estilos adicional para admitir el elemento. Además, después de la visita inicial, el usuario ya tendrá el archivo core.css descargado de la memoria caché. Al usar estilos que forman parte del archivo core.css, se asegurará de que no se necesitan descargas adicionales para admitir el estilo.

Si necesita estilos personalizados con el elemento, puede usar una hoja de estilos personalizada que se pueda usar con la memoria caché BLOB. Si la almacena en una biblioteca de documentos, la hoja de estilos puede tener asociada una directiva de capacidad de almacenamiento en caché para no tener que descargarla después de la primera visita a la página. Esto tendrá un impacto menor en el sitio que si se usan estilos incorporados, por ejemplo, que se transmitirían por la red cada vez que se representara el elemento.

Estado de almacenamiento

Es posible que los elementos web necesiten realizar el seguimiento de información, como el usuario, la solicitud y el origen de datos. Existen varias opciones para el estado de almacenamiento, aunque en este artículo no se describen de forma individual. Sin embargo, suele haber dos opciones comunes que puede usar con los elementos web: ViewState y la memoria caché del servidor. En un entorno con ancho de banda bajo o una latencia alta, evite ViewState en la medida de lo posible, ya que agrega contenido a la página tanto en la descarga como en cualquier devolución (postback). Esto se podría aplicar a otros estados que también implican la transmisión de datos a través de la red, como las cadenas de consulta, los campos ocultos y las cookies.

Al usar la clase Cache del servidor, se puede almacenar información de estado en el nivel del servidor. El inconveniente de usar la memoria caché del servidor es que no está realmente diseñada para usarla como un mecanismo de estado por usuario (aunque según las circunstancias, se puede hacer que funcione de ese modo). Además, la información de la memoria caché no se replica en todos los servidores cliente web de la granja de servidores. Si el elemento depende de tener esa información de estado, independientemente del servidor cliente web al que acaba por tener acceso un usuario, la memoria caché del servidor no es una buena elección.

En ese escenario, se puede usar la opción Estado de sesión. El estado de sesión está desactivado de forma predeterminada, pero se habilita cuando se activa Microsoft Office InfoPath Forms Services (IPFS) en una granja de servidores. Cuando se habilita, usa Microsoft SQL Server para realizar el seguimiento del estado, lo que significa que los valores de estado de sesión se pueden usar sin importar cuál sea el servidor cliente web que recibe la solicitud HTTP. El inconveniente del estado de sesión es que esos datos permanecen en la memoria hasta que se eliminan o expiran. Los conjuntos de datos de gran tamaño almacenados en estado de sesión pueden disminuir el rendimiento del servidor si no se administran cuidadosamente. Debido a estas limitaciones, no se recomienda usar el estado de sesión a menos que sea absolutamente necesario.

También puede probar a desactivar ViewState en todas las páginas del sitio al editar el archivo web.config de una aplicación web. Contiene un elemento pages que tiene un atributo enableViewState habilitado. Si le preocupa el tamaño de ViewState en la página, puede probar a establecer este atributo en false (la configuración predeterminada es true). Si hace esto, deberá probar el sitio y toda la funcionalidad de forma exhaustiva para garantizar que todo funciona correctamente, ya que algunos controles y elementos web pueden esperar que ViewState esté activado.

Si desarrolla un elemento web y necesita usar algo similar a ViewState pero no está seguro de si estará disponible en una página, puede usar el estado de control en su lugar, que es nuevo en ASP.NET 2.0. En resumen, necesitaría realizar las siguientes acciones:

  1. Llamar a Page.RegisterRequiresControlState durante la inicialización para registrarse para el estado de control.

  2. Reemplazar el método LoadControlState y el método SaveControlState.

  3. Administrar de forma manual la parte de la colección de objetos que está asignada al estado de control.

Maximización del rendimiento de elementos web que muestran datos

Si el elemento web se usa para mostrar datos, puede intentar maximizar la experiencia de rendimiento para los usuarios finales de varias formas. En general, debe encontrar el equilibrio entre el número de viajes al servidor que son necesarios y la cantidad de datos que se debe recuperar para una solicitud.

Establecimiento de límites de representación

En el caso de los controles que emiten filas de datos, incluya una propiedad que permita a un administrador controlar la cantidad de filas que se muestran. En función de la latencia y el ancho de banda con los que se va a usar el control, la flexibilidad puede incrementar o disminuir la cantidad de datos que se representan en cada solicitud de página. Esto puede tener un impacto en el número de solicitudes que se necesitan para ver todos los datos.

Si el número de filas que se devuelve es una propiedad que pueden establecer los usuarios finales, considere la posibilidad de agregar limitaciones para que las opciones de un usuario no sobrecarguen la red.

Uso de islas de datos XML incorporadas

Otra alternativa para mostrar los datos es usar una isla de datos XML incorporados y realizar la representación en el cliente. Con este método, todos los datos que se mostrarán se emiten en la página como una isla de datos XML. El script del cliente se usa para representar realmente los datos en la página y se encarga de usar el XML DOM para recuperar y mostrar los datos de la isla.

Con esto, puede recuperar todos los datos en una única solicitud, de forma que el número de viajes de ida y vuelta al servidor se minimiza. Sin embargo, el tamaño de descarga será mayor, de forma que el tiempo de carga de página inicial aumentará. Además, si se usa en una página donde se usan otros controles que producen devoluciones (postbacks), forzará a que esta descarga de datos de gran tamaño se produzca todas las veces. Esta técnica se ajusta mejor a los escenarios donde se sabe que esto no ocurrirá, o si se usa como un método de recuperación de datos opcional. Por ejemplo, cree una propiedad pública para realizar el seguimiento de si todos los datos se deben descargar para que un administrador del sitio pueda controlar el comportamiento.

A continuación se muestra un ejemplo relativamente sencillo de un elemento web que hace esto: lee el contenido completo de una lista, lo emite a una página en formato XML y usa ECMAScript del cliente (JScript, JavaScript) para representar los datos y avanzar por las páginas de datos.

Nota

El código de este artículo no está pensado para representar los procedimientos recomendados de codificación de Microsoft. El código se simplifica para que el ejercicio sea más sencillo; los elementos del código deberían cambiarse para un entorno de producción.

El elemento web reemplaza al método Render y se inicia al obtener el contenido de una lista del sitio web denominado Contactos.

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

Después de obtener la referencia a la lista, se usa un objeto StringBuilder para crear la isla de datos XML que se emitirá en la 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());

Con el XML agregado a la página, también necesita un elemento en el que mostrar los datos. Para este requisito, se agrega una etiqueta <div> sencilla a la página.

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

La siguiente parte de la interfaz que necesita se usará para desplazarse por los datos. En este caso, se agregan dos botones de vínculo a la página. Existen dos puntos que se deben tener en cuenta: la propiedad OnClientClick y la colección Attributes. La propiedad OnClientClick se establece para usar una función de ECMAScript (JScript, JavaScript) personalizada que se escribe para mostrar los datos en el cliente.

La colección Attributes se usa para establecer la URL de navegación para LinkButton. En este caso, queremos que LinkButton se represente como un hipervínculo para que el usuario obtenga la información de que puede hacer clic en el elemento y se pueden realizar algunas acciones al hacer clic. En este caso, el vínculo # se usa como la URL de navegación, ya que en realidad no queremos navegar por ninguna ubicación, sólo queremos representar un vínculo y realizar la captura cuando se hace clic.

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

A continuación, se agregan algunos campos ocultos a la página para realizar el seguimiento de la paginación del control. En este caso, se desea realizar el seguimiento del tamaño de página (el número de registros que se muestran a la vez), el número de registro actual y el 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());

Cuando se cuenta con todos los elementos, el último paso del método Render es registrar un script de inicio que ejecute el javascript para representar los datos.

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

El javascript se encuentra en un archivo independiente del proyecto denominado Scripts.js. Está configurado para ser un recurso incrustado y se envía a la página como un recurso web (por ejemplo, webresource.axd). El código que lo configura para la descarga se ejecuta en el evento OnPreRender del elemento web. Primero llama al método IsClientScriptIncludeRegistered para comprobar que la referencia del script no se ha agregado ya a la página y, si no es así, lo registra como include para la 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 también requiere el registro del recurso web en la clase AssemblyInfo.cs para el proyecto.

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

Una vez representada la página, incluye un vínculo parecido al siguiente.

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

La función ECMAScript (JScript, JavaScript) lee el XML y representa los datos. Primero determina los límites de paginación: el número de registros del primer registro y el último registro que se van a representar. Cuando conoce los registros que debe mostrar, usa un método bastante sencillo para crear una instancia de MSXML en el 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.
      }
   }
}

Se usan variables para almacenar datos XML y una referencia al elemento DIV donde se mostrarán los resultados.

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

A continuación, se cargan los datos en DOMDocument de MSXML y se selecciona la lista de elementos 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");  

Cuando los nodos están seleccionados, los resultados pueden enumerarse para el conjunto de páginas específico que se necesita y representarse en la 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;
}

Cuando finaliza el proceso, el elemento muestra los registros en el cliente y proporciona un mecanismo para avanzar por las páginas sin hacer otro viaje de ida y vuelta al servidor. Las ilustraciones 1 y 2 muestran el elemento con dos páginas de datos diferentes.

Ilustración 1. Elemento web con datos

Primera página del elemento web de ejemplo

Ilustración 2. Elemento web con datos

Segunda página de los datos del elemento web

Uso del script del cliente que conecta con servicios web

Otra opción para administrar la cantidad de datos que se envían a través de la red es usar el script del cliente que se conecta con los servicios web para recuperar datos. Este método permite a un control recuperar los datos que necesita sin tener que devolver la página completa. Esto no sólo favorece la experiencia del usuario, sino que también coloca una carga más ligera en la red, ya que la página completa no se envía al servidor y otra vez de vuelta para recuperar los datos. La recuperación de los datos se puede realizar de forma directa con el componente XmlHttp o con Microsoft AJAX Library 1.0, que ajusta la funcionalidad de XmlHttp.

Mientras que XmlHttp y ASP.NET AJAX representan dos opciones de nivel alto, es probable que sea más sencillo usar ASP.NET AJAX junto con un servicio web personalizado que exponga los datos de la lista de SharePoint. Aunque técnicamente es posible usar las bibliotecas ASP.NET AJAX con un servicio web basado en SOAP (como los servicios web que se integran con Productos y Tecnologías de SharePoint), este proceso es bastante más complicado y no ofrece ninguna de las ventajas adicionales de los servicios web de estilo ASP.NET AJAX, como tamaños de carga más pequeños y compatibilidad con JavaScript Object Notation (JSON).

Productos y Tecnologías de SharePoint proporciona varios servicios web que puede usar para mostrar datos. El servicio web Lists permite recuperar datos tabulares de listas y bibliotecas de Productos y Tecnologías de SharePoint. Puede usarlo para representar los datos contenidos en las listas y los vínculos a los elementos de una lista, como documentos e imágenes. El servicio web Search permite buscar en el cuerpo del contenido de Productos y Tecnologías de SharePoint y cualquier otro origen externo que se rastree. También se puede usar (con consultas controladas por metadatos construidas cuidadosamente) para recuperar un conjunto de datos filtrado de una o más listas de SharePoint.

En el siguiente ejemplo se muestra la creación de un servicio web personalizado para recuperar los datos de la lista de Productos y Tecnologías de SharePoint. La clase del servicio web se anota con el atributo System.Web.Script.Services.ScriptService, que permite usar los componentes del servicio web del cliente que se proporciona con ASP.NET AJAX. A continuación, el servicio web se "registra" con Productos y Tecnologías de SharePoint para que se pueda tener acceso al mismo a través del directorio _vti_bin con todos los servicios web integrados. La página maestra se actualiza para incluir el control ScriptManager de ASP.NET AJAX y las etiquetas declarativas para el servicio web personalizado. A continuación, se escribe un elemento web que genera el script del cliente para recuperar datos a través de los componentes ASP.NET AJAX del servicio web personalizado y se muestra en la página.

Instalación de AJAX

Primero, debe instalar los archivos binarios ASP.NET AJAX 1.0 en los servidores de SharePoint. Puede descargarlos del sitio de ASP.NET AJAX (en inglés). Debe instalar estos archivos en cada servidor cliente web de la granja de servidores.

Después de instalar ASP.NET AJAX, debe actualizar el archivo web.config en cada aplicación web donde se va a usar ASP.NET AJAX. Este ejercicio es bastante laborioso; para obtener las instrucciones completas paso a paso, vea la entrada del blog de Mike Ammerlan sobre la integración de ASP.NET AJAX con SharePoint (en inglés).

Creación de un servicio web personalizado

Es necesario crear un servicio web personalizado para recuperar los datos, ya que permite aplicar System.Web.Script.Services.ScriptService a su clase de servicio web, lo cual permite usarlo con el marco de servicio web ASP.NET AJAX. En este caso, se desarrolló una clase de servicio web relativamente sencilla para recuperar datos de lista basados en parámetros básicos.

La clase de servicio web incluye una referencia a la clase System.Web.Extensions para habilitar la compatibilidad con ASP.NET AJAX. Después de agregar esa referencia, se agrega una instrucción using (Microsoft Visual C#) o Imports (Microsoft Visual Basic) a la clase.

using System.Web.Script.Services;

A continuación, la clase se decora con el atributo ScriptService para que el marco de servicio web ASP.NET AJAX lo pueda consumir directamente. Incluye un constructor sin parámetros predeterminado para que ASP.NET AJAX pueda serializar la clase.

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

      public AjaxDataWebService()
      {
         // Default constructor
   }

En el primer ejemplo, el servicio web sólo contiene un método, que devuelve una cadena. La cadena es en realidad código XML que se consumirá en el cliente. La firma del método se define como se muestra a continuación.

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

La primera parte del código obtiene una referencia al sitio, la Web y la lista que contiene los datos.

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

A continuación, se crea la semántica de la consulta en función de la dirección de paginación, el identificador de inicio y el tamaño del 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();

Una vez configurada la consulta, el siguiente paso es la ejecución. Cuando se devuelven los resultados, se vuelve a realizar una comprobación de la dirección de paginación. Si se pagina hacia atrás, se debe volver a invertir el orden de los resultados para que aparezcan ordenados del identificador más pequeño al más grande en la página. De nuevo, esto sólo es para admitir la paginación en el elemento web. Para simplificar la ordenación, los datos se recuperan en un objeto DataTable de ADO.NET. Después de recuperar los datos y ordenarlos adecuadamente, cada fila se enumera para crear el XML que se devuelve desde la llamada al 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>");

Por supuesto, el código anterior se encuentra en un bloque try…catch; en el bloque finally se liberan los recursos asociados con el objeto DataTable y, a continuación, se devuelve el XML que se creó.

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

return ret.ToString();

Registro del servicio web personalizado

Para exponer el servicio web personalizado para su uso en el elemento web habilitado para ASP.NET AJAX se requieren dos tipos de configuración. El primero consiste en configurarlo para que Productos y Tecnologías de SharePoint conozca el servicio web y lo pueda llamar en el contexto de una aplicación web de SharePoint. Esto implica varios pasos que, a nivel general, requieren las siguientes acciones:

  1. Crear la clase de código subyacente para el servicio web en un ensamblado independiente y registrarlo en la memoria caché de ensamblados global.

  2. Generar y editar un archivo de detección estático y un archivo de Lenguaje de descripción de servicios web (WSDL).

  3. Implementar el archivo de servicio web para el directorio _vti_bin.

Es necesario realizar varios pasos para completar todas las acciones anteriores. Afortunadamente, hay un artículo preceptivo que describe el procedimiento. Para obtener detalles completos, vea Tutorial: creación de un servicio web personalizado.

Después de integrar el servicio web en el directorio _vti_bin de SharePoint, debe modificar la página maestra para agregar una etiqueta <ScriptManager> ASP.NET AJAX. Dentro de la etiqueta <ScriptManager>, puede definir un punto de entrada de servicios para el servicio web personalizado que se usa; en este ejemplo, el servicio web personalizado se denomina ListData.asmx. A continuación se muestra una etiqueta completa que se agrega a la página maestra.

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

Con el servicio web configurado para que se le pueda llamar desde el directorio _vti_bin de SharePoint y la etiqueta <ScriptManager> agregada a la página maestra con una referencia al servicio web personalizado, los componentes del cliente del servicio web ASP.NET AJAX se pueden comunicar con él.

Creación de un elemento web personalizado mediante datos XML

A continuación, se necesita un elemento web personalizado para generar el script del cliente ASP.NET AJAX para recuperar datos del servicio web personalizado. El propio servicio web apenas contiene lógica del servidor; los datos masivos del código se incluyen en un archivo ECMAScript (Jscript, JavaScript) que el elemento web agrega como un recurso web (archivo WebResource.axd).

El primer paso consiste en generar todo el HTML que se necesita en la página para la interfaz de usuario. Hay dos formas básicas de generar HTML a partir de un elemento web. La forma sencilla es escribir las etiquetas directamente como cadenas; una forma más compleja, aunque más segura, es usar las bibliotecas de clases ASP.NET. Normalmente, resulta más rápido y sencillo emitir simplemente las cadenas con HTML relativamente sencillo. El HTML que se usa en este caso es ligeramente más complejo. Contiene tres etiquetas <div> principales para los datos, los controles de navegación y los elementos de la interfaz “Espere...”. La etiqueta “Espere...”<div> contiene dos etiquetas <div> anidadas para colocar correctamente la imagen y el texto en el elemento. En función de estos requisitos de HTML más complicados, las bibliotecas de clases ASP.NET se usaban para generar el HTML, como se muestra en el siguiente código.

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

A continuación, el elemento web agrega varios campos ocultos que se usan para realizar el seguimiento del estado al paginar los datos.

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

Por último, se registra un script de inicio para que el elemento web recupere la primera página de datos cuando se cargue la página:

Creación de un script de inicio para llamar al método de carga de datos cuando se carga la página.

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

El archivo EMCAScript (Jscript, JavaScript) que se usa para recuperar los datos se registra en el evento OnPreRender. Aquí también se usó el mismo proceso de agregar el script como un recurso incrustado y registrarlo en el archivo AssemblyInfo.cs, tal como se describió para el elemento web de la isla XML. El registro del archivo ECMAScript es similar al que se muestra a continuación.

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

Con el HTML que se ha creado, el archivo JScript o JavaScript puede generar una interfaz “Espere...” cuando la página se carga y se solicitan datos, y cada vez que se recupera una nueva página de datos porque el usuario ha hecho clic en los vínculos Siguiente o Anterior. En este caso, el GIF animado que se usa se incluye con Productos y Tecnologías de SharePoint para que resulte familiar a los usuarios. La interfaz “Espere...” tiene el siguiente aspecto.

Ilustración 3. Interfaz "Espere..."

Mensaje de recuperación de datos del elemento web

La recuperación de los datos y la administración de la interfaz de usuario se controlan mediante el script del cliente. El archivo JScript o el JavaScript se inicia cambiando la interfaz para ocultar los elementos DIV que contienen los datos de lista y los controles de paginación, y mostrando la interfaz “Espere...”. A continuación, usa los campos ocultos para reunir la información para los parámetros del método de servicio web y usa el marco de trabajo ASP.NET AJAX para llamar al método de servicio 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; 

La llamada a un servicio web a través de ASP.NET AJAX es algo distinta a los servicios web tradicionales basados en SOAP. La primera diferencia es que es necesario usar el nombre de clase completo al llamar a un método de servicio web. El otro punto diferente es que, además de proporcionar todos los parámetros para el método de servicio web, se agregan tres parámetros adicionales al final. Estos parámetros representan funciones en JScript o JavaScript que se llamarán cuando se devuelvan los datos (OnComplete), si el tiempo de espera de la llamada se agota (OnTimeOut), o si se produce un error (OnError). En este elemento de muestra, tanto la función OnTimeOut como la función OnError únicamente representan la información que se devolvió directamente al elemento DIV donde se suelen mostrar los datos.

La función OnComplete es el único parámetro necesario de los tres y es sólo una función JScript parecida a la siguiente.

function OnComplete(arg)
{
  …
}

El parámetro arg contiene el valor devuelto desde la llamada al método de servicio web; en este caso es una cadena que contiene XML. Para este proyecto, el servicio web devuelve XML con el mismo formato que se usó para el elemento web de la isla XML. Así, el código para enumerar los datos y representarlos en las páginas es casi idéntico. Primero crea un DOMDocument de MSXML y comprueba que se ha devuelto algún XML válido.

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

Este código de representación sólo se diferencia del elemento web de la isla XML en que los identificadores del primer y el último elemento se almacenan en los campos ocultos startID y endID para admitir la funcionalidad de paginación del control. Cuando el elemento web recupera los datos, los representa en el elemento DIV adecuado y permite avanzar página y retroceder página por el contenido. Las ilustraciones 4 y 5 muestran las dos primeras páginas de datos.

Ilustración 4. Primera página de datos

Primera página del elemento web AJAX

Ilustración 5. Segunda página de datos

Elemento web AJAX de ejemplo: segunda página

Creación de un elemento web personalizado mediante JSON

En el ejemplo anterior, el XML se devolvió desde el método de servicio web y, a continuación, se usó un XPath y un DOMDocument de MSXML para leer el contenido. Sin embargo, una de las características más importantes de ASP.NET AJAX es su capacidad para consumir datos mediante JSON (JavaScript Object Notation). Esto permite que el programador del cliente funcione con objetos y propiedades para tener acceso a los datos, en lugar de manipular XML más complejo.

Para demostrar esto, se creó un segundo método web que devuelve una clase personalizada; la firma del método es similar al siguiente código.

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

La clase Records es una clase personalizada que se desarrolló para guardar los datos devueltos; su definición es similar al siguiente código.

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

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

En la clase Records, la propiedad Count se refiere al número total de elementos encontrados y la propiedad ItemCount se refiere al número de elementos que se devuelven en esta llamada determinada. Estas propiedades se usan para paginar los datos en el cliente. Los datos reales que se muestran están contenidos en la propiedad Items, que es una lista de elementos Record. La clase Record se define como se muestra a continuación.

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

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

La clase Record sólo tiene una propiedad, que es un tipo de diccionario personalizado compatible con la serialización. La clase Dictionary personalizada de Microsoft .NET Framework 2.0 es compatible con la serialización, y JSON, de forma predeterminada, serializa todos los datos devueltos. En este caso, se necesita una clase de tipo Dictionary para que los nombres de propiedades individuales no se tengan que asignar en la clase. En su lugar, se pueden agregar como un par clave/valor. Por ejemplo, myValuePair.Item.Add(someKey, someValue).

Nota

Este artículo no trata la descripción de la clase de diccionarios personalizada; sin embargo, la clase que se usa está basada en el trabajo descrito en la entrada del blog de Paul Welter sobre el diccionario genérico serializable XML (en inglés).

El método web funciona de manera idéntica a la versión XML en cuanto a la forma de recuperar los datos. Crea el valor de retorno para el método mediante el siguiente código.

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;

Ahora que el método devuelve una clase, se pueden enumerar los datos del script del cliente mediante la firma de las propiedades de la clase. Tampoco es necesario ya crear un DOMDocument de MSXML para enumerar los resultados y el script del cliente se vuelve mucho más sencillo. El código real para recuperar los detalles es similar al siguiente.

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

La interfaz de usuario es exactamente igual a la versión que usa XML. El elemento admite la paginación por los datos tanto hacia delante como atrás. Muestra la interfaz “Espere...” cuando los datos se cargan por primera vez y cuando un usuario hace clic en los vínculos de paginación Siguiente o Anterior.

Medición de los resultados

En una red con ancho de banda bajo o una latencia alta, una solución como ésta puede tener un impacto muy positivo en los recursos de red que se consumen. Se escribió un segundo elemento web que emitió exactamente los mismos datos de la misma lista. Sin embargo, todos los datos se generaron en el elemento web del servidor y, a continuación, se escribieron como HTML en la página. Como resultado, cada vez que se hacía clic en los vínculos de paginación Siguiente o Anterior, se forzaba una devolución de información al servidor y la página completa se volvía a enviar al cliente. Es importante tener en cuenta también que si se hubiera usado un control UpdatePanel de ASP.NET AJAX, se produciría el mismo proceso. Todos los formularios disponibles para la página se devuelven al servidor y la página completa se vuelve a enviar desde la solicitud. Sin embargo, sólo se actualiza la parte de la página contenida en UpdatePanel. La ilustración 6 muestra una instantánea de la solicitud al hacer clic en el vínculo Siguiente en este segundo elemento web como en la captura de Fiddler.

Ilustración 6. Solicitud al hacer clic en el vínculo Siguiente en el segundo elemento web

Resultados de Fiddler para el elemento web

La captura de Fiddler indica que la solicitud y la respuesta de hacer una representación de estilo de devolución de los datos de lista enviaron un total de 79.424 bytes a través de la red. Como alternativa, la ilustración 7 muestra una captura de Fiddler cuando se usó el elemento web habilitado para ASP.NET AJAX para recuperar los mismos datos a través del servicio web personalizado mediante XML.

Ilustración 7. El elemento web recuperó los mismos datos a través del servicio web personalizado mediante XML

Resultados de Fiddler para el servicio web personalizado

Se recuperaron los mismos datos de lista, pero sólo se enviaron 1973 bytes a través de la red. Esto supone una diferencia tremenda y el uso eficaz de este método podría reducir significativamente el tráfico de la red. La carga menor de todas, sin embargo, se generó mediante el uso del método de servicio web que usa JSON para devolver la clase Records, como se muestra en la ilustración 8.

Ilustración 8. JSON usado para devolver la clase Records

Resultados de Fiddler para JSON

Conclusión

Con JSON se consiguió reducir la carga total enviada a través de la conexión para una solicitud a 1817 bytes, lo que supone una reducción del 98% del tamaño de la solicitud para el elemento que realiza una devolución (postback) de página completa para recuperar los datos y moverse por sus páginas. También se consiguió recortar el tamaño del archivo ECMAScript (JScript, JavaScript) que se usa para enumerar los datos, y en el proceso, simplificar también el código.

Aunque es más complicado desarrollar una solución de este tipo, si el sitio tiene un ancho de banda o una latencia limitados, este método puede ser una buena opción para mejorar el rendimiento y la experiencia del usuario final.

Recursos adicionales

Para obtener más información, vea los siguientes recursos:

Descarga de este libro

En este tema se incluye el siguiente libro descargable para facilitar la lectura y la impresión:

Vea la lista completa de libros disponibles en la página que muestra el contenido descargable para Office SharePoint Server 2007.