Thursday, July 02, 2009

List Content Type Fields & Forms: CAML vs Code

In a feature based site definition I recently made, I had a site content type hierarchy with a base content type and two child content types defined in a feature. The child types inherits their site columns and display/edit/new form from their parent.

Another dependent feature contained a list definition and a provisioned list instance that would reference the two child site content types and thus snapshot inherit them as list content types.

The columns of the list are based on the inherited site columns, in reality a snapshot copy of them at the time the list was created. All provisioning of site columns, site content type, list definitions, list instances and list content types are defined in CAML in the features.

As you can see from the above figures, everything looked fine and dandy. Until I tried to create a new list item: all the custom fields where missing in the new item form. The same problem applied to the display and edit forms also.

Using the "List settings" UI to remove and then add the list content types manually proved to fix the problem, so it had to be an issue related to the way CAML <MetaData/ ContentTypes/ ContentTypeRef> makes the snapshot copy.

Knowing that the fields of a form are rendered using the <ListFieldIterator>, it was time to check the field collection of the list content types using SharePoint Manager 2007.


In the above figure site content types are to the left and list content types to the right. The "News article" content type added through the "List settings" UI has inherited a full snapshot, but the "Stand-alone article" added by CAML has inherited just the ootb BaseType "Item" fields. So the <RenderingTemplate> for NewForm finds only two fields to render. And the actual problem is in the CAML inheritance.

The problem is that the list columns do not inherit the site columns correctly. In fact, the only official way of making <ContentTypeRef> work correctly using CAML is to repeat the site column definition inside the list definition <Fields> element. This is not a good approach with regards to maintenance of your solution.

There is a similar problem with list content types related to site content types, which is easily rectified by forcing a ContentType Update(push down changes) during provisioning of the site content type feature (see exercise 13-5 in the Building the SharePoint User Experience book).

The list feature is activated after the site content type feature. You might think that you could force another site content type update to rectify the inheritance again. That isn't very feasible, as updating child content types depends on a having actual changes to push down; and there is none at this time - remember that the site content types have already been provisioned and pushed down. There are some "best" practices out there on Custom Fields in Schema.xml, but please read on before trying those.

Just adding the list content types from code will create correct snapshots with no fuzz:

"When you add a site content type to a list using the object model, Windows SharePoint Services automatically adds any columns that content type contains that are not already on the list. This is in contrast to provisioning a list schema with content types, in which case you must explicitly add the columns to the list schema for Windows SharePoint Services to provision them."

So instead of doing this using CAML, just add them when activating the list feature. The newly created list instance is empty, so the content types can easily be added:

public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
using (SPWeb web = (SPWeb)properties.Feature.Parent)

{
//enable management of content types
SPList list = web.Lists["News"];
list.ContentTypesEnabled = true;
list.Update();
//inherit the list content types from site content types
list.ContentTypes.Add(web.ContentTypes["Stand-alone article"]);
list.ContentTypes.Add(web.ContentTypes["Series article"]);
list.Update();
//remove item as list content type
list.ContentTypes["Item"].Delete();
list.Update();
}
}

NOTE: This will only work for list instances created by the feature. The code will not run for list instances added later on - and there is no ListAdding/ListAdded events in WSS3.0/MOSS. For lists added manually, the list content types must also be added manually. Or create an "associate [my] list content types" feature that you can re-activate at will to bind the content types to lists as applicable. Use a marker content type to look for to avoid hardcoding the list names. Some like to use a timer job to bind the list content types.

The code gets a reference to the list instance, turns on content type management / content types on the new menu, adds the list content types as new snapshot copies, and finally removes a dummy "Item" content type (that is there to make the CAML list definition valid).

My changed list definition Schema.xml looks like this:

As you can see, I have commented out the CAML <ContentTypeRef> for my two child site content types, and instead just referenced the standard "Item" content type - the dummy that is removed during feature activation. If you still would like to have those <ContentTypeRef> in the Schema.xml, then remove and re-add them from the code.

Note how you still can add your custom fields as <ViewFields> even if their content type is not referenced. Content type management for the list can also be turned on using the EnableContentTypes attribute on the root <List> element in Schema.xml.

That's it. I deleted the list and reactivated the list feature - and voila: the new, edit and display forms contains all fields of the content type selected from the new menu.

[UPDATE] SharePoint 2010 adds the Inherits attribute that makes CAML inheritance work as expected, and using FieldRefs unnecessary. See also the new 2010 feature upgrade mechanism UpgradeActions AddContentTypeField element with the PushDown attribute that force changes to a site content type and all derived types.

2 comments:

Anonymous said...

You can still do this in your caml. SharePoint is COPY BY VALUE and requires you to create the columns. CAML will not create the columns like doing it through the UI will for you.

IE...you must add the Fields to the List definition that are also defined in the content type definition.

Another drawback of Copy By Value. It is by design.

Kjell-Sverre Jerijærvi said...

Yes, as it says it the post, you can always make this work with copying CAML between related definitions, but that will cause extra maintenance efforts to keep all copies in synch.

Luckily, ListAdded is in the MSS2010 SDK public preview.