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.

SharePoint 2010 - ListInstance CustomSchema attribute

The CustomSchema attribute of the ListInstance feature element is a new attribute introduced in SharePoint 2010. What it does is pretty simple – it allows you to change the metadata of the list instance that you create (I was complaining about the lack of this capability in SharePoint 2007). The idea is pretty simple – in the CustomSchema attribute you specify the feature root relative path of a list schema file which is actually a normal list definition schema.xml file and that is basically it – you have all metadata elements from the schema in your new list instance. So, it doesn’t seem like a big deal because you can achieve the same thing with a custom list definition and a list instance based on it – and yes this is obviously the preferred thing to do when you have many instances that should be based on the same list schema. But on the other hand if you have many list instances all with different schemas (basically with some minor customizations – several new fields and some list views) you can go on with this kind of short-cut implementation which is far more economical. Another good thing to know here is that since the list schema file is the same as in a list definition you can always promote it to a custom list template definition (with just adding a list definition element and moving the schema to it) in case you need to create more than one list instance based on that schema. The opposite thing is also possible – you can “demote” a list definition item with just keeping its schema for the CustomSchema attribute of a ListInstance and removing the list definition element itself. Note here that the list schema file that you use in the CustomSchema should contain all major elements of the List/MetaData section – ContentTypes, Fields, Views, Forms – it’s not like the ListInstance element uses its base list definition (specified in the FeatureId attribute) and applies the CustomSchema as modifications on top of it – actually the list schema specified in the CustomSchema attribute should be a fully-fledged list schema and if you miss some of its elements you will receive various errors on feature activation or later when you are using the list. And since you have a fully-blown list schema it is that straightforward to have the schema reused to or from a custom list definition.

Another advantage of using a ListInstance with CustomSchema is that it can be tested very easily: you don’t need an iisreset (or recycle the app pool) after you make a change in the list schema – you just need to delete the list instance and create it again. Actually you can first create and test your list schema this way and only after put it in a custom list definition.

Another thing that may be bothering is the size of the list schema files – you remember the huge schema.xml files containing tons of CAML in SharePoint 2007 – well, this is not the case any more – except several mandatory items you can now put mostly the customizations that you need and the schema file is still pretty small – several kilobytes or so – I will show you a sort of minimalistic schema.xml file below.

So here is a sample ListInstance element with a CustomSchema attribute:

  <ListInstance Title="ListInstance2"

                OnQuickLaunch="TRUE"

                TemplateType="100"

                FeatureId="00bfea71-de22-43b2-a848-c05709900100"

                Url="Lists/ListInstance2"

                CustomSchema="ListInstance2/Schema.xml"

                Description="">

And the custom (minimalistic) list schema file that I used for it:

<List xmlns:ows="Microsoft SharePoint" Title="Basic List" EnableContentTypes="TRUE" FolderCreation="FALSE" Direction="$Resources:Direction;" Url="Lists/Basic List" BaseType="0" xmlns="http://schemas.microsoft.com/sharepoint/">

  <MetaData>

    <ContentTypes>

      <ContentTypeRef ID="0x0100678499b7e7024385820d8586270c1a75" />

    </ContentTypes>

    <Fields></Fields>

    <Views>

      <View BaseViewID="1" Type="HTML" WebPartZoneID="Main" DisplayName="$Resources:core,objectiv_schema_mwsidcamlidC24;" DefaultView="TRUE" MobileView="TRUE" MobileDefaultView="TRUE" SetupPath="pages\viewpage.aspx" ImageUrl="/_layouts/images/generic.png" Url="AllItems.aspx">

        <XslLink Default="TRUE">main.xsl</XslLink>

        <RowLimit Paged="TRUE">30</RowLimit>

        <Toolbar Type="Standard" />

        <ViewFields>

          <FieldRef Name="Attachments" />

          <FieldRef Name="LinkTitle" />

          <FieldRef Name="Comments" />

        </ViewFields>

        <Query>

          <OrderBy>

            <FieldRef Name="ID" />

          </OrderBy>

        </Query>

      </View>

    </Views>

    <Forms>

      <Form Type="DisplayForm" Url="DispForm.aspx" SetupPath="pages\form.aspx" WebPartZoneID="Main" />

      <Form Type="EditForm" Url="EditForm.aspx" SetupPath="pages\form.aspx" WebPartZoneID="Main" />

      <Form Type="NewForm" Url="NewForm.aspx" SetupPath="pages\form.aspx" WebPartZoneID="Main" />

    </Forms>

  </MetaData>

</List>

A short explanation about the list schema above – so you see all major elements – ContentTypes, Fields, Views and Forms in the MetaData section – the Fields element even empty should be present (yes, otherwise you will receive an error on feature activation). In the ContentTypes element there is a single ContentTypeRef element which attaches a site content type to the list, the Fields element should normally contain all fields that should exist in the list, including the ones that are in the attached content types. A pretty nasty thing here of having to define a set of fields first as site columns and then in every list definition that uses the containing content type, isn’t it. I will demonstrate a work-around for the field redefinition issue in one of the next postings. Basically instead of ContentTypeRef you can use a ContentType element and define an inline list content type which uses site and/or list columns (both types alike provided in the Fields element).

In the Views element I’ve defined a single list view – pretty simple at that. You can see several FieldRef elements in the ViewFields of the view, the last one is actually for a field from the referenced site content type  - don’t worry that the field is missing in the Fields element – this won’t break the activation of the feature and can be fixed afterwards. The last metadata element is the Forms one – it is simply copied from a standard list definition schema and just defines the new, edit and display forms of the list.

The last bit in the sample schema worth noting is the EnableContentTypes attribute in the schema root element. It should be set to true even if you have just one content type in the list, otherwise in the new item menu in the all items view page of the list you will see the default “new item” label instead of the actual name of your content type.

Monday, March 1, 2010

SharePoint 2010 - WebTemplate feature

The new WebTemplate feature element in SharePoint 2010 was introduced mainly as a way to define site definitions in sandbox solutions (since you can't use the traditional site definitions in sandboxes). Still, this doesn't mean that you can't use WebTemplate features in a farm solution - there they can be used as an alternative to the old-style site definitions. About the pros and cons of the WebTemplate feature element – one thing that I can see at this point (we’re still lacking complete documentation on that) is that you may have one or more site definitions in a single feature and you have the option to have site definitions in separate features within a single SharePoint project. So, instead of having to maintain two different types of artifacts – features and traditional site definitions you may have the latter as a feature or a set of features too – this allows for finer control over what site definitions you may have in your farm – you can install and uninstall the WebTemplate features thus enabling or disabling the various site definitions that you have created instead of having to remove the whole solution to get the traditional site definitions removed.

Let me now show you how you can create a WebTemplate feature – so you can start with an empty SharePoint project (or with an existing project of yours) and add an empty SharePoint element to it. You can leave the default name of the element – e.g. EmptyElement1 but it is a good idea to give it the same name as you will put in the Name attribute of the WebTemplate element. I will explain shortly why this is necessary. In the elements.xml file of the empty element item you need to have the WebTemplate element’s XML similar to this one:

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">

  <WebTemplate Name="SimpleSite" Title="Simple site" BaseTemplateID="1" BaseTemplateName="STS" BaseConfigurationID="0" DisplayCategory="Simple sites" />

</Elements>

In the WebTemplate element you have four mandatory attributes – Name, BaseTemplateID, BaseTemplateName, BaseConfigurationID. While the Name attribute defines the internal name of the site template (that you can use with the object model to create sites based on that definition), the other attributes though required and seemingly specifying something like a site definition inheritance don’t actually do that. At least while I tested I didn’t see that any features or module elements defined in the base site definition got activated when a site based on the new one was created. The exact values for these attributes you can check in the webtemp.xml file or derived webtemp*.xml files in the 14/1033/XML folder. Two other attributes that are important though not mandatory are the Title and the DisplayCategory ones – the former specifies the display name of the site template that will appear in the create site collection or create site application pages and the latter - the category tab under which the template will appear on these pages.

After you have the WebTemplate element in the elements.xml file, you will need the onet.xml file which is actually exactly the same format as we know from the traditional site definitions. As was the general practice with SharePoint 2007 when we used to copy-paste and later modify onet.xml files from the standard site definitions when creating custom ones in this sample I just copied the onet.xml from the STS site definition without any modifications whatsoever. There is one difference however in the onet.xml files for WebTemplate feature elements and the traditional ones – in the former you can have just one Configuration element – actually there may be more than one in your onet.xml (as in the STS onet.xml) but just the one with ID=0 will be used. There is something important about the onet.xml file here – it needs to be in a subfolder of the feature’s root folder with the same name as the Name attribute of the WebTemplate element. That’s why it is handy to have the element item’s name the same as the Name attribute as I mentioned above, otherwise you will need to modify the DeploymentLocation property of the onet.xml file. Note also that the DeploymentType property of the onet.xml (and of the other files that you may have in the site definition) should be set to ElementFile.

The last thing that you need to set is the scope of the feature project item that contains the WebTemplate element item – the scope should be set to Farm – this is the only possible value for farm solution WebTemplate elements. The thing with farm scoped features is that you don’t need to activate them, simply installing them makes them available in the SharePoint farm – so basically you only need to deploy the containing solution to the farm to have the site definitions visible.

Here is a small screenshot of the solution explorer view of the sample project that I created for demonstrating the WebTemplate element:

testdef

So you see that besides the elements.xml and onet.xml files I also added two aspx files to the WebTemplate site definition – they are actually also copied from the standard STS template and are provisioned with a Module element inside the onet.xml file (actually this is true only for the default.aspx file since only the default configuration from the onet.xml file is used here).

After you deploy the solution containing your WebTemplate feature you will be able to see your new site template in the create site collection page in the central administration site – as in the case with my sample solution:

sitetempl

[Updated: 28 Sep 2011]

You can download the sample solution from here. Note – when you create a new site based on this sample site definition it will be exactly the same as look similar to the standard team site template – remember that I simply copied the onet.xml from the standard STS site definition. Note that all the features from the ONET.XML file of the standard STS site definition will be activated in the site based on this custom WebTemplate definition, but in the case of the standard team site definition the newly created site will have additionally all stapling features targeting the standard team site definition (whose number is quite big actually). This is an important difference to have in mind, because the feature stapling mechanism works only with the old style site definitions and not with WebTemplate definitions.

And one list thing here, quite important though – if you need to create a site collection or a site based on a WebTemplate site definition with the object model (or in a portal site definition manifest file) you need to provide the web template name in the following format:

{[id of the WebTemplate feature]}#[Name of the WebTemplate element]

In my sample solution the exact name is this:

{797fb1e4-d407-4599-9853-9a0adbaef1c7}#SimpleSite

This is because the ID of the WebTemplate feature is 797fb1e4-d407-4599-9853-9a0adbaef1c7 and the Name attribute of the WebTemplate element is SimpleSite. Note that you need the curly braces around the feature’s ID.