Enhanced Lookup Field – Part 4

This is the fourth and final part of the post describing how to create a custom lookup field for SharePoint.

Other pages in this post are:

This post describes how to implement the hidden related field for controlling the display within views.

To begin with, create a new class that inherits from the SPFieldMultiLineText class.

public class RelatedLookupDataFieldType : SPFieldMultiLineText
{
    public RelatedLookupDataFieldType(SPFieldCollection fields, string fieldName)
        : base(fields, fieldName)
    {
    }
 
    public RelatedLookupDataFieldType(SPFieldCollection fields, string typeName, string displayName)
        : base(fields, typeName, displayName)
    {
    }
}

This class will be used to control the display within views, which we implement by creating an XSLT to control which field to use for rendering.

<xsl:stylesheet xmlns:x="http://www.w3.org/2001/XMLSchema"
                xmlns:d="http://schemas.microsoft.com/sharepoint/dsp"
                version="1.0"
                exclude-result-prefixes="xsl msxsl ddwrt"
                xmlns:ddwrt="http://schemas.microsoft.com/WebParts/v2/DataView/runtime"
                xmlns:asp="http://schemas.microsoft.com/ASPNET/20"
                xmlns:__designer="http://schemas.microsoft.com/WebParts/v2/DataView/designer"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:msxsl="urn:schemas-microsoft-com:xslt"
                xmlns:SharePoint="Microsoft.SharePoint.WebControls"
                xmlns:ddwrt2="urn:frontpage:internal">
 
  <xsl:template match="FieldRef[@FieldType='ExtendedLookupFieldType']" mode="Lookup_body" ddwrt:dvt_mode="body" priority="10">
    <xsl:param name="thisNode" select="."/>
    <xsl:param name="fieldValue" select="$thisNode/@*[name()=current()/@Name]"/>
 
    <xsl:variable name="fieldDefinition" select="$XmlDefinition/ViewFields/FieldRef[@Name=current()/@Name]"/>
    <xsl:variable name="fieldId" select="$fieldDefinition/@ID"/>
 
    <xsl:variable name="lookupDataValue">
      <xsl:call-template name="string-replace-with">
        <xsl:with-param name="candidate" select="$fieldId" />
        <xsl:with-param name="replace" select="'-'" />
        <xsl:with-param name="with" select="''" />
      </xsl:call-template>
    </xsl:variable>
 
    <xsl:value-of select="$thisNode/@*[name()=concat('r_', substring($lookupDataValue,1,30))]" disable-output-escaping ="yes"/>
  </xsl:template>
 
  <xsl:template name="string-replace-with">
    <xsl:param name="candidate" />
    <xsl:param name="replace" />
    <xsl:param name="with" />
    <xsl:choose>
      <xsl:when test="contains($candidate, $replace)">
        <xsl:value-of select="substring-before($candidate,$replace)" />
        <xsl:value-of select="$with" />
        <xsl:call-template name="string-replace-with">
          <xsl:with-param name="candidate" select="substring-after($candidate,$replace)" />
          <xsl:with-param name="replace" select="$replace" />
          <xsl:with-param name="with" select="$with" />
        </xsl:call-template>
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="$candidate" />
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>

This file (fldtypes_SPS_LookupFieldType.xsl) should be created in the ~hive\Template\Layouts\XSL folder.

The first template in this file matches the ExtendedLookupFieldType field type, which is the master field used to store the lookup data.

<xsl:template match="FieldRef[@FieldType='ExtendedLookupFieldType']" mode="Lookup_body" ddwrt:dvt_mode="body" priority="10">

We start here by retrieving the field definition for the field and then get the identifier for the field.

<xsl:variable name="fieldDefinition" select="$XmlDefinition/ViewFields/FieldRef[@Name=current()/@Name]"/>
<xsl:variable name="fieldId" select="$fieldDefinition/@ID"/>

This is then passed into the string-replace-with template which performs some basic string replacement to remove the dashes (-) from the Guid, so we are left with a string 32 characters in length.

Using this, we are able to retrieve the associated hidden field which is named using the identifier for the master field, which is then used to render the content for the list item.

<xsl:value-of select="$thisNode/@*[name()=concat('r_', substring($lookupDataValue,1,30))]" disable-output-escaping ="yes"/>

We’ll come to the creation of the hidden associated field next. For this we need to edit the RelatedLookupDataFieldType class we created back in post 1.

 
public ProcessLookupFieldType(SPFieldCollection fields, string fieldName)
    : base(fields, fieldName)
{
    EnsureAssociatedField();
}
 
public ProcessLookupFieldType(SPFieldCollection fields, string typeName, string displayName)
    : base(fields, typeName, displayName)
{
    EnsureAssociatedField();
}
 
private void EnsureAssociatedField()
{
    if (Id == Guid.Empty || ParentList == null || !ParentList.Fields.ContainsFieldWithStaticName(RelatedFieldName))
        return;
 
    XmlDocument doc = new XmlDocument();
    doc.LoadXml(SchemaXml);
    if (doc.DocumentElement == null)
        return;
 
    XmlNodeList nodes = doc.DocumentElement.SelectNodes(string.Format("//FieldRefs/FieldRef[@Name='{0}']", RelatedFieldName));
    if (nodes != null && nodes.Count > 0)
        return;
 
    bool allowUnsafeUpdates = ParentList.ParentWeb.AllowUnsafeUpdates;
    try
    {
        ParentList.ParentWeb.AllowUnsafeUpdates = true;
        AssociateInternalName(RelatedFieldName);
        base.Update();
    }
    finally
    {
        ParentList.ParentWeb.AllowUnsafeUpdates = allowUnsafeUpdates;
    }
}
 
public override void OnAdded(SPAddFieldOptions op)
{
    AddRelatedField();
    base.OnAdded(op);
    Update();
}
 
public string RelatedFieldName { get { return string.Concat("r_", Id.ToString("N")).Substring(0, 32); } }
 
private void AddRelatedField()
{
    SPList list = SPContext.Current.List;
    SPFieldCollection fields = list.Fields;
 
    SPFieldMultiLineText field = new SPFieldMultiLineText(fields, "RelatedLookupDataFieldType", RelatedFieldName);
    field.ReadOnlyField = true;
    field.RichText = false;
    field.UnlimitedLengthInDocumentLibrary = true;
    field.Hidden = true;
 
    string internalName = fields.Add(field);
    list.Update();
 
    AssociateInternalName(internalName);
}
 
private void AssociateInternalName(string fieldInternalName)
{
    XmlDocument doc = new XmlDocument();
 
    doc.LoadXml(SchemaXml);
    XmlElement fieldRefs = doc.CreateElement("FieldRefs");
    fieldRefs.InnerXml = string.Format("<FieldRef Name=\"{0}\" Explicit=\"TRUE\" />", fieldInternalName);
    doc.ChildNodes[0].AppendChild(fieldRefs);
 
    SchemaXml = doc.InnerXml;
}

What we’ve done here is override the OnAdded method of the base lookup field class and call a custom AddRelatedField method.

This method creates a new field based on the one created during this part of the blog post – RelatedLookupDataFieldType. As we don’t want users to be aware of this field it’s created as a hidden field and set to read only. We set UnlimitedLengthInDocumentLibrary to true to ensure there we’re able to store the serialised object string containing the selected lookup field data. More on this later.

Next, we associate this new field with the master lookup field by adding it to the list of field references for it. This ensures that whenever the master field is added to a view, this hidden field will also be added, allowing the XSLT to find it. If we didn’t do this, the rendering would fail as there would be no way for a general user to manually add a hidden field to a view. Also, there’s no way through the object model to achieve this, hence the injection of XML into the SchemaXml property.

Lastly, we update the constructors to call the EnsureAssociatedField method. This helps to ensure the field reference has been correctly applied to the field definition as the field is instantiated.

To ensure that whenever a custom lookup field is removed from a list, the following code is implemented to ensure the hidden related field is also removed.

public override void OnDeleting()
{
    RemoveRelatedFields();
    base.OnDeleting();
}
 
private void RemoveRelatedFields()
{
    if (SPContext.Current.List.Fields.ContainsField(RelatedFieldName))
    {
        SPField field = SPContext.Current.List.Fields.GetFieldByInternalName(RelatedFieldName);
        field.ReadOnlyField = false;
        field.Hidden = false;
        field.Update();
        SPContext.Current.List.Fields.Delete(field.InternalName);
    }
}

This time, we’re overriding the OnDeleting method from the base class and calling RemoveRelatedFields to delete the field from the list. Before calling delete it’s necessary to update the field to not be read only or hidden, otherwise the delete call will fail.

Remember, the data is being stored as a serialised ExtendedLookupData object in the hidden associated field by the master field when it is saved. This is what will be passed into the GetFieldValueAsHtml method implemented next.

public override string GetFieldValueAsHtml(object value)
{
    if (value != null)
    {
        string data = SPHttpUtility.HtmlDecode(value.ToString());
        ExtendedLookupData lookupData = Serialization<ExtendedLookupData>.DeserializeObject(data);
 
        if (lookupData != null && lookupData.SavedDisplayValues != null && lookupData.SavedDisplayValues.Count > 0)
        {       
            string display = GetFieldValuesAsLinks(lookupData);
 
            if (string.IsNullOrEmpty(display))
            {
                display = GetFieldValueAsText(data);
            }
            return display;
        }
    }
 
    return null;
}

Here, the object passed into the method is deserialised into an ExtendedLookupData object, which as long as there are stored values, the object is processed to obtain the HTML to display in the view.

private string GetFieldValuesAsLinks(ExtendedLookupData itemdata)
{
    Dictionary<Guid, string> listFormUrls = CacheManager.Instance.GetValue("extendedLookupListFormUrls") as Dictionary<Guid, string>;
 
    bool retrieveList = listFormUrls == null || !listFormUrls.ContainsKey(itemdata.ListId);
    if (listFormUrls == null)
        listFormUrls = new Dictionary<Guid, string>();
 
    if (retrieveList)
    {
        using (SPSecurity.SuppressAccessDeniedRedirectInScope scope = new SPSecurity.SuppressAccessDeniedRedirectInScope())
        {
            try
            {
                using (SPWeb web = SPContext.Current.Site.OpenWeb(itemdata.WebId))
                {
                    SPList lookupList = (from list in web.Lists.Cast<SPList>()
                                            where list.ID.Equals(itemdata.ListId)
                                            select list).SingleOrDefault();
 
                    string formUrl = lookupList != null ? string.Concat(lookupList.ParentWeb.Url, "/_layouts/listform.aspx?PageType=4&ListId=", itemdata.ListId.ToString("B")) : string.Empty;
                    listFormUrls.Add(itemdata.ListId, formUrl);
 
                    CacheManager.Instance.Add("extendedLookupListFormUrls", listFormUrls, DateTime.Now.AddMinutes(10), CacheItemPriority.Normal);
                }
            }
            catch (UnauthorizedAccessException)
            {
                return null;
            }
        }
    }
 
    if (!listFormUrls.ContainsKey(itemdata.ListId))
        return null;
 
    string displayFormUrl = listFormUrls[itemdata.ListId];
 
    return GetItemLinks(displayFormUrl, itemdata);
}

This method is using a custom caching class (part of the download) to retrieve previously processed lists, where the display form url is stored.

As long as there is a valid list, the GetItemLinks method is subsequently called.

private string GetItemLinks(string displayFormUrl, ExtendedLookupData lookupData)
{
    StringBuilder sb = new StringBuilder();
 
    int position = 0;
    foreach (KeyValuePair<int, string> itemValue in lookupData.SavedDisplayValues)
    {
        position++;
        if (position > 1)
        {
            sb.Append("; ");
        }
 
        bool exists;
        string displayValue = GetCurrentFieldValue(lookupData.WebId, lookupData.ListId, lookupData.ValueId, lookupData.DisplayId, itemValue.Key, itemValue.Value, out exists);
        if (!string.IsNullOrEmpty(displayValue) && exists)
        {
            StringBuilder innerBuilder = new StringBuilder();
            innerBuilder.Append(displayFormUrl);
            innerBuilder.Append("&ID=");
            innerBuilder.Append(itemValue.Key);
            innerBuilder.Append("&RootFolder=*");
            sb.Append("<a ");
            if (!SPContext.Current.IsPopUI)
            {
                sb.Append("onclick=\"OpenPopUpPage('");
                sb.Append(innerBuilder);
                sb.Append("', RefreshPage); return false;\" ");
            }
            sb.Append("href=\"");
            sb.Append(innerBuilder);
            sb.Append("\">");
            sb.Append(SPHttpUtility.HtmlEncode(displayValue));
            sb.Append("</a>");
        }
        else
        {
            sb.Append(SPHttpUtility.HtmlEncode(string.IsNullOrEmpty(displayValue) ? itemValue.Value : displayValue));
        }
    }
 
    return sb.ToString();
}

This method iterates through the stored values and checks to see if there is a valid and current lookup item for it. If there is, a link is created to allow a user viewing the result within a view to navigate to the source lookup item. Otherwise, static text is displayed.

The method GetCurrentFieldValue returns the current display value for an existing item. This may differ to what was originally selected when creating the master field value but the point of this field is to either display the active value and when this does not exist, to display the user’s original selection.

GetCurrentFieldValue is shown below:

private string GetCurrentFieldValue(Guid webId, Guid listId, Guid valueFieldId, Guid displayFieldId, int storedValueId, string storedValue, out bool exists)
{
    string cacheKey = string.Format("extendedLookupFieldValue_{0}_{1}_{2}", webId, listId, storedValueId);
    string key = displayFieldId.ToString("B");
 
    Dictionary<string, FieldValueInfo> fields;
    fields = CacheManager.Instance.GetValue(cacheKey) as Dictionary<string, FieldValueInfo>;
 
    if (fields == null || !fields.ContainsKey(key))
    {
        fields = new Dictionary<string, FieldValueInfo>();
 
        using (SPWeb web = SPContext.Current.Site.OpenWeb(webId))
        {
            FieldValueInfo fieldInfo = new FieldValueInfo();
 
            SPList lookupList = (from list in web.Lists.Cast<SPList>()
                                    where list.ID.Equals(listId)
                                    select list).SingleOrDefault();
 
            if (lookupList != null && lookupList.Fields.Contains(valueFieldId) && lookupList.Fields.Contains(displayFieldId))
            {
                SPListItem listItem = (from item in lookupList.Items.Cast<SPListItem>()
                                        where item.ID == storedValueId
                                        select item).SingleOrDefault();
 
                if (listItem != null)
                {
                    fieldInfo.Value = listItem[displayFieldId].ToString();
                    fieldInfo.ItemExists = true;
                }
            }
 
            if (string.IsNullOrEmpty(fieldInfo.Value))
            {
                fieldInfo.Value = storedValue;
                fieldInfo.ItemExists = false;
            }
            fields.Add(key, fieldInfo);
 
            CacheManager.Instance.Add(cacheKey, fields, DateTime.Now.AddMinutes(5), CacheItemPriority.Normal);
        }
    }
 
    if (!fields.ContainsKey(key))
    {
        exists = false;
        return storedValue;
    }
 
    exists = fields[key].ItemExists;
    string value = fields[key].Value;
    return value;
}

This method also utilises caching but this time to store and retrieve the field information. If a field exists for the lookup item, its current display value is stored. If the field no longer exists, the value the user selected when creating the field is stored against the field id in the cache.

In the GetFieldValueAsHtml method, when the result is empty, caused by the source list no longer existing for example, the following method is called to produce a list of the stored values as static text, taken from the ExtendedLookupFieldControl object

public override string GetFieldValueAsText(object value)
{
    if (value != null)
    {
        ExtendedLookupData lookupData;
        if (value is ExtendedLookupData)
            lookupData = value as ExtendedLookupData;
        else
            lookupData = Serialization<ExtendedLookupData>.DeserializeObject(value.ToString());
 
        if (lookupData != null && lookupData.SavedDisplayValues != null && lookupData.SavedDisplayValues.Count > 0)
        {
            string display = string.Empty;
 
            foreach (KeyValuePair<int, string> savedValue in lookupData.SavedDisplayValues)
            {
                if (savedValue.Key > 0)
                {
                    bool exists;
 
                    string currentValue = GetCurrentFieldValue(lookupData.WebId,
                                                                lookupData.ListId,
                                                                lookupData.ValueId,
                                                                lookupData.DisplayId,
                                                                savedValue.Key, savedValue.Value, out exists);
 
                    display += string.IsNullOrEmpty(currentValue) ? savedValue.Value : currentValue;
                    display += ExtendedLookupFieldControl.DisplayDelimiter;
                }
            }
 
            display = display.Substring(0, display.Length - 2);
            return display;
        }
    }
 
    return null;
}

The last part of the post is to create the XML definition file for the hidden field, which as with the main field, should be created in the ~hive\Template\XML folder.

<?xml version="1.0" encoding="utf-8" ?>
<FieldTypes>
  <FieldType>
    <Field Name="TypeName">RelatedLookupDataFieldType</Field>
    <Field Name="ParentType">Note</Field>
    <Field Name="SQLType">ntext</Field>
    <Field Name="TypeDisplayName">Extended Lookup Data</Field>
    <Field Name="TypeShortDescription">Extended Lookup Data</Field>
    <Field Name="UserCreatable">FALSE</Field>
    <Field Name="Sortable">FALSE</Field>
    <Field Name="Filterable">FALSE</Field>
    <Field Name="ShowInSurveyCreate">FALSE</Field>
    <Field Name="ShowInDocumentLibraryCreate">FALSE</Field>
    <Field Name="ShowInColumnTemplateCreate">FALSE</Field>
    <Field Name="UnlimitedLengthInDocumentLibrary">TRUE</Field>
    <Field Name="FieldTypeClass">SharePointStu.LookupField.Fields.RelatedLookupDataFieldType, SharePointStu.LookupField, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d5173afaa03f7f2d</Field>
    <Field Name="AllowBaseTypeRendering">FALSE</Field>
    <Field Name="CAMLRendering">FALSE</Field>
  </FieldType>
</FieldTypes>

Click here to download the code for this project.

Leave a Reply

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

Solve the maths problem shown below before posting: *

Follow

Get every new post delivered to your Inbox

Join other followers: