Targeted Provisioning of Document Templates with XML

I had a request to provision Document Templates to Document Libraries in SharePoint 2013. The requirement included the ability to relatively easy add or update the Document Templates, as well as target a Document Template to all Site Collections, or only to Site Collections under a specific Managed path.

The solution is based on an XML file, that was added to the CONFIG folder in the 15 hive, template office documents added to the Layouts folder and an Event Receiver that parses the XML and creates appropriate Content Types for the Document Templates.

The XML looks like this:

<?xml version="1.0" encoding="utf-8" ?>
<DocumentTemplates Prefix="Baseic" ParentContentType="0x01010055DCB6CC4FB44A548DEE1113E5000917">
  <DocumentTemplate Target="collaboration">
    <ContentType Name="Collaboration Guideline" Group="Collaboration" Description="Create a Collaboration Guide" />
    <Template Location="/_layouts/15/Baseic.2013/DocumentTemplates/CollabGuide.docx" />
  </DocumentTemplate>
  <DocumentTemplate>
    <ContentType Name="Protocol" Group="Common" Description="Create a protocol" />
    <Template Location="/_layouts/15/Baseic.2013/DocumentTemplates/Protocol.docx" />
  </DocumentTemplate>
</DocumentTemplates>

As you can see, I specify a Parent Content Type that all the created ones will inherit from. I do this because in my scenario all documents in the solution must have a couple of fields added to them. I also specify a Prefix that will be added before the actual Content Type name.

For each Document Template we specify a Content Type and a Template.

  • The Content Type Node contains information about what the Content Type will be called, to which group it will be added and a description.
  • The Template Node specifies the location of the document to add as a template to the new Content Type

This file will be uploaded to a subfolder of the CONFIG directory (C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\CONFIG).

The template files will, as you can see in the XML be added to a subfolder of the Layouts folder (C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\TEMPLATE\LAYOUTS)

Now to the magic: The feature event receiver that makes it all happen!

The feature is Site Scoped, since we will really just want to add our Content Types to the root Web.

using System;
using System.Runtime.InteropServices;
using System.Security.Permissions;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Utilities;
using System.IO;
using System.Linq;
using System.Xml.Linq;

namespace Baseic.2013.Features.ProvisionDocTemplates
{
    [Guid("9798f1d6-4e4a-407d-8989-ad91e9f04e03")]
    public class ProvisionDocTemplatesEventReceiver : SPFeatureReceiver
    {
        public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
            // Get Rootweb
            var site = new SPSite((properties.Feature.Parent as SPSite).ID);
            var web = site.OpenWeb();

            // Locate XML file with definitions
            var definitionsFile = SPUtility.GetVersionedGenericSetupPath("CONFIG\\Baseic\\DocTemplatesDefinition.xml", 15);
            
            // Parse file
            XDocument definitionsDocument;
            using (StreamReader oReader = new StreamReader(definitionsFile))
            {
                definitionsDocument = XDocument.Load(oReader);
            };

            var contentTypePrefix = (from c in definitionsDocument.Descendants("DocumentTemplates") select c).First().Attribute("Prefix").Value;
            var parentContentTypeId = (from c in definitionsDocument.Descendants("DocumentTemplates") select c).First().Attribute("ParentContentType").Value;
            var documentTemplates = from c in definitionsDocument.Descendants("DocumentTemplate") select c;

            // Get the parent content type from web
            var parentContentType = web.AvailableContentTypes[new SPContentTypeId(parentContentTypeId)];

            if (parentContentType == null)
            {
                // Logger.WriteError(this.ToString(), "Failed to apply content type: Failed to find parent content type");
                web.Dispose();
                site.Dispose();
                return;
            }

            // Call provisioning method for each CT to deploy 
            foreach (var docTemplate in documentTemplates)
            {
                var target = docTemplate.Attribute("Target");

                // Depending on the Target, provison or not
                if (target == null)
                {
                    CreateContentTypeAndAddDocTemplate(web, contentTypePrefix, parentContentType, docTemplate);
                }
                else if (site.ServerRelativeUrl.Split('/')[1].ToLowerInvariant().Equals(target.Value.ToLowerInvariant())) // the split gives us the managed path of the current sitecollection
                {
                    CreateContentTypeAndAddDocTemplate(web, contentTypePrefix, parentContentType, docTemplate);
                }
            }

            // Make sure to dispose
            web.Update();
            web.Dispose();
            site.Dispose();
        }

        private void CreateContentTypeAndAddDocTemplate(SPWeb web, string contentTypePrefix, SPContentType parentContentType, XElement docTemplate)
        {
            try
            {
                // Parse node data
                var contentTypeNode = (from c in docTemplate.Descendants("ContentType") select c).First();

                var newContentTypeName = string.Concat(contentTypePrefix, " ", contentTypeNode.Attribute("Name").Value);
                var newContentTypeGroup = contentTypeNode.Attribute("Group").Value;
                var newContentTypeDescription = contentTypeNode.Attribute("Description").Value;

                var docTemplateLocation = (from c in docTemplate.Descendants("Template") select c).First().Attribute("Location").Value;

                // Add or update Content Type
                var contentType = web.AvailableContentTypes[newContentTypeName]; 
  
                if (contentType == null) // Does not exist
                {
                    var newContentType = new SPContentType(parentContentType, web.ContentTypes, newContentTypeName);
                    web.ContentTypes.Add(newContentType);

                    newContentType.DocumentTemplate = docTemplateLocation;
                    newContentType.Group = newContentTypeGroup;
                    newContentType.Description = newContentTypeDescription;

                    newContentType.Update(true);
                }
                else // Does exists, update
                {
                    contentType = web.ContentTypes[contentType.Id];

                    contentType.DocumentTemplate = docTemplateLocation;
                    contentType.Group = newContentTypeGroup;
                    contentType.Description = newContentTypeDescription;

                    contentType.Update(true);
                }
            }
            catch (Exception e)
            {
                // Logger.WriteError(this.ToString(), e.Message);
            }
        }
    }
}

So basically, as you can see, it all comes down to using some nice Linq statements to get the correct stuff out of the XML file and then create or update Content Types on the root Web.

You can activate the feature through e.g. the WebTemplate’s ONET.XML or PowerShell.

A heads up though, this code requires that the specified Parent Content Type does already exist on the root Web.

For part two in this blog post I will show you how to bind these new Content Types to a Document Library, using the same XML file!

Best of luck
Robert

Advertisements

2 responses to “Targeted Provisioning of Document Templates with XML

  1. Not me

    Sweet, dont know how Sharepoints works with XML but I dislike the use of attributes.
    Another note in the XML is “baseic” instead of “basic”

    • Hi! Thanks for the feedback!

      You could easily modify the XML to not use attributes at all, I just think that for end users it is sometimes easier to comprehend with som attributes.

      Regarding the use of Baseic instead of Basic, that is on purpose. I tend to use Baseic as my alias online!

      /Robert

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: