A few weeks ago I promised to post a few articles about extending CAML to deal with some of the elements that you’d normally have to write code in order to package. To be honest I’ve been putting it off in the hope that SharePoint 2010 and VisualStudio 2010 would make all these problems go away but it seems that that isn’t going to happen, at least not completely. So no more excuses, I may as well get on with it!
I wanted to cover Lookup fields in this post. Somebody recently asked about this on the forums and although there are a lot of great articles (Such as this one from Chris O’Brien) covering this already, I thought I’d revisit it with a view to providing a CAML extension that will fix the problem once and for all.
The problem
If you’re using VSeWSS or some other CAML generation tool, you’ll find that you can’t generate CAML to automatically create a lookup field. If we examine the CAML that would be required to create a lookup field we’ll see why:
<Field DisplayName="MyLookup" Type="Lookup"
List="{712AB5C7-01E7-43B2-82DC-9BD444E119A1}"
ID="{f70a79db-b6b5-4268-8d3c-a62cf43c339c}"
SourceID="{2d097a06-bcc0-4be7-974a-5b290088b326}"
/>
The List attribute requires the guid for the list that will become the source of the lookup and the SourceId requires the guid for the column in the list that will be displayed. No big deal you might think, we’ll just stick them in there. If the list is already on your site then that’ll work just fine but if you’re creating the list in a feature you’ll have a problem – the guids are generated by SharePoint when the list is created, you’ve got absolutely no control over them. Bearing that in mind, it’s impossible to put a value in the list attribute that will enable you to create a lookup properly. (Although there are a few posts that suggest that using list names will work, personally I’ve never had any success with that)
Since VSeWSS knows that it’s impossible to create lookups, it’ll add the CAML to your elements file and comment it out. (It would be nice if it added a comment explaining why it had done that though!)
The Solution
There is only one way to fix this problem and that’s to write code to create your lookup field.
As I mentioned there’s plenty of samples of the code that you need to write on the web but I want to take that a bit further and extend CAML so that you can simply add in an elements file with your lookup fields and all will be well rather than having to cut and paste the same code for every lookup.
We’ll extend CAML by creating a feature receiver that accepts the name of an elements file as a parameter. The receiver will then parse the file and programmatically create the lookups that are required. All in all, it’s pretty simple.
First things first, we’ll create the feature receiver:
- If you don’t already have it, get WSPBuilder. Life is too short to do this any other way
- You’ll probably have a project with all your features in it, if not create a new WSPBuilder project. (If you created your features using another tool such as VSeWSS you’ll probably need to create a new WSPBuilder project then drag all your feature stuff into it)
You’ll have a project that looks a bit like this:
- Right click on the project node (in my example that’s the LookupsCamlExtension node), select Add > New Item
Select WSPBuilder in the categories pane then Feature with Receiver in the templates pane. For simplicity call it MyLookupFeature
If you’re feeling pedantic you can populate the description. Leave the scope as ‘Web’
Congratulations, you’ve just added a new feature with a receiver! Your project should now look a bit like this:
The next step in the process is to add some code to the feature receiver to do the work of creating the lookups.
- Navigate to your MyLookupFeature.cs file and add the following code:
class MyLookupFeature : SPFeatureReceiver
public override void FeatureActivated(SPFeatureReceiverProperties properties)
SPSite site = properties.Feature.Parent as SPSite;
currentWeb = site.RootWeb;
currentWeb = properties.Feature.Parent as SPWeb;
// get reference to the list..
string lookupColumnsFilename = properties.Feature.Properties["LookupColumns"].Value;
string fileName = Path.Combine(properties.Definition.RootDirectory, lookupColumnsFilename);
if (!LookupParser.Parse(fileName, currentWeb))
throw new Exception("Unable to parse Lookup data");
public override void FeatureDeactivating(SPFeatureReceiverProperties properties) { }
public override void FeatureInstalled(SPFeatureReceiverProperties properties) { }
public override void FeatureUninstalling(SPFeatureReceiverProperties properties) { }
This code is executed when you activate the feature. The first few lines check to see whether the feature is being activated at web or site level and retrieve a reference to the currentWeb as appropriate. The next few lines read the value of the “LookupColumns” property, we’ll cover that in more detail later but for now it’s enough to know that it contains the name of the elements file with the lookup definitions in it. Once we have the filename we pass it to our soon to be created LookupParser for processing.
Now that we have a basic feature receiver, the next step is to create a LookupParser class that will be responsible for parsing our lookup definition file.
- Click on the project node again, select Add > Class. Call the class LookupParser.cs
- Cut and paste this code into your class:
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
using System.Xml.Serialization;
using System.Xml;
using System.Collections;
namespace LookupsCamlExtension
{
class LookupParser
{
static public bool Parse(string filename, SPWeb currentWeb)
{
try
{
Lookups lookups;
XmlSerializer serializer = new XmlSerializer(typeof(Lookups));
XmlReader rdr = XmlReader.Create(filename, null);
lookups = serializer.Deserialize(rdr) as Lookups;
IEnumerator e = lookups.Items.GetEnumerator();
e.Reset();
e.MoveNext();
do
{
if (e.Current is LookupsList)
{
//Create List Fields
CreateListFields(e.Current as LookupsList, currentWeb);
}
else if (e.Current is LookupsContentType)
{
//Create Site Columns
CreateSiteFields(e.Current as LookupsContentType, currentWeb);
}
} while (e.MoveNext());
return true;
}
catch (Exception)
{
return false;
}
}
static private void CreateListFields(LookupsList lookups, SPWeb currentWeb)
{
string listName = lookups.listName;
SPList referencedList = currentWeb.Lists[listName];
foreach (Lookup lookupNode in lookups.Lookup)
{
//Create the lookup field
SPList linkedList = currentWeb.Lists[lookupNode.lookupListName];
referencedList.Fields.AddLookup(lookupNode.columnName, linkedList.ID, lookupNode.required);
referencedList.Update();
SPFieldLookup lookup = ((SPFieldLookup)referencedList.Fields[lookupNode.columnName]);
lookup.LookupField = linkedList.Fields[lookupNode.lookupField].InternalName;
lookup.Update();
}
}
static private void CreateSiteFields(LookupsContentType lookups, SPWeb currentWeb)
{
string contentTypeId = lookups.contentTypeId;
SPContentType contentType = currentWeb.ContentTypes[new SPContentTypeId(contentTypeId)];
foreach (Lookup lookupNode in lookups.Lookup)
{
//Create the lookup field
SPList linkedList = currentWeb.Lists[lookupNode.lookupListName];
SPFieldLookup lookup;
try
{
//Try to get a reference to an existing field
lookup = currentWeb.Fields[lookupNode.columnName] as SPFieldLookup;
}
catch (ArgumentException)
{
//Create the field if it doesn't exist
currentWeb.Fields.AddLookup(lookupNode.columnName,
linkedList.ID,
currentWeb.ID,
lookupNode.required);
currentWeb.Update();
lookup = currentWeb.Fields[lookupNode.columnName] as SPFieldLookup;
}
//Set the field that provides the data
lookup.ShowInDisplayForm = true;
lookup.LookupField = linkedList.Fields[lookupNode.lookupField].InternalName;
if (!string.IsNullOrEmpty(lookupNode.groupName))
{
lookup.Group = lookupNode.groupName;
}
lookup.Update();
//Link to content type if no link has been defined in CAML
if (contentType.FieldLinks[lookup.InternalName] == null)
{
SPFieldLink fieldLink = new SPFieldLink(lookup);
contentType.FieldLinks.Add(fieldLink);
contentType.Update(true);
}
}
}
}
}
You’ll see that our lookup parser is pretty simple, it makes use of Xml Deserialization to convert our lookup definitions into objects that we can work with programmatically. Our main Parse method then calls out two two private methods, CreateListFields, which adds lookup fields at the list level and CreateSiteFields, which adds lookups fields at the ContentType level.
The next thing we need to add in to make this work is the Xml Schema and the serialization details.
- Again, click on the project node and select Add > New Item > XML Schema. Call it LookupCAML.xsd
- Paste in the following XML:
<?xml version="1.0" encoding="utf-8"?>
<xs:schema id="LookupCAML"
attributeFormDefault="unqualified"
elementFormDefault="qualified"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="Lookups">
<xs:choice minOccurs="1" maxOccurs="unbounded">
<xs:element maxOccurs="unbounded" name="ContentType">
<xs:element maxOccurs="unbounded" ref="Lookup">
<xs:attribute name="contentTypeId" type="xs:string" use="required" />
<xs:element maxOccurs="unbounded" name="List">
<xs:element maxOccurs="unbounded" ref="Lookup">
<xs:attribute name="listName" type="xs:string" use="required" />
<xs:element name="Lookup">
<xs:attribute name="columnName" type="xs:string" use="required" />
<xs:attribute name="lookupListName" type="xs:string" use="required" />
<xs:attribute name="lookupField" type="xs:string" use="required" />
<xs:attribute name="required" type="xs:boolean" use="optional" default="false"/>
<xs:attribute name="groupName" type="xs:string" use="optional" />
This stuff is pretty self explanatory, it just defines the elements and attributes that we expect to see in our lookup definition file. Strictly speaking you don’t actually need this but it’ll provide IntelliSense support later on when it comes to creating our definitions.
The next step is to create the classes that the Xml Serializer will populate for us.
- Add a new class called LookupCAML.cs then paste in the following code:
using System.Xml.Serialization;
using System.ComponentModel;
[SerializableAttribute()]
[XmlTypeAttribute(AnonymousType = true)]
[XmlRootAttribute(Namespace = "", IsNullable = false)]
public partial class Lookups
private object[] itemsField;
[XmlElement("ContentType", typeof(LookupsContentType))]
[XmlElement("List", typeof(LookupsList))]
[XmlType(AnonymousType = true)]
public partial class LookupsContentType
private Lookup[] lookupField;
private string contentTypeIdField;
this.lookupField = value;
public string contentTypeId
return this.contentTypeIdField;
this.contentTypeIdField = value;
[XmlType(AnonymousType = true)]
[XmlRoot(Namespace = "", IsNullable = false)]
public partial class Lookup
private string columnNameField;
private string lookupListNameField;
private string lookupFieldField;
private bool requiredField;
private string groupNameField;
this.requiredField = false;
return this.columnNameField;
this.columnNameField = value;
public string lookupListName
return this.lookupListNameField;
this.lookupListNameField = value;
public string lookupField
return this.lookupFieldField;
this.lookupFieldField = value;
return this.requiredField;
this.requiredField = value;
return this.groupNameField;
this.groupNameField = value;
[XmlType(AnonymousType = true)]
public partial class LookupsList
private Lookup[] lookupField;
private string listNameField;
this.lookupField = value;
return this.listNameField;
this.listNameField = value;
You’ll notice that when you do this your code is automatically attached as a code-behind for the LookupCAML schema.
So now that we have all the pieces in place, check that your code compiles and resolve any missing references etc then we’re ready to actually use our new tool.
The Configuration
Whenever you need to add a new lookup field (or a pile of lookup fields since it makes sense to do the whole lot in once configuration file), here’s what you need to do:
1. Make sure that your Feature.xml file is pointing to your new receiver assembly.
2. Make sure your Feature.Xml file contains a reference to your elements file containing your lookup field definitions.
So your feature.xml file should look a bit like this:
<Feature Id="dcd5261c-6f92-468e-8148-5d9b0ab63f3d"
Title="MyLookupFeature"
Description="Description for MyLookupFeature"
Version="12.0.0.0"
Hidden="FALSE"
Scope="Web"
DefaultResourceFile="core"
ReceiverAssembly="LookupsCamlExtension, Version=1.0.0.0, Culture=neutral, PublicKeyToken=dad9d0edab34cf93"
ReceiverClass="LookupsCamlExtension.MyLookupFeature"
xmlns="http://schemas.microsoft.com/sharepoint/">
<Properties>
<Property Key="LookupColumns" Value="LookupColumns.xml"/>
</Properties>
<ElementManifests>
<ElementFile Location="LookupColumns.xml"/>
</ElementManifests>
</Feature>
3. Add a file containing your lookup definitions (we’ve called it LookupColumns.xml in the example but you can call it whatever you like as long as you update the feature.xml accordingly). To add a file, click on your feature folder then select Add > New Item, then select Data from the categories pane and XML File from the Templates pane. Call your file LookupColumns.xml.
4. Once your file is created you can change the schema to point to your LookupCAML.xsd file so that you’ll have Intellisense support in the designer. To do this click on the ellipsis next to the Schemas property in the Properties window
In the XML Schemas windows that pops up select Use this schema for the LookupCAML.xsd schema
You’re now ready to enter details of the lookups that you want created. Here’s an example of lookup fields being defined on a content type and in a list:
<?xml version="1.0" encoding="utf-8" ?>
<Lookups>
<ContentType contentTypeId="0x0193">
<Lookup columnName="Community" lookupListName="Communities" lookupField="WidgetCommunity" required="true" groupName="My Custom Columns"/>
<Lookup columnName="ItemType" lookupListName="ItemTypes" lookupField="WidgetType" required="true" groupName="My Custom Columns"/>
<Lookup columnName="ItemGroup" lookupListName="ItemGroups" lookupField="WidgetGroup" required="true" groupName="My Custom Columns"/>
</ContentType>
<List listName="MyList">
<Lookup columnName="TheColumnInMyLookupList" lookupListName="MyLookupList" lookupField="TheNameOfTheColumnInMyList"/>
</List>
</Lookups>
The syntax is pretty intuitive. Basically it uses names rather than guid’s to hook everything up.
Deployment
So now you’ve built this fabulous tool, how do you deploy it? This is where WSPBuilder does it’s thing. Right click on your project node, select WSPBuilder > Build WSP. The tool will now compile all your bits and bobs into a WSP that’s ready to be deployed to SharePoint. To deploy it right click on the project node again, select WSPBuilder > Deploy. Job Done!
Now when you go into a site in SharePoint and view the features page you’ll see a new feature called MyLookupFeature, clicking on this will create your lookups. Of course you probably wont use it like this, more than likely you’ll reference it in a site definition so that lookups are automatically created as part of provisioning a new site but that’s a whole other story!
posted @ Wednesday, November 11, 2009 11:24 PM