Sunday 15 April 2007

Sample code - creating lookup columns as a feature

I promised in an earlier post to post the code I used to create lookup columns as a feature. The basic idea here to use CAML as far as possible to define the site column, but use code to inject the list ID which the column refers to (unknown at feature design-time since SharePoint decides the GUID for lists). Extracted below is the code for the feature receiver.

A couple of key points:-

  • this assumes a file exists at the location D:\COB.Demo.ListBasedSiteColumns.Fields.xml with the following contents (note the 'List' attribute with an empty value):-
    <?xml version="1.0" encoding="utf-8"?>
    <elements xmlns="http://schemas.microsoft.com/sharepoint/">
    <!-- _filecategory="ContentType" _filetype="File" _filename="fields.xml" _uniqueid="c0188da6-e320-44c0-b50c-cb6eaecec512" -->
    <Field
    Type="Lookup"
    Displayname="Locations"
    Required="FALSE"
    List=""
    ShowField="LinkTitleNoMenu"
    UnlimitedLengthInDocumentlibrary="FALSE"
    Group="COBDemo"
    ID="{ae8e6ab8-b151-4fa4-8694-3cd24ad3b1bc}"
    SourceId="{8c066b26-5a3e-4e1b-85ec-7e584cf178d7}"
    StaticName="Locations"
    Name="Locations">
    </elements>

  • Since most of the column definition is in CAML, this allows you to specify the ID of the column. This is useful later on when we deploy content types or lists which need to reference this site column by ID.
  • Site columns cannot be deleted when they are in use i.e. used in content types or lists. I prefer to try to delete the column on feature deactivation or reactivation, but omit error-handling so SharePoint throws an exception. This makes it clear to me why the feature cannot be deactivated. See comments in code for more information.
  • Note that the assembly which contains the code below must be available (in the GAC, or site bin directory with appropriate CAS policy) when the feature is activated. You must also specify the FeatureAssembly and FeatureReceiver attributes in your feature.xml file to register the feature receiver.
  • In terms of deploying the assembly, note that whilst you certainly can deploy assemblies to the GAC or site bin using the feature framework, you can't do it in this feature. Despite the 'FeatureActivated' name of the event handler, when your code executes the CAML won't yet have been processed. Hence your assembly won't yet have been copied and you'll get a FileNotFoundException when it tries to load the assembly. A good solution is to use a feature dependency. Here you can create a 2nd feature which deploys the assembly, and set the main feature to be dependent on this one. Additionally you can mark the assembly deployment feature to be hidden thus making the implementation a bit tidier. If it's useful to see the entire set of files I used leave me a comment and I'll put them up somewhere.
Apologies for the formatting, I will try to do something about this when I get time:-


public class FeatureReceiver : SPFeatureReceiver
{
private readonly string f_csSITE_COLS_DEFINITION_PATH = @"D:\COB.Demo.ListBasedSiteColumns.Fields.xml";

public override void FeatureActivated(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)
{
// get reference to the list..
SPList referencedList = currentWeb.Site.RootWeb.Lists["LocationsList"];
string sFieldElement = null;
XmlTextReader xReader = new XmlTextReader(f_csSITE_COLS_DEFINITION_PATH);
while (xReader.Read())
{
if (xReader.LocalName == "Field")
{
sFieldElement = xReader.ReadOuterXml();
break;
}
}


string sFinalCaml = replaceListGuidString(sFieldElement, referencedList);
createLookupColumn(currentWeb, sFinalCaml, "Locations");
currentWeb.Update();
}
}
}


private string replaceListGuidString(string sFieldElement, SPList referencedList)
{
string sPopulatedGuid = string.Format("List=\"{0}\"", referencedList.ID);
return sFieldElement.Replace("List=\"\"", sPopulatedGuid);
}

/// <summary>
/// Attempt to delete the column. Note that this will fail if the column is inuse,
/// 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.
/// </summary>
/// <param name="column">Column to delete.</param>

private void attemptColumnDelete(SPFieldLookup column)
{
try
{
column.Delete();
}
catch (SPException e)
{
// 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..
SPFieldLookup lookupColumn = null;
lookupColumn = web.Fields[sColumnName] as SPFieldLookup;
if (lookupColumn != null)
{
attemptColumnDelete(lookupColumn);
}

// 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;
lookupColumn.LookupWebId = web.Site.RootWeb.ID;
lookupColumn.Update();
}


public override void FeatureDeactivating(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;
}

SPFieldLookup lookupColumn = null;
lookupColumn = currentWeb.Fields["LocationsList"] as SPFieldLookup;

if (lookupColumn != null)
{
attemptColumnDelete(lookupColumn);
}
}
}



public override void FeatureInstalled(SPFeatureReceiverProperties properties)
{
}


public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
{
}
}


If it's useful to have the full set of files illustrating the 2 features(particularly since the above isn't very readable!), leave me a comment and I'll make them available somewhere..

[Update - these have now been uploaded to Codeplex, see http://sharepointnutsandbolts.blogspot.com/2007/04/feature-to-create-lookup-fields-on.html]

24 comments:

Anonymous said...

Hello Chris,
big thanks to sharing your code, yesterday was my birthday, and for my current work, it helped me a lot, so a big thank to you.

Laurent

Anonymous said...

Hello Chris,
it would be great to access the full set of files.

Many thanks

Laurent

Anonymous said...

Hey...It's really nice but the code in downloadable format should be much better.
Chris could you please upload the files somewhere?

Thank you

David T.

Chris O'Brien said...

Hi,

I've now uploaded the files, see my post at http://sharepointnutsandbolts.blogspot.com/2007/04/feature-to-create-lookup-fields-on.html.

Anonymous said...

It seems, that the GUID of the field is changed during the field recreations, which bareaks the field references from site columns / content types... and eventually will cause huge problems.

Chris O'Brien said...

Hi,

I'm not sure if I follow your point exactly, but essentially yes, that's the problem this technique addresses - the fact the lists get a new GUID when they are created. By using this code, it means that your site columns/content type Features are less tightly-coupled to the lists.

HTH,

Chris.

Anonymous said...

This is good stuff but I'm just wondering - your code refers to a list by it's name. How would this work when a site has been provisioned in a different language because the list name could potentially be in a different language?

Would it be better to use a resource string when referring to the list name?

Chris O'Brien said...

Hi,

Indeed - I guess I didn't mean to suggest that hardcoding the list name is the best way for you to use this code - I simplified it there to focus on actually creating the lookup link.

I think in real life I got the list name from a Feature property, so I pass the list name in from the Feature XML. You could also choose to get some of the details from say a SharePoint list - along these lines, I thing I like to do is have a hidden 'config' web which stores config data like this.

HTH,

Chris.

Kwoque said...

Hi Chris,

Big help for this post. I'm trying to get it in my own feature as I type this post :P but instead of locating the XML file hard-coded in the source, you could use
properties.Feature.Definition.RootDirectory

Chris O'Brien said...

Hi Kwoque,

Yes, that's a good suggestion. There are a few things hardcoded in my example for simplicity - I completely agree in the real world you'd generally avoid hardcoding paths/object names/IDs etc.

As suggested earlier, another option to your suggestion would be to pass in these parameters using Feature properties.

Cheers,

Chris.

Jason Apergis said...

Chris,

I am getting this error - The local device name is already in use. (Exception from HRESULT: 0x80070055) - when I run Fields.AddFieldAsXml. It is absolutely killing me and there is nothing out there on it. The only way I can fix it is blow away the entire site collection and start over which is not an option if thise goes into production. Any ideas???

Thanks,
Jason

Chris O'Brien said...

Hi Jason,

Hmm, to me this is indicative of something weird to do with the filesystem - I've certainly never seen/heard this one before. I would check for things such as:

- are you trying to access files (e.g. Feature files) over a mapped drive?
- are you out of disk space?
- is the SQL transaction log full?

Your error is most likely not coming from the Feature framework or SharePoint itself, but something underneath.

HTH,

Chris.

Anonymous said...

Hey Chris,

Have you tried to deploy a site with lookup columns created in this manner using the Content Deployment wizard. You will get a strange error that says: "The element 'FieldTemplate' in namespace 'urn:deployment-manifest-schema' has invalid child element 'Field' in namespace 'http://schemas.microsoft.com/sharepoint/'. List of possible elements expected: 'Field' in namespace 'urn:deployment-manifest-schema'. at..."
I haven't found a fix yet. Any ideas as to why content deployment api gets confused?

Chris O'Brien said...

Hi Jim,

Yikes, that's an interesting one - I actually think I have used these things together. I didn't run into this issue, but it was a while ago and I no longer have access to the site.

Looking at the problem logically, the deployment API is complaining that the 'Field' element is in the wrong namespace. Aside from the fact that this shouldn't happen, my comments would be:

- the error suggests something is wrong with the field definition. I'm wondering if this is more to do with the CAML definition rather than the GUID fix-up which happens in code.
- I don't have a solution, but a potential workaround could be to rename the .cmp file to a cab, then manipulate the offending file in there (you'd have to search for it somehow). Then you would regenerate the cab file (including the newly-modified file) and finally rename back to .cmp. This will most likely import successfully.

Obviously the ideal solution would be to fix the problem at source though. Let me know if you find a solution.

Best of luck,

Chris.

Anonymous said...

Hi Chris,

Great work!
One question:
I noticed, that when I query a list containing a lookup column, the lookup column doesn't appear in the list of columns.

For example if you query the GetList method of _vti_bin/Lists.asmx

Hope you can point to some solution.

Chris O'Brien said...

Hi Morten,

Hmm, afraid I don't know why this would be the case. Is this just restricted to the lists web service? What happens if you iterate through the SPList.Fields collection, is the lookup column present?

Thanks,

Chris.

Chris O'Brien said...

Hi Morten,

Hmm, afraid I don't know why this would be the case. Is this just restricted to the lists web service? What happens if you iterate through the SPList.Fields collection, is the lookup column present?

Thanks,

Chris.

Anonymous said...

Hi Chris,

I found out that the xmlns attribute of the SchemaXML is causing this behaviour.
I don't know, if you have a specific reason for including the attribute?
If not, maybe you could remove it from your project on codeplex.

Chris O'Brien said...

Hi Morten,

Yes I think a couple of other people have picked this up. Appreciate you pointing this out to me, I'll try and update the Codeplex project as soon as I can.

In the meantime other users should take note of this key amendment!

Thanks,

Chris.

Anonymous said...

Chris

Thanks for this post. Your technique works well, but there is a simple amendment you may want to make to the Codeplex project.

I found a problem when I used Word 2007 to create documents in a library with a content type that includes a lookup field created using your feature activation code. When Word presents the form to populate the list item's columns a 'library not found' exception is displayed and the dropdown for the lookup column's values is empty.

After hours of frustration, I found that using the braces format of the guid in the List attribute( For example List="{e34bb359-4f60-49cc-8b28-77d82ec758a3}") fixed the problem.

The code line in your feature activation code

string sPopulatedGuid = string.Format("List=\"{0}\"", referencedList.ID);

creates the attribute value without braces, which seems to define a satisfactory lookup column until Word tries to use it via a Sharepoint Web Service.

Replacing the line with the following should do the trick

string sPopulatedGuid = "List=\"{"+ referencedList.ID.ToString() +"\"}";

Tom

Chris O'Brien said...

Hi Tom,

Many thanks for the correction - I'll update the Codeplex project when I can.

Much appreciated,

Chris.

Anonymous said...

A follow up to Jim Brown's comment above concerning the namespace error: "The element 'FieldTemplate' in namespace 'urn:deployment-manifest-schema' has invalid child element 'Field' in namespace 'http://schemas.microsoft.com/sharepoint/'. List of possible elements expected: 'Field' in namespace 'urn:deployment-manifest-schema'. at..."

See the following issue under Chris's codeplex project for a possible solution:

http://www.codeplex.com/SP2007LookupFields/WorkItem/View.aspx?WorkItemId=10172

This suggestion by sgoodyear worked like a charm for me...

Rick Mason said...

To anyone having a problem with "The local device name is already in use", I have just got past this one.

The problem started when I tried using AddFieldAsXml using poorly formed XML. That's obviously got a screwed-up setting in the database somehow.

The problem went away when I changed the internal name of the field I was trying to add.

I also found a copy of the site column which couldn't be deleted in the UI, but I deleted it using code in the feature receiver, then removed the code.

Anonymous said...

I know this is an old thread, but thought I'd ask.
I successfully created features for a list, a site column (looks up data from the list), and a content type (uses the site column). All scoped at "Site". Down one level, in a site, I created a library, associated the content type, uploaded a file, but my lookup (site column) is empty on the edit page. When I look at the column in library settings, I see that the "Get Information from" is blank. Yet in the site column gallery I check it and it correctly points to my list.
Any thoughts from anyone on this.
Thanks,
KevinHou