Copy Content Types

Posted on Posted in SharePoint 2007, STSADM Commands

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.

   1: private static void CreateContentType(string targetUrl, SPContentType sourceCT, SPFieldCollection sourceFields, bool verbose)

   2: {

   3:     // Make sure any parent content types exist - they have to be there before we can create this content type.

   4:     if (availableTargetContentTypes[sourceCT.Parent.Id] == null)

   5:     {

   6:         Log(string.Format("Progress: Parent of content type '{0}' does not exist - creating...", sourceCT.Name));

   7:  

   8:         CreateContentType(targetUrl, sourceCT.Parent, sourceFields, verbose);

   9:  

  10:         // Reset the fields and content types.

  11:         GetAvailableTargetContentTypes(verbose, targetUrl);

  12:     }

  13:  

  14:     Log(string.Format("Progress: Creating content type '{0}'...", sourceCT.Name));

  15:  

  16:     // Create a new content type using information from the source content type.

  17:     SPContentType newCT = new SPContentType(availableTargetContentTypes[sourceCT.Parent.Id], targetContentTypes, sourceCT.Name);

  18:  

  19:     Log(string.Format("Progress: Setting fields for content type '{0}'...", sourceCT.Name));

  20:  

  21:     // Set all the core properties for the content type.

  22:     newCT.Group = sourceCT.Group;

  23:     newCT.Hidden = sourceCT.Hidden;

  24:     newCT.NewDocumentControl = sourceCT.NewDocumentControl;

  25:     newCT.NewFormTemplateName = sourceCT.NewFormTemplateName;

  26:     newCT.NewFormUrl = sourceCT.NewFormUrl;

  27:     newCT.ReadOnly = sourceCT.ReadOnly;

  28:     newCT.RequireClientRenderingOnNew = sourceCT.RequireClientRenderingOnNew;

  29:     newCT.Description = sourceCT.Description;

  30:     newCT.DisplayFormTemplateName = sourceCT.DisplayFormTemplateName;

  31:     newCT.DisplayFormUrl = sourceCT.DisplayFormUrl;

  32:     newCT.EditFormTemplateName = sourceCT.EditFormTemplateName;

  33:     newCT.EditFormUrl = sourceCT.EditFormUrl;

  34:  

  35:     string xml = (string)Utilities.GetFieldValue(newCT, "m_strXmlNonLocalized");

  36:     xml = xml.Replace(string.Format("ID=\"{0}\"", newCT.Id), string.Format("ID=\"{0}\"", sourceCT.Id));

  37:     Utilities.SetFieldValue(newCT, typeof (SPContentType), "m_strXmlNonLocalized", xml);

  38:     Utilities.SetFieldValue(newCT, typeof(SPContentType), "m_id", sourceCT.Id);

  39:  

  40:     Log(string.Format("Progress: Adding content type '{0}' to collection...", sourceCT.Name));

  41:  

  42:     // Add the content type to the content types collection and update all the settings.

  43:     targetContentTypes.Add(newCT);

  44:     newCT.Update();

  45:  

  46:     // Add all the peripheral items

  47:  

  48:     try

  49:     {

  50:         if (copyColumns)

  51:         {

  52:             Log(string.Format("Progress: Adding site columns for content type '{0}'...", sourceCT.Name));

  53:  

  54:             AddSiteColumns(newCT, sourceCT, sourceFields, verbose);

  55:         }

  56:  

  57:         if (copyWorkflows)

  58:         {

  59:             Log(string.Format("Progress: Adding workflow associations for content type '{0}'...", sourceCT.Name));

  60:  

  61:             AddWorkflowAssociations(newCT, sourceCT, verbose);

  62:         }

  63:  

  64:         if (copyDocTemplate)

  65:         {

  66:             Log(string.Format("Progress: Adding document template for content type '{0}'...", sourceCT.Name));

  67:  

  68:             AddDocumentTemplate(newCT, sourceCT);

  69:         }

  70:  

  71:         if (copyDocConversions)

  72:         {

  73:             Log(string.Format("Progress: Adding document conversion settings for content type '{0}'...", sourceCT.Name));

  74:  

  75:             AddDocumentConversionSettings(newCT, sourceCT);

  76:         }

  77:  

  78:         if (copyPolicies)

  79:         {

  80:             Log(string.Format("Progress: Adding information rights policies for content type '{0}'...", sourceCT.Name));

  81:  

  82:             AddInformationRightsPolicies(newCT, sourceCT, verbose);

  83:         }

  84:  

  85:         if (copyDocInfoPanel)

  86:         {

  87:             Log(string.Format("Progress: Adding document information panel for content type '{0}'...", sourceCT.Name));

  88:  

  89:             AddDocumentInfoPanelToContentType(sourceCT, newCT);

  90:         }

  91:     }

  92:     finally

  93:     {

  94:         newCT.ParentWeb.Site.Dispose();

  95:         newCT.ParentWeb.Dispose();

  96:     }

  97: }

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:

   1: private static void AddSiteColumns(SPContentType targetCT, SPContentType sourceCT, SPFieldCollection sourceFields, bool verbose)

   2: {

   3:     // Store the field order so that we can reorder after adding all the fields.

   4:     List<string> fields = new List<string>();

   5:     foreach (SPField field in sourceCT.Fields)

   6:     {

   7:         if (!field.Hidden && field.Reorderable)

   8:             fields.Add(field.InternalName);

   9:     }

  10:     // Add any columns associated with the content type.

  11:     foreach (SPFieldLink field in sourceCT.FieldLinks)

  12:     {

  13:         // First we need to see if the column already exists as a Site Column.

  14:         SPField sourceField;

  15:         try

  16:         {

  17:             // First try and find the column via the ID

  18:             sourceField = sourceFields[field.Id];

  19:         }

  20:         catch

  21:         {

  22:             try

  23:             {

  24:                 // Couldn't locate via ID so now try the name

  25:                 sourceField = sourceFields[field.Name];

  26:             }

  27:             catch

  28:             {

  29:                 sourceField = null;

  30:             }

  31:         }

  32:         if (sourceField == null)

  33:         {

  34:             // Couldn't locate by ID or name - it could be due to casing issues between the linked version of the name and actual field

  35:             // (for example, the Contact content type stores the name for email differently: EMail for the field and Email for the link)

  36:             foreach (SPField f in sourceCT.Fields)

  37:             {

  38:                 if (field.Name.ToLowerInvariant() == f.InternalName.ToLowerInvariant())

  39:                 {

  40:                     sourceField = f;

  41:                     break;

  42:                 }

  43:             }

  44:         }

  45:         if (sourceField == null)

  46:         {

  47:             Log(string.Format("WARNING: Unable to add column '{0}' to content type.", field.Name));

  48:             continue;

  49:         }

  50:  

  51:         if (!targetFields.ContainsField(sourceField.InternalName))

  52:         {

  53:             Log(string.Format("Progress: Adding column '{0}' to site columns...", sourceField.InternalName));

  54:  

  55:             // The column does not exist so add the Site Column.

  56:             targetFields.Add(sourceField);

  57:         }

  58:  

  59:         // Now that we know the column exists we can add it to our content type.

  60:         if (targetCT.FieldLinks[sourceField.InternalName] == null) // This should always be true if we're here but I'm keeping it in as a safety check.

  61:         {

  62:             Log(string.Format("Progress: Associating content type with site column '{0}'...", sourceField.InternalName));

  63:             

  64:             // Now add the reference to the site column for this content type.

  65:             try

  66:             {

  67:                 targetCT.FieldLinks.Add(field);

  68:             }

  69:             catch (Exception ex)

  70:             {

  71:                 Log("WARNING: Unable to add field '{0}' to content type: {1}", sourceField.InternalName, ex.Message);

  72:             }

  73:         }

  74:     }

  75:     // Save the fields so that we can reorder them.

  76:     targetCT.Update(true);

  77:  

  78:     // Reorder the fields.

  79:     try

  80:     {

  81:         targetCT.FieldLinks.Reorder(fields.ToArray());

  82:     }

  83:     catch

  84:     {

  85:         Log("WARNING: Unable to set field order.");

  86:     }

  87:     targetCT.Update(true);

  88: }

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):

   1: private static void AddWorkflowAssociations(SPContentType targetCT, SPContentType sourceCT, bool verbose)

   2: {

   3:     // Remove the default workflows - we're going to add from the source.

   4:     while (targetCT.WorkflowAssociations.Count > 0)

   5:     {

   6:         targetCT.RemoveWorkflowAssociation(targetCT.WorkflowAssociations[0]);

   7:     }

   8:  

   9:     // Add workflows.

  10:     foreach (SPWorkflowAssociation wf in sourceCT.WorkflowAssociations)

  11:     {

  12:         Log(string.Format("Progress: Adding workflow '{0}' to content type...", wf.Name));

  13:  

  14:         targetCT.AddWorkflowAssociation(wf);

  15:     }

  16:     targetCT.Update();

  17: }

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:

   1: private static void AddDocumentTemplate(SPContentType targetCT, SPContentType sourceCT)

   2: {

   3:     if (string.IsNullOrEmpty(sourceCT.DocumentTemplate))

   4:         return;

   5:  

   6:     // Add the document template.

   7:     SPFile sourceFile = null;

   8:     try

   9:     {

  10:         sourceFile = sourceCT.ResourceFolder.Files[sourceCT.DocumentTemplate];

  11:     }

  12:     catch (ArgumentException) {}

  13:     if (sourceFile != null && !string.IsNullOrEmpty(sourceFile.Name))

  14:     {

  15:         SPFile targetFile = targetCT.ResourceFolder.Files.Add(sourceFile.Name, sourceFile.OpenBinary(), true);

  16:         targetCT.DocumentTemplate = targetFile.Name;

  17:         targetCT.Update();

  18:     }

  19:     else

  20:     {

  21:         targetCT.DocumentTemplate = sourceCT.DocumentTemplate;

  22:         targetCT.Update();

  23:     }

  24: }

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:

   1: private static void AddDocumentConversionSettings(SPContentType targetCT, SPContentType sourceCT)

   2: {

   3:     // Add document conversion settings if document converisons are enabled.

   4:     // ParentWeb and Site will be disposed later.

   5:     if (targetCT.ParentWeb.Site.WebApplication.DocumentConversionsEnabled)

   6:     {

   7:         // First, handle the xml that describes what is enabled and each setting

   8:         string sourceDocConversionXml = sourceCT.XmlDocuments["urn:sharePointPublishingRcaProperties"];

   9:         if (sourceDocConversionXml != null)

  10:         {

  11:             XmlDocument sourceDocConversionXmlDoc = new XmlDocument();

  12:             sourceDocConversionXmlDoc.LoadXml(sourceDocConversionXml);

  13:  

  14:             targetCT.XmlDocuments.Delete("urn:sharePointPublishingRcaProperties");

  15:             targetCT.XmlDocuments.Add(sourceDocConversionXmlDoc);

  16:         }

  17:         // Second, handle the xml that describes what is excluded (disabled).

  18:         sourceDocConversionXml =

  19:             sourceCT.XmlDocuments["http://schemas.microsoft.com/sharepoint/v3/contenttype/transformers"];

  20:         if (sourceDocConversionXml != null)

  21:         {

  22:             XmlDocument sourceDocConversionXmlDoc = new XmlDocument();

  23:             sourceDocConversionXmlDoc.LoadXml(sourceDocConversionXml);

  24:  

  25:             targetCT.XmlDocuments.Delete("http://schemas.microsoft.com/sharepoint/v3/contenttype/transformers");

  26:             targetCT.XmlDocuments.Add(sourceDocConversionXmlDoc);

  27:         }

  28:         targetCT.Update();

  29:     }

  30: }

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):

   1: private static void AddInformationRightsPolicies(SPContentType targetCT, SPContentType sourceCT, bool verbose)

   2: {

   3: #if MOSS

   4:     // Set information rights policy - must be done after the new content type is added to the collection.

   5:     using (Policy sourcePolicy = Policy.GetPolicy(sourceCT))

   6:     {

   7:         if (sourcePolicy != null)

   8:         {

   9:             PolicyCatalog catalog = new PolicyCatalog(targetCT.ParentWeb.Site);

  10:             PolicyCollection policyList = catalog.PolicyList;

  11:  

  12:             Policy tempPolicy = null;

  13:             try

  14:             {

  15:                 tempPolicy = policyList[sourcePolicy.Id];

  16:                 if (tempPolicy == null)

  17:                 {

  18:                     XmlDocument exportedSourcePolicy = sourcePolicy.Export();

  19:                     try

  20:                     {

  21:                         Log(string.Format("Progress: Adding policy '{0}' to content type...", sourcePolicy.Name));

  22:  

  23:                         PolicyCollection.Add(targetCT.ParentWeb.Site, exportedSourcePolicy.OuterXml);

  24:                     }

  25:                     catch (Exception ex)

  26:                     {

  27:                         if (ex is NullReferenceException || ex is SEHException)

  28:                             throw;

  29:                         // Policy already exists

  30:                         Log("ERROR: An error occured creating the information rights policy: {0}", ex.Message);

  31:                     }

  32:                 }

  33:                 Log(string.Format("Progress: Associating content type with policy '{0}'...", sourcePolicy.Name));

  34:                 // Associate the policy with the content type.

  35:                 Policy.CreatePolicy(targetCT, sourcePolicy);

  36:             }

  37:             finally

  38:             {

  39:                 if (tempPolicy != null)

  40:                     tempPolicy.Dispose();

  41:             }

  42:         }

  43:         targetCT.Update();

  44:     }

  45: #endif

  46: }

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:

   1: private static void AddDocumentInfoPanelToContentType(SPContentType sourceCT, SPContentType targetCT)

   2: {

   3:     XmlDocument sourceXmlDoc = null;

   4:     string sourceXsnLocation;

   5:     bool isCached;

   6:     bool openByDefault;

   7:     string scope;

   8:  

   9:     // We first need to get the XML which describes the custom information panel.

  10:     string str = sourceCT.XmlDocuments["http://schemas.microsoft.com/office/2006/metadata/customXsn"];

  11:     if (!string.IsNullOrEmpty(str))

  12:     {

  13:         sourceXmlDoc = new XmlDocument();

  14:         sourceXmlDoc.LoadXml(str);

  15:     }

  16:     if (sourceXmlDoc != null)

  17:     {

  18:         // We found settings for a custom information panel so grab those settings for later use.

  19:         XmlNode node;

  20:         string innerText;

  21:         XmlNamespaceManager nsmgr = new XmlNamespaceManager(sourceXmlDoc.NameTable);

  22:         nsmgr.AddNamespace("cust", "http://schemas.microsoft.com/office/2006/metadata/customXsn");

  23:         sourceXsnLocation = sourceXmlDoc.SelectSingleNode("/cust:customXsn/cust:xsnLocation", nsmgr).InnerText;

  24:         node = sourceXmlDoc.SelectSingleNode("/cust:customXsn/cust:cached", nsmgr);

  25:         isCached = (node != null) && (node.InnerText == bool.TrueString);

  26:         innerText = sourceXmlDoc.SelectSingleNode("/cust:customXsn/cust:openByDefault", nsmgr).InnerText;

  27:         openByDefault = !string.IsNullOrEmpty(innerText) && (innerText == bool.TrueString);

  28:     }

  29:     else

  30:         return;

  31:  

  32:     // This should never be null but just in case...

  33:     if (sourceXsnLocation == null)

  34:         return;

  35:  

  36:     // Grab the source file and add it to the target resource folder.

  37:     SPFile sourceFile = null;

  38:     try

  39:     {

  40:         sourceFile = sourceCT.ResourceFolder.Files[sourceXsnLocation];

  41:     }

  42:     catch (ArgumentException)

  43:     {

  44:     }

  45:     if (sourceFile != null)

  46:     {

  47:         SPFile file2 = targetCT.ResourceFolder.Files.Add(targetCT.ParentWeb.Url + "/" + sourceFile.Url, sourceFile.OpenBinary(), true);

  48:  

  49:         // Get the target and scope to use in the xsn for the custom information panel.

  50:         string targetXsnLocation = targetCT.ParentWeb.Url + "/" + file2.Url;

  51:         scope = targetCT.ParentWeb.Site.MakeFullUrl(targetCT.Scope);

  52:  

  53:         XmlDocument targetXmlDoc = BuildCustomInformationPanelXml(targetXsnLocation, isCached, openByDefault, scope);

  54:         // Delete the existing doc so that we can add the new one.

  55:         targetCT.XmlDocuments.Delete("http://schemas.microsoft.com/office/2006/metadata/customXsn");

  56:         targetCT.XmlDocuments.Add(targetXmlDoc);

  57:     }

  58:     targetCT.Update();

  59: }

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.

82 thoughts on “Copy Content Types

  1. Very nice. I need to copy content types from a subsite to the top level which I assume I can do with your code. Anyway I can get the exe for this? Thanks,
    Chris (chrisgoblue@gmail.com)

  2. Thanks for the feedback. There’s actually no exe for the extensions. You can download the code which contains the dll and xml files in the Package folder (the link is in the right nav bar). You then need to GAC the dll and copy the xml file to C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\CONFIG. Once you’ve done this you can use the out of the box stsadm executable to run the commands.

  3. Gary – sorry for my ignorance, what’s the easiest way to sign the DLL? From the downloadable package the DLL won’t drag into the GAC.

    Tim

  4. No worries – the dll is already signed so you don’t have to worry about that. If you’re having trouble dragging into the GAC then I’d suggest just using gacutil (gacutil -i “Edfinancial.SharePoint.STSADM.Commands.dll”. I’ve found that there are some scenarios where dragging into the GAC doesn’t work (for instance, if you drag in using a single explorer instance it won’t work – try using two different explorer instances, also make sure that the file is on a local drive and not a network share).

  5. Thx – I’ll get back in there and find out what the issue is. I find 99% of my time is spent finding that illusive gremlin and what should be monstrous tasks seem to disolve before my eyes…!

  6. Gary, great stuff you’ve created – I can see some of it saving a ton of time. I’m having an issue copying from http://siteA to http://siteB – the command is returning that http://siteA cannot be found.

    http://siteA is on a different box from where I am running stsadm, while siteB is local. I’m logged into windows with an account that has full privs on both boxes in terms of sharepoint, the os, and the databases.

    Is there a problem that would prohibit stsadm from “finding” siteA when running from a box containing siteB? I’m stuck scratching my head since in theory everything should be fine.

  7. There may be a way around it but as far as I understand it you cannot talk cross farms using SPSite (which is the API object that is used to load up the site). One way around it – use import/export to bring SiteA over to SiteB’s farm and then copy the content type from there – once done, delete the temp site.

  8. First, thanks a lot for your brilliant work. I use your extensions everyday.

    Apparently though, this command doesn’t replicate the Version attribute of my content types columns (they’re at 0 on my target site).
    I wouldn’t mind, however it generates a “The object has been updated by another user since it was last fetched” error when someone tries to update one of those site columns.
    I just have to put the proper Version attribute right in the database and it works well afterwards.

    Do you think you could extract that property and set it in the xml flow ? I checked on the msdn and it seems the SPContentType.Version property is read only 🙁

    Hope it helps, thanks again.

  9. Pierrick – is the issue with the SPContentType object or the SPField objects? It’s easy enough to set the version to something explicit using reflection but I can’t tell from your post which is the issue (I don’t really have time to play with it right now in order to figure it out on my own).

  10. Gary i have problems with my content types that inherits from another content types: I have content type “a” that inherits from content type “parent” and content type “b” that inherits from the same content type “parent”. If i copy the content type “a” all works ok but whe i copy the content type “b” i had an error: “A duplicate name conten “parent” was found…
    Thanks

  11. Javier – thanks for the info – the problem was that I have no way to directly set the content type ID when I create the content type (not sure why MSFT didn’t allow that). So I added a couple of calls using reflection to simply set the content type ID manually so now when the content types get copied it will preserve the original IDs which it was not previously doing. Hate that I had to use reflection for this but it’s the only way. I’ve pushed out a new build – let me konw if you have any issues with it.

  12. Thanks Gary – worked great in the test build, but on the production system I’m having problems as some site columns and parent document types already exist resulting in duplicate name errors.

    eg I have a parent type “ParentDoc” in both systems, I then develop a “ChildDocA” in the dev environment and want to port over, as “ParentDoc” already exists the process fails. A similar thing happens if site columns already exist in both environments.

    Any suggestions on how to work around this?
    Thanks,
    Ben

  13. Ben – sorry for not getting back to you sooner – take a look at my previous comment – I believe I’ve fixed the issue with parent content types but I’ll have to look into the site column issue.

  14. Gary,

    This utility seems to be very handy. I am having issues off the bat however. Several content types are not being copied from http://dev.loc to http://dev.loc/hr

    When I specify the content type using -sourcename, the missing content types will come over but without the information management policy settings. Please help if you can.

  15. Can you give me some more info? What is the command you are running? How many content types are there (can you give me the names of the ones that are failing)? What groups are they in? Are you getting any sort of errors (I’m wondering if it’s actually just failing on one and therefore not continuing with the others)? Is there anything in the log file? The information rights policy stuff is some of the last pieces to get copied so it’s possible it’s erroring out and causing the rest to fail (or possibly the doc converter stuff is erroring).

  16. Gary,

    I have tried this command

    stsadm -o gl-copycontenttypes -sourceurl http://dev.site.com -targeturl http://dev.site.com/it

    but this will only copy some of the content types, none of the custom ones and none of the custom groups as you can see in screenshots below…

    Here are the source content types BEFORE gl-copycontenttypes: http://screencast.com/t/YX2NhU93VSS

    Here are the destination content types BEFORE gl-copycontenttypes: http://screencast.com/t/aTQ7VyvcaRA

    Here are the destination content types AFTER gl-copycontenttypes: http://screencast.com/t/c6cFW7dkXla

    That failed to copy the custom content types such as “Corporate Benefits – Appeals” and I did not get any errors in the command line. “Operation completed successfully”. Where should I look for the log file?

    Next I tried using the -sourcename parameter:

    stsadm -o gl-copycontenttypes -sourceurl http://dev.site.com -targeturl http://dev.site.com/it -sourcename “Corporate Benefits – Appeals”

    Here is the specific source content type that I copied (on http://dev.site.com): http://screencast.com/t/bRlT5Caw1

    Here is that content type’s information management policy (on http://dev.site.com): http://screencast.com/t/HFFCxkBJjj

    When I run the command again and specify sourcename, the content type, group and columns copy over but not the information management policy…

    Destination content types AFTER sourcename parameter: http://screencast.com/t/KeiibMeq2

    Specific content type summary AFTER sourcename parameter: http://screencast.com/t/Q1sjeZEfn

    Specific content type information management policy settings: http://screencast.com/t/3HIVmckO

    There is one thing odd about this last screenshot which may be an indicator for you. “Use a site collection policy” is selected but disabled when copied over. We are not using site collection policies but rather defining the policies on the individual content types.

    I hope this information helped. If you want to talk one on one, this account is my email at gmail. Thanks!

  17. as i use the code to copy the content type throughout the site collection,Title column disappers from all the site collection.Is there is any way to restore them

  18. I feel ignorant. I’m trying to install the wsp package and can’t manage to do it. I installed and deployed the solution: the feature was not there. Then registered the dll in the gac: the feature still not there. I copied the xl files into a new directory in features then tried the installfeature and activate feature… I get “unable to cast object of type “microsoft.SharePoint.Administration.SPSolution” to type “Microsoft.SharePoint.Administration.SPFeatureDefinition”

    Do you have a script to install this feature.

    Thanks?

  19. Run the following stsadm commands to deploy the solution:

    stsadm -o addsolution -filename “Lapointe.SharePoint.STSADM.Commands.wsp”
    stsadm -o deploysolution -local -allowgacdeployment -name “Lapointe.SharePoint.STSADM.Commands.wsp”
    stsadm -o execadmsvcjobs

    After running these you should see the all my commands when you do an
    “stsadm -help” command. Nothing goes into the Features folder so you won’t see anything there.

  20. I realized why the policies were not copying. They were defined on the content type. This extension only copies site collection policies.

    Is there a way to overwrite an existing content type? If a change is made to a CT in SiteCollectionA and needs to be replicated to SiteCollectionB, how can I overwrite the existing content type in SiteCollectionB?

  21. Not easily – take a look at the code for the gl-propagatecontenttype command I created – you’d have to adapt it to work with the content type found in another site collection – it’s entirely possible to do you just have to be careful about breaking changes (specifically removal of fields).

  22. Hi,

    I’ve tried moving content types from a site on one server to a site on another server with the help of your stsadm extensions.

    The message I’m getting is “The web app at “http://server2/xxx” could not be found. Verify that you have typed the URL correctly. If the URL should be serving existing content, the system admin may need to add a new request URL mapping to the intended application”

    Any ideas appreciated. Obviously both servers are on the domain and I can browse to sites on both. The old server was used as dev and the new one is live.

    I’ve tried using a site template to copy everything across but as you know the site columns and content types are not copied.

  23. I really like the idea of these extensions and couldn’t wait to give them a try. I might misunderstand what is meant to happen though. The content types are copied but not the site columns which belonged to the content type in the source site. I tried using the list copy extension to get the columns in place first as well in a separate try but that didn’t help.

    Sorry, I’m anonymous right now (Jonathan), but will sign up right away.

  24. Jonathan – I’m not sure why the site columns aren’t being copied -they should be getting copied (at least they were during all my tests that I did). You can try the gl-exportsitecolumns/gl-importsitecolumns commands to move the columns over first.

  25. Ran into an error in AddDocumentTemplate(…) –

    It appears that the line
    SPFile sourceFile = sourceCT.ResourceFolder.Files[sourceCT.DocumentTemplate];

    will throw an exception if the code executes this far and the file doesn’t actually exist.

    This is like a number of the collections in the SharePoint OM – instead of returning null, they throw an Exception.

    Given that the next line in the code checks for this variable to be null, I got around it by declaring the SPFile first, setting it null, and then assigning it that value in a small try-catch block. Not sure if this problem is specific to my environment or if others might encounter it, but I figured I’d pass it along.

  26. It seems that there is a memory leak somewhere. We need to copy over around 1,200 fields and 1,200 content types – while we expected this to take some time we didn’t expect it to use 700M of RAM. It just kept eating up more and more memory as the process went on. We had to eventually kill it. Just wondering if you had similar problems copying lots of fields and content types and if you’d know where to look to plug the leak. It probably wasn’t designed to handle this kind of load…

    Otherwise, thank you again for all of your hard work. Even after finding the leaks you have saved us a ton of time! 🙂

  27. I’ve made a couple of changes which should address the memory leak issue. The updated build will go out today. That being said – why on earth do you have 1200 content types? I think you have a bigger design issue here that needs to be solved.

  28. Gary,
    In our case we are using Content Types for record retention. We have a few hundred record retention policies and a CT for each policy. Would you suggest that we do not use CTs for this?

  29. Gary –
    Essentially, we are in the design stages of a project and the management team needs to have a large number of field-sets available to the user creating a site. We have a organizational hierarchy that everything resides in (Site > Department > Practice Group > Area of Law, etc.) and each level has some fields that inherit down to another level. So the site may have “Matter Name” and the department may have a field called “Department Chair” and then the Area of Law would have all of the fields above it.

    Currently there is a site that holds all of the default content-types and fields and when a new site is created those objects need to be available – and thus, the massive copy operation.

    We have gone over the requirements and are still planning on the same architecture of many levels of content types but are now interested in just bringing over the used content fields as-needed. There will be some extensive synchronization routines between the new site and the default site, but I think that it is feasible. Your code has definitely set us on the right track. Thank you.

  30. Gary,
    I have noticed that if my Source content type contains a Field Type of “User” or “Choice” that these fields were not created in the Target copy of the content type. Are there known limitations with various field types?

  31. Field types shouldn’t matter – I know I’ve tested using Choice fields (can’t remember about user fields) but it should work – I’ll have to dig into it some – thanks for the info.

  32. Thanks for such a great set of tools!

    One thing I have noticed while using the gl-copycontenttypes is that it does not appear to correctly copy the Document Template value when set to the URL of an existing document template.

    All the best, Ryan

  33. Hi Gary, you muyst really love me by now but I have the same error as Pierrick above and am unable to modify any site columns. 🙂

    The object has been updated by another user since it was last fetched. at Microsoft.SharePoint.SPField.UpdateSchemaXmlInWeb(String strXml, Boolean bToggleSealed, Boolean bRichText, String strFmla, List`1 fldTitles, List`1 fids)
    at Microsoft.SharePoint.SPField.UpdateInWeb(Boolean bToggleSealed)
    at Microsoft.SharePoint.SPField.UpdateCore(Boolean bToggleSealed)
    at Microsoft.SharePoint.SPField.Update()
    at Microsoft.SharePoint.SPFieldMultiChoice.Update()
    at Microsoft.SharePoint.ApplicationPages.ManageFieldPage.UpdateFieldWithPropertiesFromXml(SPField fld, String strXml)
    at Microsoft.SharePoint.ApplicationPages.ManageFieldPage.ProcessWeb()
    at Microsoft.SharePoint.ApplicationPages.ManageFieldPage.OnLoad(EventArgs e)
    at System.Web.UI.Control.LoadRecursive()
    at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)

    I found a reference to version info being the cause if it’s any help. If I knew how to change this myself I would. :\

    http://www.sharepointblogs.com/abdrasin/archive/2007/10/31/site-column-update-error-the-object-has-been-updated-by-another-user-since-it-was-last-fetched.aspx

    Many thanks as always.

  34. hello, excellent toll tnx
    i have a problem that after the copy of contenttypes, the contenttypes exists, but all the columes are not included.what can i do?

  35. tnx, very nice. but i have problem after copy of content types i can not see the colums. it is possible to copy also the colums?

  36. Great work! How about upgrading already copied content types? Like changing the type of a column or another setting on your “master” site collection types and then propagating the changes. The code for that would be something similar wouldn’t it? Do you know if anyone has looked into that?

    /Fredrik

  37. Thank you for this command. I was able to deploy the solution but when I tried to use the actual copy command I received an error, “Object Reference is not set to an instance of an object”. I confirmed that I’m the farm administrator. (I logged onto the server as such.) Any other ideas? This would be a huge asset as we’re building our site. ThanX.

  38. I have a problem with content types that i wouldn’t be able to resolve. I have already generated document libraries with files with metadata in them. The lists were created with content type document and list modification. Now i want to use content type for those libraries because it is imposible to manage individualy. Is there a way to change content type that library uses and stil keep meta data of the documents.

    i tried creating new content type with same colums that library has but i couldn’t copy metadata to new content type columns.

  39. Create your new content type and then associate it with the library. You’ll then need to modify the content type for each item to have it point to the new content type – as long as you’re using site columns for the existing content type and the new one then you shouldn’t have to migrate the data – if not then you’ll have to copy the old field values to the new field values.

  40. I was in a similar boat with content types being modified and new columns being added at the list level. I’ve since learnt to lock them down.

    Provided the column names match (internal!) you’ll find that when you add the column to your content type – and then associate that with the list item – the metadata will be present.

    You can do this quickly using Datasheet view.

    Call it a feature, but all custom metadata actually DOES get stored with the document as well. Check custom properties.

    Now if only someone could provide an easy way to REMOVE unwanted content types I would be very happy!

    I’ve turned off versioning, disabled check-out, published major versions, deleted all items – the works! But due to previous major versions using the old CT I’m unable to delete it from the list or site collection.

    One for you, Gary. 🙂

  41. You mentioned that you had created code using reflection that set a Content Type’s ID programatically. Can you post this code? I couldn’t find it in the source code on codeplex…

  42. Here’s the code snippet (you’ll need to pull the relevant utility methods):
    string xml = (string)Utilities.GetFieldValue(newCT, “m_strXmlNonLocalized”);
    xml = xml.Replace(string.Format(“ID=\”{0}\””, newCT.Id), string.Format(“ID=\”{0}\””, sourceCT.Id));
    Utilities.SetFieldValue(newCT, typeof (SPContentType), “m_strXmlNonLocalized”, xml);
    Utilities.SetFieldValue(newCT, typeof(SPContentType), “m_id”, sourceCT.Id);

  43. Hi Gary,

    Can you please put me in to right direction?

    I am struggling to get this right. All i am trying to do is that copying content typr from site A to site B

    stsadm -o gl-copycontenttypes -sourceurl “http://sitea” -targeturl “http://siteb” -sourcename xyz document

    It keep saying that, Command line error. Dont know where I am doing wrong

    Any help would be much appreciated.

  44. Hi Gary,

    I managed to copy content across all site collection. How can make it available to all document libraries?

    Now I can find the content type in Site settings / site content types

  45. Hi Gary,

    Cannot get it to work, all I get is ‘a syntax error’ message,

    stsadm -o gl-copycontenttypes -sourceurl “http://sitea/subs/sita” -targeturl “http://siteb/subs/siteb”

    MOSS2007 win 2008 64bit

    What am I doing wrong ?
    Casey

  46. Hi Gary,

    Ok can copy the CT now, but as with other posts no site columns,

    Even created a new site col and new CT but no joy, any ideas.
    Kerry

    stsadm -o gl-copycontenttypes -sourceurl “http://xxx/sites/aaa” -targeturl “http://yyy/sites/bbb
    /Home” -sourcename “TestCT”

  47. Gary,
    Some More details.
    It seams the content type is copied but has none of the custom site columns.
    I used the export import site columns routine to copy the site cols to the target site, OK
    With a fresh site colection and a fresh content type,columns present (or not) when copied I always get the errors like below.

    Kerry

    A duplicate name “FileLeafRef” was found.
    A duplicate name “ContentType” was found.

  48. Jay – I wasn’t disposing a couple of items such as the Policy objects. Looks like the code in my post was not updated with those changes so I’m going to take a look at that now and see if I can update it.

  49. I have the same issue come up that Pierrick was having while copying content type columns. He says he just had to change the Version attributes and it worked. What version attributes is he talking about and where woudl i change them?

  50. @Anonymous: He’s referring to changes made directly to the SharePoint content database. This is not supported, nor recommended. I have never found version attributes for columns in the DB, only contenttypes.

    I have suffered from this issue every time I try to use the command but Gary does not appear able to replicate it. I have to assume it only affects a minority. Or perhaps others just aren’t trying to modify the CT after copying it to a new collection.

    1. Downside of using a script to rebuild my blog from cached search results – thanks for letting me know – I’ve updated the post so should be good now (hopefully there aren’t any others like this).

  51. I have a need to copy documents and metadata from a SharePoint document library to a file share. Is there a best practice for pulling docs and associated metdata out of SharePoint?

    1. Easiest way is to use PowerShell to iterate through your docs and save them out. As for best practice – not really – you’ll have to figure out how you want to store the metadata. You might also look at third party options (not sure if there’s something available but worth a look).

  52. Hi Gary,

    I have successfully copied the content type, site columns and document template across to another site collection.

    However, the document template this is copied across does not behave correctly. I have inserted the document property, which refers to the site column in the content type. So in the orginal site collection, when I create a new document, the document property gets replaced with the metadata value. But this is not the case for the template copied across to the new site collection.

    Do you know if there is any ID reference to the content type in the document template itself?

  53. Hello Gary,
    Thanks for such a nice utility.Just wanted to check if it is possible to copy the content types scoped at list level i.e. content types are not present in site gallery as users have created the list based out of saved list templates from some other site which had some custom content types. So do you have any suggestion how we can copy content types associated with the list to some other site’s content type gallery.

    Thanks
    Sumit

  54. I am having the following issues :
    1. It is not copying columns (e.g. custom columns)
    2. it is copying new content types but not updating existing one
    Please help.

Leave a Reply

Your email address will not be published. Required fields are marked *

CAPTCHA

*