malloryCode.com

Get Sharepoint Resources in Sitefinity

Sitefinity now has a Sharepoint connector, but you do need the license for that. This post will show you how to manually use the Sharepoint asmx List web service to get resources from Sharepoint into your Sitefinity implementation.

To be clear, the endpoint used is something like: https://admin.share.myorg.org/vtibin/lists.asmx. For this example, we'll retrieve a list of documents localized to a particular language using the GetListItems() method. This assumes the documents have been tagged appropriately in Sharepoint.

There is a lot of information on the web on how to do this, but I had much trouble until I used this technique. Basically, I have a wrapper class around the web service. The only thing the wrapper does is add a single, though magic, header tag. Here it is:

internal class MyOrgListsService : SitefinityWebApp.org.myorg.share.admin.Lists
{
    protected override System.Net.WebRequest GetWebRequest( Uri uri )
    {
        System.Net.WebRequest wr = null;
        wr = base.GetWebRequest( uri );
        wr.Headers.Add( "X-FORMS_BASED_AUTH_ACCEPTED", "f" );
        return wr;
    }
}

When you add the web service, the namespace will be SitefinityWebApp.org.myorg.share.admin. and Lists is the name of the generated class. It's the Lists service that is used to retrieve document information. To use this class and get the document list I started with a standard UserControl with just a placeholder.

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="LocalizedDocumentsViewer.ascx.cs" Inherits="SitefinityWebApp.UserControls.LocalizedDocumentsViewer" %>
<asp:PlaceHolder runat="server" ID="PlacehHolder1" />

In the code behind, include a property which will be the language tag used to query the Sharepoint List service. This would be set on the page the control is used.

// include the service namespace
using SitefinityWebApp.org.myorg.share.admin;

public partial class LocalizedDocumentsViewer : System.Web.UI.UserControl
{
    private string _locationTag;

    public string LocationTag
    {
        get { return _locationTag; }
        set { _locationTag = value; }
    }

The work is done in the Page_Load event. I'm checking the query string for a docStart to support paging. The I call my List service wrapper:

protected void Page_Load( object sender, EventArgs e )
{
    int docStart = 0;
    if (Request.Params["docStart"] != null
        && !String.IsNullOrEmpty(Request.Params["docStart"].ToString()))
    {
        docStart = Int32.Parse(Request.Params["docStart"].ToString());
    }

    string buffer = "<ul class=\"list\">";
    try
    {
        using (MyOrgListsService listService = new MyOrgListsService())
        {
            listService.Credentials = new NetworkCredential("UserName", "Password", "Domail");

Then the query sent to Sharepoint needs to be in XML, and Sharepoint is expecting a the query to be formatted in a exact way.

/*Use the CreateElement method of the document object to create 
    elements for the parameters that use XML.*/
/* Instantiate an XmlDocument object */
System.Xml.XmlDocument xmlDoc = new System.Xml.XmlDocument();

System.Xml.XmlElement query = xmlDoc.CreateElement("Query");
System.Xml.XmlElement viewFields = xmlDoc.CreateElement("ViewFields");
System.Xml.XmlElement queryOptions = xmlDoc.CreateElement("QueryOptions");

I settled on using the XmlElement class to construct the query. Next, add in the criteria:

/*To specify values for the parameter elements (optional), assign 
CAML fragments to the InnerXml property of each element.*/
query.InnerXml = "<Where><Contains><FieldRef Name='Tags'/><Value Type='Text'>" + _locationTag + "</Value></Contains></Where>";

viewFields.InnerXml = "<FieldRef Name='Name'/><FieldRef Name='ID'/>";
queryOptions.InnerXml = "<IncludeMandatoryColumns>True</IncludeMandatoryColumns><DateInUtc>False</DateInUtc>";

In this case, I'm just building up a list of links to these documents, so I specify the Name, Id and mandatory fields to be returned. Then, make the call. Note that the first argument is the name of the list 'Website Documents'. This was specific to the project I worked on and your list will have a different name (probably).

XmlNode Data = listService.GetListItems("Website Documents", "", query, viewFields, "500000", queryOptions, "");

Then the fun begins: parsing the response. This was a bit more of trial and error as the document returned is a bit convoluted. In this loop I'm calling several helper methods. I also created a struct made up of only the fields I was interested in, SPDocumentInfo.

int rowCount = 0;
SPDocumentParser parser = new SPDocumentParser();
List<SPDocumentInfo> allDocuments = new List<SPDocumentInfo>();
foreach (System.Xml.XmlNode listItem in Data) // expecting 3 nodes ws, data, ws
{
    if (listItem.LocalName == "data")
    {
        foreach (XmlNode dataItem in listItem) // expecting ws and zero or more rows
        {
            if (dataItem.LocalName == "row")
            {
                allDocuments.Add(parser.DocumentInfo(dataItem, Request.Url.AbsolutePath));
            }
        }
    }
}

The struct, all the info I needed to put together a list of links to the actual documents.

public struct SPDocumentInfo
{
    public string Name;
    public string Title;
    public string Type;
    public string Tags;
    public string Description;
    public DateTime Posted;
}

I wouldn't be surprised if others have come up with a two line XPath query or RegEx to do all the data extraction, but I ended up doing it manually:

public SPDocumentInfo DocumentInfo( XmlNode node, string path )
{
    SPDocumentInfo doc = new SPDocumentInfo();
    doc.Posted = DateTime.Now;

    if(node.Attributes["ows_Modified"] != null)
    {
        if(!DateTime.TryParse( node.Attributes["ows_Modified"].Value, out doc.Posted ))
        {
            doc.Posted = DateTime.Now;
        }
    }

    if(node.Attributes["ows_MetaInfo"] != null)
    {
        string[] attributePieces = node.Attributes["ows_MetaInfo"].Value.Split( new string[] { Environment.NewLine }, StringSplitOptions.None );
        foreach(string attributePiece in attributePieces)
        {
            // vti_title:SW|
            string[] piecePieces = attributePiece.Split( ':' );
            if(piecePieces[0] == "vti_title")
            {
                doc.Title = piecePieces[1].Split( '|' )[1];
            }
            else if(piecePieces[0] == "Tags")
            {
                // Tags:SW|;#Spanish;#
                string tagPieces = piecePieces[1].Split( '|' )[1];
                foreach(string tag in tagPieces.Split( new string[] { ";#" }, StringSplitOptions.RemoveEmptyEntries ))
                {
                    doc.Tags += "<a onclick=\"$('#docFilterBy').val('" + tag + "');__doPostBack();\" href=\"#\">" + tag + "</a>, ";
                }
                doc.Tags = doc.Tags.Substring( 0, doc.Tags.Length - 2 ); // remove last comma
            }
        }
    }

    if(node.Attributes["ows_DocIcon"] != null)
    {
        doc.Type = " (" + node.Attributes["ows_DocIcon"].Value.ToUpper() + ") ";
    }

    if(node.Attributes["ows_FileRef"] != null)
    {
        string[] attributePieces = node.Attributes["ows_FileRef"].Value.Split( ';' );
        if(attributePieces.Length == 2)
        {
            doc.Name = attributePieces[1].Remove( 0, 19 ); // strip #/Website Documents/
        }
    }

    if(String.IsNullOrEmpty( doc.Title ))
    {
        doc.Title = doc.Name;
    }
    return doc;
}

That bit with the tags probably needs a bit of explanation. When the document list was retrieved, the requirement was that all the documents tags should be displayed. The user would then be able to click on that tag to get a new list of documents, with that tag. That required the client to manually maintain the same tags in Sitefinity and Sharepoint. To the tag parsing above just builds up the links that will resubmit to get the new list of documents. Obviously this is not a great separation of concerns, but in this case it was much easier building that link right here were they were being iterated over anyway.

When done, I iterated over the List<SPDocumentInfo> building up a string for a generic html control, which presented the list to the user.

List<SPDocumentInfo> sorted = new List<SPDocumentInfo>();
// paging 
sorted = allDocuments.OrderBy(x => x.Posted).Skip(docStart).Take(5).ToList();

foreach (SPDocumentInfo doc in sorted)
{
    if (!String.IsNullOrEmpty(doc.Name)) // && !String.IsNullOrEmpty( fileId ))
    {
        rowCount++;
        buffer += "<li><h2><a class=\"doci\" href=\"/FileDownload.ashx?fileName=" + doc.Name + "\">" + doc.Title + "</a>" + doc.Type + "</h2>";
        buffer += "<p class=\"date\">Posted: " + doc.Posted.ToString("MM/dd/yyyy") + "</p>";
        // buffer += "<p>Description</p>";
        buffer += "<p class=\"tags\"><span>Tags: </span>" + doc.Tags + "</p>";
        buffer += "</li>";
    }
}

HtmlGenericControl docList = new HtmlGenericControl();
docList.InnerHtml = buffer;
PlacehHolder1.Controls.Add(docList);

So clicking on the file name directed the user to an .ashx handler, which actually delivered the file. And here is the handler code:

public void ProcessRequest( HttpContext context )
{
    if(context.Request.Params["fileName"] != null)
    {
        string fileName = context.Request.Params["fileName"].ToString();
        string tempFileName = string.Empty;

        if(fileName.Length > 0)
        {
            // get file from sharepoint
            using(MyOrgCopyService copyService = new MyOrgCopyService())
            {
                copyService.Credentials = new NetworkCredential( "User", "Pass", "Domain" );

                FieldInformation myFieldInfo = new FieldInformation();
                FieldInformation[] myFieldInfoArray = { myFieldInfo };
                byte[] myByteArray;

                copyService.GetItem( "https://admin.share.myorg.org/Website%20Documents/" + fileName, out myFieldInfoArray, out myByteArray );

                if(myByteArray.Length == 0)
                {
                    context.Response.Write( "<script type=\"text/javascript\" language=\"javascript\">alert('Error: The requested file is no longer on the server.');</script>" );
                    context.Response.End();
                }
                else
                {
                    // Convert into Base64 String
                    string base64String;
                    base64String = Convert.ToBase64String( myByteArray, 0, myByteArray.Length );

                    // Convert to binary array
                    byte[] binaryData = Convert.FromBase64String( base64String );

                    // Create a temporary file to write the text of the form to
                    tempFileName = Path.GetTempPath() + "\\" + myFieldInfoArray[14].Id.ToString();

                    // Write the file to temp folder
                    FileStream fs = new FileStream( tempFileName, FileMode.Create, FileAccess.ReadWrite );
                    fs.Write( binaryData, 0, binaryData.Length );
                    fs.Close();

                    //Write the file directly to the HTTP content output stream.
                    context.Response.ContentType = "application/octet-stream";
                    context.Response.AppendHeader( "Content-Disposition", "attachment; filename=" + fileName );
                    context.Response.TransmitFile( tempFileName );
                    context.Response.End();
                }
            }
        }
    }
}

To get the contents of the file, you use the Copy service. Again, I have a wrapper around the service that adds the X-FORMSBASEDAUTH_ACCEPTED header. After a basic check, I found I had to jump through a few hoops to convert the byte[] into an actual file that could be sent to the browser.