Create Lookup Site Columns & Target Lists Through a Feature

Here’s another provisioning gotcha. I had some problems provisioning a site column which pointed to a list I was also provisioning through a feature. Below are some code examples of how I implemented a quick solution.

Say you want to create a new list instance in your feature…

<Elements ...>
 <ListInstance Id="{A6E8895F-6C4D-4834-9769-CBA700B2356A}"
 FeatureId="{00bfea71-de22-43b2-a848-c05709900100}"
 TemplateType="100"
 Title="Campus Locations"
 Description="Campus Locations List"
 Url="CampusLocations"
 OnQuickLaunch="false" />
 </Elements>

Now you want to create a site column which will point to the above list in the same feature:

<Field ID="{E9635D9A-3D47-45b3-B718-CFA54CF0FE50}"
 SourceID="{8c066b26-5a3e-4e1b-85ec-7e584cf178d7}"
 Name="Campus"
 DisplayName="Campus"
 StaticName="Campus"
 Description="The campus or campuses to which this item relates."
 Group="Custom Site Columns"
 Type="LookupMulti"
 List="{A6E8895F-6C4D-4834-9769-CBA700B2356A}"
 ShowField="Title"
 Required="FALSE"
 Mult="TRUE"/>

You would assume that putting the GUID of the list in the List attribute of the Field element would hook the field to the list, right? Wrong. SharePoint re-creates the GUID of the lists you provision through <ListInstance …>, so any reference to that hard-coded GUID will fail. Hmm…

We can repair the reference to the list through a feature receiver once the column and list have been provisioned. It take a little prying, but it’s possible. I found a reference on how to do this on CodePlex, but it included a dependency on the feature manifest file, and I didn’t like that. The code below uses the XML in the database and recreates a site column pointing to the correct list, based on a string reference you pass in through the ‘List’ attribute of the <Field …> element:

public class FeatureReceiver : SPFeatureReceiver
{
public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
RepairLookupColumnReferences(properties);
}
public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
CleanupLookupColumns(properties);
}

public override void FeatureInstalled(SPFeatureReceiverProperties properties)
 {
 }
public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
 {
 }
private void RepairLookupColumnReferences(SPFeatureReceiverProperties properties)
 {
 // feature is scoped at Site, so the parent is type SPSite rather than SPWeb..
 using (SPSite site = properties.Feature.Parent as SPSite)
 {
 SPWeb currentWeb = null;
 Guid gRootWebId = Guid.Empty;
 if (site != null)
 {
 currentWeb = site.RootWeb;
 gRootWebId = currentWeb.ID;
 }
 else
 {
 currentWeb = properties.Feature.Parent as SPWeb;
 gRootWebId = currentWeb.Site.RootWeb.ID;
 }
using (currentWeb)
 {
 // read details from CAML..
string sFieldElement = null;
 string sColumnName = null;
 string sListName = null;
SPElementDefinitionCollection coll =
 properties.Feature.Definition.GetElementDefinitions(System.Globalization.CultureInfo.CurrentCulture);
 foreach (SPElementDefinition def in coll)
 {
 XmlNode node = def.XmlDefinition;
 XmlNodeReader nReader = new XmlNodeReader(node);
 while (nReader.Read())
 {
 if (nReader.LocalName == "Field")
 {
 //check to see if this is a lookup field
 if (nReader.MoveToAttribute("Type"))
 {
 string type = nReader.Value;
 nReader.MoveToElement();
 if (type.ToLower() != "lookup" && type.ToLower() != "lookupmulti")
 continue;
 }
if (nReader.MoveToAttribute("Name"))
 {
 sColumnName = nReader.Value;
 nReader.MoveToElement();
 }
if (nReader.MoveToAttribute("List"))
 {
 sListName = nReader.Value;
 nReader.MoveToElement();
 }
sFieldElement = nReader.ReadOuterXml();
// now get reference to list, fix up CAML and create column..
 SPList referencedList = currentWeb.Site.RootWeb.Lists[sListName];
string sFinalCaml = replaceListGuidString(sFieldElement, sListName, referencedList);
 //createLookupColumn(currentWeb, sFinalCaml, sListName);
 //CORRECTED ABOVE BECAUSE LIST NAME WAS BEING PASSED IN INTEAD OF COLUMN NAME
 createLookupColumn(currentWeb, sFinalCaml, sColumnName);
 }
 }
nReader.Close();
}
}
 }
 }
private string replaceListGuidString(string sFieldElement, string sListName, SPList referencedList)
 {
 string sListWithName = string.Format("List="{0}"", sListName);
 string sListWithGuid = string.Format("List="{{{0}}}"", referencedList.ID);
 return sFieldElement.Replace(sListWithName, sListWithGuid);
 }
///
 /// Attempt to delete the column. Note that this will fail if the column is in use,
 /// i.e. it is used in a content type or list. I prefer to not catch the exception
 /// (though it may be useful to add extra logging), hence feature deactivation/re-activation
 /// will fail. This effectively means this feature cannot be deactivated whilst the column
 /// is in use.
 ///
 ///
 Column to delete.   private void attemptColumnDelete(SPWeb web, string sColumnName)
 {
 SPFieldLookup lookupColumn = null;
// we don't care if there was an exception retrieving the field from the collection,
 // since if it's not there we can't delete it anyway..
 try
 {
 lookupColumn = web.Fields.GetFieldByInternalName(sColumnName) as SPFieldLookup;
 }
 catch (ArgumentException argExc)
 {
}
if (lookupColumn != null)
 {
 try
 {
 lookupColumn.Delete();
 }
 catch (SPException SpExc)
 {
 // consider logging full explanation..
throw;
 }
 }
 }
private void createLookupColumn(SPWeb web, string sColumnDefinitionXml, string sColumnName)
 {
 // delete the column if it exists already and is not yet in use..
 attemptColumnDelete(web, sColumnName);
SPFieldLookup lookupColumn = null;
// now create the column from the CAML definition..
 string sCreatedColName = web.Fields.AddFieldAsXml(sColumnDefinitionXml);
// also set LookupWebId so column can be used in webs other than web which hosts list..
 //lookupColumn = web.Fields[sCreatedColName] as SPFieldLookup;
 //CORRECTED ABOVE LINE:
 lookupColumn = web.Fields.GetFieldByInternalName(sCreatedColName) as SPFieldLookup;
 lookupColumn.LookupWebId = web.Site.RootWeb.ID;
 lookupColumn.Update();
 }
private void CleanupLookupColumns(SPFeatureReceiverProperties properties)
 {
 // delete the column if it exists already and is not yet in use..
// feature is scoped at Site, so the parent is type SPSite rather than SPWeb..
 using (SPSite site = properties.Feature.Parent as SPSite)
 {
 SPWeb currentWeb = null;
 Guid gRootWebId = Guid.Empty;
 if (site != null)
 {
 currentWeb = site.RootWeb;
 gRootWebId = currentWeb.ID;
 }
 else
 {
 currentWeb = properties.Feature.Parent as SPWeb;
 gRootWebId = currentWeb.Site.RootWeb.ID;
 }
using (currentWeb)
 {
 string sColumnName = null;
SPElementDefinitionCollection coll =
 properties.Feature.Definition.GetElementDefinitions(System.Globalization.CultureInfo.CurrentCulture);
 foreach (SPElementDefinition def in coll)
 {
 XmlNode node = def.XmlDefinition;
 XmlNodeReader nReader = new XmlNodeReader(node);
 while (nReader.Read())
 {
 if (nReader.LocalName == "Field")
 {
 //check to see if this is a lookup field
 if (nReader.MoveToAttribute("Type"))
 {
 string type = nReader.Value;
 nReader.MoveToElement();
 if (type.ToLower() != "lookup" && type.ToLower() != "lookupmulti")
 continue;
 }
if (nReader.MoveToAttribute("Name"))
 {
 sColumnName = nReader.Value;
 nReader.MoveToElement();
 }
attemptColumnDelete(currentWeb, sColumnName);
 }
 }
nReader.Close();
 }
 }
 }
 }
 }

7 Comments

  • Abdallah Mossad
    October 13, 2008 - 12:50 pm | Permalink

    Please use the name of the list insted of the GUID .

    e.g. List =”Lists/{yourListName}”

    Good Luck.

  • Charlito_
    October 31, 2008 - 1:48 pm | Permalink

    You are a savior, Abdallah Mossad!
    Thank you!

    I’ve seen a few blogs on this. But as you said, the List value in the Field element should actually match the Url field (not the Id nor Title) of the ListInstance.

  • Pingback: Definir Lookup Site Columns desde una Feature - Mario Cortés Flores

  • Shrike
    September 16, 2009 - 9:30 am | Permalink

    Hi. I’m an administrator who needs this. How do I use this code?

    • September 16, 2009 - 3:03 pm | Permalink

      Hi Shrike! You’ll have to create a SharePoint feature with a feature receive set up as the code I posted above. A quick google shows a couple of pages showing how to create a feature and the wire up code to run when the feature has been activated. Hope that helps a little!

  • Shrike
    September 18, 2009 - 8:34 pm | Permalink

    Hi Marc

    Great thanks for the prompt response!!

    I’ll do my homework with your advice when I get back to business in 1 week. Perhaps I’d ask again if I get stuck at some point. All the best.

  • September 18, 2009 - 9:44 pm | Permalink

    No problem. Let me know if I can help you at all.

  • Leave a Reply

    Your email address will not be published. Required fields are marked *

    *

    You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>