Enhanced Lookup Field – Part 3

This is part three of the post on how to create a custom lookup field for SharePoint.

Other pages in this post are:

For this post, I’ll describe the implementation of the field control class.

As previously mentioned, a Visual Studio project will be available for download once this series is finished.

For this class, we’ll inherit from the BaseFieldControl class.

public class ExtendedLookupFieldControl : BaseFieldControl
{
    public ExtendedLookupFieldControl(ExtendedLookupFieldType extendedLookupFieldType)
    {
        if (extendedLookupFieldType == null)
            throw new ArgumentNullException("extendedLookupFieldType");
 
        _isInitialised = false;
        _webSourceId = extendedLookupFieldType.LookupWebId;
        _lookupListId = new Guid(extendedLookupFieldType.LookupList);
        _displayColumnId = new Guid(extendedLookupFieldType.LookupField);
        _valueColumnId = extendedLookupFieldType.ValueColumnId;
        _allowMultipleItemSelection = extendedLookupFieldType.AllowMultipleValues;
        _relatedFieldName = extendedLookupFieldType.RelatedFieldName;
        _isRequired = extendedLookupFieldType.Required;
    }
}

In the constructor for this class we’re taking the settings defined in the field’s definition and storing them for use later on.

The first thing I’ll describe is how the UI is implemented. This covers the creating\editing and displaying of field data.

protected override void CreateChildControls()
{
    if (Field == null) return;
 
    base.CreateChildControls();
 
    switch (ControlMode)
    {
        case SPControlMode.Display:
            CreateControlsForDisplay();
            break;
        case SPControlMode.Edit:
        case SPControlMode.New:
            CreateControlsForNewEdit();
            break;
    }
}
 
protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);
 
    InitialiseControls();
}
 
private void InitialiseControls()
{
    if (_isInitialised)
        return;
 
    string data = Item[_relatedFieldName] == null ? string.Empty : Item[_relatedFieldName].ToString();
 
    _lookupData = !string.IsNullOrEmpty(data) ? Serialization<ExtendedLookupData>.DeserializeObject(data) : new ExtendedLookupData();
 
    switch (ControlMode)
    {
        case SPControlMode.Display:
            InitialiseForDisplay();
            break;
        case SPControlMode.Edit:
        case SPControlMode.New:
            InitialiseForNewEdit();
            break;
    }
 
    _isInitialised = true;
}

During the control creation stage, the control mode is queried and the class either creates controls for displaying or for editing\creation. Similar logic is applied during the load phase of the class.

One thing worth noting here is the deserialization of an ExtendedLookupData object. This object is stored in the related field (see the fourth post for the implementation of this) and is used to ensure the selected lookup values are displayed in the event the source list\items being deleted.

Let’s have a look at the display mode implementation:

private void CreateControlsForDisplay()
{
    ltlDisplayItems = new Literal { ID = "ltlDisplayItems" };
    Controls.Add(ltlDisplayItems);
}
 
private void InitialiseForDisplay()
{
    ltlDisplayItems.Text = string.Empty;
 
    SPList lookupList = GetLookupList();
    List<SharePointHelper.ExtendedLookupColumnValues> currentValues = GetSelectedLookupColumns(lookupList, _lookupData, false);
 
    if (currentValues != null && currentValues.Count > 0)
    {
        string displayLinks = GetFieldValuesAsLinks(_lookupData.WebId, _lookupData.ListId, _lookupData.ValueId, currentValues);
 
        if (string.IsNullOrEmpty(displayLinks))
        {
            foreach (SharePointHelper.ExtendedLookupColumnValues value in currentValues)
            {
                ltlDisplayItems.Text += value.Display + DisplayDelimeter;
            }
 
            ltlDisplayItems.Text = ltlDisplayItems.Text.Substring(0, ltlDisplayItems.Text.Length - DisplayDelimeter.Length);
        }
        else
        {
            ltlDisplayItems.Text = displayLinks;
        }
    }
}

The CreateControlsForDisplay method is pretty self explanatory.

InitialiseForDisplay starts by retrieving the list object containing the lookup items by calling GetLookupList().

The next stage calls GetSelectedLookupColumns to retrieve (see next code block) the current display values and updating any items that no longer exist (because they’ve been deleted) with the original value that was set during the item creation. Again, for further details on the ExtendedLookupColumnValues and the hidden related field, have a look at the fourth post for this series (coming soon).

private List<SharePointHelper.ExtendedLookupColumnValues> GetSelectedLookupColumns(SPList lookupList, ExtendedLookupData lookupData, bool removeInvalidEntries)
{
    List<SharePointHelper.ExtendedLookupColumnValues> columns = new List<SharePointHelper.ExtendedLookupColumnValues>();
 
    if (lookupData != null && lookupData.SavedDisplayValues != null && lookupData.SavedDisplayValues.Count > 0)
    {
        foreach (KeyValuePair<int, string> value in lookupData.SavedDisplayValues)
        {
            string currentDisplayValue = null;
            if (lookupList != null)
            {
                currentDisplayValue = (from item in lookupList.Items.Cast<SPListItem>()
                                        where DoesEqual(item[_valueColumnId], value.Key)
                                        select item.Title).FirstOrDefault();
            }
 
            if (string.IsNullOrEmpty(currentDisplayValue) && !removeInvalidEntries)
            {
                currentDisplayValue = value.Value;
            }
 
            if (!string.IsNullOrEmpty(currentDisplayValue))
            {
                columns.Add(new SharePointHelper.ExtendedLookupColumnValues { Value = value.Key, Display = currentDisplayValue });
            }
        }
    }
 
    return columns;
}

Next, the InitialiseForDisplay method calls GetFieldValuesAsLinks() to get the display values in the form of hyperlinks to the corresponding lookup items. Where all of the lookup items no longer exist and it’s not possible to create hyperlinks the display names from the hidden related field are used to display static titles of the lookup items.

Lastly, for the display implementation, we render the literal control by overriding the RenderFieldForDisplay method.

protected override void RenderFieldForDisplay(HtmlTextWriter output)
{
    ltlDisplayItems.RenderControl(output);
}

Create\Edit Implmentation

Now onto the creation\editing implementation.

private void CreateControlsForNewEdit()
{
    if (_allowMultipleItemSelection)
    {
        CreateMultipleItemSelectionControls();
    }
    else
    {
        ddlLookupItems = new DropDownList {ID = "ddlLookupItems"};
        if (!Field.Required)
        {
            ddlLookupItems.DataBound += DropDownList_OnDataBound;
        }
        Controls.Add(ddlLookupItems);
    }
}

During the control creation, a simple drop down list is created when the field definition specifies that only single selection is permitted. When multiple items are allowed, CreateMultipleItemSelectionControls is called which creates the controls required to allow the user to select more than one lookup item.

private void CreateMultipleItemSelectionControls()
{
    const string SEPARATOR_CELL_STYLE = "padding-left: 10px;";
    const string CSS_MSLONG = "ms-long";
    const string CSS_MSINPUT = "ms-input";
    const string CSS_MSBUTTONHEIGHTWIDTH = "ms-ButtonHeightWidth";
 
    gipLookupItems = new GroupedItemPicker
                            {
                                ID = "gipLookupItems",
                                CandidateControlId = "SelectCandidate",
                                ResultControlId = "SelectResult",
                                AddButtonId = "btnAdd",
                                RemoveButtonId = "btnRemove"
                            };
    Controls.Add(gipLookupItems);
 
    Table tblMultipleValue = new Table { ID = "tblMultipleValue", CellPadding = 0, CellSpacing = 0, CssClass = CSS_MSLONG };
 
    TableRow row = new TableRow();
    TableCell cell = new TableCell { CssClass = CSS_MSINPUT };
    SPHtmlSelect selectCandidate = new SPHtmlSelect {ID = "SelectCandidate", Width = 143, Height = 125, Multiple = true };
    cell.Controls.Add(selectCandidate);
    row.Cells.Add(cell);
    tblMultipleValue.Rows.Add(row);
 
    cell = new TableCell();
    cell.Attributes.Add("style", SEPARATOR_CELL_STYLE);
    row.Cells.Add(cell);
    tblMultipleValue.Rows.Add(row);
 
    cell = new TableCell { CssClass = CSS_MSINPUT, HorizontalAlign = HorizontalAlign.Center, VerticalAlign = VerticalAlign.Middle };
    HtmlButton btnAdd = new HtmlButton { ID = "btnAdd", InnerText = "Add" };
    btnAdd.Attributes.Add("class", CSS_MSBUTTONHEIGHTWIDTH);
    cell.Controls.Add(btnAdd);
    Literal litBreak = new Literal();
    litBreak.Text = "<br /><br />";
    cell.Controls.Add(litBreak);
    HtmlButton btnRemove = new HtmlButton { ID = "btnRemove", InnerText = "Remove" };
    btnRemove.Attributes.Add("class", CSS_MSBUTTONHEIGHTWIDTH);
    cell.Controls.Add(btnRemove);
    row.Cells.Add(cell);
    tblMultipleValue.Rows.Add(row);
 
    cell = new TableCell();
    cell.Attributes.Add("style", SEPARATOR_CELL_STYLE);
    row.Cells.Add(cell);
    tblMultipleValue.Rows.Add(row);
 
    cell = new TableCell { CssClass = CSS_MSINPUT };
    SPHtmlSelect selectResult = new SPHtmlSelect { ID = "SelectResult", Width = 143, Height = 125, Multiple = true };
    cell.Controls.Add(selectResult);
    row.Cells.Add(cell);
    tblMultipleValue.Rows.Add(row);
 
    Controls.Add(tblMultipleValue);
}

The control I chose to use to allow users to select multiple items was the GroupedItemPicker control, which is part of the Microsoft.SharePoint assembly.

The rest of this method simply creates a table to contain the group picker and the associated controls it requires, such as the add\remove buttons and the candidate and selection list boxes.

Now onto the loading phase:

private void InitialiseForNewEdit()
{
    if (_allowMultipleItemSelection)
    {
        InitialiseMultipleItemSelectionControls();
    }
    else
    {
        if (!Page.IsPostBack)
        {
            SPList lookupList = GetLookupList();
            if (lookupList != null)
            {
                List<SharePointHelper.ExtendedLookupColumnValues> values = SharePointHelper.GetColumnValues(lookupList, _displayColumnId, _valueColumnId);
 
                BindLookupColumnValues(ddlLookupItems, values);
 
                if (ddlLookupItems.Items.Count > 0)
                {
                    if (IsEditModeValid(_lookupData, lookupList.ID))
                    {
                        List<SharePointHelper.ExtendedLookupColumnValues> selectedValues = GetSelectedLookupColumns(lookupList, _lookupData);
                        ListItem currentItem = null;
                        if (selectedValues.Count > 0)
                        {
                            // Select the first item in the selectedValues list, this may happen if the field was configured for multiple items
                            // and was then changed to a single selection.
                            currentItem = ddlLookupItems.Items.FindByValue(selectedValues[0].Value.ToString());
                        }
                        ddlLookupItems.SelectedIndex = (currentItem != null) ? ddlLookupItems.Items.IndexOf(currentItem) : 0;
                    }
                    else // Not edit mode, so select the first item in the list
                    {
                        ddlLookupItems.SelectedIndex = 0;    
                    }
                }
            }
        }
    }
}

As with the CreateControlsForNewEdit method, the above method checks if the field allows multiple item selection and processes the initialisation accordingly.

For single selection (using the drop down list) similar logic to initialising the display mode is implemented. The difference here is that lookup items that no longer exist are removed from the available options. Subsequently, saving changes when this is the case will remove the deleted item from the stored values and it will no longer appear in display mode.

For multiple item selection the following is implemented:

private void InitialiseMultipleItemSelectionControls()
{
    if (Page.IsPostBack)
        return;
 
    SPList lookupList = GetLookupList();
    if (lookupList != null)
    {
        List<SharePointHelper.ExtendedLookupColumnValues> values = SharePointHelper.GetColumnValues(lookupList, _displayColumnId, _valueColumnId);
 
        _itemDetails = new Dictionary<int, string>();
        foreach (SharePointHelper.ExtendedLookupColumnValues value in values)
        {
            _itemDetails.Add(Convert.ToInt32(value.Value), value.Display);
        }
 
        if (IsEditModeValid(_lookupData, lookupList.ID))
        {
            List<SharePointHelper.ExtendedLookupColumnValues> selectionValues = GetSelectedLookupColumns(lookupList, _lookupData);
 
            foreach (SharePointHelper.ExtendedLookupColumnValues value in selectionValues)
            {
                gipLookupItems.AddSelectedItem(value.Value.ToString(), value.Display);
            }
 
            UpdateCandidateColumns(values, selectionValues);
        }
 
        foreach (SharePointHelper.ExtendedLookupColumnValues value in values)
        {
            gipLookupItems.AddItem(value.Value.ToString(), value.Display, string.Empty, string.Empty);
        }
    }
}

This method has to fill two list boxes (part of the GroupItemPicker control) one for the available lookup items and the other for the currently selected items.

If the mode is set to edit, the latter drop down list is bound to the item’s selected lookup items, minus those where their source has been deleted.

The candidate drop down list is then bound to the complete list of available lookup items. Where the mode is edit any selected items are removed from the candidate list by calling UpdateCandidateColumns:

An important part of the above method is the setting of the _itemDetails variable. Here, the lookup item’s id and display values are being stored into a Dictionary object. The reason for doing this is that the GroupItemPicker control only exposes a property that allows you to read the selected item’s ids. The property that returns the display values is set to internal, so this is a workaround to allow the field control to save the selected id and display values.

private void UpdateCandidateColumns(List<SharePointHelper.ExtendedLookupColumnValues> candidate, List<SharePointHelper.ExtendedLookupColumnValues> selection)
{
    if (candidate.Count > 0 && selection.Count > 0)
    {
        foreach (SharePointHelper.ExtendedLookupColumnValues item in selection)
        {
            var candidateItem = (from i in candidate
                                 where i.Display == item.Display && DoesEqual(i.Value, item.Value)
                                 select i).FirstOrDefault();
 
            if (candidateItem != null)
            {
                candidate.Remove(candidateItem);
            }
        }
    }
}

That’s about it for the creation an initialisation of the display\edit controls.

Saving

The last part of this post is to describe the saving process for the field.

Before a save is allowed to progress, the Validate method if overridden to ensure a value has been selected when the field settings dictate that it is a required field.

public override void Validate()
{
    if (_isRequired)
    {
        ErrorMessage = "You must specify a value for this required field.";
 
        if (_allowMultipleItemSelection)
        {
            IsValid = gipLookupItems != null && gipLookupItems.SelectedIds.Count > 0;
        }
        else
        {
            IsValid = ddlLookupItems != null && !string.IsNullOrEmpty(ddlLookupItems.SelectedItem.Value);
        }
    }
    else
    {
        IsValid = true;
    }
}

Now onto the save methods. First, the UpdateFieldValueInItem method is overridden:

public override void UpdateFieldValueInItem()
{
    base.UpdateFieldValueInItem();
 
    if (Field.Required)
    {
        IsValid = Value != null;
    }
 
    if (_lookupData != null)
    {
        string data = Serialization<ExtendedLookupData>.SerializeObject(_lookupData);
 
        Item[_relatedFieldName] = data;
    }
}

This method is called whenever a save is initiated against the field. Here additional validation is performed before serializing the ExtendedLookupData object to the hidden related field – see the fourth post for details on how this is implemented.

The call to the base method ensures the Value property is hit. This property is used to get the value for the field.

public override object Value
{
    get
    {
        EnsureChildControls();
        InitialiseControls();
 
        _lookupData = new ExtendedLookupData();
        _lookupData.WebId = _webSourceId;
        _lookupData.ListId = _lookupListId;
        _lookupData.DisplayId = _displayColumnId;
        _lookupData.ValueId = _valueColumnId;
 
        if (_allowMultipleItemSelection)
        {
            SPFieldLookupValueCollection lookupValues = GetMultipleItemSelectionValues();
            AddLookupValuesToLookupData(lookupValues);
            return lookupValues;
        }
 
        SPFieldLookupValue lookupValue = GetSingleItemSelectionValue();
        if (lookupValue != null)
        {
            AddLookupValueToLookupData(lookupValue);
        }
        return lookupValue;
    }
    set
    {
    }
}

During the setter, a new ExtendedLookupData class is initialised and configured with the identifiers for the lookup list.

Where multiple items are permitted the GetMultipleItemSelectionValues method is called and the returned collection saved against the ExtendedLookupData object.

For single selection, the GetSingleItemSelectionValue method is called and, once again, the returned collection saved against the ExtendedLookupData object.

private SPFieldLookupValueCollection GetMultipleItemSelectionValues()
{
    SPFieldLookupValueCollection lookupValues = new SPFieldLookupValueCollection();
 
    if (gipLookupItems.SelectedIds == null || gipLookupItems.SelectedIds.Count > 0)
    {
        // From all available items, filter by the selected item ids.
        lookupValues.AddRange(from KeyValuePair<int, string> kvp in _itemDetails
                              where (from gip in gipLookupItems.SelectedIds.Cast<string>()
                                     where Convert.ToInt32(gip) == kvp.Key
                                     select gip).Contains(kvp.Key.ToString())
                              select new SPFieldLookupValue(kvp.Key, kvp.Value));
    }
 
    return lookupValues;
}

Here, the list of selected id values from the GroupItemPicker is queried against the _itemDetails variable (created during the InitialiseMultipleItemSelectionControls method) and where the id from the GroupItemPicker matches a key from _itemDetails a new LINQ object is created containing the id and the display value. Remember, the GroupItemPicker control does not expose the display property, only id and I don’t like to use reflection in case the underlying object changes.

The GetSingleItemSelectionValue method is a bit simpler:

private SPFieldLookupValue GetSingleItemSelectionValue()
{
    SPFieldLookupValue lookupValue = null;
 
    if (ddlLookupItems != null && !string.IsNullOrEmpty(ddlLookupItems.SelectedItem.Value))
    {
        lookupValue = new SPFieldLookupValue(Convert.ToInt32(ddlLookupItems.SelectedItem.Value), ddlLookupItems.SelectedItem.Text);
    }
 
    return lookupValue;
}

This method returns the selected id and display values from the single selection drop down list.

Part four of this post will continue with the creation of the custom hidden related field, Click here to view it.

One comment

  1. Wissem says:

    Thinks for your solution it’s very helpfull for me. but in my case i have to set the lookup field in allitem like in view with the link not all the xml file.

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: