Propagate Site Content Types to List Content Types

[Aka: Make Content Type Inheritance Work for XML Based Content types]

[Note:Updated Nov 6]

If you are working with XML based content types, you’ll sooner or later fall into a trap well hidden by Microsoft. Simply put Content Type inheritance don’t work for XML based content types. SharePoint does not check what fields you’ve added/deleted/changed since you last deployed your content type feature, so you don’t have the “luxury” (it bloody well should work!) of choosing whether or not to propagate changes down to inherited content types as in the web interface or object model.

I found MS mention it briefly here.

The Role of List Content Types

I’ll take a small detour here to explain the concept of list content types. Whenever you assign a content type to a list (e.g. the pages document library for publishing webs) a new list content type is created which inherits from the site content type that you are assigning to the list and with the same name.

Actually the inheritance copies the site content type definition to the list content type in its entirety and assigns an “inherited” content type id to the new list content type.

If you later modify the site content type, through the web or object model, you have the option of propagating those changes to inherited content types, which in particular includes all the list content types on the site.

Don’t Disconnect the Content Type

Another small detour before I’ll present what to do about this mess. It should be stressed that any modification of the site content type through the web or object model, will disconnect the site content type from the underlying xml metadata file. If it happens you my reestablish the link as described here. This is very bad news. It not only means that you should be careful about what you do to your content types, but it also means that there is no simple way to propagate the changes code wise – we can’t just update the site content type (re-add fields) and have the changes propagate through the object model. 😦

The Code to Propagate

Finally I’m ready to present the code I used to propagate changes to my content types. 🙂

The procedure is:

  1. Locate the site content type in question (run it multiple times if needed)
  2. Start at the root web site
    1. Go through all lists on the web
    2. If the list is associated with the site content type (actually the inherited list content type)
      1. Compare every field on the site content type with the list content type
      2. Add, remove or change the field in question on the list content type
    3. Go recursively through every subweb and continue from step a

I created the following code as a new extension to stsadm, called like this:

stsadm -o cbPropagateContentType -url <site collection url> -contenttype <contenttype name> [-verbose] [-removefields] [-updatefields]

For instance:

stsadm -o cbPropagateContentType -url http://localhost -contenttype “My Article Page” –verbose

The “removefields” switch specifies whether or not fields found in the list content type that are not in the site content type should be removed or not. Default is not. New fields in the site content type will always be added to the list content types.

Note: I’ve not tried to create an “update” field. I’ve not had a use for that yet and it will also require considerably more testing to ensure that it works correctly. You really don’t want to break your list content types in case of errors…

Updated (Nov 6): An update option has now been added you should consider that option as beta.

Note 2: If the job stops prematurely no harm is done. Restart it and it will continue from where it stopped – it will examine and skip the webs/lists that it already has processed.

Finally something tangible:

Save the following as “stsadmcommands.CBPropagateContentType.xml” and save it into “\Config” in the root of the SharePoint install folder (remember to update the assembly reference to whatever you compile the code into):

<?xml version="1.0" encoding="utf-8" ?>
<commands>
  <command name="cbpropagatecontenttype"
          class="Carlsberg.SharePoint.Administration.STSAdm.CBPropagateContentType,
          Carlsberg.SharePoint.Administration, Version=1.0.0.0, Culture=neutral,  
          PublicKeyToken=55c69d084ac6678f"/>
</commands>

And finally the code you need to compile to an assembly that the xml file should specify:

Updated (Nov 6): Code has been updated a bit. Some mistakes with display name/internal name have been fixed and the update option has been added. I’m not yet satisfied with the testing of the update method so consider it to be beta.

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.StsAdmin;

namespace Carlsberg.SharePoint.Administration.STSAdm
{
    /// 
    /// A custom STSAdm command for propagating site content types to lists
    /// content types.
    /// 
    /// The code is provided as is, I don't take any responsibilty for 
    /// any errors or data loss you might encounter.
    /// 
    /// Use freely with two conditions:
    /// 1. Keep my name in there
    /// 2. Report any bugs back to https://soerennielsen.wordpress.com
    /// 
    /// Enjoy
    /// Søren L. Nielsen
    /// 
    /// 
    class CBPropagateContentType : ISPStsadmCommand
    {
        #region Input parameters
        private string providedUrl;
        private string contentTypeName;
        private bool removeFields = false;
        private bool verbose = false;
        private bool updateFields = false;

        private bool UpdateFields
        {
            get { return updateFields; }
            set { updateFields = value; }
        }

        private bool Verbose {
            get { return verbose; }
            set { verbose = value; }
        }

        private bool RemoveFields {
            get { return removeFields; }
            set { removeFields = value; }
        }

        private string ContentTypeName {
            get { return contentTypeName; }
            set { contentTypeName = value; }
        }

        private string ProvidedUrl {
            get { return providedUrl; }
            set { providedUrl = value; }
        }
        #endregion

        /// 
        /// Runs the specified command. Called by STSADM.
        /// 
        /// The command.
        /// The key values.
        /// The output.
        /// 
        public int Run(string command, StringDictionary keyValues, 
                       out string output) {
            //Parse input                        
            // make sure all settings are valid
            if (!GetSettings(keyValues)) {
                Console.Out.WriteLine(GetHelpMessage(string.Empty));
                output = "Required parameters not supplied or invalid.";
            }

            SPSite siteCollection = null;
            SPWeb rootWeb = null;

            try {
                // get the site collection specified
                siteCollection = new SPSite(ProvidedUrl);
                rootWeb = siteCollection.RootWeb;

                //Get the source site content type
                SPContentType sourceCT = 
                             rootWeb.AvailableContentTypes[ContentTypeName];
                if (sourceCT == null) {
                    throw new ArgumentException("Unable to find " 
                        + "contenttype named \"" + ContentTypeName + "\"");
                }

                // process the root website
                ProcessWeb(rootWeb, sourceCT);

                output = "Operation successfully completed.";
                Log( output, false );
                return 0;
            }
            catch (Exception ex) {
                output = "Unhandled error occured: " + ex.Message;
                Log(output, false);
                return -1;
            }
            finally {
                if (rootWeb != null) {
                    rootWeb.Dispose();
                }
                if (siteCollection != null) {
                    siteCollection.Dispose();
                }
            }
        }

        /// 
        /// Go through a web, all lists and sync with the source content 
        /// type.
        /// Go recursively through all sub webs.
        /// 
        /// 
        /// 
        private void ProcessWeb(SPWeb web, SPContentType sourceCT) {
            //Do work on lists on this web
            Log("Processing web: " + web.Url);

            //Grab the lists first, to avoid messing up an enumeration 
            // while looping through it.
            List lists = new List();
            foreach (SPList list in web.Lists) {
                lists.Add(list.ID);
            }

            foreach (Guid listId in lists) {
                SPList list = web.Lists[listId];

                if (list.ContentTypesEnabled) {
                    Log("Processing list: " + list.ParentWebUrl + "/" 
                         + list.Title);

                    SPContentType listCT = 
                                         list.ContentTypes[ContentTypeName];
                    if (listCT != null) {
                        Log("Processing content type on list:" + list);

                        if (UpdateFields) {
                          UpdateListFields(list, listCT, sourceCT);
                        }

                        //Find/add the fields to add
                        foreach (SPFieldLink sourceFieldLink in 
                                               sourceCT.FieldLinks) {
                          if (!FieldExist(sourceCT, sourceFieldLink)) {
                            Log(
                              "Failed to add field " 
                              + sourceFieldLink.DisplayName + " on list " 
                              + list.ParentWebUrl + "/" + list.Title 
                              + " field does not exist (in .Fields[]) on " 
                              + "source content type", false);
                          }
                          else {
                            if (!FieldExist(listCT, sourceFieldLink)) {
                              //Perform double update, just to be safe 
                              // (but slow)
                              Log("Adding field \"" 
                                 + sourceFieldLink.DisplayName 
                                 + "\" to contenttype on " 
                                 + list.ParentWebUrl + "/" + list.Title, 
                                   false);
                              if (listCT.FieldLinks[sourceFieldLink.Id] 
                                                                != null) {
                                listCT.FieldLinks.Delete(sourceFieldLink.Id);
                                listCT.Update();
                              }
                              listCT.FieldLinks.Add(new SPFieldLink(
                                      sourceCT.Fields[sourceFieldLink.Id]));
                              listCT.Update();
                            }
                          }
                        }


                      if (RemoveFields) {
                            //Find the fields to delete
                            //WARNING: this part of the code has not been 
                            // adequately tested (though
                            // what could go wrong? ;-) ... )

                            //Copy collection to avoid modifying enumeration
                            // as we go through it
                            List listFieldLinks = 
                                                  new List();
                            foreach (SPFieldLink listFieldLink in 
                                                     listCT.FieldLinks) {
                                listFieldLinks.Add(listFieldLink);
                            }

                            foreach (SPFieldLink listFieldLink in 
                                                        listFieldLinks) {
                                if (!FieldExist(sourceCT, listFieldLink)) {
                                    Log("Removing field \"" 
                                       + listFieldLink.DisplayName 
                                       + "\" from contenttype on :" 
                                       + list.ParentWebUrl + "/" 
                                       + list.Title, false);
                                    listCT.FieldLinks.Delete(
                                                        listFieldLink.Id);
                                    listCT.Update();
                                }
                            }
                        }
                    }
                }
            }


            //Process sub webs
            foreach (SPWeb subWeb in web.Webs) {
                ProcessWeb(subWeb, sourceCT);
                subWeb.Dispose();
            }
        }


      /// 
      /// Updates the fields of the list content type (listCT) with the 
      /// fields found on the source content type (courceCT).
      /// 
      /// 
      /// 
      /// 
      private void UpdateListFields(SPList list, SPContentType listCT, 
                                    SPContentType sourceCT) {
        Log("Starting to update fields ", false);
        foreach (SPFieldLink sourceFieldLink in sourceCT.FieldLinks) {
          //has the field changed? If not, continue.
          if (listCT.FieldLinks[sourceFieldLink.Id]!= null 
               && listCT.FieldLinks[sourceFieldLink.Id].SchemaXml 
                  == sourceFieldLink.SchemaXml) {
            Log("Doing nothing to field \"" + sourceFieldLink.Name 
                + "\" from contenttype on :" + list.ParentWebUrl + "/" 
                + list.Title, false);
            continue;
          }
          if (!FieldExist(sourceCT, sourceFieldLink)) {
            Log(
              "Doing nothing to field: " + sourceFieldLink.DisplayName 
               + " on list " + list.ParentWebUrl 
               + "/" + list.Title + " field does not exist (in .Fields[])"
               + " on source content type", false);
            continue;
                              
          }

          if (listCT.FieldLinks[sourceFieldLink.Id] != null) {

            Log("Deleting field \"" + sourceFieldLink.Name 
                + "\" from contenttype on :" + list.ParentWebUrl + "/" 
                + list.Title, false);

            listCT.FieldLinks.Delete(sourceFieldLink.Id);
            listCT.Update();
          }

          Log("Adding field \"" + sourceFieldLink.Name 
              + "\" from contenttype on :" + list.ParentWebUrl 
              + "/" + list.Title, false);

          listCT.FieldLinks.Add(new SPFieldLink(
                                     sourceCT.Fields[sourceFieldLink.Id]));
          //Set displayname, not set by previus operation
          listCT.FieldLinks[sourceFieldLink.Id].DisplayName 
                      = sourceCT.FieldLinks[sourceFieldLink.Id].DisplayName;
          listCT.Update();
          Log("Done updating fields ");
        }
      }

      private static bool FieldExist(SPContentType contentType, 
                                                     SPFieldLink fieldLink)
        {
            try
            {
                //will throw exception on missing fields
                return contentType.Fields[fieldLink.Id] != null;
            }
            catch(Exception)
            {
                return false;
            }
        }

        private void Log(string str, bool verboseLevel) {
            if (Verbose || !verboseLevel) {
                Console.WriteLine(str);
            }
        }

        private void Log(string str) {
            Log(str, true);
        }


        /// 
        /// Parse the input settings
        /// 
        /// 
        /// 
        private bool GetSettings(StringDictionary keyValues) {
            try {
                ProvidedUrl = keyValues["url"];
                //test the url
                new Uri(ProvidedUrl);

                ContentTypeName = keyValues["contenttype"];
                if (string.IsNullOrEmpty(ContentTypeName)) {
                    throw new ArgumentException("contenttype missing");
                }

                if (keyValues.ContainsKey("removefields")) {
                    RemoveFields = true;
                }

                if (keyValues.ContainsKey("verbose")) {
                    Verbose = true;
                }

                if (keyValues.ContainsKey("updatefields"))
                {
                    UpdateFields = true;
                }
                return true;
            }
            catch (Exception ex) {
                Console.Out.WriteLine("An error occuring in retrieving the"
                    + " parameters. \r\n(" + ex + ")\r\n");
                return false;
            }
        }

        /// 
        /// Output help to console
        /// 
        /// 
        /// 
        public string GetHelpMessage(string command) {
            StringBuilder helpMessage = new StringBuilder();

            // syntax
            helpMessage.AppendFormat("\tstsadm -o {0}{1}{1}", command, 
                                                 Environment.NewLine);
            helpMessage.Append("\t-url " + Environment.NewLine);
            helpMessage.Append("\t-contenttype " + Environment.NewLine);
            helpMessage.Append("\t[-removefields]" + Environment.NewLine);
            helpMessage.Append("\t[-updatefields]" + Environment.NewLine);
            helpMessage.Append("\t[-verbose]" + Environment.NewLine);

            // description
            helpMessage.AppendFormat("{0}This action will propagate a site"
                + " content type to all list content types within the "
                + "site collection.{0}Information propagated is field "
                + "addition/removal.{0}{0}", Environment.NewLine);
            helpMessage.AppendFormat("{0}Søren Nielsen (soerennielsen." 
                + "wordpress.com){0}{0}", Environment.NewLine);

            return helpMessage.ToString();
        }
    }
}

Final Comments

I’ve made zero attempts to optimize the code. It doesn’t really matter how long it takes, does it? Give it 10 minutes till a couple of hours for huge site collection (I’ve tested with about 400 sub sites).

I recommend that you use the verbose flag and pipe the output to a file, so that you can review that it did everything correctly.

The code does not handle site content types on sub sites I’ll probably add it fairly soon if I need it or time permits (does it ever?)

License

Use the above code freely, with two conditions:

  1. Leave my name and link in there 😉
  2. Report bug and improvements back to me
Advertisements

About Søren Nielsen
Long time SharePoint Consultant.

36 Responses to Propagate Site Content Types to List Content Types

  1. Thomas Carpe says:

    Very helpful and thought provoking article. I’ll be putting your code to use later. 🙂 For now, I thought you might like to know that I’ve cracked (part of) the mystery of the inherited content type in XML. Check out the link to see the article I wrote, and thanks for getting me started in the right direction!

  2. Pingback: Lock Down Security: My Best Practices (and restoring usability) « Thoughts on computing, SharePoint and all the rest

  3. Mats Boisen says:

    A really good article. Thanks a lot for giving me valuable insight.

    While looking into our own implementation we discovered the SPContentTypeUsage class. It will return a list of all content types based on a given content type. By using it you do not have to recurse through all sub webs and check the lists.
    It ought to be significantly faster.

    // Mats

  4. Mats: That sounds very interesting. I’ll look into it when I update it the next time.

    Thanks for the input.

    I really need to include those site/web scoped content types as well…

  5. Pingback: Audit your Content Type Hierarchy « Thoughts on computing, SharePoint and all the rest

  6. Lylelf says:

    thanks much, man

  7. Pingback: Dr. SharePoint : Featurega tehtud Content Type'i muutmine

  8. Dan says:

    Excellent article! I’m not finding much on the web about some of the nuances of site vs list content types, so I was ecstatic to run across yours!

    I’m a little concerned about our planned implementation of our MOSS ’07 site after reading this, though. The idea was that we could create custom content types for all the types of lists that we have on our sub-webs and then if a new field was necessary, changing the content type would propogate the change to all the inherited types. Now after reading this article, it sounds like this is quite a fragile relationship between lists and content types, which greatly concerns me.

    Are you suggesting that lists based on custom content types may not be the most maintainable/upgradable way to go?

  9. Pingback: Dr. SharePoint : Dokumendiliikide ja metadata väljade update

  10. xsolon says:

    I just wanted to share this fix.
    I created a list from a content type and therefore content type management of the list wasn’t enabled

    so this line in ProcessWeb
    if (list.ContentTypesEnabled)
    was false when in fact the list is using the content type

    Instead I replaced this line with a call to this function:

    bool ListUsesContentType(SPList list, SPContentType contentType)
    {
    try
    {
    if (list.ContentTypes.Count == 0)
    return false;

    foreach (SPContentType cType in list.ContentTypes)
    {
    if (cType.Parent == contentType)
    return true;
    }
    }
    catch (Exception ex)
    {
    Log(ex.Message);
    }

    return false;
    }

  11. Simon says:

    Hi Soren,
    This is fantastic. Couple of things that I have updated in your code. As you mention when a content type is assigned to a list it becomes a new child content type is assigned to the list and a new contenttypeid is assigned.

    I had an issue where a content type on a list had a different name to the parent content type and therefore the stsadm command didnt find it.

    To fix this I modified the code so that there is a new private class member called contentTypeID, a new private property ContentTypeID and the ProcessWeb function is updated to:-

    private void ProcessWeb(SPWeb web, SPContentType sourceCT)
    {
    //Do work on lists on this web
    Log(“Processing web: ” + web.Url);

    //Grab the lists first, to avoid messing up an enumeration
    // while looping through it.
    List lists = new List();
    foreach (SPList list in web.Lists)
    {
    lists.Add(list.ID);
    }

    foreach (Guid listId in lists)
    {
    SPList list = web.Lists[listId];

    if (list.ContentTypesEnabled)
    {
    Log(“Processing list: ” + list.ParentWebUrl + “/”
    + list.Title);

    SPContentType listCT=null;
    try
    {
    SPContentTypeId childCTId = list.ContentTypes.BestMatch(ContentTypeID);
    //validate that the contenttype returned via Best Match is actually is related to the content type!
    if(childCTId.IsChildOf(ContentTypeID))
    listCT = list.ContentTypes[childCTId];

    }
    catch (Microsoft.SharePoint.SPException spex)
    {

    }
    catch (Exception ex)
    {

    }
    I also added the try/catch as I was seeing getting an error 0x81070215 which is mostly likely down to my dev environment.

    I have started to use this on our test system and will keep you updated if anything new is learnt.

    Thanks for your hard work though – the code works a dream!

    Hope that helps.
    Regards
    Simon

  12. Lily Collins says:

    Sorry for a very basic question. I just want to make sure I am doing the steps correctly.

    I am creating an stsadm extension to propagate content types changes – being specific I want to change the fields available in a content type used in a page layout).

    I have updated the site columns and site content types by upgrading and reactivating the feature solutions that contain the original definitions.

    I can see the new columns in the Site Settings > Site Column Gallery and Site Settings > Site Content Type Gallery > Site Content Type.

    I know I can’t see the columns in the List Content Type as I need to propagate them, hence the reason for the stsadm extension.

    My questions.

    1. Am I correct in making my changes to the original feature solution? i.e. to add the Site Columns/update the Site Content Type.

    2. Is it better to use an stsadm extension or a Feature Receiver or are they just two potential ways of doing the same thing?

    Thank you.

    • Sorry for slow response.

      Hi Lily

      You are correct – right place to update 🙂

      Feature receiver or stsadm command is interchangeable. Depends on how easily you want to kick it off. I consider it a very useful operation, however not one I do lightly therefore it’s not a receiver in my deployments.
      You risk overwritting some (legitimate) customizations on the list content type when you propagate, therefore I’ll want to doublecheck before I press the button.

  13. LordFlies says:

    Does it propagate a new Event Receiver ???

  14. xsolon and simon

    Thank you for sharing your fixes 🙂

    I guess the point is that it’s impossible to fully test and anticipate the behaviour in other installations.

  15. Pingback: Propagating Content Type Changes in MOSS 2007 « Brandon.ToString()

  16. John says:

    Thanks, you’ve broken my sharepoint site.

    Don’t update the display name property, that won’t work and will cancel the effect of re-applying the field. You should trust the fact that you are re-attaching the field, no updates on display name needed.

    Cheers

    • you’re welcome ;-( Please tell me that you did this on your test environment first..

      To be honest I don’t believe that I tested it with display names.

      I’m fairly certain that they follow the same rules as every other property in regards to whether or not they update automatically (i.e. only updated at the very root and subsequently created instances).

      You should probably take note of the fact that the code actually uses the displayname to figure out what fields it should touch.

  17. Pingback: Propagating Updates to Content Types « Johan Leino

  18. mario says:

    in your method UpdateListFields isnt the line

    if (!FieldExist(sourceCT, sourceFieldLink)) {

    always true? Why would it not? Maybe the line must be:
    if (!FieldExist(listCT, sourceFieldLink)) {

    Thanks

  19. Manoj says:

    I am problem in fields display name. It’s showing internal name as display name. Can some anyone help on this? Thx.

  20. Priya says:

    Hi Soren,
    The above solution is not propagating the readonly, ShowInNewForm,ShowInEditForm attribute changes of FieldRef. Do you have any idea how to achieve that?

  21. Perry says:

    How about checking if the content type is readonly or sealed, and then temporarily removing those properties, and resetting them afterward?

    That allows the content type to be kept as Sealed or ReadOnly from the users’ perspective, but it will can be updated via administrator-deployed code.

  22. Pingback: Principles of SharePoint Development Structures and Packaging | Raam's Blog

  23. Pingback: CleverWorkarounds » Sack Justin Bieber with SPD2010 and Forms Services – Part 2

  24. satheesh says:

    Removing of columns like below is just disconnecting the list columns with the associated content type.

    listCT.FieldLinks.Delete(sourceFieldLink.Id);
    listCT.Update();

    When i tested the above, the fields are still physically there in the list though they are removed from the site content type.
    I had to remove the fields physically afterwards from the list (In the code though). If you do not do, then the list content type shows the field as disconnected from its parent content type.

  25. xiao says:

    Romantic pink is a tender pretty girl can best embody the links of london color; the stars also began to become followers of and romanticpink, aroused a sweet and romantic agitation. See below nine groups pink star.

  26. Pingback: Propagate Content Type Changes « SharePoint Automation

  27. Pingback: Making Features that Deploy Content Types Compatible with the Content Type Hub « Nick Hadlee's Blog on SharePoint and Other Occasional Rants…

  28. Pingback: Confluence: Sharepoint

  29. Wilfred says:

    Hi Soren,

    Thanks for such thorough blog.

    In the SP Object Model, there is a Update method for SPContentType that allows you to specify whether to update child.

    I have been trying to use that, i.e. myCT.Update(true), however, it keeps returning “The security validation for this page is invalid”.

    Have you come across this?

    • Søren Nielsen says:

      It sounds to me like you are hitting the standard security validation mechanism in SharePoint. The easy way out is to set “allowunsafeupdates” to true on your site or web (will only affect your request).

      I think you should have a look at the spcontenttypeusage property (google it) that lists where a particular content type is in use – then you can manually update them if ghosting is important to you. Update content type with children will likely disconnect it from the XML source.

  30. Pingback: Updating declarative XML content types « Thornton Technical

  31. Darya says:

    Thanks for posting this blog. Saved me a TON of time!

  32. Pingback: >SharePoint Content pushdown | Brian

  33. Pingback: Propagate Site Content Types to List Content Types | Serge Ulyan

Leave a Reply

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

WordPress.com Logo

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

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s

%d bloggers like this: