For the last little bit I have been working on learning how to package up all site infrastructure into a single feature. All of this is pretty straight forward and I didn't really have any issues.....UNTIL I tried to get lookup columns working.
Using CAML
I did some reading on the subject and apparently you can do all of this using CAML alone. I came across a post from Josh Gaffery supporting this claim, but I simply could not get this working...so i gave up after spinning my wheels on it for longer than I wanted to. Josh's approach is to use the list URL as opposed to the GUID that links the column to the source list and he has put up an update explaining it further. Nonetheless....didn't work for me.
Using Feature Receiver
I knew that at this point I would have to take the feature reciever approach and modify the fields in place or create them. I found two sources that both take different approachs to this problem.
- Chris O'Brien has put together the a project on CodePlex that will create the lookup columns at activation time. This actully sounds like a pretty good approach but unfortunately it didn't work for me. I dont know if there is something wrong with my environment but I encountered a few errors doing this...things like the fields not rendering on the page layouts and getting the "The local device name is already in use. (Exception from HRESULT: 0x80070055)" error.
- Waldek Mastykarz has a great post on creating the columns via code here.
Basically I took a hybrid approach to doing this by mixing the two approaches mentioned above. I created a custom XML file that I deploy into the layouts directory and then use a feature reciever to read the xml content and create lookup columns based on this. I also added a deactiving event to remove the fields when the feature is deactivated. Heres the feature reciever code:
public override void FeatureActivated(SPFeatureReceiverProperties properties) { using (SPSite site = properties.Feature.Parent as SPSite) { string contentTypes = null; string listName = null; string fieldName = null; string groupName = null; string staticName = null; string lookupFieldName = "Title"; bool mult = false; bool required = false; string filePath = properties.Feature.Properties[ "ColumnDefinitionPath"].Value; XmlTextReader xReader = new XmlTextReader( HttpContext.Current.Server.MapPath(@"~\_layouts\" + filePath)); while (xReader.Read()) { if (xReader.LocalName == "Field") { #region Get values from attributes if (xReader.MoveToAttribute("List")) { listName = xReader.Value; xReader.MoveToElement(); } if (xReader.MoveToAttribute("Name")) { fieldName = xReader.Value; xReader.MoveToElement(); } if (xReader.MoveToAttribute("StaticName")) { staticName = xReader.Value; xReader.MoveToElement(); } if (xReader.MoveToAttribute("Group")) { groupName = xReader.Value; xReader.MoveToElement(); } if (xReader.MoveToAttribute("LookUpField")) { lookupFieldName = xReader.Value; xReader.MoveToElement(); } if (xReader.MoveToAttribute("ExistInContentTypes")) { contentTypes = xReader.Value; xReader.MoveToElement(); } if (xReader.MoveToAttribute("Mult")) { bool.TryParse(xReader.Value, out mult); xReader.MoveToElement(); } if (xReader.MoveToAttribute("Required")) { bool.TryParse(xReader.Value, out required); xReader.MoveToElement(); } #endregion SPFieldLookup lookup = CreateLookupField( fieldName, groupName, required, mult, site.RootWeb, site.RootWeb.Lists[listName], lookupFieldName, staticName); if (contentTypes != null) foreach (string s in contentTypes.Split(',')) { LinkFieldToContentType(s.Trim(), (SPField)lookup); } } } xReader.Close(); } } public static SPFieldLookup CreateLookupField( string fieldName, string group, bool required, bool allowMultipleValues, SPWeb w, SPList lookupList, string lookupField, string staticName) { w.Fields.AddLookup(fieldName, lookupList.ID, lookupList.ParentWeb.ID, required); SPFieldLookup lookup = (SPFieldLookup)w.Fields[fieldName]; lookup.AllowMultipleValues = allowMultipleValues; lookup.LookupField = lookupField; lookup.StaticName = staticName; lookup.Group = group; lookup.Update(true); return lookup; } public static void LinkFieldToContentType(string contentType, SPField field) { using (SPSite site = SPContext.Current.Web.Site as SPSite) { SPContentType ct = site.RootWeb.ContentTypes[contentType]; ct.FieldLinks.Add(new SPFieldLink(field)); ct.Update(true); // will update children } }
As you can read from the above code the xml file would need to have a node like below for each lookup column:
<Field
Type="Lookup"
List="Access Type"
Name="AccessTypeColumn"
StaticName="Access_x0020_Type"
Group="Infrastructure"
ExistInContentTypes="THIS IS A COMMA DELIMITED LIST OF CONTENT NAMES"
LookUpField="Title"
Mult="TRUE"
Required="FALSE"
/>
The Final Project
So here are all the pieces of my infrastructure project. Notice the placement of the lookupfields xml file...this is because the layouts directory is setup as a virtual directory for every sharepoint site and we can read files from there without a permissions problem.
| 12/TEMPLATES/FEATURES/myfeature/lists.xml | this contains the source lists for the lookup fields |
| 12/TEMPLATES/FEATURES/myfeature/contenttypes.xml | this contains the content type definitions MINUS the lookup fields |
| 12/TEMPLATES/FEATURES/myfeature/sitecolumns.xml | this contains all the other fields included in the content types |
| 12/TEMPLATES/FEATURES/myfeature/feature.xml | the feature def |
| 12/TEMPLATES/LAYOUTS/myfeature/lookupfields.xml | this contains all the lookup fields that need to be provisioned |
Hopefully this helps anyone who's been having problems getting this going. And a big thanks to Waldek, Chris and Josh for their posts.
