Monday, June 28, 2010

Making your SharePoint 2010 applications ECM aware (Part Three – Using Document Sets with the client object model)

This is part three of a multi-part series on how to make your SharePoint applications ECM aware, by taking advantage of the new ECM features in SP2010. My last post explained how you could leverage the new feature “Document Sets” to put a process around your document management. The post explained how you could do this using the server side object model and how difficult it is to try using document sets remotely through any built-in SharePoint remote API like the client object model or a web service. Well after much research it is possible to provision document sets with the client object model, albeit, a bit convoluted.

I started by looking at the way the server object model allows you to create a document set. This is done by using the static Create method of the DocumentSet class in the Microsoft.Office.DocumentManagement assembly. Below is code showing how simple this can be done. The method takes the folder where the document set will be located, the content type id for the document set you are creating, the name of the new document set, a hashtable of properties to be given to the document set, and most importantly a bool value stating whether to provision any default documents to the new document set. Very easy from a server side point a view, however, doing the same from the client object model proved to be difficult.

public static void CreateDocumentSet(SPFolder folder,
           SPContentTypeId id,
           string documentSetName,
           Hashtable properties,
           bool provisionDefaultDocuments) 
{
           DocumentSet ds = DocumentSet.Create(folder,
               documentSetName, id, properties,
               provisionDefaultDocuments); 
}

When trying to do this through the client object model I remembered that there are many settings for a document set and these settings are used when a new document set is provisioned. For instance, you can choose the allowable content types, the default documents, and the shared properties among the documents and whether the default documents have the document set name appended to them.

Below is the AddDocSet method which uses the client object model to create a new document set based on a document set content type. I have included this top level method along with another method in this posting, to show you all the different steps needed to accomplish what the server side object model does so easily. The full code which is included in the DocumentSetsRemote static class can be downloaded here.

DocumentSetsRemote

The AddDocSet method takes the following arguments, the url to the site, the name of the document library you want to create the document set in, the name of the document set content type, the name of the new document set you want to create, and a hash table of properties for the metadata you want to give the new document set. These would include the display name of the SharePoint field along with the value.

public static void AddDocSet(string siteUrl, string listName,
          string docSetContentTypeName,
          string newDocSetName,
          Dictionary<string, string> properties)
{
          ClientContext clientContext = new ClientContext(siteUrl);
          Web web = clientContext.Web;
          List list = clientContext.Web.Lists.GetByTitle(listName);

          clientContext.Load(clientContext.Site);

          ContentTypeCollection listContentTypes = list.ContentTypes;
          clientContext.Load(listContentTypes, types => types.Include
                                            (type => type.Id, type => type.Name,
                                            type => type.Parent));

          var result = clientContext.LoadQuery(listContentTypes.Where
              (c => c.Name == docSetContentTypeName));

          clientContext.ExecuteQuery();

          ContentType targetDocumentSetContentType = result.FirstOrDefault();

          ListItemCreationInformation newItemInfo = new ListItemCreationInformation();
          newItemInfo.UnderlyingObjectType = FileSystemObjectType.Folder;
          newItemInfo.LeafName = newDocSetName;
          ListItem newListItem = list.AddItem(newItemInfo);

          newListItem["ContentTypeId"] = targetDocumentSetContentType.Id.ToString();
          newListItem["Title"] = newDocSetName;
          newListItem.Update();

          clientContext.Load(list);
          clientContext.ExecuteQuery();

          List<ContentTypeId> allowedContentTypes = null;

          //get the allowed content types from the document set's schema
          if (targetDocumentSetContentType != null)
              allowedContentTypes =
                  GetAllowedContentTypes(
                  targetDocumentSetContentType.SchemaXml,
                  listContentTypes,
                  clientContext);


          //get the new document set created as folder in order
          //to access the UniqueContentTypeOrder property
          string targetDocSetUrl = listName + "/" + newDocSetName;
          Folder folder = web.GetFolderByServerRelativeUrl(targetDocSetUrl);

          clientContext.Load(folder, f => f.UniqueContentTypeOrder);
          clientContext.ExecuteQuery();

          if (allowedContentTypes != null)
          {
              //set the document set's allowed content types using
              //the UniqueContentTypeOrder property
              folder.UniqueContentTypeOrder = allowedContentTypes;
              clientContext.Load(folder);
              folder.Update();

              clientContext.ExecuteQuery();
          }

          //update the document set's docset_LastRefresh property
          UpdateFolder(clientContext.Site.Url, list.Title, newListItem.Id.ToString());

          //set default documents and shared properties
          SetDefaultDocuments(targetDocumentSetContentType,
              targetDocSetUrl, newDocSetName,
              properties, list, web, clientContext);

}

The first step is to use the client object model to create a folder listitem and set the contenttypeid field to the content type id of the type of document set you are creating. The second step is to set the “allowable content types” that this document set can contain. So when configuring a document set I was wondering how to obtain all this configuration data via the client object model. The allowable content types, default documents, shareable properties and other information is stored in the the SchemaXml property of the ContentType class. I used xml linq to pull this data from the schema as show  in the GetAllowableContentTypes method below:

private static List<ContentTypeId> GetAllowedContentTypes(string listSchemaXml,
     ContentTypeCollection listContentTypes, ClientContext context)
{

          List<string> schemaContentypeIds = new List<string>(); ;
          List<ContentTypeId> allowableContentTypeIds = new List<ContentTypeId>();

          XNamespace act =
              "http://schemas.microsoft.com/office/documentsets/allowedcontenttypes";

          XDocument document = XDocument.Parse(listSchemaXml);

          var result = from e in document.Descendants().Elements("XmlDocuments")
                           .Elements("XmlDocument").
                           Elements(act + "AllowedContentTypes")
                           .Elements("AllowedContentType").Attributes("id")
                       select e.Value;

          if (result != null && result.Count() > 0)
              schemaContentypeIds = result.ToList<string>();

          foreach (string schemaContentTypeId in schemaContentypeIds)
          {
              foreach (ContentType listContentType in listContentTypes)
              {
                  if (listContentType.Parent.Id.ToString() == schemaContentTypeId)
                      allowableContentTypeIds.Add(listContentType.Id);

              }
          }

          return allowableContentTypeIds;

}

This method returns a list of content type ids to set the new document set’s UniqueContentTypeOrder property. You will find in the downloadable code other methods using the same technique to obtain information.

The next step is to update the “docset_LastRefresh” property of the document set. Why? This  apparently is a stamp of approval by the SharePoint UI that the document set was configured correctly. If you do not set this, then you will have a nagging yellow bar at the top of the SharePoint UI stating that the document set is missing some content types and needs updating. You can click on the link and it will generate the property for you. This value is stored in the SPFolder.Properties property bag. Unfortunately, the client object model does not support this. So, the UpdateFolder method uses the Lists.asmx web service to update this.

The final step to create a document set is to provision the default documents that are defined. These are the documents that are created by default whenever a new document set is created.

 

When creating the default documents there are many details that you must implement using the client object model. First, you must get the document from the server, assign the appropriate content type, assign any shareable properties, and optionally append the name of the document set to the document’s name. Once this is done you add the document to the document set. Provisioning default documents is inefficient when doing it remotely. This is because the default document must be downloaded from the list’s forms folder and then uploaded again into the document set. Unfortunately, there is no way around this. This is all implemented in the SetDefaultDocuments method in the downloadable code. The only problem I had was with shareable document set fields that had the same display name as fields defined on the list. The client object model cannot handle duplicate field names.

You can download all the code listed here along with other private methods for the complete solution. The code is contained in one static class called DocumentSetsRemote.cs.

DocumentSetsRemote

In summary, using document sets can be done remotely without having to use a custom web service. SharePoint does not expose any out of the box web services that deal explicitly with document sets, however, it does have the client object model and other standard web services you can take advantage of. With some basic research you can make your remote SharePoint applications ECM aware and leverage the new “Document Set” feature.

10 comments:

Anonymous said...

Wonderful article..Thanks..have few questions.

1. Which web service reference should be added?
2. If have to get documents from a set, can you please share approach or methods that can be used from here?

Thanks a bunch
Balaji

Anonymous said...

Sorry to disturb..got these with these beautiful lines

string targetDocSetUrl = listName + "/" + newDocSetName;
Folder folder = web.GetFolderByServerRelativeUrl(targetDocSetUrl);

clientContext.Load(folder, f => f.UniqueContentTypeOrder);
clientContext.ExecuteQuery();

Thanks
Balaji

Anonymous said...

Wonderful article! Thanks a lot!
Nevertheless, there is still one question open to me: What is the variable "listservice" initialized with? What excactly should that be?
Thanks
Dave

Tarun Arora said...

Can the document set be downloaded using the client object model?

Anonymous said...

Can you provide the code for the UpdateFolder method that uses the Lists.asmx web service? I am running into a brick wall trying to use the Lists.asmx web service UpdateListItems Method to update the docset_LastRefresh property using the ID of the document set. The yellow bar still remains.

Steve Curran said...

The code is in the DocumentSetsRemote.zip file you can download from this post.

Sharepointer said...

Hi Steve

We have a project where we need to work on sand box solution and document sets. The only way i could create a document set is by creating a SPFolder and then converting it to DocumentSet. The only issue which i have is i am getting a message on the welcome page that "Content Types allowed to this Document Set have been added or deleted Update the document set" i have tried setting the property "docSet_LastRefresh" on the Folder but still its not working. I dont think i can even use the webservice method as mentioned by you in sandbox solution. Can you help me with this.

Thanks in advance

Anonymous said...

steve,

thanks for the code, however I noticed that you still have a reference to a non-client sharepoint library - inside updatefolder
string dateTimeString = Microsoft.SharePoint.Utilities.SPUtility.CreateISO8601DateTimeFromSystemDateTime(DateTime.UtcNow);

use
ClientResult dateTimeString = Microsoft.SharePoint.Client.Utilities.Utility.FormatDateTime(clientContext, web, DateTime.Now, Microsoft.SharePoint.Client.Utilities.DateTimeFormat.DateTime);
date is in dateTimeString.Value

Thanks

Anonymous said...

what is the listservice in updateFolder method?

Anonymous said...

what should be the listservice? please reply me sooon awaiting for your reply

Thanks
Pedda

Post a Comment