Thursday, April 12, 2007

Fixing the Lookup fields in List Definition Schema

[Update 24/04/07]
I tried Donal's solution and it didn't work for me. Did it work for anyone else?.
Also, I should mention that this is for WSS3 (2007)

[Update 14/04/07]
A commenter by the name of Donal McWeeney updates me that it is possible to refer to other lists by using the relative path in the "List" attribute in the schema. That will make most of the code below unnessecary - which is good. but will not solve the List="Self" problem (when we want the lookup to look into the list we are creating), because we want the user to be able to create many lists with different relative paths. So my code is still useful, but please read the comment by Donal before starting. If his method solves your problem you should use it!
[End Update 14/04/07]

Here is the first output out of the nested tasks project - a solution for the challange of createing list definitions with lookups.

The Problem
If you want to create a feature that will deploy two (or more) lists, with a lookup field connecting them, you are in trouble.
The schema for defining a lookup field requires you to put the ID (Guid) for the list you are referancing. This is a problem since you don't know the ID - this is randomly generated when the list is created.

The Solution

  1. Define the lookup list as you would:
    <Field Type="Lookup" DisplayName="Parent Task" Required="FALSE" List="Self" ShowField="Title" UnlimitedLengthInDocumentLibrary="FALSE" ID="{94f89715-e097-4e8b-ba79-ea02aa8b7abc}" SourceID="" StaticName="ParentTask" Name="ParentTask" ColName="int3" RowOrdinal="0" />
  2. Install my "LookupFixer" web control. Here is the Source code (I will create this as a download as part of the Nested Tasks project when I can):
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Xml;
    using System.Text;
    using System.Web;
    using System.Web.UI.WebControls;
    using System.Xml.Serialization;
    using Microsoft.SharePoint;
    using Microsoft.SharePoint.WebControls;

    namespace SharePointTips.SharePoint.WebControls
    public class LookupFixer : WebControl
    private string lookupFieldName = "";        
    public string LookupFieldName
    return lookupFieldName;
                    lookupFieldName = 
    private string targetListName = "";
    public string TargetListName
    return targetListName;
                    targetListName = 
    protected override void OnLoad(EventArgs e)
    SPList currentList = SPContext.Current.List;
    public void FixLookUps(SPList list)
    string fieldName = this.LookupFieldName;
    if (list.Fields.ContainsField(fieldName))
    SPFieldLookup field = (SPFieldLookup)list.Fields[fieldName];
    SPList targetList = GetTargetList();
    //check if the field link to the list is broken. 
                    if (field.SourceId != targetList.ID.ToString())
    //create the new lookup field from the old field
                        string newFieldXml = field.SchemaXml;
    string title = field.Title;
    //put the ID of the current list as the source for the lookup
                        newFieldXml = newFieldXml.Replace("", targetList.ID.ToString());
                        newFieldXml = newFieldXml.Replace(
    "Self", targetList.ID.ToString());
    //change the ID of the field to create it as a unique new field.
                        newFieldXml = newFieldXml.Replace(field.Id.ToString(), System.Guid.NewGuid().ToString());
    //TODO - check if the old field was in the default view, and set it in the optionns
                        string newFieldID = list.Fields.AddFieldAsXml(newFieldXml);
    private SPList GetTargetList()
    if (TargetListName.Length == 0)
                    TargetListName = 
    return SPContext.Current.Web.Lists[TargetListName];
  3. Modify the "AllItems.aspx" to include this control in the following format:
    <%@ Register Tagprefix="SharePointTips" Namespace="SharePointTips.SharePoint.WebControls" Assembly="LookupFixer, Version=, Culture=neutral, PublicKeyToken=074b83dc3e6008ae" %>
    <SharePointTips:LookupFixer ID="LookupFixer1" runat="server" TargetListName="Announcements"  LookupFieldName="Parent Task" />

What did we just do?
The web control has two properties that allow you to configure the lookup. TargetListName can remain empty if you want the lookup to be to the same list (self referencing list) - like we want it to work in nested tasks, and LookupFieldName is the name of the lookup field that we are trying to create and is broken.
The control, since it is embedded in the "AllItems.aspx" view, will get triggered when the list is created from the list definition, because the AllItems.aspx is the default view.
(*If you change the default view in the schema, make sure you also embedd the control in that aspx file*)
The code in the control reads the properties, loads the list, loads the field, gets the field settings from the schema, and then - because it is impossible to change the broken field - we delete the field, and create a new one from the schema.

The only thing missing here is changing the code to also add the new field to the views that the old field was part of. I didn't have time to look into that yet.

This is an ugly workaround that works. If anyone has a better solution - please let me know!


Donal McWeeney said...

Not sure if you know this but when you provision a list through OM or UI or ONET, wss will try and resolve lookup fields for you to correct target lists. You need to specify the expected list name in the List attribute of the lookup field. eg.


Of course if the tasks list does not exist when this list is being created then the lookup field wont get fixed up.

Anonymous said...

Can you precise us what version of SharePoint u are working with? Both of you.

dattard said...

I had a similar problem with List Guids in datasources. When creating a custom data source, they were referencing the ListId. These resulted in the DataSources breaking down once they were exported to the production server. Updating the DataSource to the following

<SharePoint:SPDataSource runat="server" DataSourceMode="List" UseInternalName="true" selectcommand="<View></View>" id="Announcements1">
<WebPartPages:DataFormParameter Name="ListName" ParameterKey="ListName" PropertyName="ParameterValues" DefaultValue="Announcements"/>

and adding a ParameterBinding:

<ParameterBinding Name="ListName" Location="None" DefaultValue="Announcements"/>

resulted in a SPDataSource which does not break when it is exported.

Anonymous said...

I'm curious why you do a check each and every time when the list is loaded. (allitems.aspx).

Isnt a task like this better suited for a feature receiver installer? or deployment.

Anonymous said...

The Solution to add the relative path to the lookup list works for me:

Field Type="LookupMulti" DisplayName="Classification" Required="FALSE" List="$Resources:core,lists_Folder;/Classifications" ...

Anonymous said...

I tried donal's solution and it worked for me as well.

The only thing I would add is that in the following sample Xml code from donal, really you should supply your list url, NOT your list name:


For example, my lists have long names, such as "Station Setting". In this case, you will need to make sure you supply the list url correctly.

For example, the following probably won't work:

List="Lists/Station Setting"

Instead, you may need to use the following, depending on the actual url of your list:


You can always obtain your list url from the list definition or just from the list url in the browser once the list is deployed.

Hope this helps someone.


John Developer said...

i think some people should help tutorial. In this blog is kind of described approach. - Lookup field