Mar 16, 2012

Custom LookupField with EntityPicker

If we review the standard usage of the lookup field, we may face situations in which picking a lookup value is not quite straight-forward and unambiguous.

For the sake of the example let’s take one very popular scenario from the relational databases: the relation between employees and departments. An employee’s record includes the selection of a department to which he (she) belongs.

Well, in SharePoint’s world that means a lookup field that relates a record from Employees list with a record of Departments list.

So, if we have the two lists defined and if we have the Departments list populated with some values:

DepartmentsList

when we add new row in Employees list, we can select a department from a dropdown, which is the lookup value behind (based on the code of a department).

FieldsLookup

DefineEmployeeLookup

Now, imagine what would be if you have hundreds or thousands of departments and each of them has much longer (in length) and complex department code.

You cannot know all of them by heart, right?

In such cases the entity picker might be very elegant and handy solution, because it gives the functionality of searching, previewing and selected much more detailed data.

DefineEmployeeLookupWithPicker

PickerDialog

For defining a custom lookup value field I pretty much had to implement next components and to make them work together:

· Custom Field, which inherits SPFieldLookup

· Custom Field declarative definition

· Field Control, which derives from BaseFieldControl

· Picker Control, which derives from EntityEditorWithPicker

· PickerDialog Control, which derives from PickerDialog

· PickerQueryControl, which derives from SimpleQueryControl

Let’s review each of the mentioned components in details:

1) SPFieldLookup

I defined a class which derives from SPFieldLookup class

public class DEMOLookupPicker : SPFieldLookup {…}

There are two pieces of this class, that worth mentioning them:

public string SearchableFields
{
get
{
string value = GetProperty("SearchableFields");

return value;
}
set
{

UpdateFieldSchema("SearchableFields", value);
}
}
I will use this property for defining, which columns from Departments list will be searchable criteria in the entity picker dialog. When we define the list, we can use this property this way:

CustomFieldListSchema

 public override BaseFieldControl FieldRenderingControl
{
get
{
BaseFieldControl fieldControl = new RecordLookupFieldControl() { FieldName = InternalName };
return fieldControl;
}
}

The getter property of “says”, which control to render on opening the edit/add/view forms of the list.

Along with the code, I had to define in declarative way the custom lookup field. The term “fldtypes” always prefixes these field-defining files, which are located in Templates\XML folder.

xmldefinition

2) RecordLookupFieldControl

This component is responsible for instantiating the RecordPicker control.

I had to implement next methods and properties:

public override void Validate()
{
base.Validate();
if (_recordPicker != null)
{
if (base.Field.Required)
IsValid = _recordPicker.IsValid;
else
IsValid = true;
}

}

protected override void CreateChildControls()
{
if (Field == null)
{
return;
}
base.CreateChildControls();

if (base.ControlMode == SPControlMode.Edit || base.ControlMode == SPControlMode.New)
{

_recordPicker = new RecordPicker() { FieldSeachColumns = SearchableFields };
this.Controls.Add(_recordPicker);
}
base.ChildControlsCreated = true;
}


public override object Value
{
get
{
this.EnsureChildControls();

SPFieldLookupValue val = null;
if (this._recordPicker != null)
{
if (this._recordPicker.ResolvedEntities.Count > 0)
{
PickerEntity pickerEntity = (PickerEntity)this._recordPicker.ResolvedEntities[0];
val = new SPFieldLookupValue(Int32.Parse(pickerEntity.Key), pickerEntity.EntityData[pickerEntity.Key].ToString());
}

if (val == null)
_recordPicker.IsValid = false;
}

return val;
}
set
{
this.EnsureChildControls();
DEMOLookupPicker recordLookupField = (DEMOLookupPicker)base.Field;

SPFieldLookupValue lookupValue = (recordLookupField.FieldRenderingControl as RecordLookupFieldControl).ItemFieldValue as SPFieldLookupValue;


PickerEntity pickerEntity = new PickerEntity
{
Key = lookupValue.LookupId.ToString(),
IsResolved = true,
DisplayText = lookupValue.LookupValue,
Description = "" //TODO: you can execute SPQuery and build up the description here
};
pickerEntity.EntityData.Add(lookupValue.LookupId.ToString(), lookupValue.LookupValue);
ArrayList arrayList = new ArrayList();
arrayList.Add(pickerEntity);
this._recordPicker.UpdateEntities(arrayList);
}
}

The setter of Value property is called when data (entity) is loaded into the picker control.

The getter of Value property is called when data (entity) is saved.

In CreateChildControls() method i instantiate the RecordPicker control.

3) RecordPicker (EntityEditorWithPicker)

The RecordPicker class is the main editing control. By deriving from the EntityEditorWithPicker class I get the search box and buttons for free out-of-the box.

RecordPicker implementation defines which PickerDialog is launched on clicking the “Browse” button and some record picker properties as: multi-selection, type in capability in the picker, etc.

EntityPicker

he most important method which I had to define is ValidateEntity:

public override PickerEntity ValidateEntity(PickerEntity needsValidation)
{
PickerEntity result;

if (needsValidation.IsResolved)
{
result = needsValidation;

}
else
{
string displayText = needsValidation.DisplayText;

List<DepartmentEntity> list = ListHelpers.FindDepartments(Constants.DEPARTMENT_CODE, displayText);

switch (list.Count)
{
case 0:
{
needsValidation.Description = "No match";
break;
}
case 1:
{
DepartmentEntity department = list[0];
needsValidation.Key = department.ID.ToString();
needsValidation.DisplayText = department.Code;
needsValidation.Description = department.Code + ":" + department.Name;
needsValidation.IsResolved = true;
needsValidation.EntityData.Add(department.ID.ToString(), department.Code);
break;
}
default:
{
needsValidation.Description = "Multiple matches";
break;
}
}
result = needsValidation;
}

return result;
}

It is called every time when the selected entity is validated.

In my sample, if i type in the department code and click “check names”, the method will be triggered and selection of department based on typed value will be done.

4) RecordPickerDialog (PickerDialog)

To facilitate the search of list-items I use the PickerDialog implementation, which allows the user to search for items instead of entering them manually in the search box.

The dialog provides a dropdown list for searching by code and name of department. The content of the dropdown list is based on the SearchableFields custom property definition in Schema.xml

PickerDialogSearch

PickerDialog type must be registered as safe control or you will end up with an exception like on the screenshot below:

PickerDialogSafeControlsError

To resolve this issue I did two things:

a) I excluded the assembly from the package

ProjectProperties

b) I registered the PickerDialog namespace in the safe controls section of the package

Adding2SafeControls

5) RecordPickerQueryControl (SimpleQueryControl)

The RecordPickerQueryControl class is responsible for searching the data and populating the PickerDialog control. All I had to do is to override the IssueQuery method and to execute SPQuery against the Departments list depending on the search criteria.

 protected override int IssueQuery(string search, string groupName, int pageIndex, int pageSize)
{
DataTable resultsTable = GetDataTable(search, groupName);

RecordPickerDialog.Results = resultsTable;
return resultsTable.Rows.Count;
}


GetEntity is executed for every result item, and in its body we define a custom object (PickerEntity) for every result item. A PickerEntity object is a simple data type that has a key, a display name, and a Boolean value indicating whether the PickerEntity is valid, known as a resolved entity.

 public override PickerEntity GetEntity(DataRow dr)
{
var entity = new PickerEntity
{
Key = (string)dr["ID"],
IsResolved = true,
DisplayText = (string)dr[Constants.DEPARTMENT_CODE],
Description = (string)dr[Constants.DEPARTMENT_CODE] + ":" + (string)dr[Constants.DEPARTMENT_NAME]
};

entity.EntityData.Add((string)dr["ID"], (string)dr[Constants.DEPARTMENT_CODE]);
return entity;
}

The most interesting part is the communication between the building parts of the custom entity picker…or how do we get data and how do we pass our search criteria to the PickerDialog and do we get back the results.

The communication flow, when we open the entity picker is:

DEMOLookupPicker -- RecordLookupFieldControl -- RecordPicker --RecordPickerDialog -- RecordPickerQueryControl

On selecting a given department the communication flow is:

RecordPickerQueryControl (GetEntity method) -- RecordPicker (ValidateEntity Method ) -- RecordLookupFieldControl (Value property getter)

There are 2 possible ways for passing value from the entity picker (RecordPicker) control to the PickerDialog control. Both rely on the CustomProperty property of the Entity Picker Dialog.

a) Using the HttpContex for accessing the value of the CustomProperty

pickerDialogGetCustomProperty

If you want to access the CustomProperty from the HttpContext, you must set its value in OnInit method of the EntityEditorWithPicker control.

entityPickerOnInit

b) The other way is accessing the CustomProperty from the EditControl property of the PickerDialog class after type cast.

pickerDialogGetCustomProperty2

entityPickerConstructor

We can pass even a collection of objects if we serialize it and set the CustomProperty value.

Bear in mind that the OnInit method of the RecordPicker will execute after the constructor of the PickerDialog control!

In the source code, beside the Custom LookupField definition, there are definitions and instances for our two lists – Employees and Departments with content type definitions, and some helper methods for querying the Departments lists.


You can get the source code for this article here.

I will recommend you to take a look on this article. It was very valuable to me.

Read full article!