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:

  1using System;
  2using System.Collections.Generic;
  3using System.Collections.Specialized;
  4using System.Diagnostics;
  5using System.Text;
  6using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
  7using Lapointe.SharePoint.STSADM.Commands.SPValidators;
  8using Microsoft.SharePoint;
  9 
 10namespace 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.