This turned out to be a lot more challenging than I was expecting it to be. Duplicating a content type required that I learn a lot about how to programmatically create things like site columns and workflows associations and policies. I also had to do a lot of reverse engineering to figure out seemingly simple things such as how the document information panel and document templates are stored.
In some cases copying elements became as simple as adding xml to a collection or adding an object from the source collection to the target with basically no real logic needed. In other cases I had to painstakingly reconstruct the original element – considering the limited documentation on some of this stuff I’m not convinced it could have been done without Reflector – Microsoft does some rather odd things that I can’t seem to find documented anywhere.
I think this command, perhaps more than some of the others, has potential for the most use post deployment. If you’ve got several site collections and content types that you need duplicated across those site collections then you’ll want to check this out – it’s a real pain to have to recreate a content type more than once (it’s so easy to miss something when you consider all the custom columns, policies, workflows, templates, etc.). Of course if you built your content types using Features then you wouldn’t have this issue but I know that there are many that do not 🙂
The nice thing about this command is that it can be used to copy all content types from one site collection to another or just a single content type. The code to accomplish this consists of 7 key methods – the primary method, CreateContentType() creates the content type itself based on the source content type. This method then calls out to the remaining six methods to add peripheral items such as workflows and templates. To create a new content type you have to make sure that the parent content type exists first – the CreateContentType method does a recursive call to address the Parent and then once all the parent types exist it will set the basic properties for the new content type.
After creating the content type and saving it the code then adds the other elements according to flags that can be passed in (the default is to copy everything but you can turn off individual items). The first thing I do is add any needed columns. A content type doesn’t actually store any details about columns (or fields as they are referred to in code) rather it stores a link to the columns. The columns themselves are part of the Fields collection which you can get from an SPWeb object. So the first step in setting the columns for the content type is to create the columns if they don’t already exist and then link that column to the content type:
After adding the columns I add the workflows (note that adding these peripheral items can be done in any order as long as the content type has been saved and therefore belongs to a content type collection). When I first started researching how to do this I thought I was going to have to do a whole lot of work to recreate the workflow (based on what I was seeing in Reflector and how Microsoft was handling the creation of workflows for a content type). On a whim I decided to simply try adding a workflow association object (SPWorkflowAssociation) directly to the targets WorkflowAssociations collection and to my surprise it worked perfectly. Only catch was that I had to make sure I cleared out any existing workflows on the target (the default items that are added behind the scenes when you create a content type):
Adding the document templates was another one that I thought was going to be a lot more difficult but in the end turned out pretty easy. Document templates (and custom document information panels) are stored in the resource folder which is located at "http://yourdomain/yoursitecollection/_cts/yourcontenttypename/". The SPContentType object has a ResourceFolder property which contains all the documents located here. To copy the template all I had to do was get a reference to the SPFile object returned by the ResourceFolder.Files collection and then add it to my targets ResourceFolder.Files collection:
Adding the document conversion settings turned out to be only marginally more difficult than adding the document template settings. When I first started researching how to do this I was expecting an object for which I could set various properties. Unfortunately (and fortunately as it turned out) I was wrong. Microsoft effectively breaks the pattern here (as well as with the document information panel) by choosing to use an XmlDocument object to store the settings. This threw me for a loop when trying to figure out how to do this but the result was that I could easily copy the settings by simply copying the XML from one object to another.
The SPContentType object has an XmlDocuments property which is a collection of documents that contain settings for certain services. There are two documents that we need here – one is named "urn:sharePointPublishingRcaProperties" and the other is named "http://schemas.microsoft.com/sharepoint/v3/contenttype/transformers". The first stores the settings for any document conversions that are set up. The second contains all the converters that are excluded (doesn’t really make sense to me to do it this way but whatever). So all I had to do was add these two documents to my target content type:
Setting the information rights policies was another one that took me a while – mainly because some of the core classes that Microsoft uses to do this have been obfuscated so I couldn’t disassemble them. In the end the solution once again turned out pretty simple – just took me a while to get there. To copy the policies I had to create a PolicyCatalog object which I then use to return a PolicyCollection object. If the policy doesn’t already exist in that collection then I export the policy from the source policy (which I got by calling the static GetPolicy() method of the Policy object).
The export method returns an XmlDocument object which I then use add to my target site collection by using the static PolicyCollection.Add() method. Once the policy exist at the site collection level then I can associate it with the content type by calling Policy.CreatePolicy and passing in my Policy object that I got from the source (the method name is a bit confusing as it’s not actually creating a policy – it’s just assocating a policy with your content type):
The last piece I had to address was the document information panel. This was hands down the most annoying to work on. I ended up grabbing a lot of code from Reflector in order to pull this off – mainly the code necessary to construct the resultant XmlDocument object that stores all the settings as well as the code to read those settings out of the source content type. I couldn’t just copy the XML directly as the values needed to be different based on the different URLs of the target and source. The XML of interest can be retrieved using the XmlDocuments property collection and passing in the name http://schemas.microsoft.com/office/2006/metadata/customXsn.
Once you have this you simply have to manipulate the values or construct a new doc as I did (I won’t show that code here as it’s a bit of a mess considering it’s practically a straight copy and paste from Reflector). Beyond setting the XML settings you also have to copy the template file itself. This is actually very similar to what was needed for the document template:
What’s interesting about many pieces of the code above is that they can easily be extracted out and used to copy other elements such as policies and site columns. I expect I’ll eventually do just that as I can see the need to have these items copied across site collections (replication is something that I don’t even want to think about as it introduces so many issues when dealing with content that is already consuming these elements – perhaps if the issue comes up I’ll look into it). The syntax of the command can be seen below:
C:\>stsadm -help gl-copycontenttypes stsadm -o gl-copycontenttypes Copies all Content Types from one gallery to another. Parameters: -sourceurl <site collection url containing the source content types> -targeturl <site collection url where the content types will be copied to> [-sourcename <name of an individual content type to copy - if ommitted all content types are copied if they don't already exist> [-noworkflows] [-nocolumns] [-nodocconversions] [-nodocinfopanel] [-nopolicies] [-nodoctemplate]
Here’s an example of how to copy all the content types from one site collection to another without copying document templates:
stsadm –o gl-copycontenttypes –sourceurl "http://intranet/operations" -targeturl "http://intranet/hr" -nodoctemplate
Update 11/26/2007: I’ve fixed a bug with copying site columns. I had omitted an Update() call after copying the columns (it worked for me during testing because later calls were calling Update but for certain types of content types this wasn’t happening because there was nothing to do in the later calls). I also fixed null argument issue when copying content types that are not based on documents (so no document template is present). Thanks to Chris Rivera for helping me find these.