Monday, March 8, 2010

ContentTypeBinding vs ContentTypeRef and the fields’ redefinition issue

These two elements do basically the same thing – attaching site content types to a SharePoint list. So there’s been the question now and then which one to use and what are the advantages and disadvantages of the two approaches.

So, let me start with several words about the two elements - first the ContentTypeBinding element:

  • the ContentTypeBinding element is actually a feature manifest element (like the ListInstance or ListTemplate ones) while the ContentTypeRef is just an XML element from the list schema file of a list definition (which may be used directly by a ListInstance element as specified in the CustomSchema attribute – see my previous posting on that)
  • it attaches site content types to existing SharePoint lists – this also means that the element is updateable – if you activate the feature containing the ContentTypeBinding element to a site a second or consecutive times with the force parameter set and the content type is missing in the list it will be attached again.
  • this one is an important advantage – all fields of the content type that are not already present in the destination list are automatically provisioned.
  • and one disadvantage – you can attach content types but you cannot delete a content type from a list or change the content type order or visibility of the content types in the list declaratively – basically this means that after you attach your content type to a list you need to bother about the default “Item” or “Document” or “Page” that remains in the list or document library (unless you need the default content type as well).

Second - the ContentTypeRef element:

  • It is actually an XML element in the list schema file as I mentioned above
  • One ugly thing about it is that you specify a site content type to be attached to the list based on that list definition but the framework doesn’t provision the fields in the content type if they are missing in the list – so you need to add manually all content type’s fields in the Fields element of the list schema file. This is actually what I called the fields’ redefinition issue in the posting’s title and it can get pretty unpleasant if you have a site content type used in many list definitions – then for a change in one of the content type’s fields you will need to make changes not only in the site column definition but in all list schema files that use the containing content type.
  • The list schema files in SharePoint 2010 can be used not only in list definition feature elements but directly in a ListInstance elements via the CustomSchema attribute, which is a pretty nice new feature.

So, having said that in my opinion the ContentTypeRef element which is packed directly into the list schema file is the neater approach having it not been for the ugly and uneconomical thing with the fields’ redefinition. And actually the thing is that this issue is quite solvable and I will mention two workarounds for it:

  1. I have some doubts about this approach since it seems like some sort of a side effect not a deliberate feature (I maybe wrong here though) – so it is actually pretty simple – when you define your content type you just add the new Overwrite attribute to its definition – when set to true this attribute forces the using of the object model for the content type creation – so the content type gets created directly into the content database:

<ContentType ID="0x0100678499b7e7024385820d8586270c1a75"

               Name="MyContentType"

               Group="Custom Content Types"

               Description="My Content Type"

               Overwrite="TRUE" >

    <FieldRefs>

      <FieldRef ID="{fa564e0f-0c70-4ab9-b863-0177e6ddd247}" Name="Title" Required="TRUE" ShowInNewForm="TRUE" ShowInEditForm="TRUE" />

      <FieldRef ID="{9da97a8a-1da5-4a77-98d3-4bc10456e700}" Name="Comments" DisplayName="Description" Required="FALSE" />

    </FieldRefs>

  </ContentType>

So, you see the Overwrite attribute set to true and another important thing – all fields that you want to appear in the item forms should be explicitly added in the definition – the content type “inheritance” for fields doesn’t seem to work in this case. So with these small modifications to the content type you don’t have to redefine the content type fields in the list schema file, though it doesn’t seem right to me that in order to fix one artifact you need to modify another. And it’s not only that – there is another side effect to this “side effect” – the site content type that you specify with the ContentTypeRef element gets added with the default “Item” name and default “Item” description. This may not be a problem in some cases (leaving declarative definitions only it’s a matter of a line of code to get that fixed in a feature receiver or just leave it as it is) but this is a serious problem when you want to attach more than one content types – then you simply receiver an error that two content types with the same name cannot be added – and this makes this approach practically unusable with more than one content types attached in the list schema (to be frank with you – I found a solution to that (wholly declarative), but it’s a bit dirty so I won’t mention it).

And then we come to the second approach of solving the redefinition issue:

  1. This one uses code – I just created a small method which provided with the SPFeatureReceiverProperties parameter of the feature receiver’s FeatureActivated method does the job. It simply iterates all ListInstance manifest elements with CustomSchema attribute using list schema files that you have defined in that feature (this doesn’t work for list definition elements, depending on the scenario it can be more complicated there) and adds the site fields used in the referenced content types to the new lists. And this is the code of the method itself:

        private void FixListSchemas(SPFeatureReceiverProperties properties)

        {

            // get the web we're activating the feature on

            SPWeb web = properties.Feature.Parent as SPWeb;

            if (web == null) return;

 

            // two handy maps for the site content types and fields - so that we can quickly look them up

            Dictionary<string, SPContentType> siteCTypes = web.ContentTypes.Cast<SPContentType>().ToDictionary(ct => ct.Id.ToString(), StringComparer.OrdinalIgnoreCase);

            Dictionary<Guid, SPField> siteFields = web.Fields.Cast<SPField>().ToDictionary(f => f.Id);

 

            // iterate over the feature's element definitions and pick the ListInstance ones containing CustomSchema attribute

            List<XElement> listInstanceElements =

            properties.Definition.GetElementDefinitions(CultureInfo.GetCultureInfo ((int)web.Language)).Cast<SPElementDefinition>()

                .Select(def => XElement.Parse(def.XmlDefinition.OuterXml))

                .Where(liel => liel.Name.LocalName == "ListInstance" && liel.Attribute("CustomSchema") != null)

                .ToList();

 

            // iterate the ListInstance elements

            foreach (XElement listInstanceElement in listInstanceElements)

            {

                // get the site relative list url from the Url attribute

                string listUrl = listInstanceElement.Attribute("Url").Value;

                // get the absolute path of the schema file - combining the feature's root folder and the value of the CustomSchema attribute

                string customSchemaPath = Path.Combine(properties.Feature.Definition.RootDirectory, listInstanceElement.Attribute("CustomSchema").Value);

                // get the SPList instance using the list url

                SPList list = web.GetList(web.ServerRelativeUrl.TrimEnd('/') + "/" + listUrl);

 

                // two handy sets for the list fields - one with the fields' IDs, the other with the fields' names

                HashSet<Guid> fieldIDs = new HashSet<Guid> (list.Fields.Cast<SPField>().Select(f => f.Id).Distinct());

                HashSet<string> fieldNames = new HashSet<string> (list.Fields.Cast<SPField>().Select(f => f.InternalName).Distinct());

 

                // load the custom schema file and extract the IDs of the available ContentTypeRef elements - some fancy LINQ to XML is used since the SharePoint xmlns may be there but may be missing as well

                List<string> contentTypeRefs = XDocument.Load(customSchemaPath).Root.Descendants()

                    .Where(el => el.Name.LocalName == "ContentTypeRef" && el.Parent.Name.LocalName == "ContentTypes")

                    .Select(el => el.Attribute("ID").Value)

                    .Where(id => siteCTypes.ContainsKey(id))

                    .ToList();

 

                // iterate the FieldRefs of all ContentTypeRef-s checking if the list already contains a field with the same ID and that there exists a site column with that ID

                foreach (SPFieldLink fieldRef in contentTypeRefs

                    .Select(cr => siteCTypes[cr])

                    .SelectMany(ct => ct.FieldLinks.Cast<SPFieldLink>())

                    .Where(fr => !fieldIDs.Contains(fr.Id) && siteFields.ContainsKey(fr.Id)))

                {

                    // get the corresponding site column for the current FieldRef

                    SPField siteField = siteFields[fieldRef.Id];

                    // get the field name

                    string fieldName = !string.IsNullOrEmpty(fieldRef.Name) ? fieldRef.Name : siteField.InternalName;

                    // check if the field name is unique - the list may not contain a field with the same ID, but may contain a field with the same internal name - so find a unique name here - SharePoint does the same when attaching content types to lists

                    string newFieldName = fieldName;

                    for (int i = 0; i < 1000; i++) { if (!fieldNames.Contains(newFieldName)) break; newFieldName = fieldName + i; }

 

                    // parse the site field's schema

                    XElement fieldSchema = XElement.Parse(siteField.SchemaXml);

                    // reset the Name and StaticName attributes with the new unique field name found

                    fieldSchema.SetAttributeValue("Name", newFieldName);

                    fieldSchema.SetAttributeValue("StaticName", newFieldName);

                    // reset the DisplayName if the FieldRef defines one - SharePoint does the same when attaching content types to lists

                    if (XElement.Parse(fieldRef.SchemaXml).Attribute("DisplayName") != null) fieldSchema.SetAttributeValue("DisplayName", fieldRef.DisplayName);

 

                    // create the new field in the list - use the SPAddFieldOptions.AddFieldInternalNameHint here, otherwise the DisplayName will be used as internal name

                    list.Fields.AddFieldAsXml(fieldSchema.ToString(), false, SPAddFieldOptions.AddFieldInternalNameHint | SPAddFieldOptions.AddToNoContentType);

                    // update the two list fields sets in case we hit the same FieldRef for another content type

                    fieldIDs.Add(siteField.Id); fieldNames.Add(newFieldName);

                }

            }

        }

You can check the comments in the code for a more detailed picture of the steps taken to add the site columns to the list instances.

So, to briefly recap – you can have your list schema files in SharePoint (at least for ListInstance elements) short and simple enough but … with a little pinch of code.

5 comments:

  1. Additionally, the ContentTypeBinding doesn't seem to work if you're using the element inside of the .

    If you are provisioning the list with data, you are forced to use the ContentTypeRef.

    ReplyDelete
  2. Hi Stefan,

    Can you share your trick that can allow adding multiple content types declaratively?

    Regards,
    Vamshi

    ReplyDelete
  3. Hi Vamshi,
    Unfortunately I can't remember what exactly the declarative solution was and when I checked my dev server I found out that I had deleted the original sample project for this posting that I had developed last year. If I find something or redescover the thing again I will post another comment.

    Greetings
    Stefan

    ReplyDelete
  4. I've run into the same issue with content types -- adding a content type using ContentTypeRef to a list yields the correct content type, but wrongly named. After some lucky decisions and lots of time, I found you could add the content type using ContentType and get the desired results.

    In my list definition (using SP2010)...
    <MetaData>
      <ContentTypes>
        <ContentTypeRef ID="0x0120" />
        <ContentType ID="0x010200D8D37F60760C40D2B845185E0F575EA5" Name="Content Type 1" />
      </ContentTypes>
    ...
    </MetaData>

    When I do this, I also avoid the fields redefinition issue you mention. For whatever reason, having the folder content type, and having it first, makes all the difference. So far, I have not been able to add multiple content type this way. However, I didn't spend too much time on it.

    Ken

    ReplyDelete
  5. I can confirm Ken Duenke, ContentTypeRef can be used like that very nicely without any fields redefinition issue

    ReplyDelete