in

SharePoint Blogs

The Best Place for SharePoint-related Blogs

Bob's SharePoint Bonanza

Deploying site columns and content types to sharepoint webs

Hi everyone!

 It's been a long time since my last (and first) post. Mostly because my job is keeping me insanely busy these days, but also since there are so many excellent SharePoint bloggers out there that it's not every day you discover something worth blogging that someone hasn't already written about. But last week i ran into a problem when we were about to create some features and site definitions to deploy a portal for one of our customers that I thought maybe would be of interest to other people.

The portal consisted of one site collection on the top level and several levels of webs underneath. On some of the subwebs we had created site columns with lookups to lists on that web and based content types on them. We knew that we could deploy content types and site columns using the feature framework and we tried to do just that only to discover that stsadm would spurt out nasty error messages telling us that features with web scope cannot be used to deploy that type of content.

"No Problem", I thought, "I'll just create a small app that uses the API to add them instead". But that proved to be easier said than done since the API require your content type to inherit from existing content types and when you do, it will create new GUIDS for them which just cut it for us. So what I ended up doing (once again) was to reverse engineer selected parts of SharePoint and trick it into adding content types and site columns on webs using specific GUIDs and now I'll tell you how to do the same.

The first step for me was to get the information about the content types and site columns out of our development environment so I could move it from one machine to another. For this I created a small console application. If I had more time I would probably made and STSADM extension out of it, but I'll leave that as an exercise for the reader Wink.

The application iterates through the entire site and generates an XML file with each web in it's own node to keep the information structured and so that I'll be able to read out which web each field belongs to when I import it later. This time I was in luck since the SPField object in the Fields collection on the SPWeb object actually has a property called "SchemaXml" which contains the definition of the field.

The SchemaXml property of a column looks something like this:

<Field Type="LookupMulti" DisplayName="Target Segments" Required="FALSE" List="{848b897d-e7e7-4efa-a859-c7c13fe00270}" WebId="c5fe1b80-87d2-41a3-ba0e-ba1bfc43c65b" ShowField="Title" Mult="TRUE" Sortable="FALSE" UnlimitedLengthInDocumentLibrary="FALSE" Group="My columns" ID="{89f0caed-ff04-43e5-ba04-9accbaa50776}" SourceID="{c5fe1b80-87d2-41a3-ba0e-ba1bfc43c65b}" StaticName="TargetSegments" Name="TargetSegments" />

But unfortunately, look-up columns refer to the source lists and webs by GUID (the List and WebId attributes above) which means that there's no way we can just import this XML into another farm and expect everything to work. We have to examine the schema XML and fiddle with it a little by inserting a little extra information to help us determine the correct GUIDs during import.

So let's load up the XML in an XMLDocument in the loop that iterates through all the site columns in the web:

XmlDocument doc = new XmlDocument();
doc.LoadXml(field.SchemaXml);

And then check if the field has a "List"-attribute and if it does, append a "ListName"-attribute with the name of the list that the GUID points to:

// Fix List GUID
if (doc.DocumentElement.Attributes["List"] != null && doc.DocumentElement.Attributes["List"].Value.StartsWith("{"))
{
  Guid listId = new Guid(doc.DocumentElement.Attributes["List"].Value);
  XmlAttribute listNameAttribute = doc.CreateAttribute("ListName");
  listNameAttribute.Value = web.Lists[listId].Title;
  doc.DocumentElement.Attributes.Append(listNameAttribute);
}

Then check if there's a WebId attribute and give it a similar treatment by adding a WebName attribute:

// Fix Web GUID
if (doc.DocumentElement.Attributes["WebId"] != null)
{
  Guid webId = new Guid(doc.DocumentElement.Attributes["WebId"].Value);
  XmlAttribute webNameAttribute = doc.CreateAttribute("WebName");
  using (SPWeb sourceWeb = web.Site.OpenWeb(webId))
  {
    webNameAttribute.Value = sourceWeb.ServerRelativeUrl;
  }
doc.DocumentElement.Attributes.Append(webNameAttribute);

Now we have fully importable look-up site column XML! Next, let's export some content types!

Here's where I stumbled upon my first hurdle in my endeavor. SPContent type doesn't have any SchemaXml property! But after some research with Reflector I found out that SPContentType has an internal method called "SaveCore" that takes an XmlTextWriter and exports the content type definition to XML which is just what we need! So let's write a helper method that calls SaveCore on an SPContentType object and returns the exported XML using reflection:

private static string GetInternalSchema(SPContentType ctype)
{
  MethodInfo saveCoreMethod = ctype.GetType().GetMethod(
    "SaveCore",
    BindingFlags.NonPublic | BindingFlags.Instance,
    null,
    new Type[] { typeof(XmlTextWriter), typeof(bool), typeof(bool) },
    null);
  StringBuilder sb = new StringBuilder();
  XmlTextWriter xwtr = new XmlTextWriter(new StringWriter(sb, CultureInfo.InvariantCulture));
  saveCoreMethod.Invoke(ctype,
new object[] { xwtr, false, false });
  xwtr.Flush();
  xwtr.Close();
  return sb.ToString();
}

Here's an example of what this method writes for one of my content types:

<ContentType ID="0x01040074079E5E8E5DA5419F1CE8300AA67B68" Name="MyContentType" Group="MyContentTypes" Version="2">
  <
Folder TargetName="_cts/MyContentType" />
    <
FieldRefs>
      <
FieldRef ID="{c042a256-787d-4a6f-8a8a-cf6ab767f12d}" Name="ContentType" />
      <
FieldRef ID="{fa564e0f-0c70-4ab9-b863-0177e6ddd247}" Name="Title" Required="TRUE" ShowInNewForm="TRUE" ShowInEditForm="TRUE" />
      <
FieldRef ID="{7662cd2c-f069-4dba-9e35-082cf976e170}" Name="Body" />
      <
FieldRef ID="{3805f629-5035-4a13-9362-8d1b216bbc05}" Name="CustomColumnHere" />
      <
FieldRef ID="{51d39414-03dc-4bd0-b777-d3e20cb350f7}" Name="PublishingStartDate" />
      <
FieldRef ID="{a990e64f-faa3-49c1-aafa-885fda79de62}" Name="PublishingExpirationDate" />
    </
FieldRefs>
    <
XmlDocuments>
      <
XmlDocument NamespaceURI="http://schemas.microsoft.com/sharepoint/v3/contenttype/forms">
        <
FormTemplates xmlns="http://schemas.microsoft.com/sharepoint/v3/contenttype/forms">
          <
Display>ListForm</Display>
          <
Edit>ListForm</Edit>
          <
New>ListForm</New>
        </
FormTemplates>
      </
XmlDocument>
    </
XmlDocuments>
</
ContentType>

Ahhh. I love .NET Smile

Anyway! Let's move on the next step: Importing the site columns!

Before we can feed SharePoint the exported Schema XML for the columns, we have check each column for the ListName and WebName attributes we added earlier and use them to find the GUIDs of the list and web in the target enviroment. We should also remove the *Name attributes since SharePoint isn't expecting them:

// Fix List GUID
if (field.Attributes["ListName"] != null)
{
  field.Attributes[
"List"].Value = web.Lists[field.Attributes["ListName"].Value].ID.ToString("B");
  field.Attributes.RemoveNamedItem(
"ListName");
}
// Fix Web GUID
if (field.Attributes["WebName"] != null)
{
  using (SPWeb sourceWeb = web.Site.OpenWeb(field.Attributes["WebName"].Value))
  {
    field.Attributes[
"WebId"].Value = sourceWeb.ID.ToString("D");
  }
  field.Attributes.RemoveNamedItem(
"WebName");
}

I also noticed the SourceID attribute in the schema and that it was (in my case) always the same as the WebId, so just to be safe, I replaced that one too:

// Fix source ID
if (field.Attributes["SourceID"] != null && field.Attributes["SourceID"].Value.StartsWith("{"))
{
  field.Attributes[
"SourceID"].Value = web.ID.ToString("B");
}

You may wish to examine your generated XML for the site columns and make sure that WebId and SourceId are the same in your case too before using this code Wink

Now that the XML has been updated with GUIDs in the new environment we can finally add the column to the web:

web.Fields.AddFieldAsXml(field.OuterXml);

Let's proceed directly to the grand finale: Importing the content types!

As you probably have guessed (or found out the hard way since you're reading this article) There's no documented way of importing content types to webs and preserving the GUIDs. This is most likely a design decision to ensure the uniqueness of content types across all webs of a farm so you may want to examine your options carefully before going down the same dark path that I did. And keep in mind that this code probably doesn't support all content type scenarios. It worked for me, but I didn't need features like workflows, document templates or information management policies. To get a better understanding of the complexities of content types, you can take look at Gary Lapointe's post about copying content types between site collections within the same farm.

Still with me? Cool! Then let's get down to business:

First we need to create an empty SPContentType object. Again, we have to resort to reflection since the SPContentType class doesn't have any public constructors. It's time for another helper method:

private static SPContentType CreateEmptyContentType()
{
  ConstructorInfo constructor = (typeof(SPContentType)).GetConstructor(
    BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null);
  SPContentType ctype = (SPContentType)constructor.Invoke(new object[0]);
  return ctype;
}

Then we need to call the internal method "Load" to load the XML definition of the content type that we exported earlier:

private static void ReadContentTypeFromXml(SPContentType ctype, XmlTextReader xmlReader)
{
  MethodInfo loadMethod = ctype.GetType().GetMethod(
    "Load",
    BindingFlags.NonPublic | BindingFlags.Instance,
    null,
    new Type[] { typeof(XmlReader) },
    null);
  loadMethod.Invoke(ctype,
new object[] { xmlReader });
}

After that we have to set the scope of the content type and the field reference collection, yet again using reflection:

private static void SetContentTypeScope(SPContentType ctype, SPWeb web)
{
  string scope = web.ServerRelativeUrl.TrimStart('/');
  MethodInfo setCTScopeMethod = ctype.GetType().GetMethod(
    "SetScope",
    BindingFlags.NonPublic | BindingFlags.Instance,
    null,
    new Type[] { typeof(string) },
    null);
  setCTScopeMethod.Invoke(ctype,
new object[] { scope });

  PropertyInfo fieldLinksScopeProperty = ctype.FieldLinks.GetType().GetProperty(
    "Scope",
    BindingFlags.NonPublic | BindingFlags.Instance,
    null,
    typeof(string),
    Type.EmptyTypes,
    null);
  fieldLinksScopeProperty.SetValue(ctype.FieldLinks, scope,
new object[0]);

  PropertyInfo contentTypeWebProperty = ctype.GetType().GetProperty(
    "Web",
    BindingFlags.NonPublic | BindingFlags.Instance,
    null,
    typeof(SPWeb),
    Type.EmptyTypes,
    null);
  contentTypeWebProperty.SetValue(ctype, web, new object[0]);
}

Now all that remains is to call the helper methods and add the content type to the web:SPContentType ctype = CreateEmptyContentType();
XmlTextReader xmlReader = new XmlTextReader(contentType.OuterXml, XmlNodeType.Element, new XmlParserContext(doc.NameTable, nsmgr,"", XmlSpace.Default));
ReadContentTypeFromXml(ctype, xmlReader);
SetContentTypeScope(ctype, web);
web.ContentTypes.Add(ctype);

As soon as I get around to cleaning up my code, I'll post an example application that makes use of the code in this post.

Questions, comments and improvements are of course always welcome!

Comments

 

types of columns said:

Pingback from  types of columns

May 25, 2008 11:55 AM

Leave a Comment

(required )  
(optional )
(required )  
Add

About Robert Fridén

Hi, My name is Robert Fridén and I work as a software architect at Strand Interconnect. I've been focusing on SharePoint completely since the 2007 beta was first released. SharePoint is a huge subject, and reading other peoples blogs has helped me a lot in my work, and writing this blog is my attempt to give something back to the community. Unfortunately, my work is keeping me incredibly busy but I hope to have enough time to post something useful from time to time.

Need SharePoint Training? Attend a SharePoint Bootcamp!

Posts (c) their respective authors. Everything else (c) 2007 SharePoint Experts