SharePoint Automation Gary Lapointe – Founding Partner, Aptillon, Inc.

13Sep/080

Listing Event Receivers using STSADM

This post wraps up my event receiver posts.  I just finished documenting the gl-addeventreceiver and gl-deleteeventreceiver commands and this final command would be gl-enumeventreceivers.

This command is the simplest of the three as it's just looping through all the event receivers belonging to the specified target and dumping the results out as XML.  I've tried to structure the XML so that it conforms to the CAML schema therefore allowing the results to be put into a Feature if desired.

   1: /// <summary>
   2: /// Gets the XML.
   3: /// </summary>
   4: /// <param name="url">The URL.</param>
   5: /// <param name="contentTypeName">Name of the content type.</param>
   6: /// <param name="target">The target.</param>
   7: /// <returns></returns>
   8: public static string GetXml(string url, string contentTypeName, TargetEnum target)
   9: {
  10:     using (SPSite site = new SPSite(url))
  11:     using (SPWeb web = site.OpenWeb())
  12:     {
  13:         SPEventReceiverDefinitionCollection eventReceivers;
  14:         if (target == TargetEnum.List)
  15:         {
  16:             SPList list = Utilities.GetListFromViewUrl(web, url);
  17:  
  18:             if (list == null)
  19:             {
  20:                 throw new Exception("List not found.");
  21:             }
  22:             eventReceivers = list.EventReceivers;
  23:         }
  24:         else if (target == TargetEnum.Site)
  25:             eventReceivers = web.EventReceivers;
  26:         else
  27:         {
  28:             SPContentType contentType = null;
  29:             try
  30:             {
  31:                 contentType = web.AvailableContentTypes[contentTypeName];
  32:             }
  33:             catch (ArgumentException)
  34:             {
  35:             }
  36:             if (contentType == null)
  37:                 throw new SPSyntaxException("The specified content type could not be found.");
  38:  
  39:             eventReceivers = contentType.EventReceivers;
  40:         }
  41:         StringBuilder sb = new StringBuilder();
  42:         XmlTextWriter xmlWriter = new XmlTextWriter(new StringWriter(sb));
  43:         xmlWriter.Formatting = Formatting.Indented;
  44:  
  45:         xmlWriter.WriteStartElement("Receivers");
  46:         foreach (SPEventReceiverDefinition erd in eventReceivers)
  47:         {
  48:             xmlWriter.WriteStartElement("Receiver");
  49:  
  50:             xmlWriter.WriteElementString("Name", erd.Name);
  51:             xmlWriter.WriteElementString("Type", erd.Type.ToString());
  52:             xmlWriter.WriteElementString("Assembly", erd.Assembly);
  53:             xmlWriter.WriteElementString("Class", erd.Class);
  54:             xmlWriter.WriteElementString("SequenceNumber", erd.SequenceNumber.ToString());
  55:  
  56:             xmlWriter.WriteEndElement();
  57:         }
  58:         xmlWriter.WriteEndElement();
  59:  
  60:         return sb.ToString();
  61:     }
  62: }

The help for the command is shown below:

C:\>stsadm -help gl-enumeventreceivers

stsadm -o gl-enumeventreceivers


Enumerates all event receivers associated with the specified target object and outputs the list of receivers as XML.

Parameters:
        -url <web or list URL>
        -target <site | list | contenttype>
        [-contenttype <content type name if target is ContentType>]

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-enumeventreceivers WSS v3, MOSS 2007 Released: 9/13/2008

Parameter Name Short Form Required Description Example Usage
url   Yes The URL to the web or list to display the event receivers. -url http://portal/pages
target   No The target object to display the event receivers from.  Must be either "list", "site", or "contenttype".  If omitted defaults to "list". -target list
contenttype ct No, unless target is contenttype The name of the content type to to get the event receivers from. -contenttype "Page"

-ct "Page"

The following is an example of how to display all the event receivers associated with a pages library:

stsadm -o gl-enumeventreceivers -url http://portal/pages -target list

The following is an example output from running the above command:

<Receivers>
  <Receiver>
    <Name />
    <Type>ItemUpdating</Type>
    <Assembly>Lapointe.WebPartPageHistory, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3216c23aba16db08</Assembly>
    <Class>Lapointe.WebPartPageHistory.ListEventReceivers.SourceListEventReceiver</Class>
    <SequenceNumber>10000</SequenceNumber>
  </Receiver>
  <Receiver>
    <Name>PagesListEventReceiverName</Name>
    <Type>ItemDeleting</Type>
    <Assembly>Microsoft.SharePoint.Publishing, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c</Assembly>
    <Class>Microsoft.SharePoint.Publishing.PagesListCPVEventReceiver</Class>
    <SequenceNumber>1005</SequenceNumber>
  </Receiver>
  <Receiver>
    <Name>PagesListEventReceiverName</Name>
    <Type>ItemAdded</Type>
    <Assembly>Microsoft.SharePoint.Publishing, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c</Assembly>
    <Class>Microsoft.SharePoint.Publishing.PagesListCPVEventReceiver</Class>
    <SequenceNumber>1004</SequenceNumber>
  </Receiver>
  <Receiver>
    <Name>PagesListEventReceiverName</Name>
    <Type>ItemUpdated</Type>
    <Assembly>Microsoft.SharePoint.Publishing, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c</Assembly>
    <Class>Microsoft.SharePoint.Publishing.PagesListCPVEventReceiver</Class>
    <SequenceNumber>1002</SequenceNumber>
  </Receiver>
  <Receiver>
    <Name />
    <Type>ItemUpdated</Type>
    <Assembly>Lapointe.WebPartPageHistory, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3216c23aba16db08</Assembly>
    <Class>Lapointe.WebPartPageHistory.ListEventReceivers.SourceListEventReceiver</Class>
    <SequenceNumber>10000</SequenceNumber>
  </Receiver>
  <Receiver>
    <Name>PagesListEventReceiverName</Name>
    <Type>ItemDeleted</Type>
    <Assembly>Microsoft.SharePoint.Publishing, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c</Assembly>
    <Class>Microsoft.SharePoint.Publishing.PagesListCPVEventReceiver</Class>
    <SequenceNumber>1006</SequenceNumber>
  </Receiver>
  <Receiver>
    <Name>PagesListEventReceiverName</Name>
    <Type>ItemCheckedIn</Type>
    <Assembly>Microsoft.SharePoint.Publishing, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c</Assembly>
    <Class>Microsoft.SharePoint.Publishing.PagesListCPVEventReceiver</Class>
    <SequenceNumber>1003</SequenceNumber>
  </Receiver>
  <Receiver>
    <Name />
    <Type>ItemCheckedIn</Type>
    <Assembly>Lapointe.WebPartPageHistory, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3216c23aba16db08</Assembly>
    <Class>Lapointe.WebPartPageHistory.ListEventReceivers.SourceListEventReceiver</Class>
    <SequenceNumber>10000</SequenceNumber>
  </Receiver>
</Receivers>

13Sep/080

Deleting an Event Receiver using STSADM

I had some time so I decided to add the counterparts to the gl-addeventreceiver command that I just published.  I created two new commands, gl-deleteeventreceiver and gl-enumeventreceivers.  This post will cover the delete command and I'll go over the enum command in my next post.

From a code perspective the delete command is very similar to the add command.  The main difference (aside from the obvious call to Delete instead of Add) is that I've made the assembly, class, and type parameters optional (though at least one must be provided) so the code to get the event receiver to delete needed to be expanded so that it now evaluates each receiver and checks to see if a valid match is found.  Once all the receivers have been identified then we simply delete them all.

   1: /// <summary>
   2: /// Deletes an event receiver from the specified target
   3: /// </summary>
   4: /// <param name="url">The URL.</param>
   5: /// <param name="contentTypeName">Name of the content type.</param>
   6: /// <param name="target">The target.</param>
   7: /// <param name="assembly">The assembly.</param>
   8: /// <param name="className">Name of the class.</param>
   9: /// <param name="type">The type.</param>
  10: /// <param name="typeNotProvided">if set to <c>true</c> [type not provided].</param>
  11: public static void Delete(string url, string contentTypeName, TargetEnum target, string assembly, string className, SPEventReceiverType type, bool typeNotProvided)
  12: {
  13:     using (SPSite site = new SPSite(url))
  14:     using (SPWeb web = site.OpenWeb())
  15:     {
  16:         SPEventReceiverDefinitionCollection eventReceivers;
  17:         if (target == TargetEnum.List)
  18:         {
  19:             SPList list = Utilities.GetListFromViewUrl(web, url);
  20:  
  21:             if (list == null)
  22:             {
  23:                 throw new Exception("List not found.");
  24:             }
  25:             eventReceivers = list.EventReceivers;
  26:         }
  27:         else if (target == TargetEnum.Site)
  28:             eventReceivers = web.EventReceivers;
  29:         else
  30:         {
  31:             SPContentType contentType = null;
  32:             try
  33:             {
  34:                 contentType = web.AvailableContentTypes[contentTypeName];
  35:             }
  36:             catch (ArgumentException)
  37:             {
  38:             }
  39:             if (contentType == null)
  40:                 throw new SPSyntaxException("The specified content type could not be found.");
  41:  
  42:             eventReceivers = contentType.EventReceivers;
  43:         }
  44:         Delete(eventReceivers, type, assembly, className, typeNotProvided);
  45:     }
  46: }
  47:  
  48: /// <summary>
  49: /// Deletes the event receiver matching the provided values from the passed in collection.
  50: /// </summary>
  51: /// <param name="eventReceivers">The event receivers.</param>
  52: /// <param name="eventReceiverType">Type of the event receiver.</param>
  53: /// <param name="assembly">The assembly.</param>
  54: /// <param name="className">Name of the class.</param>
  55: /// <param name="typeNotProvided">if set to <c>true</c> [type not provided].</param>
  56: private static void Delete(SPEventReceiverDefinitionCollection eventReceivers, SPEventReceiverType eventReceiverType, string assembly, string className, bool typeNotProvided)
  57: {
  58:     List<SPEventReceiverDefinition> toDelete = new List<SPEventReceiverDefinition>();
  59:     foreach (SPEventReceiverDefinition erd in eventReceivers)
  60:     {
  61:         if ((erd.Assembly == assembly || assembly == null) && 
  62:             (erd.Class == className || className == null) && 
  63:             ((erd.Type == eventReceiverType && !typeNotProvided) || typeNotProvided))
  64:         {
  65:             toDelete.Add(erd);
  66:         }
  67:     }
  68:     foreach (SPEventReceiverDefinition erd in toDelete)
  69:     {
  70:         Log("Deleting the {0} event for class {1} in assembly {2}.", erd.Type.ToString(), erd.Class,
  71:             erd.Assembly);
  72:  
  73:         erd.Delete();
  74:     }
  75: }

The help for the command is shown below:

C:\>stsadm -help gl-deleteeventreceiver

stsadm -o gl-deleteeventreceiver


Deletes an event receiver from a list, web, or content type.

Parameters:
        -url <web or list URL>
        -target <site | list | contenttype>
        [-assembly <assembly>]
        [-class <class name>]
        [-type <itemadding | itemupdating | itemdeleting | itemcheckingin | itemcheckingout | itemuncheckingout | itemattachmentadding | itemattachmentdeleting | itemfilemoving | fieldadding | fieldupdating | fielddeleting | sitedeleting | webdeleting | webmoving | itemadded | itemupdated | itemdeleted | itemcheckedin | itemcheckedout | itemuncheckedout | itemattachmentadded | itemattachmentdeleted | itemfilemoved | itemfileconverted | fieldadded | fieldupdated | fielddeleted | sitedeleted | webdeleted | webmoved | emailreceived | contextevent | invalidreceiver>]
        [-contenttype <content type name if target is ContentType>]

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-deleteeventreceiver WSS v3, MOSS 2007 Released: 9/13/2008

Parameter Name Short Form Required Description Example Usage
url   Yes The URL to the web or list to remove the event receiver from. -url http://portal/pages
assembly a No The fully qualified assembly name containing the event receiver class to delete from the target. -assembly "Lapointe.WebPartPageHistory, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3216c23aba16db08"

-a "Lapointe.WebPartPageHistory, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3216c23aba16db08"
class c No The fully qualified class name of the event receiver to delete from the target. -class Lapointe.WebPartPageHistory.ListEventReceivers.SourceListEventReceiver

-c Lapointe.WebPartPageHistory.ListEventReceivers.SourceListEventReceiver
type   No The event type to delete. -type itemupdated
target   No The target type of which the receiver will be deleted.  Must be either "list", "site", or "contenttype".  If omitted defaults to "list". -target list
contenttype ct No, unless target is contenttype The name of the content type to remove the event receiver from if the target is contenttype. -contenttype "Page"

-ct "Page"

The following is an example of how to delete all the event receivers belonging to the web part page history assembly:

stsadm -o gl-deleteeventreceiver -url http://portal/pages -assembly "Lapointe.WebPartPageHistory, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3216c23aba16db08" -target list

Note that you should be particularly careful when deleting event receivers and not specifying the assembly and class attributes as you could inadvertently delete event receivers that are critical to the normal functioning of the list.

13Sep/081

Adding Event Receivers using STSADM

A few months ago I created a CodePlex project that allows you to configure a library to allow the storing of web part page history.  The project at its core used three item event receivers to listen to certain key events and either backup the page or restore the page based on the event.  Recently I was thinking about what might be the best way to allow people to script the settings that enable the storing of page history.  I decided that I would start off by providing a simple STSADM command that would allow you to script the adding of event receivers to a list (fortunately making it work for webs and content types didn't really require any additional work so I covered those as well).  The new command I created is called gl-addeventreceiver.

I'll probably eventually come up with something else but this at least is a start - I primarily don't like it because it requires that people know how the web part history project works so I'll probably create a command specific to enabling the history so that it abstracts out the technical details which could always change (there just hasn't been a whole lot of interest in the project so I've not really spent much time on it).  In the meantime this command is a nice generic tool that could be used by developers and administrators for various reasons (I'll probably create an enum and remove command to supplement this one - just haven't gotten around to it yet).

Adding an event receiver via code is really very simple - just a matter of calling the Add method of the SPEventReceiverDefinitionCollection object which you can get via the EventReceivers property of either the SPList, SPContentType, or SPWeb object.  To get an existing event receiver (to check for existence as we don't want to add twice) we have to loop through the collection comparing the assembly name, class name, and event receiver type.

   1: /// <summary>
   2: /// Adds an event receiver to the specified target
   3: /// </summary>
   4: /// <param name="url">The URL.</param>
   5: /// <param name="contentTypeName">Name of the content type.</param>
   6: /// <param name="target">The target.</param>
   7: /// <param name="assembly">The assembly.</param>
   8: /// <param name="className">Name of the class.</param>
   9: /// <param name="type">The type.</param>
  10: /// <param name="sequence">The sequence.</param>
  11: /// <param name="name">The name.</param>
  12: public static void Add(string url, string contentTypeName, TargetEnum target, string assembly, string className, SPEventReceiverType type, int sequence, string name)
  13: {
  14:     using (SPSite site = new SPSite(url))
  15:     using (SPWeb web = site.OpenWeb())
  16:     {
  17:         SPEventReceiverDefinitionCollection eventReceivers;
  18:         if (target == TargetEnum.List)
  19:         {
  20:             SPList list = Utilities.GetListFromViewUrl(web, url);
  21:  
  22:             if (list == null)
  23:             {
  24:                 throw new Exception("List not found.");
  25:             }
  26:             eventReceivers = list.EventReceivers;
  27:         }
  28:         else if (target == TargetEnum.Site)
  29:             eventReceivers = web.EventReceivers;
  30:         else
  31:         {
  32:             SPContentType contentType = null;
  33:             try
  34:             {
  35:                 contentType = web.AvailableContentTypes[contentTypeName];
  36:             }
  37:             catch (ArgumentException)
  38:             {
  39:             }
  40:             if (contentType == null)
  41:                 throw new SPSyntaxException("The specified content type could not be found.");
  42:  
  43:             eventReceivers = contentType.EventReceivers;
  44:         }
  45:         SPEventReceiverDefinition def = Add(eventReceivers, type, assembly, className, name);
  46:         if (sequence >= 0)
  47:         {
  48:             def.SequenceNumber = sequence;
  49:             def.Update();
  50:         }
  51:     }
  52: }
  53:  
  54: /// <summary>
  55: /// Adds an event receiver to a the specified event receiver definition collection.
  56: /// </summary>
  57: /// <param name="eventReceivers">The event receivers.</param>
  58: /// <param name="eventReceiverType">Type of the event receiver.</param>
  59: /// <param name="assembly">The assembly.</param>
  60: /// <param name="className">Name of the class.</param>
  61: /// <param name="name">The name.</param>
  62: /// <returns></returns>
  63: private static SPEventReceiverDefinition Add(SPEventReceiverDefinitionCollection eventReceivers, SPEventReceiverType eventReceiverType, string assembly, string className, string name)
  64: {
  65:     if (GetEventReceiver(eventReceivers, eventReceiverType, assembly, className) == null)
  66:     {
  67:         eventReceivers.Add(eventReceiverType, assembly, className);
  68:         SPEventReceiverDefinition def = GetEventReceiver(eventReceivers, eventReceiverType, assembly, className);
  69:         if (!string.IsNullOrEmpty(name))
  70:         {
  71:             def.Name = name;
  72:             def.Update();
  73:         }
  74:         return def;
  75:     }
  76:     return null;
  77: }
  78:  
  79: /// <summary>
  80: /// Gets the event receiver.
  81: /// </summary>
  82: /// <param name="eventReceivers">The event receivers.</param>
  83: /// <param name="eventReceiverType">Type of the event receiver.</param>
  84: /// <param name="assembly">The assembly.</param>
  85: /// <param name="className">Name of the class.</param>
  86: /// <returns></returns>
  87: private static SPEventReceiverDefinition GetEventReceiver(SPEventReceiverDefinitionCollection eventReceivers, SPEventReceiverType eventReceiverType, string assembly, string className)
  88: {
  89:     foreach (SPEventReceiverDefinition erd in eventReceivers)
  90:     {
  91:         if (erd.Assembly == assembly && erd.Class == className && erd.Type == eventReceiverType)
  92:         {
  93:             return erd;
  94:         }
  95:     }
  96:     return null;
  97: }

The help for the command is shown below:

C:\>stsadm -help gl-addeventreceiver

stsadm -o gl-addeventreceiver


Adds an event receiver to a list, web, or content type.

Parameters:
        -url <web or list URL>
        -assembly <assembly>
        -class <class name>
        -type <itemadding | itemupdating | itemdeleting | itemcheckingin | itemcheckingout | itemuncheckingout | itemattachmentadding | itemattachmentdeleting | itemfilemoving | fieldadding | fieldupdating | fielddeleting | sitedeleting | webdeleting | webmoving | itemadded | itemupdated | itemdeleted | itemcheckedin | itemcheckedout | itemuncheckedout | itemattachmentadded | itemattachmentdeleted | itemfilemoved | itemfileconverted | fieldadded | fieldupdated | fielddeleted | sitedeleted | webdeleted | webmoved | emailreceived | contextevent | invalidreceiver>
        -target <site | list | contenttype>
        [-contenttype <content type name if target is ContentType>]
        [-sequence <sequence number>]
        [-name <the name to give to the event receiver>]

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-addeventreceiver WSS v3, MOSS 2007 Released: 9/13/2008

Parameter Name Short Form Required Description Example Usage
url   Yes The URL to the web or list to add the event receiver to. -url http://portal/pages
assembly a Yes The fully qualified assembly name containing the event receiver class to add. -assembly "Lapointe.WebPartPageHistory, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3216c23aba16db08"

-a "Lapointe.WebPartPageHistory, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3216c23aba16db08"
class c Yes The fully qualified class name of the event receiver to add. -class Lapointe.WebPartPageHistory.ListEventReceivers.SourceListEventReceiver

-c Lapointe.WebPartPageHistory.ListEventReceivers.SourceListEventReceiver
type   Yes The event type to add.  The command does not validate that you are adding the correct type for the specified target or that the specified class contains handlers for the type specified. -type itemupdated
target   No The target type to add the event receiver to.  Must be either "list", "site", or "contenttype".  If omitted defaults to "list". -target list
contenttype ct No, unless target is contenttype The name of the content type to add the event receiver to if the target is contenttype. -contenttype "Page"

-ct "Page"
sequence seq No The sequence number specifies the order of execution of the event receiver. -sequence 1000

-seq 1000
name n No The name to give to the event receiver.  The name has no significance but can be useful when later listing the event receivers. -name "Handle Saving of Page History"

-n "Handle Saving of Page History"

The following is an example of how to add three event receivers to a pages library - the three commands illustrated below constitute the required event receivers that must be enabled to turn on the web part page history:

stsadm -o gl-addeventreceiver -url http://portal/pages -assembly "Lapointe.WebPartPageHistory, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3216c23aba16db08" -class "Lapointe.WebPartPageHistory.ListEventReceivers.SourceListEventReceiver" -target list -sequence 1000 -type itemcheckedin

stsadm -o gl-addeventreceiver -url http://portal/pages -assembly "Lapointe.WebPartPageHistory, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3216c23aba16db08" -class "Lapointe.WebPartPageHistory.ListEventReceivers.SourceListEventReceiver" -target list -sequence 1000 -type itemupdating

stsadm -o gl-addeventreceiver -url http://portal/pages -assembly "Lapointe.WebPartPageHistory, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3216c23aba16db08" -class "Lapointe.WebPartPageHistory.ListEventReceivers.SourceListEventReceiver" -target list -sequence 1000 -type itemupdated

7Aug/0810

Setting List Content Types using STSADM

Sometimes when I'm working on a new Feature I find it easier to take certain snippets that I need to test and pull them out into custom STSADM commands.  This enables me to quickly and easily test the core code and without having to go through all the deployment steps.  It was for this purpose that I originally created this new command, gl-setlistcontenttypes.  I did find later that the command ended up being real useful in a scripted deployment that I'm working on so I managed to knock out a well tested new command and some reusable code for a Feature that I'm working on.

The code is really simple - I've got a primary method, SetContentTypes, which takes in 3 string arrays containing the content types to remove and add as well as another array for the new button order.  There's also a couple of helper methods that are called.  I then just loop through each array and call the necessary methods (Add or Delete) on the ContentTypes property which returns an SPContentTypeCollection object.  The only tricky part is setting the order of the content types - this is done via RootFolder property.  There's a UniqueContentTypeOrder property that must be set to an IList<SPContentType> object.  Manipulating the IList object directly will not work as it does not set the dirty flag and therefore the Update() method will appear to do nothing so you must create a new object and reset the property:

   1: /// <summary>
   2: /// Sets the content types.
   3: /// </summary>
   4: /// <param name="web">The web.</param>
   5: /// <param name="list">The list.</param>
   6: /// <param name="contentTypesToAdd">The content types to add.</param>
   7: /// <param name="contentTypesToDelete">The content types to delete.</param>
   8: /// <param name="contentTypeOrder">The content type order.</param>
   9: public static void SetContentTypes(SPWeb web, SPList list, string[] contentTypesToAdd, string[] contentTypesToDelete, string[] contentTypeOrder)
  10: {
  11:     foreach (string ct in contentTypesToAdd)
  12:     {
  13:         AddContentTypeToList(web, list, ct.Trim());
  14:     }
  15:     foreach (string ct in contentTypesToDelete)
  16:     {
  17:         foreach (SPContentType contentType in list.ContentTypes)
  18:         {
  19:             if (contentType.Name.ToLowerInvariant() == ct.Trim().ToLowerInvariant())
  20:             {
  21:                 list.ContentTypes.Delete(contentType.Id);
  22:                 break;
  23:             }
  24:         }
  25:     }
  26:     ChangeContentTypeOrder(list, contentTypeOrder);
  27: }
  28:  
  29: /// <summary>
  30: /// Changes the content type order.
  31: /// </summary>
  32: /// <param name="list">The list.</param>
  33: /// <param name="contentTypes">The content types.</param>
  34: public static void ChangeContentTypeOrder(SPList list, string[] contentTypes)
  35: {
  36:     if (contentTypes.Length == 0)
  37:         return;
  38:  
  39:     IList<SPContentType> contentTypeOrder = new List<SPContentType>();
  40:     foreach (string contentTypeName in contentTypes)
  41:     {
  42:         SPContentType contentType = null;
  43:         try
  44:         {
  45:             contentType = list.ContentTypes[contentTypeName.Trim()];
  46:         }
  47:         catch (ArgumentException)
  48:         {
  49:             Log("WARNING: Unable to set content type order for '{0}'.  Content type was not found.",
  50:                 contentTypeName);
  51:         }
  52:         if (contentType != null)
  53:             contentTypeOrder.Add(contentType);
  54:         else
  55:             Log("WARNING: Unable to set content type order for '{0}'.  Content type was not found.",
  56:                 contentTypeName);
  57:     }
  58:     list.RootFolder.UniqueContentTypeOrder = contentTypeOrder;
  59:     list.RootFolder.Update();
  60:  
  61: }
  62:  
  63:  
  64: /// <summary>
  65: /// Adds the content type to list.
  66: /// </summary>
  67: /// <param name="web">The web.</param>
  68: /// <param name="list">The list.</param>
  69: /// <param name="contentTypeName">Name of the content type.</param>
  70: /// <returns></returns>
  71: public static SPContentType AddContentTypeToList(SPWeb web, SPList list, string contentTypeName)
  72: {
  73:     SPContentType contentType = null;
  74:     try
  75:     {
  76:         contentType = list.ContentTypes[contentTypeName];
  77:     }
  78:     catch (ArgumentException)
  79:     { }
  80:     if (contentType == null)
  81:     {
  82:         try
  83:         {
  84:             // Get the content type from the web and add to the list.
  85:             contentType = web.ContentTypes[contentTypeName];
  86:         }
  87:         catch (ArgumentException)
  88:         {
  89:         }
  90:         if (contentType == null)
  91:             throw new SPException(string.Format("Unable to find content type '{0}'", contentTypeName));
  92:  
  93:         return list.ContentTypes.Add(contentType);
  94:     }
  95:     return null;
  96: }

The help for the command is shown below:

C:\>stsadm -help gl-setlistcontenttypes

stsadm -o gl-setlistcontenttypes


Adds or removes content typews associated with the given list.

Parameters:
        -url <list view url>
        [-add <comma separated list of content type names to add to the list>]
        [-remove <comma separated list of content type names to remove from the list>]
        [-order <new button order, comma separated, first will be the default>]

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-setlistcontenttypes WSS v3, MOSS 2007 8/6/2008

Parameter Name Short Form Required Description Example Usage
url   Yes The URL to the list of which the actions will be performed -url http://portal/Lists/Events
add a No A comma separated list of content type names to add to the list.  It's recommended to wrap the value in quotes if more than one. -add "Holiday,Vacation,Company Event"

-a "Holiday,Vacation,Company Event"
remove r No A comma separated list of content type names to remove from the list.  It's recommended to wrap the value in quotes if more than one. -remove Event

-r Event
order   No A comma separated list of content type names.  Sets the order of the items in the "New" button of the list.  The first item will be the default content type. -order "Vacation,Holiday,Company Event"

The following is an example of how to set the content types for a list:

stsadm -o gl-setlistcontenttypes -url http://portal/Lists/Events -add "Holiday,Vacation,Company Events" -remove "Event" -order "Vacation,Holiday,Company Event"

1May/0852

Propagate Content Type Changes

This is something that I put together a while ago but I'm only just now getting to the point where I can document it.  I was looking for a solution to a common problem of propagating changes to content types deployed via a Feature and I came across a post by Søren Nielsen.  Søren created a custom stsadm command which handles pushing down the content type changes.  I didn't want to re-invent the wheel but I needed the ability to call the code in different ways and I wanted to try my hand at using the SPContentTypeUsages class so I decided to use what he created and just refactor it to meet my goals.  Søren does a great job at explaining the problem and what the code is doing so I won't reiterate it here.  My modified version of the code can be seen below:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Collections.Specialized;
   4: using System.Diagnostics;
   5: using System.Text;
   6: using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
   7: using Lapointe.SharePoint.STSADM.Commands.SPValidators;
   8: using Microsoft.SharePoint;
   9:  
  10: namespace Lapointe.SharePoint.STSADM.Commands.ContentTypes
  11: {
  12:     /// <summary>
  13:     /// This code was derived from Søren Nielsen's code that he provides on his blog: 
  14:     /// http://soerennielsen.wordpress.com/2007/09/11/propagate-site-content-types-to-list-content-types/
  15:     /// </summary>
  16:     public class PropagateContentType : SPOperation
  17:     {
  18:         /// <summary>
  19:         /// Initializes a new instance of the <see cref="PropagateContentType"/> class.
  20:         /// </summary>
  21:         public PropagateContentType()
  22:         {
  23:             SPParamCollection parameters = new SPParamCollection();
  24:             parameters.Add(new SPParam("url", "url", true, null, new SPNonEmptyValidator()));
  25:             parameters.Add(new SPParam("contenttype", "ct", true, null, new SPNonEmptyValidator()));
  26:             parameters.Add(new SPParam("verbose", "v"));
  27:             parameters.Add(new SPParam("updatefields", "uf"));
  28:             parameters.Add(new SPParam("removefields", "rf"));
  29:  
  30:             StringBuilder sb = new StringBuilder();
  31:             sb.Append("\r\n\r\nPropagates a site scoped content type to list scoped instances of that content type.\r\n\r\nParameters:");
  32:             sb.Append("\r\n\t-url <site collection url>");
  33:             sb.Append("\r\n\t-contenttype <content type name>");
  34:             sb.Append("\r\n\t[-verbose]");
  35:             sb.Append("\r\n\t[-updatefields]");
  36:             sb.Append("\r\n\t[-removefields]");
  37:  
  38:             Init(parameters, sb.ToString());
  39:         }
  40:  
  41:         /// <summary>
  42:         /// Gets the help message.
  43:         /// </summary>
  44:         /// <param name="command">The command.</param>
  45:         /// <returns></returns>
  46:         public override string GetHelpMessage(string command)
  47:         {
  48:             return HelpMessage;
  49:         }
  50:  
  51:         /// <summary>
  52:         /// Runs the specified command.
  53:         /// </summary>
  54:         /// <param name="command">The command.</param>
  55:         /// <param name="keyValues">The key values.</param>
  56:         /// <param name="output">The output.</param>
  57:         /// <returns></returns>
  58:         public override int Execute(string command, StringDictionary keyValues, out string output)
  59:         {
  60:             output = string.Empty;
  61:             
  62:             using (SPSite site = new SPSite(Params["url"].Value.TrimEnd('/')))
  63:             {
  64:                 Process(site, Params["contenttype"].Value, Params["verbose"].UserTypedIn, Params["updatefields"].UserTypedIn,
  65:                     Params["removefields"].UserTypedIn);
  66:             }
  67:             return OUTPUT_SUCCESS;
  68:         }
  69:  
  70:         /// <summary>
  71:         /// Processes the content type.
  72:         /// </summary>
  73:         /// <param name="site">The site.</param>
  74:         /// <param name="contentTypeName">Name of the content type.</param>
  75:         /// <param name="verbose">if set to <c>true</c> [verbose].</param>
  76:         /// <param name="updateFields">if set to <c>true</c> [update fields].</param>
  77:         /// <param name="removeFields">if set to <c>true</c> [remove fields].</param>
  78:         public void Process(SPSite site, string contentTypeName, bool verbose, bool updateFields, bool removeFields)
  79:         {
  80:             Verbose = verbose;
  81:             try
  82:             {
  83:                 Log("Pushing content type changes to lists for '" + contentTypeName + "'");
  84:                 // get the site collection specified
  85:                 using (SPWeb rootWeb = site.RootWeb)
  86:                 {
  87:                     //Get the source site content type
  88:                     SPContentType sourceCT = rootWeb.AvailableContentTypes[contentTypeName];
  89:                     if (sourceCT == null)
  90:                     {
  91:                         throw new ArgumentException("Unable to find Content Type named \"" + contentTypeName + "\"");
  92:                     }
  93:  
  94:                     IList<SPContentTypeUsage> ctUsageList = SPContentTypeUsage.GetUsages(sourceCT);
  95:                     foreach (SPContentTypeUsage ctu in ctUsageList)
  96:                     {
  97:                         if (!ctu.IsUrlToList)
  98:                             continue;
  99:  
 100:                         using (SPWeb web = site.OpenWeb(ctu.Url))
 101:                         {
 102:  
 103:                             SPList list = web.GetList(ctu.Url);
 104:                             SPContentType listCT = list.ContentTypes[ctu.Id];
 105:                             ProcessContentType(list, sourceCT, listCT, updateFields, removeFields);
 106:                         }
 107:                     }
 108:                 }
 109:                 return;
 110:             }
 111:             catch (Exception ex)
 112:             {
 113:                 Log("Unhandled error occured: " + ex.Message, EventLogEntryType.Error);
 114:                 throw;
 115:             }
 116:             finally
 117:             {
 118:                 Log("Finished pushing content type changes to lists for '" + contentTypeName + "'");
 119:             }
 120:         }
 121:  
 122:         /// <summary>
 123:         /// Processes the content type.
 124:         /// </summary>
 125:         /// <param name="list">The list.</param>
 126:         /// <param name="sourceCT">The source CT.</param>
 127:         /// <param name="listCT">The list CT.</param>
 128:         /// <param name="updateFields">if set to <c>true</c> [update fields].</param>
 129:         /// <param name="removeFields">if set to <c>true</c> [remove fields].</param>
 130:         private static void ProcessContentType(SPList list, SPContentType sourceCT, SPContentType listCT, bool updateFields, bool removeFields)
 131:         {
 132:             if (listCT == null)
 133:                 return;
 134:  
 135:             Log("Processing content type on list:" + list);
 136:  
 137:             if (updateFields)
 138:             {
 139:                 UpdateListFields(list, listCT, sourceCT);
 140:             }
 141:  
 142:             //Find/add the fields to add
 143:             foreach (SPFieldLink sourceFieldLink in sourceCT.FieldLinks)
 144:             {
 145:                 if (!FieldExist(sourceCT, sourceFieldLink))
 146:                 {
 147:                     Log(
 148:                       "Failed to add field "
 149:                       + sourceFieldLink.DisplayName + " on list "
 150:                       + list.ParentWebUrl + "/" + list.Title
 151:                       + " field does not exist (in .Fields[]) on "
 152:                       + "source content type", EventLogEntryType.Warning);
 153:                 }
 154:                 else
 155:                 {
 156:                     if (!FieldExist(listCT, sourceFieldLink))
 157:                     {
 158:                         //Perform double update, just to be safe
 159:                         // (but slow)
 160:                         Log("Adding field \""
 161:                            + sourceFieldLink.DisplayName
 162:                            + "\" to contenttype on "
 163:                            + list.ParentWebUrl + "/" + list.Title,
 164:                              EventLogEntryType.Information);
 165:                         if (listCT.FieldLinks[sourceFieldLink.Id] != null)
 166:                         {
 167:                             listCT.FieldLinks.Delete(sourceFieldLink.Id);
 168:                             listCT.Update();
 169:                         }
 170:                         listCT.FieldLinks.Add(new SPFieldLink(sourceCT.Fields[sourceFieldLink.Id]));
 171:                         listCT.Update();
 172:                     }
 173:                 }
 174:             }
 175:  
 176:             if (removeFields)
 177:             {
 178:                 //Find the fields to delete
 179:                 //WARNING: this part of the code has not been
 180:                 // adequately tested (though what could go wrong?  … :) 
 181:  
 182:                 //Copy collection to avoid modifying enumeration as we go through it
 183:                 List<SPFieldLink> listFieldLinks = new List<SPFieldLink>();
 184:                 foreach (SPFieldLink listFieldLink in listCT.FieldLinks)
 185:                 {
 186:                     listFieldLinks.Add(listFieldLink);
 187:                 }
 188:  
 189:                 foreach (SPFieldLink listFieldLink in listFieldLinks)
 190:                 {
 191:                     if (!FieldExist(sourceCT, listFieldLink))
 192:                     {
 193:                         Log("Removing field \""
 194:                            + listFieldLink.DisplayName
 195:                            + "\" from contenttype on :"
 196:                            + list.ParentWebUrl + "/"
 197:                            + list.Title, EventLogEntryType.Information);
 198:                         listCT.FieldLinks.Delete(listFieldLink.Id);
 199:                         listCT.Update();
 200:                     }
 201:                 }
 202:             }
 203:         }
 204:  
 205:         /// <summary>
 206:         /// Updates the fields of the list content type (listCT) with the
 207:         /// fields found on the source content type (courceCT).
 208:         /// </summary>
 209:         /// <param name="list">The list.</param>
 210:         /// <param name="listCT">The list CT.</param>
 211:         /// <param name="sourceCT">The source CT.</param>
 212:         private static void UpdateListFields(SPList list, SPContentType listCT, SPContentType sourceCT)
 213:         {
 214:             Log("Starting to update fields ", EventLogEntryType.Information);
 215:             foreach (SPFieldLink sourceFieldLink in sourceCT.FieldLinks)
 216:             {
 217:                 //has the field changed? If not, continue.
 218:                 if (listCT.FieldLinks[sourceFieldLink.Id] != null
 219:                      && listCT.FieldLinks[sourceFieldLink.Id].SchemaXml
 220:                         == sourceFieldLink.SchemaXml)
 221:                 {
 222:                     Log("Doing nothing to field \"" + sourceFieldLink.Name
 223:                         + "\" from contenttype on :" + list.ParentWebUrl + "/"
 224:                         + list.Title, EventLogEntryType.Information);
 225:                     continue;
 226:                 }
 227:                 if (!FieldExist(sourceCT, sourceFieldLink))
 228:                 {
 229:                     Log(
 230:                       "Doing nothing to field: " + sourceFieldLink.DisplayName
 231:                        + " on list " + list.ParentWebUrl
 232:                        + "/" + list.Title + " field does not exist (in .Fields[])"
 233:                        + " on source content type", EventLogEntryType.Information);
 234:                     continue;
 235:  
 236:                 }
 237:  
 238:                 if (listCT.FieldLinks[sourceFieldLink.Id] != null)
 239:                 {
 240:  
 241:                     Log("Deleting field \"" + sourceFieldLink.Name
 242:                         + "\" from contenttype on :" + list.ParentWebUrl + "/"
 243:                         + list.Title, EventLogEntryType.Information);
 244:  
 245:                     listCT.FieldLinks.Delete(sourceFieldLink.Id);
 246:                     listCT.Update();
 247:                 }
 248:  
 249:                 Log("Adding field \"" + sourceFieldLink.Name
 250:                     + "\" from contenttype on :" + list.ParentWebUrl
 251:                     + "/" + list.Title, EventLogEntryType.Information);
 252:  
 253:                 listCT.FieldLinks.Add(new SPFieldLink(sourceCT.Fields[sourceFieldLink.Id]));
 254:                 //Set displayname, not set by previous operation
 255:                 listCT.FieldLinks[sourceFieldLink.Id].DisplayName = sourceCT.FieldLinks[sourceFieldLink.Id].DisplayName;
 256:                 listCT.Update();
 257:                 Log("Done updating fields ");
 258:             }
 259:         }
 260:  
 261:         /// <summary>
 262:         /// Fields the exist.
 263:         /// </summary>
 264:         /// <param name="contentType">Type of the content.</param>
 265:         /// <param name="fieldLink">The field link.</param>
 266:         /// <returns></returns>
 267:         private static bool FieldExist(SPContentType contentType, SPFieldLink fieldLink)
 268:         {
 269:             try
 270:             {
 271:                 //will throw exception on missing fields
 272:                 return contentType.Fields[fieldLink.Id] != null;
 273:             }
 274:             catch (Exception)
 275:             {
 276:                 return false;
 277:             }
 278:         }
 279:     }
 280: }

 

By refactoring the code slightly I'm now able to use the code via the stsadm command, which I called gl-propagatecontenttype, or I can call the Process method via my Feature Receiver by just adding a reference to the assembly - this way I can push changes to content types down to the lists they are bound to when my Feature is re-activated.  Here's the syntax of the command:

C:\>stsadm -help gl-propagatecontenttype

stsadm -o gl-propagatecontenttype


Propagates a site scoped content type to list scoped instances of that content type.

Parameters:
        -url <site collection url>
        -contenttype <content type name>
        [-verbose]
        [-updatefields]
        [-removefields]

 

I suggest you avoid the use of the -removefields parameter if possible - I left it there because I thought I might need it but it's usually not a good thing to do something so destructive in batch like that (just make sure you at least test the change before going to production with it).

Again - props to Søren - I just retooled his code some.

17Feb/0823

Export Content Types

I was recently working on a client project where there was an environment already setup with several custom content types and custom site columns that had been created. We wanted to reverse engineer those content types and columns so that we could create a Feature that could be used to deploy them into the various environments. I poked around a bit and couldn't find anything that would simply give me the CAML that I needed for my Feature. So I decided to create a couple of new commands to help me do this quickly: gl-exportcontenttypes and gl-exportsitecolumns.

I'll cover gl-exportsitecolumns in a follow up post. Looking at the code you can see that there's really not much going on. I simply grab the content type(s) to export and get the SchemaXml property. One thing of note is that the SchemaXml property returns back XML that contains the complete field definition - because this won't work in a Feature I replace the <Field /> elements with a corresponding <FieldRef /> element. If the field definitions are needed then you can simply provide the includefielddefinitions parameter. Note that I'm outputting everything in one file but if you intend to use this in a Feature you'll want to break this up into multiple files. For the field definitions I also just output the SchemaXml of the SPField objects associated with the content type (note that there will be some attributes that you'll need to pull when using the resultant output in a Feature). To get the list bindings I loop through all the lists throughout the site collection and add a ContentTypeBindings element for each list that contains a reference to the identified content types.

   1: public class ExportContentTypes : SPOperation
   2: {
   3:     private const string ENCODED_SPACE = "_x0020_";
   4:     /// <summary>
   5:     /// Initializes a new instance of the <see cref="CopyContentTypes"/> class.
   6:     /// </summary>
   7:     public ExportContentTypes()
   8:     {
   9:         SPParamCollection parameters = new SPParamCollection();
  10:         parameters.Add(new SPParam("url", "url", true, null, new SPUrlValidator(), "Please specify the source site collection."));
  11:         parameters.Add(new SPParam("name", "n", false, null, new SPNonEmptyValidator()));
  12:         parameters.Add(new SPParam("group", "g", false, null, new SPNonEmptyValidator()));
  13:         parameters.Add(new SPParam("listname", "list", false, null, new SPNonEmptyValidator()));
  14:         parameters.Add(new SPParam("outputfile", "output", false, null, new SPDirectoryExistsAndValidFileNameValidator()));
  15:         parameters.Add(new SPParam("includelistbindings", "ilb"));
  16:         parameters.Add(new SPParam("includefielddefinitions", "ifd"));
  17:         parameters.Add(new SPParam("excludeparentfields", "epf"));
  18:         parameters.Add(new SPParam("removeencodedspaces", "res"));
  19:  
  20:         StringBuilder sb = new StringBuilder();
  21:         sb.Append("\r\n\r\nExports Content Types to an XML file.\r\n\r\nParameters:");
  22:         sb.Append("\r\n\t-url <url containing the content types>");
  23:         sb.Append("\r\n\t-outputfile <file to output results to>");
  24:         sb.Append("\r\n\t[-name <name of an individual content type to export>]");
  25:         sb.Append("\r\n\t[-group <content type group name to filter results by>]");
  26:         sb.Append("\r\n\t[-listname <name of a list to export content types from>]");
  27:         sb.Append("\r\n\t[-includelistbindings]");
  28:         sb.Append("\r\n\t[-includefielddefinitions]");
  29:         sb.Append("\r\n\t[-excludeparentfields]");
  30:         sb.Append("\r\n\t[-removeencodedspaces (removes '_x0020_' in field names)]");
  31:         Init(parameters, sb.ToString());
  32:     }
  33:  
  34:     #region ISPStsadmCommand Members
  35:  
  36:     /// <summary>
  37:     /// Gets the help message.
  38:     /// </summary>
  39:     /// <param name="command">The command.</param>
  40:     /// <returns></returns>
  41:     public override string GetHelpMessage(string command)
  42:     {
  43:         return HelpMessage;
  44:     }
  45:  
  46:     /// <summary>
  47:     /// Runs the specified command.
  48:     /// </summary>
  49:     /// <param name="command">The command.</param>
  50:     /// <param name="keyValues">The key values.</param>
  51:     /// <param name="output">The output.</param>
  52:     /// <returns></returns>
  53:     public override int Run(string command, StringDictionary keyValues, out string output)
  54:     {
  55:         output = string.Empty;
  56:  
  57:         InitParameters(keyValues);
  58:  
  59:         string url = Params["url"].Value.TrimEnd('/');
  60:         string contentTypeName = null;
  61:         if (Params["name"].UserTypedIn)
  62:             contentTypeName = Params["name"].Value;
  63:         string contentTypeGroup = null;
  64:         if (Params["group"].UserTypedIn)
  65:             contentTypeGroup = Params["group"].Value.ToLowerInvariant();
  66:         if (Params["group"].UserTypedIn && Params["listname"].UserTypedIn)
  67:             throw new SPSyntaxException("The parameters group and listname are incompatible");
  68:         bool excludeParentFields = Params["excludeparentfields"].UserTypedIn;
  69:         bool removeEncodedSpaces = Params["removeencodedspaces"].UserTypedIn;
  70:  
  71:         StringBuilder sb = new StringBuilder();
  72:         XmlTextWriter xmlWriter = new XmlTextWriter(new StringWriter(sb));
  73:         xmlWriter.Formatting = Formatting.Indented;
  74:  
  75:         Dictionary<Guid, SPField> ctFields = new Dictionary<Guid, SPField>();
  76:  
  77:         using (SPSite site = new SPSite(url))
  78:         {
  79:             using (SPWeb web = site.AllWebs[Utilities.GetServerRelUrlFromFullUrl(url)])
  80:             {
  81:                 SPContentTypeCollection availableContentTypes;
  82:  
  83:                 if (Params["listname"].UserTypedIn)
  84:                 {
  85:                     SPList list = web.Lists[Params["listname"].Value];
  86:                     availableContentTypes = list.ContentTypes;
  87:                 }
  88:                 else
  89:                 {
  90:                     availableContentTypes = web.AvailableContentTypes;
  91:                 }
  92:                 List<SPContentType> contentTypes = new List<SPContentType>();
  93:                 try
  94:                 {
  95:                     xmlWriter.WriteStartElement("Elements");
  96:                     xmlWriter.WriteAttributeString("xmlns", "http://schemas.microsoft.com/sharepoint/");
  97:  
  98:                     // Gather up all the content types we want to export out.
  99:                     if (contentTypeName != null)
 100:                     {
 101:                         SPContentType ct = availableContentTypes[contentTypeName];
 102:                         if (ct == null)
 103:                         {
 104:                             output += "The content type specified could not be found.";
 105:                             return 0;
 106:                         }
 107:                         else
 108:                         {
 109:                             contentTypes.Add(ct);
 110:                         }
 111:                     }
 112:                     else
 113:                     {
 114:                         // Loop through all the source content types and create them at the target.
 115:                         foreach (SPContentType ct in availableContentTypes)
 116:                         {
 117:                             if (contentTypeGroup == null || ct.Group.ToLowerInvariant() == contentTypeGroup)
 118:                             {
 119:                                 contentTypes.Add(ct);
 120:                             }
 121:                         }
 122:                     }
 123:  
 124:                     if (Params["includefielddefinitions"].UserTypedIn)
 125:                     {
 126:                         // If we're including field definitions then we want to show them first as they'll need to appear first when using within a Feature
 127:                         foreach (SPContentType ct in contentTypes)
 128:                         {
 129:                             SPContentType parentCT = ct.Parent;
 130:                             foreach (SPField field in ct.Fields)
 131:                             {
 132:                                 // If the parent content type contains the current field and the user wants to exclude parent fields then continue to the next field.
 133:                                 if (parentCT != null && excludeParentFields && parentCT.Fields.ContainsField(field.InternalName))
 134:                                     continue;
 135:  
 136:                                 if (!ctFields.ContainsKey(field.Id))
 137:                                     ctFields.Add(field.Id, field);
 138:                             }
 139:                         }
 140:                         xmlWriter.WriteString("\r\n\r\n");
 141:                         foreach (SPField field in ctFields.Values)
 142:                         {
 143:                             string schema = field.SchemaXml;
 144:                             if (field.InternalName.Contains(ENCODED_SPACE) && removeEncodedSpaces)
 145:                             {
 146:                                 schema = schema.Replace(string.Format("Name=\"{0}\"", field.InternalName),
 147:                                                         string.Format("Name=\"{0}\"", field.InternalName.Replace(ENCODED_SPACE, string.Empty)));
 148:                             }
 149:  
 150:                             xmlWriter.WriteRaw(schema);
 151:                             xmlWriter.WriteString("\r\n");
 152:                         }
 153:                     }
 154:  
 155:                     xmlWriter.WriteString("\r\n");
 156:  
 157:                     foreach (SPContentType ct in contentTypes)
 158:                     {
 159:                         WriteContentTypeXml(ct, excludeParentFields, removeEncodedSpaces, xmlWriter);
 160:                     }
 161:  
 162:                     if (Params["includelistbindings"].UserTypedIn)
 163:                         GetListBindings(web, contentTypes, xmlWriter);
 164:  
 165:                     xmlWriter.WriteEndElement(); // Elements
 166:                 }
 167:                 finally
 168:                 {
 169:                     xmlWriter.Flush();
 170:                     xmlWriter.Close();
 171:                 }
 172:             }
 173:         }
 174:  
 175:         File.WriteAllText(Params["outputfile"].Value, sb.ToString());
 176:         return 1;
 177:     }
 178:  
 179:  
 180:     #endregion
 181:  
 182:  
 183:     /// <summary>
 184:     /// Writes the content type XML.
 185:     /// </summary>
 186:     /// <param name="ct">The ct.</param>
 187:     /// <param name="excludeParentFields">if set to <c>true</c> [exclude parent fields].</param>
 188:     /// <param name="removeEncodedSpaces">if set to <c>true</c> [remove encoded spaces].</param>
 189:     /// <param name="xmlWriter">The XML writer.</param>
 190:     private static void WriteContentTypeXml(SPContentType ct, bool excludeParentFields, bool removeEncodedSpaces, XmlTextWriter xmlWriter)
 191:     {
 192:         SPContentType parentCT = ct.Parent;
 193:         XmlDocument xmlDoc = new XmlDocument();
 194:         xmlDoc.LoadXml(ct.SchemaXml);
 195:         XmlElement fieldRefsElement = xmlDoc.CreateElement("FieldRefs");
 196:         xmlDoc.DocumentElement.AppendChild(fieldRefsElement);
 197:         foreach (XmlElement field in xmlDoc.SelectNodes("//Fields/Field"))
 198:         {
 199:             // If the parent content type contains the current field and the user wants to exclude parent fields then continue to the next field.
 200:             if (parentCT != null && excludeParentFields && parentCT.Fields.ContainsField(field.GetAttribute("Name")))
 201:                 continue;
 202:  
 203:             XmlElement fieldRefElement = xmlDoc.CreateElement("FieldRef");
 204:             fieldRefElement.SetAttribute("ID", field.GetAttribute("ID"));
 205:  
 206:             string name = field.GetAttribute("Name");
 207:             if (name.Contains(ENCODED_SPACE) && removeEncodedSpaces)
 208:                 name = name.Replace(ENCODED_SPACE, string.Empty);
 209:  
 210:             fieldRefElement.SetAttribute("Name", name);
 211:             fieldRefsElement.AppendChild(fieldRefElement);
 212:         }
 213:         xmlDoc.DocumentElement.RemoveChild(xmlDoc.SelectSingleNode("//Fields"));
 214:  
 215:         xmlWriter.WriteString("\r\n");
 216:         xmlWriter.WriteRaw(xmlDoc.OuterXml);
 217:     }
 218:  
 219:     /// <summary>
 220:     /// Gets the list bindings.
 221:     /// </summary>
 222:     /// <param name="web">The web.</param>
 223:     /// <param name="contentTypes">The content types.</param>
 224:     /// <param name="xmlWriter">The XML writer.</param>
 225:     private static void GetListBindings(SPWeb web, List<SPContentType> contentTypes, XmlTextWriter xmlWriter)
 226:     {
 227:         foreach (SPList list in web.Lists)
 228:         {
 229:             foreach (SPContentType listCT in list.ContentTypes)
 230:             {
 231:                 foreach (SPContentType ct in contentTypes)
 232:                 {
 233:                     //if ((listCT.Scope != ct.Scope && listCT.Parent.Id == ct.Id) || listCT.Id == ct.Id)
 234:                     if (listCT.Name == ct.Name && listCT.Group == ct.Group)
 235:                     {
 236:                         xmlWriter.WriteStartElement("ContentTypeBinding");
 237:                         xmlWriter.WriteAttributeString("ContentTypeId", ct.Id.ToString());
 238:                         xmlWriter.WriteAttributeString("ListUrl", list.RootFolder.ServerRelativeUrl);
 239:                         xmlWriter.WriteEndElement(); // ContentTypeBinding
 240:                     }
 241:                 }
 242:             }
 243:         }
 244:  
 245:         foreach (SPWeb subWeb in web.Webs)
 246:         {
 247:             try
 248:             {
 249:                 GetListBindings(subWeb, contentTypes, xmlWriter);
 250:             }
 251:             finally
 252:             {
 253:                 subWeb.Dispose();
 254:             }
 255:         }
 256:     }
 257: }
 258:  
The syntax of the command can be seen below:
C:\>stsadm -help gl-exportcontenttypes

stsadm -o gl-exportcontenttypes


Exports Content Types to an XML file.

Parameters:
        -url <url containing the content types>
        -outputfile <file to output results to>
        [-name <name of an individual content type to export>]
        [-group <content type group name to filter results by>]
        [-listname <name of a list to export content types from>]
        [-includelistbindings]
        [-includefielddefinitions]
        [-excludeparentfields]
        [-removeencodedspaces (removes '_x0020_' in field names)]
Here's an example of how to dump the XMl for all the content types in a particular group:
stsadm -o gl-exportcontenttypes -url "http://intranet" -outputfile c:\contentTypes.xml -group "Custom Content Types" -includelistbindings -includefielddefinitions
4Sep/07250

Convert a Sub-site to a Site Collection

I finally figured it out! This was supposed to be one of those very simple tasks that I should have been able to do without any custom code. Turning a sub-site (or web) into a site collection (or top level site) turned out to be the most difficult task I've yet to face with SharePoint 2007. In theory you should be able to do this using the following commands which could be put into a batch file:

REM Create a test web for exporting
stsadm -o createweb -url "http://intranet/testweb" -sitetemplate "SPSTOPIC#0"

REM Export the test web to the filesystem
stsadm -o export -url "http://intranet/testweb" -filename "c:\testweb" -includeusersecurity -versions 4 -nofilecompression -quiet

REM Create a managed path for the new top level site
stsadm -o addpath -url "http://intranet/testsite" -type explicitinclusion

REM Create an empty site with a default site template (note that if you don't specify a template you have to manually activate the required features)
stsadm -o createsite -url "http://intranet/testsite" -owneremail "someone@example.com" -ownerlogin "domain\username" -sitetemplate "SPSTOPIC#0"

REM Import the site
stsadm -o import -url "http://intranet/testsite" -filename "c:\testweb" -includeusersecurity -nofilecompression -quiet

Unfortunately what you get is only a partially functional site (and in some case not functional at all). There are several errors that you are likely to encounter after running the above using the created testweb or your own existing web. The first and most obvious error is that when you load the default.aspx page of the new site you may get a File Not Found error (note that running the above as is will not give you this error). This is the result of the publishing pages PageLayout URL getting messed up (effectively still pointing to an old value). I addressed this specific issue with a separate command (http://blog.falchionconsulting.com/index.php/2007/08/fix-publishing-pages-page-layout-url/) and the I've encapsulated that functionality into the new commands I've created which are detailed below.

The next error you're likely to see is on the Area Template Settings page (Site Settings -> Page layouts and site templates). The specific error is "Data at the root level is invalid. Line 1, position 1".

Anyone who's done a lot of XML work should recognize this error as an XML parsing error. This error occurs if the web you imported from was set to inherit it's page layouts from it's parent. When a web is setup this way there's a property called "__PageLayouts" which gets set to "__inherit".

For a top level site collection this value should always be either an empty string (all page layouts are available) or XML describing which layouts are available. The import operation does not consider this and leaves the value as is thus resulting in the XML error when attempting to parse "__inherit" as XML. The fix for this is simple enough - change the value to an empty string. Unfortunately that's not all we have to do. Fixing the above error results in the page loading without errors, however, the page layouts section does not load. There's still several issues that need to be resolved. If you now go and view the master gallery (Site Settings -> Master pages and page layouts) you should see all the default page layouts. If you have any custom page layouts those won't exist and will cause problems.

Also, if you attempt to edit a file you'll notice that even though we used a publishing template it doesn't prompt you to check the file out. What's more is that once you view the form for a layout you should see that only the core fields are present (no Content Type, Associated Content Type, Variations, etc.).

There are several things that need to be fixed here - first we need to activate all the features that would otherwise be activated on a new site collection (need this so that we can get the publishing workflow options enabled). Second we need to reset all the properties for the gallery to match that of our source gallery (namely we need to allow management of content types).

Third we have to change the ContentType field from being a Text field to being a Choice field (more about this in a minute). Fourth we need to re-associate each file as a Page Layout file by setting all the necessary properties (Content Type, Associated Content Type, etc.). In regards to changing the ContentType field this is the one that caused me the most headache to figure out. For some reason during the import of the site this field gets a bit messed up (note that I'm not referring to the ContentType field that is linked in from the Page content type which is associated with the library but rather another field that is part of the gallery definition itself). The field should be a Choice field with options such as "Page Layout", "Publishing Mater Page", "Master Page", and "Folder". However, during the import the field is converted to a Text field - don't ask me why.

The result is that when you query for the list of available page layouts the unmanaged code that Microsoft uses to do the actual query chokes because it can't find a matching value in the list so it produces an invalid query which will always return no results back. To fix this I copy the source master page gallery on top of the target gallery using the content deployment API. I also found that (with SP2) the PublishingResources feature seemed to correct the issue (at least in the tests that I ran).

UPDATE 9/4/2007: I just discovered that another issue is related to the global navigation. If you view the navigation via the browser it will look as though everything is just peachy but if you attempt to manipulate the navigation programmatically you'll find that the PublishingWeb's GlobalNavigationNodes property is empty (no items are present). This is because the global navigation is stored at the site collection level so when you import the web it does not take the global navigation with it, just the current. The fix is simple enough - just loop through the current navigation collection and copy it to the global navigation collection. This will help to allow other programmatic manipulations of the global navigation to succeed.

UPDATE 7/6/2009:  I am now calling the code that I created to copy content types from one site collection to another.  This solves issues that can occur if a site collection content type was not created via a Feature.  I've also added additional logging to better show what is happening.

So, to summarize the things that need to be repaired after importing into your empty site collection:

  1. Activate any features that are needed by the site
  2. Set the master page gallery settings
    1. Enable content type management
    2. Set your publishing options
    3. Fix the Content Type field so that it's a Choice field type by copying the source master page gallery
  3. Copy missing content types from the source site collection.
  4. Fix the Page Layouts and Site Templates
    1. Set the "__PageLayouts" property to an empty string (can then be set to something else using SetAvailablePageLayouts() but first needs to be set to an empty string as SetAvailablePageLayouts() will not work until it's fixed)
    2. Copy any missing page layouts from the source
    3. Set all appropriate properties on each page layout
  5. Fix all the publishing pages PageLayout property to have the correct URL
  6. UPDATE 9/4/2007: Update the global navigation

Some of the above can be done via the browser but most of it requires programmatic changes. In order to solve all these problems I've created two custom stsadm commands - the first will take a site make all the repairs identified above (so it assumes you've already imported the site).

The second basically just abstracts the whole process of exporting a web, creating a site, importing into the site, and then repairing the site (this way the entire process can be done with just one command). The commands I created are detailed below (forgive the verbosity of the names - I had trouble coming up with something shorter).

1. gl-repairsitecollectionimportedfromsubsite

The code is fairly well documented so rather than discuss it all (there's a lot of it) I've linked it to this post here. The syntax of the command can be seen below:

C:\>stsadm -help gl-repairsitecollectionimportedfromsubsite

stsadm -o gl-repairsitecollectionimportedfromsubsite

Repairs a site collection that has been imported from an exported sub-site.  Note that the sourceurl can be the actual source site or any site collection that can be used as a model for the target.

Parameters:
        -sourceurl <source location of the existing sub-site or model site collection>
        -targeturl <target location for the new site collection>

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-repairsitecollectionimportedfromsubsite WSS 3, MOSS 2007 Released: 9/4/2007
Updated: 7/6/2009

Parameter Name Short Form Required Description Example Usage
sourceurl source Yes The URL to the source sub-site to convert. -sourceurl http://portal/subsite

-source http://portal/subsite
targeturl target Yes The URL of the new site collection to create. -targeturl http://portal/sites/site

-target http://portal/sites/site

Here’s an example of how to repair the site created using the batch file above:

stsadm –o gl-repairsitecollectionimportedfromsubsite –sourceurl "http://intranet/testweb/" -targeturl "http://intranet/testsite/"

2. gl-convertsubsitetositecollection

As stated above, this command is just an abstraction of other commands - it simply calls out to stsadm to do export the site (note that you can provide a previously exported site file/folder), create the managed path, create the empty site, import the site, and finally repair the imported site. As there's nothing spectacular going on here I didn't bother culling the code out in this post (download the project if you're interested in the details). The syntax of the command can be seen below:

C:\>stsadm -help gl-convertsubsitetositecollection

stsadm -o gl-convertsubsitetositecollection


Converts a sub-site to a top level site collection via a managed path.

Parameters:

        -sourceurl <source location of the existing sub-site or model site collection>
        -targeturl <target location for the new site collection>
        -owneremail <someone@example.com>
        [-createmanagedpath]
        [-haltonwarning]
        [-haltonfatalerror]
        [-includeusersecurity]
        [-suppressafterevents (disable the firing of "After" events when creating or modifying list items)]
        [-exportedfile <filename of exported site if previously exported>]
        [-nofilecompression]
        [-ownerlogin <DOMAIN\name>]
        [-ownername <display name>]
        [-secondaryemail <someone@example.com>]
        [-secondarylogin <DOMAIN\name>]
        [-secondaryname <display name>]
        [-lcid <language>]
        [-title <site title>]
        [-description <site description>]
        [-hostheaderwebapplicationurl <web application url>]
        [-quota <quota template>]
        [-deletesource]
        [-createsiteinnewdb]
        [-createsiteindb]
        [-databaseuser <database username>]
        [-databasepassword <database password>]
        [-databaseserver <database server name>]
        [-databasename <database name>]
        [-verbose]

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-convertsubsitetositecollection WSS 3, MOSS 2007 Released: 9/4/2007
Updated: 7/6/2009

Parameter Name Short Form Required Description Example Usage
sourceurl source Yes The URL to the source sub-site to convert. -sourceurl http://portal/subsite

-source http://portal/subsite
targeturl target Yes The URL of the new site collection to create. -targeturl http://portal/sites/site

-target http://portal/sites/site
owneremail oe Yes

The site owner's e-mail address.  Must be valid e-mail address, in the form someone@example.com.

-owneremail someone@example.com

-oe someone@example.com
createmanagedpath createpath No Create a new managed path for the site collection. -createmanagedpath

-createpath
haltonwarning warning No Stop execution of the command if a warning event occurs during the export or import process. -haltonwarning

-warning
haltonfatalerror error No Stop execution of the command if a fatal error occurs during the export or import process. -haltonfatalerror

-error
exportedfile file No Use a previously exported site (created using stsadm's export command). -exportedfile c:\exportdata\site

-file c:\exportdata\site
nofilecompression   No Do not compress the site when exporting (or if previously exported use an uncompressed file for the import). -nofilecompression
ownerlogin ol

If your farm does not have Active Directory account creation mode enabled, then this parameter is required.

This parameter should not be provided if your farm has Active Directory account creation mode enabled, as Microsoft Office SharePoint Server 2007 will automatically create a site collection owner account in Active Directory based on the owner e-mail address.

The site owner's user account.  Must be a valid Windows user name, and must be qualified with a domain name, for example, domain\name

-ownerlogin domain\name

-ol domain\name
ownername on No

The site owner's display name.

-ownername "Gary Lapointe"

-on "Gary Lapointe"
secondaryemail se No

The secondary site owner's e-mail address.  Must be valid e-mail address, in the form someone@example.com.

-secondaryemail someone@example.com

-se someone@example.com
secondarylogin sl

If your farm does not have Active Directory account creation mode enabled, then this parameter is required.

This parameter should not be provided if your farm has Active Directory account creation mode enabled, as Microsoft Office SharePoint Server 2007 will automatically create a site collection owner account in Active Directory based on the owner e-mail address.

The secondary site owner's user account.  Must be a valid Windows user name, and must be qualified with a domain name, for example, domain\name

-secondarylogin domain\name

-sl domain\login
secondaryname sn No

The secondary site owner's display name.

-secondaryname "Pam Lapointe"

-sn "Pam Lapointe"
lcid   No

A valid locale ID, such as "1033" for English.  You must specify this parameter when using a non-English template.

-lcid 1033
title t No

The title of the new site collection (this value will be overwritten when the site is imported - it is available only to help in situations in which the import fails).

-title "New Site"
description desc No

Description of the site collection (this value will be overwritten when the site is imported - it is available only to help in situations in which the import fails).

-description "New Site Description"

-desc "New Site Description"
hostheaderwebapplicationurl hhurl No

A valid URL assigned to the Web application by using Alternate Access Mapping (AAM), such as "http://server_name".

When the hostheaderwebapplicationurl parameter is present, the value of the url parameter is the URL of the host-named site collection and value of the hostheaderwebapplicationurl parameter is the URL of the Web application that will hold the host-named site collection.

-hostheaderwebapplicationurl http://newsite

-hhurl http://newsite
quota   No

The quota template to apply to sites created on the virtual server.

-quota Portal
deletesource   No Delete the source site after conversion (only recommended if significant testing has occurred). -deletesource
createsiteinnewdb newdb No Create the site collection in a content database. -createsiteinnewdb

-newdb
createsiteindb db No Create the site collection in an existing content database. -createsiteindb

-db
databaseserver ds No The database server containing the specified content database.  If not specified then the default database server is used. -databaseserver spsql1

-ds spsql1
databaseuser du No

The administrator user name for the SQL Server database.

-databaseuser domain\user

-du domain\user
databasepassword dp No

The password that corresponds to the administrator user name for the SQL Server database.

-databasepassword password

-dp password
databasename dn Yes if createsiteinnewdb or createsiteindb is specified.

The name of the content database to put the site collection in (will be created if createsiteinnewdb is specified).

-databasename SharePoint_Content1

-db SharePoint_Content1
suppressafterevents sae No Disable the firing of After events when creating or modifying files or list items during the import. -suppressafterevents

-sae
verbose v No Displays logging information when executing. -verbose

-v

Here's an example of how to do all that the batch file above is doing (minus the creation of the testweb) as well as the repair operation all with one command:

stsadm –o gl-convertsubsitetositecollection –sourceurl "http://intranet/testweb/" -targeturl "http://intranet/testsite/" -createmanagedpath -nofilecompression -owneremail "someone@example.com" -ownerlogin "domain\user" -deletesource

One area of improvement may be to pull the owner and secondary owner information from the source site collection so that this information does not have to be provided - maybe I'll do that if I feel I have the time or if people express enough interest. Note that you can specify a title and description but they'll be overwritten during the import - I only included them so that if the import fails and you're left with an incomplete site you'll at least have a name for it if you should forget to delete it and stumble upon it a year later.

Figuring out how to solve all the issues surrounding converting a web to a site collection was a real pain the a$$ so any feedback that people have on this would be greatly appreciated - hopefully if there are others out there that have stumbled on this then they'll benefit from it as well. Keep in mind also that though I think I've solved all the errors related to the conversion it's possible that different implementations may have additional errors that I have not seen - if that's the case please let me know (especially if you've solved the problems) so that I can share with others.

Update 9/21/2007: I've fixed a couple minor bugs that pop up when converting a non-publishing site. I've also enhanced the command to take advantage of another new command I created: gl-updatev2tov3upgradeareaurlmappings (updates the url mapping of V2 bucket webs to V3 webs thereby reflecting the change of url as a result of the move so if a user tries to hit the V2 url it will redirect to the new and updated V3 url).

Update 10/2/2007: I've enhanced the command to take advantage of another new command I created: gl-retargetcontentquerywebpart (fixes Grouped Listings web parts that remained pointed at the old list rather than the newly imported list).

Update 10/12/2007: I've removed the retainobjectidentity parameter. If you attempt to use this parameter you will receive a syntax error. Turns out that retaining the object identity when going from a sub-site to a site collection just created a nightmare. However, because I still had to handle these web parts that were broken I decided to enhance the repair routines to manually retarget the DataFormWebPart and ContentByQueryWebPart web parts. So if a matching list can be found on the source then any of these web parts on your pages should be fixed so that you don't have to manually fix them (the gl-repairsitecollectionimportedfromsubsite command will do the same).

Update 7/6/2009: I removed the direct DB access code and added support for copying content types from the source.

23Aug/0779

Copy Content Types

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.