Ottimizzazione di web part personalizzate per la rete WAN

Riepilogo: informazioni su come ridurre l'impatto sulla larghezza di banda in web part personalizzate, con suggerimenti di carattere generale, informazioni specifiche ed esempi di codice relativi a modalità alternative per recuperare e visualizzare dati di elenchi di SharePoint (24 pagine stampate).

Steve Peschka - Microsoft Corporation

Gennaio 2008

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

Sommario

  • Introduzione all'ottimizzazione di web part personalizzate per la rete WAN

  • Riutilizzo di stili incorporati o creazione di stili personalizzati

  • Archiviazione dello stato

  • Ottimizzazione delle prestazioni di web part per la visualizzazione di dati

  • Conclusioni

  • Risorse aggiuntive

Introduzione all'ottimizzazione di web part personalizzate per la rete WAN

Lo sviluppo di web part personalizzate da utilizzare nei siti caratterizzati da latenza elevata o larghezza di banda limitata richiede particolare attenzione relativamente agli stessi principi generali di progettazione adottati quando si creano pagine per questo tipo di ambienti. È consigliabile cercare di progettare web part in grado di ridurre al minimo il numero di round trip al server e la quantità di dati inviata sulla rete. In questo articolo vengono illustrate diverse tecniche utilizzabili per conseguire tali obiettivi di progettazione.

Riutilizzo di stili incorporati o creazione di stili personalizzati

Quando le web part elaborano HTML, utilizzare le classi di stile incorporate nel foglio di stile di Microsoft Office SharePoint Server 2007 e Windows SharePoint Services 3.0: core.css. In questo articolo si fa riferimento a Office SharePoint Server e a Windows SharePoint Services collettivamente come Prodotti e tecnologie Microsoft SharePoint. Riutilizzando tali stili, è possibile ridurre al minimo l'impatto sulla pagina in quanto non sarà necessario scaricare un ulteriore foglio di stile solo per il supporto della web part. Inoltre, dopo la prima visita al sito, il file core.css sarà scaricato nella cache dell'utente e quindi potrà essere rapidamente riutilizzato per le visite successive. Utilizzando gli stili che fanno parte di core.css, è possibile evitare che siano necessari ulteriori download per il supporto degli stili.

Se sono necessari stili personalizzati per la web part, è possibile prendere in considerazione l'utilizzo di un foglio di stile personalizzato utilizzabile con la cache BLOB. Se lo si archivia in una raccolta di documenti, al foglio di stile verrà associata la direttiva di memorizzazione nella cache in modo che non sia più necessario scaricarlo dopo il primo caricamento della pagina. Ciò determinerà un impatto inferiore sul sito rispetto all'utilizzo di stili in linea i quali verrebbero ritrasmessi sulla rete ogni volta che viene eseguito il rendering della web part.

Archiviazione dello stato

Potrebbe essere necessario registrare nelle web part informazioni quali l'utente, la richiesta e l'origine dati. Sono disponibili diverse opzioni per l'archiviazione dello stato, le quali vengono descritte in questo articolo. In generale, tuttavia, esistono due opzioni più comuni che è possibile utilizzare con le web part: la proprietà ViewState e la classe Cache del server. In un ambiente con larghezza di banda limitata o a latenza elevata, evitare se possibile la proprietà ViewState in quanto essa aggiunge contenuto alla pagina sia per il download che per eventuali postback. Ciò si applica ad altre forme di stato che implicano la trasmissione di dati in rete, ad esempio stringhe di query, campi nascosti e cookie.

La classe Cache del server consente di archiviare informazioni sullo stato a livello del server. Lo svantaggio dell'utilizzo della classe Cache del server consiste nel fatto che essa non è specificatamente progettata come meccanismo di stato per utente (sebbene a seconda delle circostanze possa essere utilizzata in questo modo). Inoltre, le informazioni della cache non vengono replicate in tutti i server Web front-end della server farm. Se il funzionamento della web part dipende dalla disponibilità delle informazioni sullo stato indipendentemente dal server Web front-end caricato in seguito alla richiesta dell'utente, la classe Cache del server non rappresenta la scelta ideale

In tale scenario, un'altra opzione consiste nell'utilizzare Stato sessione. L'opzione Stato sessione è disattivata per impostazione predefinita, ma viene attivata quando si attiva Microsoft Office InfoPath Form Services (IPFS) in una server farm. Se l'opzione è attivata, viene utilizzato Microsoft SQL Server per registrare lo stato, il che significa che i valori di stato della sessione possono essere utilizzati indipendentemente dal server Web front-end che riceve la richiesta HTTP. Lo svantaggio di questo tipo di soluzione consiste nel fatto che i dati rimangono nella memoria finché non vengono rimossi o scadono. La presenza di set di dati di grandi dimensioni in Stato sessione può ridurre le prestazioni del server se non viene gestita con attenzione. A causa di questi vincoli, è sconsigliabile utilizzare Stato sessione se non è assolutamente necessario.

È inoltre possibile provare a disattivare ViewState per tutte le pagine nel sito modificando il file web.config per un'applicazione Web. Il file contiene un elemento pages che presenta un attributo enableViewState. In caso di problemi significativi dovuti alle dimensioni di ViewState nella pagina, è possibile provare a impostare questo attributo su false (è impostato su true per impostazione predefinita). In questo caso, è necessario testare con attenzione l'intero sito e tutte le caratteristiche per verificare che funzionino correttamente, in quanto alcuni controlli e web part potrebbero richiedere l'attivazione di ViewState.

Se si sviluppa una web part ed è necessario utilizzare qualcosa di analogo a ViewState ma non si è sicuri se sarà disponibile in una pagina, è possibile utilizzare lo stato del controllo, una novità in ASP.NET 2.0. In breve, è necessario eseguire le operazioni seguenti:

  1. Eseguire la registrazione dello stato del controllo chiamando Page.RegisterRequiresControlState durante l'inizializzazione.

  2. Eseguire l'override del metodo LoadControlState e del metodo SaveControlState.

  3. Gestire manualmente la parte dell'insieme dell'oggetto mappata allo stato del controllo.

Ottimizzazione delle prestazioni di web part per la visualizzazione di dati

Se la web part viene utilizzata per visualizzare i dati, è possibile provare a ottimizzare le prestazioni per gli utenti finali in diversi modi. In generale, è necessario raggiungere un equilibrio tra il numero di trip al server necessari e la quantità di dati che devono essere recuperati per una richiesta.

Impostazione di limiti di rendering

Per i controlli che elaborano righe di dati, includere una proprietà che consenta all'amministratore di controllare il numero di righe visualizzate. A seconda della latenza e della larghezza di banda in cui verrà utilizzato il controllo, ciò assicura la flessibilità necessaria per aumentare o ridurre la quantità di dati visualizzati in ogni richiesta di pagina. Ciò può inoltre influire sul numero di richieste necessarie per visualizzare tutti i dati.

Se il numero di righe restituito è una proprietà che può essere impostata dagli utenti finali, valutare l'opportunità di aggiungere vincoli in modo che le opzioni a disposizione degli utenti non possano sovraccaricare la rete.

Utilizzo di isole di dati XML in linea

Un'altra alternativa per la visualizzazione dei dati consiste nell'utilizzo di un'isola di dati XML in linea e nell'esecuzione del rendering sul lato client. Con questo approccio tutti i dati che verranno visualizzati vengono elaborati nella pagina come isola di dati XML. Lo script del lato client viene utilizzato per il rendering effettivo dei dati nella pagina ed è responsabile per l'utilizzo del modello DOM XML allo scopo di recuperare e visualizzare i dati dell'isola.

In questo modo, è possibile recuperare tutti i dati in una singola richiesta in modo da ridurre al minimo il numero di round trip al server. Tuttavia, la dimensione di download risulterà ovviamente maggiore e quindi il caricamento iniziale della pagina richiederà più tempo. Se viene inoltre utilizzato in una pagina in cui sono presenti altri controlli che provocano postback, il download dei dati verrà imposto ogni volta. Questa tecnica è ideale per gli scenari in cui si è certi che ciò non accadrà oppure come metodo alternativo per il recupero dei dati. Creare ad esempio una proprietà pubblica per verificare se tutti i dati devono essere scaricati in modo che un amministratore del sito possa controllare il comportamento.

Di seguito è disponibile un esempio relativamente semplice di web part che utilizza questo approccio; legge tutto il contenuto di un elenco, lo elabora nella pagina come XML e utilizza lo script ECMAScript (JScript, JavaScript) del lato client per eseguire il rendering dei dati nella pagina.

Nota

Il codice in questo articolo non è concepito per rappresentare le procedure di programmazione consigliate di Microsoft. Il codice è semplificato per agevolare l'esercizio. Gli elementi di codice devono essere modificati per un ambiente di produzione.

La web part esegue l'override del metodo Render e inizia a recuperare il contenuto da un elenco nel sito Web denominato Contacts.

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

Dopo aver ottenuto il riferimento all'elenco, viene utilizzato un oggetto StringBuilder per creare l'isola di dati XML che verrà elaborata nella pagina.

// 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 l'XML aggiunto alla pagina, è inoltre necessario un elemento in cui visualizzare i dati. Per questo requisito, è sufficiente aggiungere un semplice tag <div> alla pagina.

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

La parte successiva dell'interfaccia necessaria verrà utilizzata per scorrere i dati. In questo caso, si aggiungono due pulsanti di collegamento alla pagina. Due elementi da notare sono la proprietà OnClientClick e l'insieme Attributes. La proprietà OnClientClick è impostata per l'utilizzo della funzione personalizzata ECMAScript (JScript, JavaScript) che viene scritta per visualizzare i dati nel client.

L'insieme Attributes viene utilizzato per impostare l'URL di spostamento per LinkButton. In questo caso, l'obiettivo è che LinkButton sia visualizzato come collegamento ipertestuale, riconoscibile come tale dall'utente finale e utilizzabile quindi per eseguire un'azione quando viene fatto clic su di esso. In questo caso viene utilizzato il collegamento # come URL di spostamento poiché in realtà non ci si sposterà altrove, bensì si desidera effettuare il rendering di un collegamento e registrare che è stato fatto clic su di esso.

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

Successivamente, è necessario aggiungere alcuni campi nascosti alla pagina per registrare il paging del controllo. In questo caso, si desidera tenere traccia delle dimensioni della pagina (il numero di record da visualizzare in un determinato momento), il numero di record corrente e il numero totale di record.

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

Con tutti gli elementi presenti, l'ultimo passaggio nel metodo Render è destinato a registrare uno script di avvio che esegue il JavaScript per il rendering dei dati.

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

Il codice JavaScript è contenuto in un file separato del progetto denominato Scripts.js. Esso è configurato in modo da essere una risorsa incorporata e viene inviato alla pagina come risorsa Web (ad esempio, webresource.axd). Il codice che lo configura per il download viene eseguito nell'evento OnPreRender della web part. Viene innanzitutto verificato che il riferimento allo script non sia già aggiunto alla pagina chiamando il metodo IsClientScriptIncludeRegistered. In caso negativo, lo script viene registrato come elemento include per la pagina.

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

Questo metodo richiede inoltre la registrazione della risorsa Web nella classe AssemblyInfo.cs per il progetto.

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

Quando viene eseguito il rendering della pagina, essa include un collegamento analogo a quello riportato di seguito.

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

L'elemento ECMAScript (JScript e JavaScript) legge il codice XML ed esegue il rendering dei dati. Vengono innanzitutto determinati i limiti di paging: il numero del primo e dell'ultimo record da visualizzare. Dopo aver determinato i record da visualizzare, viene utilizzato un metodo estremamente semplice per creare un'istanza di MSXML sul client.

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

Le variabili sono utilizzate per memorizzare sia i dati XML sia un riferimento all'elemento DIV in cui verranno visualizzati i risultati.

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

I dati vengono quindi caricati nell'elemento DOMDocument da MSXML e quindi viene selezionato l'elenco degli elementi 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");  

Dopo che i nodi sono stati selezionati, è possibile enumerare i risultati per il set di pagine specifico necessario e visualizzato nella pagina.

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

Al termine, la web part visualizza i record sul client e offre un meccanismo per scorrere i dati senza ulteriori round trip al server. Nella figura 1 e nella figura 2 è illustrata la web part con due diverse pagine di dati.

Figura 1. Web part con dati

Prima pagina di una web part di esempio

Figura 2. Web part con dati

Seconda pagina dei dati della web part

Utilizzo dello script del lato client per la connessione ai servizi Web

Un'altra opzione per la gestione della quantità di dati inviati sulla rete consiste nell'utilizzare lo script del lato client che si connette a servizi Web per recuperare i dati. Questo metodo consente a un controllo di recuperare i dati necessari senza che sia necessario il postback dell'intera pagina. Ciò non solo assicura migliori prestazioni per l'utente, ma implica inoltre un carico minore sulla rete poiché per recuperare i dati non è necessario inviare e ricevere nuovamente l'intera pagina dal server. Il recupero dei dati può essere eseguito con il componente XmlHttp direttamente o con la libreria Microsoft AJAX 1.0, che incapsula funzionalità intorno a XmlHttp.

Mentre XmlHttp e ASP.NET AJAX rappresentano due opzioni di alto livello, è probabile che risulti più semplice da utilizzare ASP.NET AJAX in combinazione con un servizio Web personalizzato che espone i dati dell'elenco di SharePoint. Sebbene sia tecnicamente possibile utilizzare le librerie ASP.NET AJAX con un servizio Web basato su SOAP (quali sono i servizi Web incorporati in Prodotti e tecnologie SharePoint), ciò risulta estremamente più complicato e non garantisce alcuno degli ulteriori vantaggi dei servizi Web di tipo ASP.NET AJAX, ad esempio dimensioni di payload inferiori e il supporto per JSON (JavaScript Object Notation).

Prodotti e tecnologie SharePoint include diversi servizi Web che è possibile utilizzare per la visualizzazione dei dati. Il servizio Web Lists consente di recuperare dati in formato tabella da elenchi e raccolte di Prodotti e tecnologie SharePoint. È possibile utilizzarlo per eseguire il rendering dei dati contenuti negli elenchi e stabilire collegamenti agli elementi di un elenco, ad esempio documenti o immagini. Il servizio Web di Search consente di eseguire ricerche nel corpo del contenuto all'interno di Prodotti e tecnologie SharePoint e di eventuali altre origini dati esterne sottoposte a ricerca per indicizzazione. Con query guidate dai metadati attentamente realizzate, il servizio può inoltre essere utilizzato per recuperare un set di dati filtrato da uno o più elenchi di SharePoint.

Nell'esempio seguente viene illustrata la creazione di un servizio Web personalizzato per recuperare i dati di un elenco di Prodotti e tecnologie SharePoint. La classe del servizio Web viene annotata con l'attributo System.Web.Script.Services.ScriptService che consente di utilizzare i componenti del servizio Web sul lato client forniti con ASP.NET AJAX. Il servizio Web viene quindi "registrato" in Prodotti e tecnologie SharePoint in modo che sia possibile accedervi tramite la directory _vti_bin con tutti i servizi Web incorporati. La pagina master viene aggiornata in modo da includere il controllo ASP.NET AJAX ScriptManager e tag dichiarativi per il servizio Web personalizzato. Viene quindi scritta una web part che genera lo script lato client per recuperarre i dati tramite i componenti ASP.NET AJAX dal servizio Web personalizzato e visualizzarli nella pagina.

Installazione di AJAX

In primo luogo, è necessario installare i file binari di ASP.NET AJAX 1.0 nei server SharePoint. È possibile scaricarli dal sito ASP.NET AJAX (informazioni in lingua inglese). È necessario installare questi file in ogni server Web front-end della server farm.

Dopo aver installato ASP.NET AJAX, è necessario aggiornare il file web.config per ogni applicazione Web in cui ASP.NET AJAX verrà utilizzato. Si tratta di un'attività che richiede tempo. Per istruzioni dettagliate complete, vedere il post del blog di Mike Ammerlan sull'integrazione di ASP.NET AJAX con SharePoint (informazioni in lingua inglese).

Creazione di un servizio Web personalizzato

La creazione di un servizio Web personalizzato per recuperare i dati è necessaria poiché consente di applicare System.Web.Script.Services.ScriptService alla classe del servizio Web in modo che quest'ultimo possa essere utilizzato con il framework del servizio Web ASP.NET AJAX. In questo caso, è disponibile una classe del servizio Web relativamente semplice appositamente sviluppata per recuperare i dati di elenco in base a parametri semplici.

La classe del servizio Web include un riferimento alla classe System.Web.Extensions per attivare il supporto di ASP.NET AJAX. Dopo aver aggiunto il riferimento, alla classe viene aggiunta un'istruzione using (Microsoft Visual C#) o Imports (Microsoft Visual Basic).

using System.Web.Script.Services;

Alla classe viene quindi aggiunto l'attributo ScriptService in modo che possa essere utilizzata direttamente dal framework dei servizi Web ASP.NET AJAX. Include un costruttore senza parametri predefinito in modo che la classe possa essere serializzata da ASP.NET AJAX.

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

      public AjaxDataWebService()
      {
         // Default constructor
   }

Nel primo esempio, il servizio Web contiene un solo metodo, che restituisce una stringa. La stringa è in effetti l'XML che verrà utilizzato nel client. La firma del metodo viene definita come segue.

[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 prima parte del codice ottiene un riferimento al sito, Web ed elenco che contiene i dati.

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

Successivamente, viene creata la semantica delle query in base alla direzione di paging, l'ID di inizio e le dimensioni del set di risultati.

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

Dopo aver configurato la query, non rimane che passare all'esecuzione. Quando i risultati vengono restituiti, viene eseguito un controllo per la direzione di paging. Se ci si sposta indietro allora l'ordine dei risultati deve essere di nuovo invertito in modo che nella pagina si passi dagli ID più piccoli a quelli più grandi. Ciò è anche in questo caso esclusivamente relativo al supporto del paging nella web part. Per semplificare l'ordinamento, i dati vengono recuperati in un oggetto DataTable di ADO.NET. Dopo aver recuperato i dati e averli ordinati in modo appropriato, ogni riga viene enumerata per creare l'XML restituito dalla chiamata al metodo.

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

Tutto il codice precedente è incluso in un blocco try…catch, mentre, naturalmente, nel blocco finally vengono rilasciate le risorse associate all'oggetto DataTable e quindi viene restituito l'XML creato.

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

return ret.ToString();

Registrazione del servizio Web personalizzato

L'esposizione del servizio Web personalizzato per l'utilizzo nella web part ASP.NET AJAX richiede due tipi di configurazione. Un tipo consiste nel configurare il servizio in modo che venga rilevato e quindi chiamato da Prodotti e tecnologie SharePoint nel contesto di un'applicazione Web di SharePoint. Ciò prevede numerosi passaggi che a livello generale richiedono di effettuare quanto segue:

  1. Generare la classe code-behind per il servizio Web in un assembly separato e registrarlo nella Global Assembly Cache (GAC).

  2. Generare e modificare un file di individuazione statica e un file WSDL (Web Services Description Language).

  3. Distribuire i file del servizio Web nella directory _vti_bin.

Per completare tutte le attività precedenti è necessario eseguire diverse operazioni. Fortunatamente, è già disponibile un articolo di istruzioni in cui viene descritto come procedere. Per dettagli completi, vedere Scenario: Creazione di un servizio Web personalizzato (informazioni in lingua inglese).

Dopo aver integrato il servizio Web nella directory _vti_bin di SharePoint, è necessario modificare la pagina master per aggiungere un tag ASP.NET AJAX <ScriptManager>. All'interno del tag <ScriptManager> vengono definiti i punti di ingresso per il servizio Web personalizzato in uso. In questo esempio il servizio Web personalizzato è denominato ListData.asmx. Di seguito è indicato il tag completo aggiunto alla pagina master.

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

Dopo aver configurato il servizio Web in modo che possa essere richiamato dalla directory _vti_bin di SharePoint e aver aggiunto il tag <ScriptManager> alla pagina master con un riferimento al servizio Web ASP.NET AJAX personalizzato, i componenti client di quest'ultimo possono comunicare con esso.

Creazione di una web part personalizzata mediante dati XML

A questo punto è necessaria una web part personalizzata per generare lo script client ASP.NET AJAX per recuperare dati dal servizio Web personalizzato. Nella web part è quasi del tutto assente logica lato server. La maggior parte del codice è contenuto in uno script ECMAScript (Jscript, JavaScript) che la web part aggiunge come risorsa Web (file WebResource.axd).

Il primo passaggio consiste nella generazione di tutto il codice HTML necessario nella pagina per l'interfaccia utente. Esistono due modi di base per generare codice HTML da una web part. Il modo semplice consiste nello scrivere i tag direttamente come stringhe. Con codice HTML relativamente semplice, è in genere più rapido e facile limitarsi a elaborare le stringhe. Il codice HTML utilizzato in questo caso è leggermente più complesso. Contiene tre tag <div> principali per i dati, i controlli di spostamento e gli elementi di interfaccia che richiedono di attendere ("please wait"). Il tag <div> relativo a questi ultimi contiene due tag <div> nidificati per posizionare in modo corretto l'immagine e il testo nella web part. In base a questi requisiti HTML più complessi, le librerie di classi ASP.NET sono state utilizzate per generare il codice HTML, come illustrato nell'esempio riportato di seguito.

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

Successivamente, la web part aggiunge diversi campi nascosti che vengono utilizzati per tenere traccia dello stato durante lo scorrimento dei dati.

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

Infine, viene registrato uno script di avvio che consente il recupero della prima pagina di dati nella web part al caricamento della pagina:

Creare uno script di avvio per chiamare il metodo di caricamento dei dati al caricamento della pagina.

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

Lo script EMCAScript (JScript, JavaScript) che viene utilizzato per recuperare i dati è registrato nell'evento OnPreRender. Viene qui utilizzato lo stesso processo di aggiunta dello script come risorsa incorporata e di registrazione dello script nel file AsseemblyInfo già descritto per la web part con isola XML. La registrazione del file ECMAScript è analoga a quanto segue.

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

Dopo aver creato l'HTML, il file JScript o JavaScript può generare un'interfaccia di attesa durante il caricamento della pagina e la richiesta dei dati nonché ogni volta che viene recuperata una nuova pagina di dati perché l'utente ha fatto clic sui collegamenti Next o Prev. In questo caso, l'immagine GIF animata utilizzata è inclusa in Prodotti e tecnologie SharePoint in modo da risultare familiare agli utenti. L'interfaccia di attesa appare analoga a quanto segue.

Figura 3. Interfaccia di attesa

Messaggio recupero dati web part

Tutti gli aspetti della gestione dell'interfaccia utente e del recupero dei dati vengono gestiti dallo script del lato client. Il file JScript o JavaScript viene avviato modificando l'interfaccia per nascondere gli elementi DIV contenenti i dati dell'elenco e i controlli di spostamento e per visualizzare l'interfaccia di attesa. Quindi vengono utilizzati i campi nascosti per raccogliere le informazioni per i parametri del metodo del servizio Web e il framework ASP.NET AJAX per chiamare il metodo del servizio Web personalizzato.

// 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 chiamata a un servizio Web tramite ASP.NET AJAX è leggermente diversa rispetto ai servizi Web basati su SOAP tradizionali. La prima cosa da notare è che è necessario utilizzare il nome della classe completo quando si chiama un metodo del servizio Web. L'altra differenza consiste nel fatto che oltre a impostare tutti i parametri del metodo del servizio Web, alla fine vengono aggiunti tre ulteriori parametri. Essi rappresentano funzioni in JScript o JavaScript che verranno chiamate quando vengono restituiti i dati (OnComplete), se si verifica un timeout della chiamata (OnTimeOut) o se si verifica un errore (OnError). In questa web part di esempio, le funzioni OnTimeOut e OnError eseguono solo il rendering delle informazioni restituite direttamente nell'elemento DIV in cui vengono normalmente visualizzati i dati.

La funzione OnComplete è l'unico parametro obbligatorio ed è una semplice funzione JScript analoga a quanto segue.

function OnComplete(arg)
{
  …
}

Il parametro arg contiene il valore restituito dalla chiamata al metodo del servizio Web. In questo caso è una stringa contenente codice XML. Per questo progetto, il servizio Web restituisce XML nello stesso formato utilizzato per la web part dell'isola XML. Pertanto il codice per enumerare i dati ed eseguire il rendering nella pagina è pressoché identico. Viene innanzitutto creato un DOMDocument MSXML e viene verificato che sia stato restituito codice XML valido.

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

Il codice di rendering in questo caso è diverso dalla web part con isola XML solo per il fatto che gli ID del primo e dell'ultimo elemento sono archiviati nei campi nascosti startID ed endID per supportare la funzionalità di paging del controllo. Dopo il recupero dei dati, questi ultimi vengono visualizzati nell'elemento DIV appropriato ed è possibile scorrere avanti e indietro i contenuti. Nella figura 4 e nella figura 5 vengono illustrate due pagine di dati.

Figura 4. Prima pagina di dati

Prima pagina della web part AJAX

Figura 5. Seconda pagina di dati

Web part AJAX di esempio - seconda pagina

Creazione di una web part personalizzata tramite JSON

Nell'esempio precedente, l'XML è stato restituito dal metodo del servizio Web e quindi sono stati utilizzati XPath e un DOMDocument MSXML per leggere il contenuto. Tuttavia, una delle funzionalità più avanzate di ASP.NET AJAX è la possibilità di utilizzare i dati mediante JSON (JavaScript Object Natation). Ciò consente allo sviluppatore sul lato client di utilizzare oggetti e proprietà per accedere ai dati anziché modificare codice XML più complesso.

Per illustrare questo aspetto, è stato creato un secondo metodo Web che restituisce una classe personalizzata. La firma del metodo è illustrata nel codice seguente.

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

La classe Records è una classe personalizzata sviluppata per contenere i dati restituiti. La definizione di tale classe è illustrata nel codice seguente.

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

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

Nella classe Records la proprietà Count fa riferimento al numero totale di elementi trovati e la proprietà ItemCount al numero di elementi restituiti nella chiamata. Queste proprietà vengono utilizzate per lo scorrimento dei dati sul lato client. I dati effettivi visualizzati sono contenuti nella proprietà Items, ovvero un elenco di elementi record. La classe Record viene definita come segue.

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

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

La classe Record contiene solo una proprietà, che è un tipo dizionario personalizzato che supporta la serializzazione. La classe predefinita Dictionary in Microsoft .NET Framework 2.0 non supporta la serializzazione e JSON, per impostazione predefinita, serializza tutti i dati restituiti. In questo caso, una classe di tipo Dictionary è necessaria affinché non occorra mappare i nomi delle singole proprietà nella classe. Al contrario, possono essere aggiunti come coppia chiave/valore. Ad esempio, myValuePair.Item.Add(chiave, valore).

Nota

La descrizione della classe Dictionary personalizzata esula dall'ambito di questo articolo, tuttavia la classe utilizzata si basa sul lavoro descritto nel post del blog di Paul Welter relativo al dizionario generico serializzabile di XML (informazioni in lingua inglese).

Il metodo Web funziona in modo identico alla versione XML per quanto concerne il recupero dei dati. Crea il valore restituito per il metodo utilizzando il codice riportato di seguito.

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;

Dopo che il metodo ha restituito una classe, è possibile enumerare i dati nello script del lato client segnando le proprietà della classe. Non è più necessario creare un DOMDocument MSXML per enumerare i risultati e lo script del lato client diventa molto più semplice. Il codice effettivo per eseguire il rendering dei dettagli è analogo a quanto segue.

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

L'aspetto dell'interfaccia utente è esattamente lo stesso della versione che utilizza XML. La web part supporta lo scorrimento dei dati sia avanti sia indietro. Visualizza l'interfaccia di attesa al primo caricamento e quando un utente fa clic sui collegamenti Next o Prev.

Misurazione dei risultati

In una rete con larghezza di banda limitata o a latenza elevata, una soluzione come questa può avere un impatto molto positivo sulle risorse di rete utilizzate. È stata scritta una seconda web part che elabora esattamente gli stessi dati del medesimo elenco. Tuttavia, tutti i dati sono stati generati nella web part sul server e quindi scritti in formato HTML nella pagina. Ne consegue che ogni volta che si fa clic sui collegamenti Prev o Next, viene imposto un postback al server e l'intera pagina deve essere inviata di nuovo al client. È importante notare che scegliendo di utilizzare un controllo UpdatePanel di ASP.NET AJAX, si verificherebbe il medesimo processo. Viene eseguito il postback al server di tutte le variabili di modulo della pagina e l'intera pagina viene inviata nuovamente dalla richiesta. Tuttavia, viene aggiornata solo la parte della pagina contenuta in UpdatePanel. Nella figura 6 viene illustrata un'istantanea acquisita dal Fiddler della richiesta dopo aver fatto clic sul collegamento Next in questa web part.

Figura 6. Richiesta dal clic sul collegamento Next nella seconda web part

Risultati dello strumento Fiddler per la web part

L'acquisizione del Fiddler indica che la richiesta e la risposta a partire dal rendering di tipo postback dell'elenco di dati hanno inviato un totale di 79.424 byte nella rete. In alternativa, nella figura 7 è illustrata un'acquisizione del Fiddler relativa alla web part ASP.NET AJAX utilizzata per recuperare gli stessi dati tramite il servizio Web personalizzato con XML.

Figura 7. Recupero dei dati nella web part tramite il servizio Web personalizzato con XML

Risultati dello strumento Fiddler per il servizio Web personalizzato

Sebbene siano stati recuperati gli stessi dati, sulla rete vengono inviati solo 1973 byte. Si tratta di una differenza notevole e l'utilizzo di questa metodologia può ridurre sensibilmente il traffico di rete. Il payload più piccolo di tutti, tuttavia, è stato generato utilizzando il metodo del servizio Web che utilizza JSON per restituire la classe Records, come mostrato nella figura 8.

Figura 8. JSON utilizzato per restituire la classe Records

Risultati dello strumento Fiddler per JSON

Conclusioni

L'utilizzo di JSON consente di ridurre il payload totale inviato tramite la rete per una richiesta a 1817 byte, ovvero il 98% in meno delle dimensioni della richiesta per la web part che esegue il postback dell'intera pagina per recuperare e scorrere i dati. È stato inoltre possibile ridurre le dimensioni dello ECMAScript (JScript e JavaScript) utilizzato per enumerare i dati e nel contempo semplificare il codice.

Sebbene sviluppare questo tipo di soluzione sia più complesso, nei casi in cui vi siano vincoli correlati alla larghezza di banda o alla latenza, questo approccio può rappresentare una buona scelta per migliorare le prestazioni e l'esperienza dell'utente finale.

Risorse aggiuntive

Per ulteriori informazioni, vedere le risorse seguenti:

Scaricare il manuale

Questo argomento è incluso nel manuale seguente, che può essere scaricato per una lettura e una stampa più agevoli:

Per un elenco completo dei manuali disponibili che è possibile scaricare per Office SharePoint Server 2007, vedere Downloadable content for Office SharePoint Server 2007 (informazioni in lingua inglese).