Thursday 3 November 2011

Calling into SharePoint Web Service using AJAX-Enabled Web Parts -Part2


This covers how to call into SharePoint-based Web Services using an AJAX-enabled Web Part.  Part1 of this series covered the server-side and client-side code for the SharePoint Web Part. In Part 2, we will focus on the custom Web Service, specifically how to develop and deploy this into our SharePoint farm.
As you’ll recall from Part 1, we have built a Web Part which takes a keyword search query and, using AJAX-powered JavaScript, calls into a custom Web Service. As we’ll study in this article, this Web Service executes the query, transforms the result to HTML using XSLT and then returns the HTML string.
In our Visual Studio solution, the Web Service is created as a regular ASP.NET Web Service project. When creating Web Services that will be consumed using AJAX (or JavaScript) clients, you need to decorate the class with the ScriptService attribute. This attribute provides two functions. One is to generate and expose a JavaScript proxy class that gets downloaded by the ScriptReference object (covered in Part 1); it also instructs the web method to support JavaScript Object Notation (JSON) serialization, in addition to SOAP-based XML. JSON, defined in RFC4627, is used as it is lightweight and more efficient for simple browser-to-web-server exchanges. Like XML, it is hierarchical and is intended to be both machine and human readable. Unlike XML, JSON is very easy to consume from JavaScript. Here is a simple example on how a record of data might look encoded in JSON:
{
    "firstName": "John",
    "lastName": "Smith",
    "address": {
        "streetAddress": "500 Ala Moana Blvd",
        "city": "Honolulu",
        "state": "HI",
        "postalCode": "96814"
    }
} 
To read more about how JSON works, see the following MSDN article.
Note: The out-of-the-box Web Services (e.g. Search.asmx or Lists.asmx) do not have the ScriptService attribute set. This prevents you from using any of these from within your AJAX-enabled Web Parts.

Web Service Project

The Web Service project (wsAjaxSearch) is just a regular ASP.NET Web Service application. The following references have been added to the project:
System.Web.Extensions
Microsoft.Office.Server
Microsoft.Office.Server.Search
Microsoft.SharePoint

Since we’re using the Microsoft.Office.Server namespaces, this solution does require MOSS and will not run on a WSS installation. The System.Web.Extensions reference (part of ASP.NET AJAX v1.0) provides the ScriptService attribute (found in the System.Web.Script.Services namespace). This attribute has been added to the ajaxSearch class as shown here:
[WebService(Namespace = "http://synergy.com/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ToolboxItem(false)]
[ScriptService]
public class ajaxSearch : System.Web.Services.WebService 

The web method that was called in our JavaScript file is called ExecuteKeywordSearch. This is the project where it is implemented, and it is the only method in the class and project. Here is the method signature:
public string ExecuteKeywordSearch(string keywords, string xslt, int maxResults)

Keywords hold the keyword query as entered by the user. Xslt holds the XSLT stylesheet, and maxResults defines the maximum number of results that should be returned. Let’s walk through each major section of this method.
//Get SSP for current web context
ServerContext context = ServerContext.GetContext(HttpContext.Current);
KeywordQuery kwq = new KeywordQuery(context); 

//Prepare and execute query
kwq.ResultTypes = ResultType.RelevantResults;
kwq.TrimDuplicates = true;
kwq.EnableStemming = true;
kwq.QueryText = keywords;
kwq.RowLimit = maxResults;
ResultTableCollection results = kwq.Execute();

As you can see, this is just a simple KeywordQuery object that we create and execute this within the Shared Services Provider’s ServerContext for the current web application. Notice that we set the RowLimit property and are only interested in the Relevant Results, which are just the main search results (i.e. no Best Bets or High Confidence results).


//consume results and load into a dataset
ResultTable resultTable = results[ResultType.RelevantResults];
DataTable tbl = new DataTable ("Results");
tbl.Load(resultTable, LoadOption.OverwriteChanges);
DataSet ds = new DataSet();
ds.Tables.Add(tbl); 

//now that it's loaded into a dataset, load into a XML DOM object
XmlDocument xml = new XmlDocument();
xml.LoadXml(Server.HtmlDecode(ds.GetXml()));

In the above code, we take the results and, via a DataTable and DataSet, load it into an XmlDocument object. We need to do this as we will be transforming the results into HTML. For those curious, here is what a single result set looks like in XML form. (Note: Some element data have been abbreviated; these are denoted with an ellipsis.)


<Results>
  <WorkId>156</WorkId>
  <Rank>714</Rank>
  <Title>Wind.docx</Title>
  <Author>SYNERGY\Administrator</Author>
  <Size>16368</Size>
  <Path>http://portal.synergy.com/teamsite/Shared Documents/Wind.docx</Path>
  <Write>2008-09-04T09:47:13-10:00</Write>
  <SiteName>http://portal.synergy.com/teamsite</SiteName>
  <CollapsingStatus>0</CollapsingStatus>
  <HitHighlightedSummary>Wind turbines convert the ...</HitHighlightedSummary>
  <HitHighlightedProperties>&lt;HHTitle&gt;Wind.docx ...</HitHighlightedProperties>
  <ContentClass>STS_ListItem_DocumentLibrary</ContentClass>
  <IsDocument>1</IsDocument>
</Results> 

Before we employ XSLT for our transformation, we need to do a little prep work. This will simplify some things that are hard to do in XSLT. One, for example, is to format our date. The other is to get the thumbnail icon properly set.


//format the results
foreach (XmlNode resultNode in xml.SelectNodes ("/NewDataSet/Results"))
{
    //format the date
    XmlNode writeNode = resultNode.SelectSingleNode ("Write");
    writeNode.InnerText = DateTime.Parse (writeNode.InnerText).ToString ("d"); 

    //set the document icon.  
    //todo: This is not a complete solution and doesn't account for custom file types 
            such as PDF
    XmlNode contentClassNode = resultNode.SelectSingleNode ("ContentClass");
    if (contentClassNode.InnerText == "STS_ListItem_DocumentLibrary")
    {
        string path = resultNode.SelectSingleNode ("Path").InnerText;
        string ext = System.IO.Path.GetExtension(path).Replace(".", "");
        contentClassNode.InnerText = "/_layouts/images/ic" + ext + ".gif";
    }
    else if (contentClassNode.InnerText.Contains ("STS_List"))
        contentClassNode.InnerText = "/_layouts/images/STS_List16.gif";
    else if (contentClassNode.InnerText.Equals ("urn:content-class:SPSPeople"))
        contentClassNode.InnerText = "/_layouts/images/urn-content-classes-spspeople16.gif";
    else if (contentClassNode.InnerText.Equals ("STS_Document"))
        contentClassNode.InnerText = "/_layouts/images/html16.gif";
    else
        contentClassNode.InnerText = "/_layouts/images/" + contentClassNode.InnerText + "16.gif";
} 
To perform these changes, we iterate through each Result node in the XmlDocument object. To convert our dates into a user-friendly format, we just do a simple date format. This will set it based on the server’s current locale. In the US, this comes out as 9/2/2008 (September 2). The next step is to set the thumbnail icon. This is converted into the icon URL based on the value from the ContentClass element. Admittedly, this code is a not a complete solution as there are icon types such as PDF or other custom ones that will not work.
The next two lines shown restore our encoded format in our XSLT document. This was covered in Part 1 and arises because the XSLT document is delivered to the browser via a TEXTAREA HTML object. This causes the encoded operators (&lt; and &gt;) to be incorrectly decoded by the browser into < and >. If anyone has a better suggestion for a workaround, please share your thoughts.

//unencode conditional operators in xslt
xslt = xslt.Replace(@"\lt", "&lt;").Replace(@"\gt", "&gt;");

Finally, we are ready to begin our transformation from XML into HTML. Here is the code:
//load xslt document for transformation
StringReader sr = new StringReader (xslt);
XmlTextReader xslSource = new XmlTextReader (sr);
XslCompiledTransform xsltDoc = new XslCompiledTransform();
xsltDoc.Load(xslSource); 

//transform and return results
StringBuilder html = new StringBuilder();
StringWriter sw = new StringWriter(html);
xsltDoc.Transform(xml, null, sw);
return html.ToString(); 

This is pretty straight-forward. We load the XSLT string into an XslCompiledTransform object and then deliver the transformed output into a StringBuilder via a StringWriter. This string is then auto-serialized into JSON and delivered to the browser where it is stored in our <SPAN> object.
Note: While this transformation was done on the web server, this could have also been done in the browser. This is tricky if you need to maintain cross-browser compatibility, but an open source library named Sarissa can assist. Daniel Larson has also published a library on CodePlex called SharedPoint AJAX Toolkit which provides JavaScript components for doing cross-browser XSLT transformations.

Deploying our Web Service

Now that our Web Service is coded, we need to deploy it into SharePoint. There are two ways this can be done, but this example will only walk through the “integrated” approach. This follows the same convention as the out-of-the-box SharePoint Web Services. The main advantages of this approach are the virtualization and redirection benefits and that it runs under the Application Pool account for the particular web application, so we are ensured sufficient access to our Farm is in place.
To deploy an “integrated” Web Service, the assembly must be strongly named and copied into the GAC for each Web Front End server in the farm. The .asmx file must be deployed in the 12\ISAPI folder (or, preferably, in a subfolder). As with .aspx application pages you deploy into _layouts, you need to ensure the .asmx page can find the compiled class inside the GAC. This requires a change to the directive. Here is the original form that is used inside Visual Studio:
<%@ WebService Language="C#" CodeBehind="ajaxSearch.asmx.cs" Class="wsAjaxSearch.ajaxSearch" %>
And here is the modified form as it is stored in the ISAPI folder:
<%@ WebService Language="C#" Class="wsAjaxSearch.ajaxSearch, wsAjaxSearch, Version=1.0.0.0, Culture=neutral, PublicKeyToken=c592860db1c06fa9" %> 
In addition to this, you need to create a dynamic .aspx page that can generate a WSDL file. This page is executed when the WSDL file is requested by a URL such as http://WebApp/_vti_bin/ajaxSearch.asmx?WSDL . The reason the WSDL must be created dynamically is because the paths could change depending on the URL. For example:
http://WebApp/_vti_bin/ajaxSearch.asmx
http://WebApp/sites/HR/_vti_bin/ajaxSearch.asmx

Both of these could be URLs as provided by the virtualization and redirection engine within SharePoint. So, when requesting a Web Service WSDL, SharePoint provides an server-side redirection to a file named ajaxSearchwsdl.aspx. The file is executed and the results of this are streamed back to the client. The ajaxSearchwsdl.aspx is a regular XML-based WSDL file with a bit of server-side code to calculate the correct URL paths. It is stored in the same ISAPI folder along with the .asmx file. Here is how the header of the file looks:
<%@ Page Language="C#" Inherits="System.Web.UI.Page" %>
<%@ Assembly Name="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Import Namespace="Microsoft.SharePoint.Utilities" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<%  Response.ContentType = "text/xml"; %>
<wsdl:definitions ...
<!-- Regular WSDL XML code appears here –>

Here is the more server-side code near the end:
<wsdl:port name="ajaxSearchSoap" binding="tns:ajaxSearchSoap">
  <soap:address location=<% SPHttpUtility.AddQuote(SPHttpUtility.HtmlEncode(SPWeb.OriginalBaseUrl(Request)),Response.Output); %> />
</wsdl:port>
<wsdl:port name="ajaxSearchSoap12" binding="tns:ajaxSearchSoap12">
  <soap12:address location=<% SPHttpUtility.AddQuote(SPHttpUtility.HtmlEncode(SPWeb.OriginalBaseUrl(Request)),Response.Output); %>  />
</wsdl:port>

While this may appear imposing, creating this dynamic WSDL file is quite simple. The code sample shown in the header is the same across all WSDL-based .aspx files in SharePoint. Thus, you can take this sample or any of the out-of-the-box ones. For the footer code, you just need to replace the data contained with the two location attributes with the server-code shown here:
SPHttpUtility.AddQuote(SPHttpUtility.HtmlEncode(SPWeb.OriginalBaseUrl(Request)), Response.Output); 
Note: If your Web Service needs to provide Discovery Services (UDDI), you need to follow a similar convention by creating a file named ajaxSearchdisco.aspx. This is shown in the project code that can be downloaded.
Once these files are configured and stored correctly, your Web Service should be ready to go. Keep in mind that testing integrated Web Services (either custom or out of the box) using the browser is disabled by default. You might consider writing an InfoPath form or some other test harness to test it.
Important Note: If your custom Web Service makes any changes using the object model, you must set the AllowUnsafeUpdates property on either the SPSite or SPWeb object to true. Otherwise, your updates will throw an exception.

XSLT StyleSheet

The XSLT code that is used within the project is very similar to the out-of-the-box XSLT used with the Search Core Results Web Part. Its purpose is to transform the XML-based query results into HTML that the browser renders. The complete code can be found in the downloadable project code. Here is the root template that is used:
<xsl:template match='/'>
  <xsl:choose>
    <xsl:when test='count (NewDataSet/Results) &gt; 0'>
      <div class='srch-stats' >
        Results <b>1-<xsl:value-of select='count (NewDataSet/Results)'/></b>
      </div>
      <br/>
      <xsl:apply-templates select='NewDataSet/Results' />
    </xsl:when>
    <xsl:otherwise>
      No results found
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

This code above invokes the Results template when results are found (i.e. the count of Result nodes is greater than 0). Otherwise, a message indicating No results found is displayed. Here is the Results template:


<xsl:template match='Results'>
  <xsl:variable name='Path' select='Path'/>
  <xsl:variable name='Icon' select='ContentClass'/>
  <span class='srch-Icon'>
    <a href='{$Path}' title='{$Path}' target='_blank' >
      <img align='absmiddle' border='0' src='{$Icon}' />
    </a>
    &#160;
    <a href='{$Path}' class='srch-Title' target='_blank'>
      <xsl:value-of select='Title'/>
    </a>
    <br/>
    <xsl:apply-templates select='HitHighlightedSummary'/>
    <p class='srch-Metadata' >
      <span class='srch-URL'>
        <a href='{$Path}' target='_blank'>
          <xsl:value-of select='Path'/>
        </a>
      </span>
      <xsl:call-template name='DisplaySize'>
        <xsl:with-param name='size' select='Size' />
      </xsl:call-template>
      &#160;-&#160;
      <xsl:value-of select='Author'/>
      &#160;-&#160;
      <xsl:value-of select='Write'/>
    </p>
  </span>
</xsl:template>

This results template is called for each result node that is contained within the XML file. For each result, it displays the item icon, the path and the formatted summary along with the size, author and date. HitHighlightedSummary contains the keywords that were found, allowing you to format these. In other templates not shown here, these are set to bold using the <b> tag. You can read the remaining templates in the downloadable project. Again, the XSLT can be adjusted using the Web Part configuration screen. For those uncomfortable with XSLT, you can use SharePoint Designer or other third-party XSLT tools to help you write it.

Limitations

So far, I am aware of two limitations with this solution. One is by design in that it does not support results paging. This would be a nice feature, and I might add this functionality later if there is enough interest.
The second, and quite annoying, is that you will lose your results if you follow links to list items and return using the back button. For example, if you follow a link to an announcement. It doesn’t happen if you link to a document as this downloads directly in a separate window. This occurs due to the nature of AJAX and is a common problem with this and other JavaScript solutions. To mitigate this problem, all links will open up in a new window. You can see this in the XSLT code above with the target=’_blank’ attribute for the <A> tags.
Also, I have only tested the solution in Internet Explorer. Since I am doing server-side XSLT transformations, I don’t expect to see major issues with other browsers. I’d be curious if any of you notice any peculiarities.

Conclusion

In these two articles, you have seen how to create an AJAX-enabled Web Part that calls into a custom Web Service. One of the advantages is the low-degree of coupling between the JavaScript and Web Service interfaces. This allows you to keep your Web Services generic allowing you to build rich, client-server-like SharePoint solutions, but still using your browser client. Hopefully, this example will give you the concepts and the baseline set of code to get you on the road to building your own AJAX-enabled Web Parts. Good luck and have fun!
You can download the Visual Studio solution here. Since the Web Part was developed using VSeWSS, you can just install the WSP Solution that is found inside the Zip file at ajax/AjaxSearch/bin/Debug/AjaxSearch.wsp. You will also need to manually deploy the Web Service files as described above

No comments:

Post a Comment