<CharlieDigital/> Programming, Politics, and uhh…pineapples

18Feb/11Off

Most Annoying Thing About SharePoint 2010?

I've been bashing my head against a SharePoint 2010 solution package for almost 10 hours now across two days.  I mean god forbid Microsoft provides us poor developers with some useful error messages or even a hint of what's going on inside that crazy contraption.

It seems that other's have also encountered this problem, but I'll summarize here: in SharePoint 2007, when you create a custom list template and list definition, you could associated a custom content type with the list quite easily (or so I thought...more on this in a sec).  Here's an example from my schema.xml file:

<?xml version="1.0" encoding="utf-8"?>
<List xmlns:ows="Microsoft SharePoint"
	Title="Basic List" FolderCreation="FALSE" Direction="$Resources:Direction;"
	Url="Lists/Basic List" BaseType="0" EnableContentTypes="TRUE">
	<MetaData>
		<ContentTypes>
			<ContentTypeRef ID="0x0100C171C000000000000000000000000003"/>
			<ContentTypeRef ID="0x0120" />
		</ContentTypes>

<!-- additional markup omitted -->

I can then use the content type fields in the views without having add the fields or field references separately:

<ViewFields>
	<FieldRef Name="LinkTitleNoMenu"></FieldRef>
	<FieldRef Name="Request_Name" ></FieldRef>
	<FieldRef Name="Request_Status" ></FieldRef>
	<FieldRef Name="Requestor" ></FieldRef>
	<FieldRef Name="Ticket_Project_Number" ></FieldRef>
	<FieldRef Name="Ticket_Group" ></FieldRef>
	<FieldRef Name="Support_Ticket_Number" ></FieldRef>
	<FieldRef Name="Support_Ticket_Name" ></FieldRef>
	<FieldRef Name="Support_Ticket_Status" ></FieldRef>
	<FieldRef Name="Support_Ticket_Res_Date" ></FieldRef>
	<FieldRef Name="Technical_Manager" ></FieldRef>
</ViewFields>

In SharePoint 2010, this doesn't work. Let me rephrase this: it doesn't work 100% correctly.  The default behavior seems to be that the content type will be copied over, but the fields aren't copied over...

Well in fact, Microsoft's documentation states that this is as designed:

When SharePoint Foundation creates a list instance, it includes only those columns that are declared in the base type schema of the list or in the list schema. If you reference a site content type in the list schema, and that content type references site columns that are not included in the base type schema of the list or in the list schema, those columns are not included. You must declare those columns in the list schema for SharePoint Foundation to include them on the list.

What you'll get is the content type will indeed be on the list, but the fields won't be copied over and any views that use those fields will be borked.  This is a really annoying problem.  Really, it is.

The solution is to either build some tool that does an XSL transform on my content types and produces the views (thereby avoiding the necessity of copying the fields over manually) or write some code to fix it after the list has deployed.  But it's problematic because I want to deploy data with my list instance using markup like so:

<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
    <ListInstance
		Title="My Custom Configuration"
		Id="18000"
		FeatureId="ACA62A34-7B27-442D-97C6-55FC3F55A2BF"
		TemplateType="18000"
		Url="Lists/MyCustomConfiguration"
        OnQuickLaunch="TRUE">
      <Data>
          <Rows>
              <Row>
                  <Field Name="ID">1</Field>
                  <Field Name="Title">some.setting.name</Field>
                  <Field Name="Configuration_Value">http://myserver.mydomain.com</Field>
                  <Field Name="Default_Value">http://myserver.mydomain.com</Field>
                  <Field Name="Usage_Description">The URL of the something something server</Field>
                  <Field Name="Property_Name">SomethingSomethingServer</Field>
                  <Field Name="Property_Type">string</Field>
              </Row>
          </Rows>
      </Data>
    </ListInstance>
</Elements>

I need to be able to deploy the content type to the list automatically.  (If you're not deploying default data with your list, the ContentTypeBinding element may do the trick.)

So I fumbled around with this for a day and a half while bitching and moaning about it to anyone who would listen to me.  Did I screw up the content type somehow?  Did I screw up the list template somehow?  Did I mess up an ID somewhere?  I mean, I've done this a million times in the past in a million different solution packages and even in SharePoint 2010...what the heck was different about this package?

Finally (in fact, just moments ago!) it dawned on me that I've always "primed" my content types when deploying them in my previous solutions based on some findings from Martin Hatch!

You do not have to declare custom fields in the schema.xml, as long as you manually update your content type before creating any libraries.

Bingo!

Martin links to a code sample, but the gist of it is pretty simple.

  1. Wire up a feature receiver for your content type feature.
  2. On feature activation, "prime" the content types that have a specific group name
  3. ???
  4. Profit!

Okay, the profiting part is questionable, the the rest of it isn't!

The first part is to wire up the feature for a feature receiver.

<Feature Id="0C4C1210-B056-4AFC-B556-A8BB30E1A9F1"
         Title="My Custom Content Types"
         Description="Installs the content types."
         Hidden="FALSE"
         Scope="Site"
         DefaultResourceFile="core"
         ReceiverAssembly="MyAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=56f39f66bb4e1d17"
         ReceiverClass="MyAssembly.Components.FeatureReceivers.MetadataPrimer"
         xmlns="http://schemas.microsoft.com/sharepoint/">
    <ElementManifests>
        <ElementManifest Location="my_contenttypes.xml" />
    </ElementManifests>
    <Properties>
        <Property Key="prime.if.contains" Value="My Custom Content Type"/>
    </Properties>
</Feature>

Note that I've added a feature property here: a key called "prime.if.contains".  This allows me to specify the group names of the content types that I want to prime.  That way, I don't have to prime all of the content types.

I create a base class for my feature receivers:

/// <summary>
///     Abstract base class for custom feature receivers.
/// </summary>
/// <remarks>
///     All custom feature receivers should be updated to inherit from this class for common functionality.
/// </remarks>
public abstract class FeatureReceiverBase : SPFeatureReceiver
{
	/// <summary>
	///     Gets the <c>SPWeb</c>.
	/// </summary>
	/// <remarks>
	///     This method allows the feature receiver to obtain a reference to the current <c>SPWeb</c>
	///     regardless of whether there is an <c>HttpContext</c> (i.e. in the case that the feature
	///     is activated from <c>STSADM</c>).
	/// </remarks>
	/// <param name = "properties">The properties.</param>
	/// <returns>The <c>SPWeb</c> instance from the properties.</returns>
	protected static SPWeb GetWeb(SPFeatureReceiverProperties properties)
	{
		SPWeb web;

		if (SPContext.Current != null && SPContext.Current.Web != null)
		{
			web = SPContext.Current.Web;
		}
		else if (properties.Feature.Parent is SPWeb)
		{
			web = (SPWeb) properties.Feature.Parent;
		}
		else if (properties.Feature.Parent is SPSite)
		{
			web = ((SPSite) properties.Feature.Parent).RootWeb;
		}
		else
		{
			throw new Exception("Unable to retrieve SPWeb - this feature is not site or web-scoped.");
		}

		return web;
	}
}

And then I add the primer class:

/// <summary>
///     <para>Receiver which "primes" the content types by updating each of the custom content types to push the changes down.</para>
///     <para><see cref = "http://mkeeper.spaces.live.com/blog/cns!60F12A60288E5607!278.entry" /></para>
/// </summary>
public class MetadataPrimer : FeatureReceiverBase
{
	private const string _primerKey = "prime.if.contains";

	/// <summary>
	///     Handles the feature activated event by priming content types specified by the primer key property.
	/// </summary>
	/// <param name = "properties">The properties.</param>
	public override void FeatureActivated(SPFeatureReceiverProperties properties)
	{
		Console.Out.WriteLine("Activating Metadata Primer");

		// Prime the content types.
		SPWeb web = GetWeb(properties);

		SPContentTypeCollection contentTypes = web.ContentTypes;

		List<string> contentTypeIds = new List<string>();

		// Capture the feature properties.
		Dictionary<string, string> featureProperties = new Dictionary<string, string>();

		foreach (SPFeatureProperty property in properties.Feature.Properties)
		{
			featureProperties[property.Name] = property.Value;
		}

		// Must find the key "prime.if.contains"
		if (!featureProperties.ContainsKey(_primerKey))
		{
			ExceptionFactory.Throw<ArgumentException>(
				"The property prime.if.contains is not defined in the feature.");
		}

		string[] tokens = featureProperties[_primerKey].Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries);

		Log("Priming content types containing: {0}", featureProperties[_primerKey]);

		foreach (SPContentType contentType in contentTypes)
		{
			bool prime = tokens.Any(token => contentType.Group.ToUpperInvariant().Contains(token.ToUpperInvariant()));

			if (!prime)
			{
				continue; // EXIT: Next iteration.
			}

			contentTypeIds.Add(contentType.Id.ToString());

			Log("Adding content type \"{0}\" ({1}) for priming", contentType.Name, contentType.Id);
		}

		// Do it in two loops since doing it one causes issues if a parent content type is primed (child content types
		// fail to prime because the parent changes).
		foreach (string id in contentTypeIds)
		{
			try
			{
				SPContentTypeId contentTypeId = new SPContentTypeId(id);

				SPContentType contentType = web.ContentTypes[contentTypeId];

				Log("Priming content type: {0}", contentType.Name);

				contentType.Update(true, false);
			}
			catch (Exception)
			{
				Log("Failed to prime content type with ID: {0}", id);
			}
		}
	}

	/// <summary>
	///     Logs the specified message to both log4net and the console.
	/// </summary>
	/// <param name = "message">The message.</param>
	/// <param name = "args">The args.</param>
	private static void Log(string message, params object[] args)
	{
		Console.Out.WriteLine(message, args);
	}

	/// <summary>
	///     Features the deactivating.
	/// </summary>
	/// <param name = "properties">The properties.</param>
	public override void FeatureDeactivating(SPFeatureReceiverProperties properties) {}

	/// <summary>
	///     Features the installed.
	/// </summary>
	/// <param name = "properties">The properties.</param>
	public override void FeatureInstalled(SPFeatureReceiverProperties properties) {}

	/// <summary>
	///     Features the uninstalling.
	/// </summary>
	/// <param name = "properties">The properties.</param>
	public override void FeatureUninstalling(SPFeatureReceiverProperties properties) {}
}

(Note: remove or replace the usages of ExceptionFactory -- this is just a custom exception helper that I use)

You can see, the gist of it is that the code will iterate all of the content types at the site, find ones that belong in the given groups, and them prime the content type by calling SPContentType.Update(bool, bool).

So there you have it; it didn't work in 2007 either, but I had completely forgotten that I had written the primer for this express purpose.  With this simple bit of code, you can save yourself the hassle of having to copy fields from your content type to your schema.xml definition file when creating custom list definitions for SharePoint and all of the fields are nicely copied over from your site level content type to your list instance without having to manually modify the schema.xml file for your list.

Posted by Charles Chen

Filed under: .Net, SharePoint Comments Off
Comments (0) Trackbacks (0)

Sorry, the comment form is closed at this time.

Trackbacks are disabled.