Optimizing custom Web parts for the WAN
Applies To: Office SharePoint Server 2007
This Office product will reach end of support on October 10, 2017. To stay supported, you will need to upgrade. For more information, see , Resources to help you upgrade your Office 2007 servers and clients.
Topic Last Modified: 2016-11-14
Summary: Learn techniques to minimize impact on bandwidth in custom Web Parts, including general style suggestions and specific information and code examples for alternative means to retrieve and render data from SharePoint lists. (24 printed pages)
Steve Peschka Microsoft Corporation
January 2008
Applies to: Microsoft Office SharePoint Server 2007, Windows SharePoint Services 3.0, ASP.NET AJAX 1.0
Contents
Introduction to Optimizing Custom Web Parts for the WAN
Reusing Built-in Styles or Creating Custom Styles
Storing State
Maximizing Performance of Web Parts that Display Data
Conclusion
Additional Resources
Introduction to Optimizing Custom Web Parts for the WAN
Developing custom Web Parts that are going to be used in sites with high latency or low bandwidth requires a focus on the same general design principles that are used when creating pages for that type of environment. You should make an effort to design parts that minimize both roundtrips to the server and the amount of data that is sent over the network. This article discusses several techniques you can employ to meet those design goals.
Reusing Built-in Styles or Creating Custom Styles
When your Web Part emits HTML, use style classes that are built into the Microsoft Office SharePoint Server 2007 and Windows SharePoint Services 3.0 stylesheet: core.css. (In this article, Office SharePoint Serverand Windows SharePoint Services are collectively referred to as Microsoft SharePoint Products and Technologies). By reusing those styles, you can minimize the impact on the page because the page won’t need to download an additional style sheet just to support your part. In addition, after the initial site visit the user will already have the core.css file downloaded to their cache. By using styles that are part of core.css, you ensure that no additional downloads are required for style support.
If you do require custom styles with your part, then consider using a custom style sheet that can be used with the blob cache. If you store it in a document library, the style sheet can have a cacheability directive associated with it so that it doesn’t need to be downloaded after the initial page hit. This will have a lower impact on the site than using inline styles, for example, which would be transmitted over the network every time the part is rendered.
Storing State
Web Parts may need to track such information as the user, the request, and the data source. You have several options for storing state; describing each one is out of scope for this article. In general, however, there are two common ones you can use with Web Parts: ViewState and server Cache. In a low-bandwidth or highly latent environment, avoid ViewState if possible because it adds content to the page both on the download and as any postback. This would apply to other forms of state that also involve transmitting data over the network, such as query strings, hidden fields, and cookies.
Using the server Cache class allows you to store state information at the server level. The drawback to using server Cache is that it is not really intended to be used as a per-user state mechanism (although depending on the circumstances it can be made to work that way). Also, the cache information is not replicated throughout all the front-end Web servers on the farm. If your part depends on having that state information present regardless of which front-end Web server a user request ends up hitting, then server Cache is not a good choice.
In that scenario, another option is to use Session State. Session State is turned off by default, but is enabled when you activate Microsoft Office InfoPath Forms Services (IPFS) in a farm. When it is enabled, it uses Microsoft SQL Server to track state, which means that session state values can be used no matter which front-end Web server receives the HTTP request. The drawback to session state is that data stays in memory until it is removed or expires. Large datasets stored in session state can therefore degrade server performance if not carefully managed. Because of these constraints, we do not recommend using use session state unless absolutely necessary.
You can also experiment with setting ViewState to off for all pages in the site by editing the web.config file for a Web application. It contains a pages element that has an enableViewState attribute. If you have a significant concern about the size of ViewState in the page, you can experiment with setting this attribute to false (it is true by default). If you do this, you need to thoroughly test your site and all of the functionality to ensure that it works properly, because some controls and Web Parts may expect that ViewState will be on.
If you’re developing a Web Part and need to use something similar to ViewState but aren’t sure whether it will be available in a page, you can instead use Control state, which is new in ASP.NET 2.0. In short, you would need to do the following:
Register for control state by calling Page.RegisterRequiresControlState during initialization.
Override the LoadControlState method and SaveControlState method.
Manually manage your portion of the object collection that is mapped to control state.
Maximizing Performance of Web Parts that Display Data
If your Web Part is used to display data, you can try to maximize the performance experience for end users in several ways. In general, you need to strike a balance between how many trips to the server are required, versus how much data should be retrieved for a request.
Providing Rendering Limits
For controls that emit rows of data, include a property that allows an administrator to control how many rows are displayed. Depending on the latency and bandwidth in which the control is going to be used, that allows the flexibility to either turn up or down the amount of data that is rendered in each page request. That can also impact the number of requests that are required to view all of the data.
If the number of rows returned is a property that can be set by end users, consider adding constraints so the choices of one user don’t overwhelm the network.
Using Inline XML Data Islands
Another alternative for displaying data is to use an inline XML data island and perform the rendering on the client side. With this approach all of the data that will be displayed is emitted to the page as an XML data island. Client-side script is used to actually render the data in the page, and it is responsible for using the XML DOM to retrieve and display the data from the island.
By doing this, you can retrieve all the data in a single request so the number of roundtrips to the server is minimized. However, the download size will obviously be larger, so the initial page load time will take longer. In addition, if it is used in a page where other controls are used that cause postbacks, it will force this large data download to occur each time. This technique is best suited to scenarios where you know this won’t occur, or else use it as an optional data retrieval method. For example, create a public property to track whether all the data should be downloaded so that a site administrator can control the behavior.
Following is a relatively simple example of a Web Part that does this; it reads all of the content from a list, emits it to a page as XML, and uses client-side ECMAScript (JScript, JavaScript) to render and page through the data.
Note
The code in this article is not intended to represent Microsoft best-coding practices. The code is streamlined to simplify the exercise; elements of the code would need to be changed for a production environment.
The Web Part overrides the Render method and starts by getting the contents of a list in the Web site named 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)
{
...
}
After the reference to the list is obtained, a StringBuilder object is used to create the XML data island that will be emitted to the page.
// 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());
With the XML added to the page, you also need an element in which to display the data. For this requirement, a simple <div> tag is added to the page.
// Add a <div> tag - this is where we'll output the data.
writer.Write("<div id='wpClientData'></div>");
The next piece of the interface that you need will be used to page through the data. In this case, two link buttons are added to the page. Two things to notice are the OnClientClick property and the Attributes collection. The OnClientClick property is set to use a custom ECMAScript (JScript, JavaScript) function that is written to display the data on the client.
The Attributes collection is used to set the navigation URL for the LinkButton. In this case, we want the LinkButton to render as a hyperlink so that the user gets feedback that the item is clickable, and we can take some action when it is clicked. In this case the # link is being used as the navigation URL because we don’t want to actually navigate anywhere; we just want to render a link and capture when it is clicked.
// Add the paging links.
LinkButton btn = new LinkButton();
btn.Text = "<< Prev";
btn.Attributes.Add("href", "#");
btn.OnClientClick = "renderPartData('PREV');";
btn.RenderControl(writer);
writer.Write(" ");
btn = new LinkButton();
btn.Text = "Next >>";
btn.Attributes.Add("href", "#");
btn.OnClientClick = "renderPartData('NEXT');";
btn.RenderControl(writer);
Next, we add some hidden fields to the page to track the paging for the control. In this case, we want to track the page size (how many records to show at a time), the current record number, and the total number of records.
// 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());
With all of the elements present, the last step in the Render method is to register a startup script that executes the javascript to render the data.
// Create a startup script to call our dataload method when the page
//loads
this.Page.ClientScript.RegisterStartupScript(this.GetType(), JSCR_START,
"renderPartData();", true);
The javascript itself is contained in a separate file in the project named Scripts.js. It is configured to be an embedded resource and is sent to the page as Web resource (for example, webresource.axd). The code that configures it for download runs in the OnPreRender event of the Web Part. It checks first to ensure that the script reference is not already added to the page by calling the IsClientScriptIncludeRegistered method, and if not, registers it as an include for the page.
// 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"));
This method also requires that you register the Web resource in the AssemblyInfo.cs class for the project.
[assembly: WebResource("Microsoft.IW.Scripts.js", "text/javascript")]
When the page is rendered, it includes a link that looks something like the following.
<script src="/WebResource.axd?d=DVBLfJiBYH_yZDWAphRaGQ2&t=633198061688768475"
type="text/javascript"></script>
The ECMAScript (JScript, JavaScript) reads the XML and renders the data. It first determines the paging bounds: the record number of the first and last record to display. After it knows the records it needs to display, it uses a fairly simple method to create an instance of MSXML on the 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.
}
}
}
Variables are used to store both the XML data and a reference to the DIV element where the results will be output.
// Get the XML.
var xData = document.getElementById("spList").innerHTML;
// Get the target.
var target = document.getElementById("wpClientData");
Next, the data is loaded into the DOMDocument from MSXML, and then the list of Item elements is selected.
// 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");
Now that the nodes are selected, the results can be enumerated for the specific page set that is needed and rendered on the page.
// 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;
}
When it is completed, the part displays the records on the client and provides a mechanism for paging through them without making another roundtrip to the server. Figure 1 and Figure 2 show the part with two different pages of data.
Figure 1. Web Part with data
Figure 2. Web Part with data
Using Client-Side Script that Connects to Web Services
Another option for managing the amount of data sent over the network is to use client-side script that connects to Web services to retrieve data. This method allows a control to retrieve the data it needs without having to post back the entire page. Not only is this a better experience for the user, but it puts a lighter load on the network because the entire page is not sent to the server and back again to retrieve the data. Retrieving the data can be done with the XmlHttp component directly or with Microsoft AJAX Library 1.0, which wraps functionality around XmlHttp.
While XmlHttp and ASP.NET AJAX represent two high-level options, you’ll likely find it easiest to use ASP.NET AJAX in conjunction with a custom Web service that exposes SharePoint list data. While it’s technically possible to use the ASP.NET AJAX libraries with a SOAP-based Web service (like those web services that are built into SharePoint Products and Technologies), it is considerably more complicated to do so and does not provide any of the additional benefits of ASP.NET AJAX-style Web services, such as smaller payload sizes and support for JavaScript Object Notation (JSON).
SharePoint Products and Technologies provide several Web services that you can use for displaying data. The Lists Web service allows you to retrieve tabular data from lists and libraries in SharePoint Products and Technologies. You can use it to render the data contained within lists and links to items in a list, such as documents or images. The Search Web service allows you to search through the body of content contained within SharePoint Products and Technologies and any other external sources that are being crawled. With carefully constructed metadata-driven queries, it can also be used to retrieve a filtered set of data from one or more SharePoint lists.
The following example demonstrates creating a custom Web service to retrieve list data from SharePoint Products and Technologies. The Web service class is annotated with the System.Web.Script.Services.ScriptService attribute, which allows you to use the client-side Web service components that are provided with ASP.NET AJAX. The Web service is then "registered" with SharePoint Products and Technologies so that it can be accessed via the _vti_bin directory with all of the built-in Web services. The master page is updated to include the ASP.NET AJAX ScriptManager control and declarative tags for the custom Web service. Then a Web Part is written that generates the client-side script to retrieve data via the ASP.NET AJAX components from the custom Web service and display it in the page.
Installing AJAX
First, you must install the ASP.NET AJAX 1.0 binaries onto your SharePoint servers. You can download them from the ASP.NET AJAX site. You must install these on each front-end Web server in the farm.
After you install ASP.NET AJAX, you must update the web.config file for each Web application in which ASP.NET AJAX is going to be used. This is a somewhat lengthy exercise; for complete step-by-step instructions, see Mike Ammerlan’s blog post on Integrating ASP.NET AJAX with SharePoint.
Creating a Custom Web Service
Creating a custom Web service to retrieve the data is necessary because it enables you to apply the System.Web.Script.Services.ScriptService to your Web service class, which enables it to be used with the ASP.NET AJAX Web service framework. In this case, a relatively simple Web service class was developed to retrieve list data based on basic parameters.
The Web service class includes a reference to the System.Web.Extensions class to enable ASP.NET AJAX support. After that reference is added, a using (Microsoft Visual C#) or Imports (Microsoft Visual Basic) statement is added to the class.
using System.Web.Script.Services;
The class is then decorated with the ScriptService attribute so it can be consumed directly by the ASP.NET AJAX Web services framework. It includes a default parameter-less constructor so that the class can be serialized by ASP.NET AJAX.
namespace Microsoft.IW
{
[ScriptService]
public class AjaxDataWebService : WebService
{
public AjaxDataWebService()
{
// Default constructor
}
In the first example, the Web service contains only one method, which returns a string. The string is actually XML that will be consumed on the client. The method signature is defined as follows.
[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;
The first part of the code gets a reference to the site, Web, and list that contains the data.
// 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];
Next, the query semantics are created based on the paging direction, starting ID, and size of the result set.
// 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();
With the query configured, the next step is to execute. When the results are returned, a check is made again for the paging direction. If we are paging backwards then the results must have their order reversed again so that they appear smallest ID to largest on the page. Again, this is purely for paging support in the Web Part. To simplify the ordering, the data is retrieved into an ADO.NET DataTable object. After the data is retrieved and sorted appropriately, each row is enumerated to create the XML that is returned from the method call.
// 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>");
All of the previous code is in a try…catch block, of course; in the finally block we release the resources associated with the DataTable object and then return the XML that was created.
finally
{
// release the datatable resources
if (res != null)
res.Dispose();
res = null;
}
return ret.ToString();
Registering the Custom Web Service
Exposing the custom Web service for use in the ASP.NET AJAX -enabled Web Part requires two types of configuration. One type is to configure it so that SharePoint Products and Technologies know about the Web service and can call it in the context of a SharePoint Web application. This involves several steps that at a high level require you to do the following:
Build the code-behind class for the Web service into a separate assembly, and register it in the global assembly cache.
Generate and edit a static discovery file and a Web Services Description Language (WSDL) file.
Deploy the Web service files to the _vti_bin directory.
Several steps are required to complete all of the previous actions. Fortunately, there is a prescriptive article already available that describes how to do this. For complete details, see Walkthrough: Creating a Custom Web Service.
After you integrate the Web service into the SharePoint _vti_bin directory, you must modify the master page to add an ASP.NET AJAX <ScriptManager> tag. Inside the <ScriptManager> tag, you define a Services entry point for the custom Web service we are using; in this example, the custom Web service is named ListData.asmx. Following is the complete tag that is added to the master page.
<asp:ScriptManager runat="server" ID="ScriptManager1">
<Services>
<asp:ServiceReference Path="_vti_bin/ListData.asmx" />
</Services>
</asp:ScriptManager>
With the Web service configured so that it can be called from the SharePoint _vti_bin directory and the <ScriptManager> tag added to the master page with a reference to the custom Web service, the ASP.NET AJAX Web service client components can now communicate with it.
Creating a Custom Web Part Using XML Data
Now we need a custom Web Part to generate the ASP.NET AJAX client script to retrieve data from the custom Web service. The Web Part itself contains almost no server-side logic; the bulk of the code is contained in an ECMAScript (Jscript, JavaScript) file that the Web Part adds as a Web resource (WebResource.axd file).
The first step is to generate all the HTML that is needed in the page for the user interface. There are two basic ways to generate HTML from a Web Part. The simple way is to write out the tags directly as strings; a more complicated but safer way is to use the ASP.NET class libraries. With relatively simple HTML, it is typically quicker and easier to just emit the strings. The HTML used in this case is slightly more complicated. It contains three main <div> tags for the data, navigation controls, and “please wait” interface elements. The “please wait”<div> tag contains two nested <div> tags within it to properly position the image and text within the part. Based on these more complicated HTML requirements, the ASP.NET class libraries were used to generate the HTML, as shown in the following code.
// 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(" ");
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();
Next, the Web Part adds several hidden fields that are used to track state when paging data.
// 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");
Finally, a startup script is registered so that when the page loads, the Web Part will retrieve the first page of data:
Create a startup script to call our dataload method when the page loads.
if (!Page.ClientScript.IsStartupScriptRegistered(JSCR_START))
Page.ClientScript.RegisterStartupScript(this.GetType(),
JSCR_START, "GetAjaxData('NEXT');", true);
The EMCAScript (Jscript, JavaScript) that is used to retrieve the data is registered in the OnPreRender event. The same process of adding the script as an embedded resource and registering it in the AssemblyInfo.cs file as was described for the XML island Web Part was also used here. The registration of the ECMAScript file looks like this.
// 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"));
With the HTML that is created, the JScript or JavaScript file can generate a “please wait” interface when the page loads and data is being requested, and every time a new page of data is retrieved because the user clicked the Next or Prev links. In this case, the animated GIF that is used is one that is included with SharePoint Products and Technologies so it will look familiar to users. The “please wait” interface looks like the following.
Figure 3. "Please wait" interface
All of the data retrieval and user interface management is handled by the client-side script. The JScript or JavaScript starts out by changing the interface to hide the DIV elements that contain the list data and paging controls, and showing the “please wait” interface. It then uses the hidden fields to gather the information for the Web service method parameters, and uses the ASP.NET AJAX framework to call the custom Web service method.
// 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;
Calling a Web service via ASP.NET AJAX is somewhat different from traditional SOAP-based Web services. The first thing to notice is that it is necessary to use the fully-qualified class name when calling a Web service method. The other thing that is different is that in addition to providing all of the parameters for the Web service method, three additional parameters are added to the end. They represent functions in JScript or JavaScript that will be called when the data is returned (OnComplete), if the call times out (OnTimeOut), or if there is an error (OnError). In this sample part, both the OnTimeOut function and OnError function just render the information that was returned directly back out into the DIV element where the data is normally displayed.
The OnComplete function is the only required parameter of the three and is just a JScript function that looks like the following.
function OnComplete(arg)
{
...
}
The arg parameter contains the return value from the Web service method call; in this case it is a string containing XML. For this project, the Web service returns XML that is in the same format that was used for the XML island Web Part. So the code to enumerate the data and render it on the page is nearly identical. It first creates an MSXML DOMDocument and verifies that some valid XML was returned.
// 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.
}
The rendering code here differs from the XML island Web Part only in that the IDs of the first and last item are stored in the startID and endID hidden fields to support the paging functionality of the control. After the Web Part retrieves the data, it renders it in the appropriate DIV element and allows you to page forward or backward through the contents. Figure 4 and Figure 5 show the first two pages of data.
Figure 4. First page of data
Figure 5. Second page of data
Creating a Custom Web Part by Using JSON
In the previous example, XML was returned from the Web service method and then XPath and an MSXML DOMDocument was used to read the contents. However, one of the strongest features of ASP.NET AJAX is its ability to consume data by using JavaScript Object Notation (JSON). This allows the client-side developer to work with objects and properties to access data rather than manipulating more complicated XML.
To demonstrate this, a second Web method was created that returns a custom class; its method signature looks like the following code.
[WebMethod]
public Records GetListDataJSON(string webUrl, string listName, int
startingID, int pageSize, string[] fieldList, string direction)
The Records class is a custom class that was developed to hold the return data; the definition of it looks like the following code.
[Serializable()]
public class Records
{
public int Count = 0;
public int ItemCount = 0;
public List<Record> Items = new List<Record>();
public Records()
{
// Default constructor.
}
}
In the Records class, the Count property refers to the total number of items found, and the ItemCount property refers to the number of items being returned in this particular call. These properties are used for paging the data on the client side. The actual data that is displayed is contained in the Items property, which is a list of Record items. The Record class is defined as follows.
[Serializable()]
public class Record
{
public SerializableDictionary<string, string> Item =
new SerializableDictionary<string, string>();
public Record()
{
// Default constructor.
}
}
The Record class has only one property, which is a custom dictionary type that supports serialization. The default Dictionary class in the Microsoft .NET Framework 2.0 does not support serialization, and JSON, by default, serializes all return data. In this case, a Dictionary type class is needed so that individual property names do not have to be mapped into the class. Instead, they can be added as a key/value pair. For example, myValuePair.Item.Add(someKey, someValue).
Note
Describing the custom dictionary class is beyond the scope of this article; however, the class used was based on the work described in Paul Welter’s blog post on the XML Serializable Generic Dictionary.
The Web method works identically to the XML version in the way it retrieves the data. It creates the return value for the method by using the following code.
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;
Now that the method is returning a class, we can enumerate through the data in the client-side script by sing the properties of the class. We also no longer have to create an MSXML DOMDocument to enumerate the results, and our client-side script becomes much simpler. The actual code to render the details looks like the following.
// 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;
}
The user interface looks exactly the same as the version that uses XML. The part supports paging through the data both forward and backward. It shows the “please wait” interface when the data first loads, and when a user clicks the Next or Prev paging links.
Measuring the Results
In a low bandwidth or high latency network, a solution such as this can have a very positive impact on the network resources that are consumed. A second Web Part was written that emitted exactly the same data from the same list. However, all of the data was generated in the Web Part on the server and then written as HTML into the page. As a result, each time the Prev or Next links were clicked, it forced a post back to the server and caused the entire page to be sent back to the client. It’s important to notice also that if we had chosen instead to use an ASP.NET AJAX UpdatePanel control, the same process would occur. All of the form variables for the page are posted back to the server, and the entire page is sent back from the request. However, only the part of the page that is contained within the UpdatePanel is updated. Figure 6 shows a snapshot of the request from clicking the Next link on this second Web Part as captured by Fiddler.
Figure 6. Request from clicking Next link on second Web Part
The Fiddler capture indicates that the request and response from doing a post back style rendering of the list data sent a total of 79,424 bytes across the network. Alternatively, Figure 7 shows a Fiddler capture when the ASP.NET AJAX-enabled Web Part was used to retrieve the same data via the custom Web service by using XML.
Figure 7. Web Part retrieved same data via custom Web service by using XML
The same list data was retrieved but only 1973 bytes was sent across the network. That’s a huge difference, and smart use of this methodology could significantly reduce network traffic. The smallest payload of all, however, was generated by the using the Web service method that uses JSON to return the Records class, as shown in Figure 8.
Figure 8. JSON used to return Records class
Conclusion
Using JSON, we were able to reduce the total payload sent across the wire for one request to 1817 bytes, which is a reduction of 98 percent of the request size for the part that does a full page post back to retrieve and page through the data. We were also able to trim down the size of the ECMAScript (JScript, JavaScript) used to enumerate the data, and in the process, also simplify the code.
While it is more complicated to develop a solution like this, if your site is bandwidth or latency constrained, this approach can be a good choice to help improve performance and the end user experience.
Additional Resources
For more information, see the following resources:
Download this book
This topic is included in the following downloadable book for easier reading and printing:
See the full list of available books at Downloadable content for Office SharePoint Server 2007.